Skip to content

Commit

Permalink
fix(json-ld): use a Skolem IRI instead of blank nodes
Browse files Browse the repository at this point in the history
  • Loading branch information
soyuka committed Apr 27, 2022
1 parent a5a86e0 commit a8c3342
Show file tree
Hide file tree
Showing 9 changed files with 69 additions and 193 deletions.
5 changes: 4 additions & 1 deletion src/JsonLd/ContextBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,12 @@ public function getAnonymousResourceContext($object, array $context = [], int $r
$shortName
),
'@type' => $shortName,
'@id' => $context['iri'] ?? '_:'.(\function_exists('spl_object_id') ? spl_object_id($object) : spl_object_hash($object)),
];

if (!isset($context['iri']) || false !== $context['iri']) {
$jsonLdContext['@id'] = $context['iri'] ?? '/.well-known/genid/'.(bin2hex(random_bytes(10)));
}

if ($context['has_context'] ?? false) {
unset($jsonLdContext['@context']);
}
Expand Down
193 changes: 24 additions & 169 deletions src/Metadata/ApiProperty.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@

namespace ApiPlatform\Metadata;

use ApiPlatform\Core\Metadata\Property\SubresourceMetadata;
use ApiPlatform\Metadata\Property\DeprecationMetadataTrait;
use Symfony\Component\PropertyInfo\Type;
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
Expand All @@ -26,8 +25,6 @@
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::TARGET_PARAMETER)]
final class ApiProperty
{
use DeprecationMetadataTrait;

/**
* @var string
*/
Expand Down Expand Up @@ -96,27 +93,29 @@ final class ApiProperty

private $schema;
private $initializable;
private $iri;

/**
* @var array
*/
private $extraProperties;

/**
* @param bool|null $readableLink https://api-platform.com/docs/core/serialization/#force-iri-with-relations-of-the-same-type-parentchilds-relations
* @param bool|null $writableLink https://api-platform.com/docs/core/serialization/#force-iri-with-relations-of-the-same-type-parentchilds-relations
* @param bool|null $required https://api-platform.com/docs/admin/validation/#client-side-validation
* @param bool|null $identifier https://api-platform.com/docs/core/identifiers/
* @param string|null $default
* @param string|null $example https://api-platform.com/docs/core/openapi/#using-the-openapi-and-swagger-contexts
* @param string|null $deprecationReason https://api-platform.com/docs/core/deprecations/#deprecating-resource-classes-operations-and-properties
* @param bool|null $fetchEager https://api-platform.com/docs/core/performance/#eager-loading
* @param array|null $jsonldContext https://api-platform.com/docs/core/extending-jsonld-context/#extending-json-ld-and-hydra-contexts
* @param array|null $openapiContext https://api-platform.com/docs/core/openapi/#using-the-openapi-and-swagger-contexts
* @param bool|null $push https://api-platform.com/docs/core/push-relations/
* @param string|null $security https://api-platform.com/docs/core/security
* @param string|null $securityPostDenormalize https://api-platform.com/docs/core/security/#executing-access-control-rules-after-denormalization
* @param array|null $types the RDF types of this property
* @param bool|null $readableLink https://api-platform.com/docs/core/serialization/#force-iri-with-relations-of-the-same-type-parentchilds-relations
* @param bool|null $writableLink https://api-platform.com/docs/core/serialization/#force-iri-with-relations-of-the-same-type-parentchilds-relations
* @param bool|null $required https://api-platform.com/docs/admin/validation/#client-side-validation
* @param bool|null $identifier https://api-platform.com/docs/core/identifiers/
* @param string|null $default
* @param string|null $example https://api-platform.com/docs/core/openapi/#using-the-openapi-and-swagger-contexts
* @param string|null $deprecationReason https://api-platform.com/docs/core/deprecations/#deprecating-resource-classes-operations-and-properties
* @param bool|null $fetchEager https://api-platform.com/docs/core/performance/#eager-loading
* @param array|null $jsonldContext https://api-platform.com/docs/core/extending-jsonld-context/#extending-json-ld-and-hydra-contexts
* @param array|null $openapiContext https://api-platform.com/docs/core/openapi/#using-the-openapi-and-swagger-contexts
* @param bool|null $push https://api-platform.com/docs/core/push-relations/
* @param string|null $security https://api-platform.com/docs/core/security
* @param string|null $securityPostDenormalize https://api-platform.com/docs/core/security/#executing-access-control-rules-after-denormalization
* @param array|null $types the RDF types of this property
* @param bool|string|null $iri the IRI representing the property, can be `false` to remove the generated "@id"
*/
public function __construct(
?string $description = null,
Expand Down Expand Up @@ -144,6 +143,8 @@ public function __construct(
?array $schema = null,
?bool $initializable = null,

$iri = null,

// attributes
array $extraProperties = []
) {
Expand All @@ -169,7 +170,9 @@ public function __construct(
$this->builtinTypes = $builtinTypes;
$this->schema = $schema;
$this->initializable = $initializable;
$this->iri = $iri;
$this->extraProperties = $extraProperties;

}

public function getDescription(): ?string
Expand Down Expand Up @@ -467,169 +470,21 @@ public function withExtraProperties(array $extraProperties = []): self
return $self;
}

/**
* @deprecated since 2.7, to be removed in 3.0
*/
public function withSubresource(SubresourceMetadata $subresourceMetadata): self
{
trigger_deprecation('api-platform/core', '2.7', 'Declaring a subresource on a property is deprecated, use alternate URLs instead.');
$self = clone $this;
$self->extraProperties['subresource'] = $subresourceMetadata;

return $self;
}

/**
* @deprecated since 2.7, to be removed in 3.0
*/
public function getSubresource(): ?SubresourceMetadata
{
return $this->extraProperties['subresource'] ?? null;
}

/**
* Represents whether the property has a subresource.
*
* @deprecated since 2.7, to be removed in 3.0
*/
public function hasSubresource(): bool
{
return isset($this->extraProperties['subresource']);
}

/**
* @deprecated since 2.6, to be removed in 3.0
*/
public function getChildInherited(): ?string
{
return $this->extraProperties['childInherited'] ?? null;
}

/**
* @deprecated since 2.6, to be removed in 3.0
*/
public function hasChildInherited(): bool
{
return isset($this->extraProperties['childInherited']);
}

/**
* @deprecated since 2.4, to be removed in 3.0
*/
public function isChildInherited(): ?string
{
trigger_deprecation('api-platform/core', '2.4', sprintf('"%s::%s" is deprecated since 2.4 and will be removed in 3.0.', __CLASS__, __METHOD__));

return $this->getChildInherited();
}

/**
* @deprecated since 2.6, to be removed in 3.0
*/
public function withChildInherited(string $childInherited): self
{
trigger_deprecation('api-platform/core', '2.6', sprintf('"%s::%s" is deprecated since 2.6 and will be removed in 3.0.', __CLASS__, __METHOD__));

$metadata = clone $this;
$metadata->extraProperties['childInherited'] = $childInherited;

return $metadata;
}

/**
* Gets IRI of this property.
*
* @deprecated since 2.7, to be removed in 3.0, use getTypes instead
*/
public function getIri(): ?string
public function getIri()
{
return $this->types[0] ?? null;
return $this->iri;
}

/**
* Returns a new instance with the given IRI.
*
* @deprecated since 2.7, to be removed in 3.0, use withTypes instead
*/
public function withIri(string $iri = null): self
{
trigger_deprecation('api-platform/core', '2.7', sprintf('"%s::%s" is deprecated since 2.7 and will be removed in 3.0, use Type instead.', __CLASS__, __METHOD__));

$metadata = clone $this;
$metadata->types = [$iri];

return $metadata;
}

/**
* Gets an attribute.
*
* @deprecated since 2.7, to be removed in 3.0, use getExtraProperties instead
*
* @param mixed|null $defaultValue
*/
public function getAttribute(string $key, $defaultValue = null)
{
trigger_deprecation('api-platform/core', '2.7', sprintf('"%s::%s" is deprecated since 2.7 and will be removed in 3.0.', __CLASS__, __METHOD__));

if (!$this->camelCaseToSnakeCaseNameConverter) {
$this->camelCaseToSnakeCaseNameConverter = new CamelCaseToSnakeCaseNameConverter();
}

$propertyName = $this->camelCaseToSnakeCaseNameConverter->denormalize($key);

if (isset($this->{$propertyName})) {
return $this->{$propertyName} ?? $defaultValue;
}

return $this->extraProperties[$key] ?? $defaultValue;
}

/**
* Gets attributes.
*
* @deprecated since 2.7, to be removed in 3.0, renamed as getExtraProperties
*/
public function getAttributes(): ?array
public function withIri($iri): self
{
return $this->extraProperties;
}

/**
* Returns a new instance with the given attribute.
*
* @deprecated since 2.7, to be removed in 3.0, renamed as withExtraProperties
*/
public function withAttributes(array $attributes): self
{
trigger_deprecation('api-platform/core', '2.7', sprintf('"%s::%s" is deprecated since 2.7 and will be removed in 3.0.', __CLASS__, __METHOD__));

$metadata = clone $this;

return $this->withDeprecatedAttributes($metadata, $attributes);
}

/**
* Gets type.
*
* @deprecated since 2.7, to be removed in 3.0, renamed as getBuiltinTypes
*/
public function getType(): ?Type
{
return $this->builtinTypes[0] ?? null;
}

/**
* Returns a new instance with the given type.
*
* @deprecated since 2.7, to be removed in 3.0, renamed as withBuiltinTypes
*/
public function withType(Type $type): self
{
trigger_deprecation('api-platform/core', '2.7', sprintf('"%s::%s" is deprecated since 2.7 and will be removed in 3.0, use builtinTypes instead.', __CLASS__, __METHOD__));

$metadata = clone $this;
$metadata->builtinTypes = [$type];
$metadata->iri = $iri;

return $metadata;
}
Expand Down
1 change: 1 addition & 0 deletions src/Metadata/Extractor/schema/properties.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
<xsd:attribute name="security" type="xsd:string"/>
<xsd:attribute name="securityPostDenormalize" type="xsd:string"/>
<xsd:attribute name="initializable" type="xsd:boolean"/>
<xsd:attribute name="iri" type="xsd:string"/>
</xsd:complexType>

<xsd:complexType name="types">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,6 @@ private function createMetadata(ApiProperty $attribute, ApiProperty $propertyMet
'getAttribute' !== $method &&
'isChildInherited' !== $method &&
'getSubresource' !== $method &&
'getIri' !== $method &&
'getAttributes' !== $method &&
// end of deprecated methods

Expand Down
4 changes: 4 additions & 0 deletions src/Serializer/AbstractItemNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,10 @@ protected function getAttributeValue($object, $attribute, $format = null, array
$childContext = $this->createChildContext($context, $attribute, $format);
unset($childContext['iri']);

if (null !== ($propertyIri = $propertyMetadata->getIri())) {
$childContext['output']['iri'] = $propertyIri;
}

return $this->serializer->normalize($attributeValue, $format, $childContext);
}

Expand Down
42 changes: 29 additions & 13 deletions tests/Hydra/Serializer/DocumentationNormalizerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use ApiPlatform\Api\ResourceClassResolverInterface;
use ApiPlatform\Api\UrlGeneratorInterface;
use ApiPlatform\Core\Api\OperationMethodResolverInterface;
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
use ApiPlatform\Core\Metadata\Property\SubresourceMetadata;
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
Expand All @@ -30,6 +31,7 @@
use ApiPlatform\Metadata\Operations;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface as LegacyPropertyMetadataFactoryInterface;
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
use ApiPlatform\Metadata\Property\PropertyNameCollection;
use ApiPlatform\Metadata\Put;
Expand Down Expand Up @@ -102,23 +104,37 @@ private function doTestNormalize(OperationMethodResolverInterface $operationMeth

$propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
$propertyNameCollectionFactoryProphecy->create('dummy', [])->shouldBeCalled()->willReturn(new PropertyNameCollection(['name', 'description', 'nameConverted', 'relatedDummy']));

$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
$propertyMetadataFactoryProphecy->create('dummy', 'name', Argument::type('array'))->shouldBeCalled()->willReturn(
(new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('name')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true)
);
$propertyMetadataFactoryProphecy->create('dummy', 'description', Argument::type('array'))->shouldBeCalled()->willReturn(
(new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('description')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true)->withJsonldContext(['@type' => '@id'])
);
$propertyMetadataFactoryProphecy->create('dummy', 'nameConverted', Argument::type('array'))->shouldBeCalled()->willReturn(
(new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('name converted')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true)
);
$propertyMetadataFactoryProphecy->create('dummy', 'relatedDummy', Argument::type('array'))->shouldBeCalled()->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, 'dummy', true, null, new Type(Type::BUILTIN_TYPE_OBJECT, false, 'relatedDummy'))])->withDescription('This is a name.')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true));

if ($resourceMetadataFactory instanceof ResourceMetadataFactoryInterface) {
$propertyMetadataFactoryProphecy = $this->prophesize(LegacyPropertyMetadataFactoryInterface::class);
$propertyMetadataFactoryProphecy->create('dummy', 'name', Argument::type('array'))->shouldBeCalled()->willReturn(
(new PropertyMetadata())->withType(new Type(Type::BUILTIN_TYPE_STRING))->withDescription('name')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true)
);
$propertyMetadataFactoryProphecy->create('dummy', 'description', Argument::type('array'))->shouldBeCalled()->willReturn(
(new PropertyMetadata())->withType(new Type(Type::BUILTIN_TYPE_STRING))->withDescription('description')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true)->withAttributes(['jsonld_context' => ['@type' => '@id']])
);
$propertyMetadataFactoryProphecy->create('dummy', 'nameConverted', Argument::type('array'))->shouldBeCalled()->willReturn(
(new PropertyMetadata())->withType(new Type(Type::BUILTIN_TYPE_STRING))->withDescription('name converted')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true)
);
$propertyMetadataFactoryProphecy->create('dummy', 'relatedDummy', Argument::type('array'))->shouldBeCalled()->willReturn((new PropertyMetadata())->withType(new Type(Type::BUILTIN_TYPE_OBJECT, false, 'dummy', true, null, new Type(Type::BUILTIN_TYPE_OBJECT, false, 'relatedDummy')))->withDescription('This is a name.')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true));
} else {
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
$propertyMetadataFactoryProphecy->create('dummy', 'name', Argument::type('array'))->shouldBeCalled()->willReturn(
(new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('name')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true)
);
$propertyMetadataFactoryProphecy->create('dummy', 'description', Argument::type('array'))->shouldBeCalled()->willReturn(
(new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('description')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true)->withJsonldContext(['@type' => '@id'])
);
$propertyMetadataFactoryProphecy->create('dummy', 'nameConverted', Argument::type('array'))->shouldBeCalled()->willReturn(
(new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('name converted')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true)
);
$propertyMetadataFactoryProphecy->create('dummy', 'relatedDummy', Argument::type('array'))->shouldBeCalled()->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, 'dummy', true, null, new Type(Type::BUILTIN_TYPE_OBJECT, false, 'relatedDummy'))])->withDescription('This is a name.')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true));
}

$subresourceOperationFactoryProphecy = null;
if ($resourceMetadataFactory instanceof ResourceMetadataFactoryInterface) {
$subresourceMetadata = new SubresourceMetadata('relatedDummy', false);
$propertyMetadataFactoryProphecy->create('dummy', 'relatedDummy')->shouldBeCalled()->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, 'dummy', true, null, new Type(Type::BUILTIN_TYPE_OBJECT, false, 'relatedDummy'))])->withDescription('This is a name.')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true)->withSubresource($subresourceMetadata));
$propertyMetadataFactoryProphecy->create('dummy', 'relatedDummy')->shouldBeCalled()->willReturn((new PropertyMetadata())->withType(new Type(Type::BUILTIN_TYPE_OBJECT, false, 'dummy', true, null, new Type(Type::BUILTIN_TYPE_OBJECT, false, 'relatedDummy')))->withDescription('This is a name.')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true)->withSubresource($subresourceMetadata));
$subresourceOperationFactoryProphecy = $this->prophesize(SubresourceOperationFactoryInterface::class);
$subresourceOperationFactoryProphecy->create('dummy')->shouldBeCalled()->willReturn([
'api_dummies_subresource_get_related_dummy' => [
Expand Down
Loading

0 comments on commit a8c3342

Please sign in to comment.