Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

iro-only: defer loading of property value #1

Open
wants to merge 24 commits into
base: iri-only-collection-property
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
857cea2
feat(jsonld): add IriOnly Option on Collection properties
GregoireHebert Jul 17, 2023
2133422
style: run cs-fixer
GregoireHebert Jul 17, 2023
6a1d956
fix: phpstan review
GregoireHebert Jul 17, 2023
c25646d
fix: xml adapter
GregoireHebert Jul 17, 2023
37bc402
fix: style and import
GregoireHebert Jul 17, 2023
553f1b6
fix: malformed json
GregoireHebert Jul 18, 2023
fe097f9
fix: typehint function return
GregoireHebert Jul 18, 2023
ba418b0
fix: Relation inverse propery
GregoireHebert Jul 18, 2023
a03a2aa
fix: use UriTemplate string instead of a boolean
GregoireHebert Jul 25, 2023
d3708dc
fix: ci review
GregoireHebert Jul 25, 2023
55465d7
fix: ci review
GregoireHebert Jul 25, 2023
b2ffff7
fix: CI review
GregoireHebert Jul 25, 2023
e17cf60
fix: CI review
GregoireHebert Jul 25, 2023
9efc278
fix: metadata
GregoireHebert Jul 25, 2023
2481540
fix: using sub-resource iri-only including parent ID in path
GregoireHebert Jul 25, 2023
a048352
fix: failing test
GregoireHebert Jul 25, 2023
729a562
cs: update local php-cs-fixer to 3.22
GregoireHebert Jul 26, 2023
91a5c88
fix: document entities attributes
GregoireHebert Jul 26, 2023
6e9221b
fix: review soyuka
GregoireHebert Aug 1, 2023
40ee9d3
fix: review soyuka
GregoireHebert Aug 1, 2023
847ee8b
re-run CI
GregoireHebert Aug 3, 2023
ac45794
defer loading of property value
usu Aug 15, 2023
d6f5f29
feat: handle uriTemplate on property for HAL format
GregoireHebert Aug 18, 2023
c319992
Merge branch 'iri-only-collection-property' into iri-only-collection-…
GregoireHebert Aug 18, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions features/hal/collection_uri_template.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
@php8
@v3
Feature: Exposing a property being a collection of resources
can return an IRI instead of an array
when the uriTemplate is set on the ApiProperty attribute

Scenario: Retrieve Resource with uriTemplate collection Property
Given there are propertyCollectionIriOnly with relations
When I add "Accept" header equal to "application/hal+json"
And I send a "GET" request to "/property_collection_iri_onlies/1"
Then the response status code should be 200
And the response should be in JSON
And the JSON should be valid according to the JSON HAL schema
And the header "Content-Type" should be equal to "application/hal+json; charset=utf-8"
And the JSON should be equal to:
"""
{
"_links": {
"self": {
"href": "/property_collection_iri_onlies/1"
},
"propertyCollectionIriOnlyRelation": {
"href": "/property-collection-relations"
},
"iterableIri": {
"href": "/parent/1/another-collection-operations"
}
},
"_embedded": {
"propertyCollectionIriOnlyRelation": [
{
"_links": {
"self": {
"href": "/property_collection_iri_only_relations/1"
}
},
"name": "relation"
}
],
"iterableIri": [
{
"_links": {
"self": {
"href": "/property_collection_iri_only_relations/9999"
}
},
"name": "Michel"
}
]
}
}
"""
34 changes: 34 additions & 0 deletions features/jsonld/iri_only.feature
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,37 @@ Feature: JSON-LD using iri_only parameter
"hydra:totalItems": 3
}
"""

Scenario: Retrieve Resource with uriTemplate collection Property
Given there are propertyCollectionIriOnly with relations
When I send a "GET" request to "/property_collection_iri_onlies"
Then the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the JSON should be valid according to this schema:
"""
{
"hydra:member": [
{
"@id": "/property_collection_iri_onlies/1",
"@type": "PropertyCollectionIriOnly",
"propertyCollectionIriOnlyRelation": "/property-collection-relations",
"iterableIri": "/parent/1/another-collection-operations"
}
]
}
"""
When I send a "GET" request to "/property_collection_iri_onlies/1"
Then the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the JSON should be valid according to this schema:
"""
{
"@context": "/contexts/PropertyCollectionIriOnly",
"@id": "/property_collection_iri_onlies/1",
"@type": "PropertyCollectionIriOnly",
"propertyCollectionIriOnlyRelation": "/property-collection-relations",
"iterableIri": "/parent/1/another-collection-operations"
}
"""
3 changes: 2 additions & 1 deletion src/Doctrine/Orm/Extension/EagerLoadingExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,9 @@ private function joinRelations(QueryBuilder $queryBuilder, QueryNameGeneratorInt
}

$fetchEager = $propertyMetadata->getFetchEager();
$uriTemplate = $propertyMetadata->getUriTemplate();

if (false === $fetchEager) {
if (false === $fetchEager || null !== $uriTemplate) {
continue;
}

Expand Down
30 changes: 29 additions & 1 deletion src/Hal/Serializer/ItemNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
namespace ApiPlatform\Hal\Serializer;

use ApiPlatform\Api\UrlGeneratorInterface;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Util\ClassInfoTrait;
use ApiPlatform\Serializer\AbstractItemNormalizer;
use ApiPlatform\Serializer\CacheKeyTrait;
Expand Down Expand Up @@ -166,7 +167,28 @@ private function getComponents(object $object, ?string $format, array $context):
continue;
}

$relation = ['name' => $attribute, 'cardinality' => $isOne ? 'one' : 'many'];
$relation = ['name' => $attribute, 'cardinality' => $isOne ? 'one' : 'many', 'uriTemplate' => null];

// if we specify the uriTemplate, generates its value for link definition
// @see ApiPlatform\Serializer\AbstractItemNormalizer:getAttributeValue logic for intentional duplicate content
if ($isMany && $itemUriTemplate = $propertyMetadata->getUriTemplate()) {
$attributeValue = $this->propertyAccessor->getValue($object, $attribute);
$resourceClass = $this->resourceClassResolver->getResourceClass($attributeValue, $className);
$childContext = $this->createChildContext($context, $attribute, $format);
unset($childContext['iri'], $childContext['uri_variables'], $childContext['resource_class'], $childContext['operation']);

$operation = $this->resourceMetadataCollectionFactory->create($resourceClass)->getOperation(
operationName: $itemUriTemplate,
forceCollection: true,
httpOperation: true
);

if ($operation instanceof GetCollection) {
$relation['uriTemplate'] = $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_PATH, $operation, $childContext);
}
}


if ($propertyMetadata->isReadableLink()) {
$components['embedded'][] = $relation;
}
Expand Down Expand Up @@ -215,6 +237,12 @@ private function populateRelation(array $data, object $object, ?string $format,
$relationName = $this->nameConverter->normalize($relationName, $class, $format, $context);
}

// if we specify the uriTemplate, then the link takes the uriTemplate defined.
if ('links' === $type && $itemUriTemplate = $relation['uriTemplate']) {
$data[$key][$relationName]['href'] = $itemUriTemplate;
continue;
}

if ('one' === $relation['cardinality']) {
if ('links' === $type) {
$data[$key][$relationName]['href'] = $this->getRelationIri($attributeValue);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public function create(string $resourceClass, string $property, array $options =

$propertySchema = $propertyMetadata->getSchema() ?? [];

if (!\array_key_exists('readOnly', $propertySchema) && false === $propertyMetadata->isWritable() && !$propertyMetadata->isInitializable()) {
if (null !== $propertyMetadata->getUriTemplate() || (!\array_key_exists('readOnly', $propertySchema) && false === $propertyMetadata->isWritable() && !$propertyMetadata->isInitializable())) {
$propertySchema['readOnly'] = true;
}

Expand Down Expand Up @@ -124,6 +124,11 @@ public function create(string $resourceClass, string $property, array $options =
$propertySchema['owl:maxCardinality'] = 1;
}

if ($isCollection && null !== $propertyMetadata->getUriTemplate()) {
$keyType = null;
$isCollection = false;
}

$propertyType = $this->getType(new Type($builtinType, $type->isNullable(), $className, $isCollection, $keyType, $valueType), $propertyMetadata->isReadableLink());
if (!\in_array($propertyType, $valueSchema, true)) {
$valueSchema[] = $propertyType;
Expand Down
22 changes: 20 additions & 2 deletions src/Metadata/ApiProperty.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ final class ApiProperty
* @param string[] $types the RDF types of this property
* @param string[] $iris
* @param Type[] $builtinTypes
* @param string|null $uriTemplate whether to return the subRessource collection IRI instead of an iterable of IRI
*/
public function __construct(
private ?string $description = null,
Expand Down Expand Up @@ -105,15 +106,16 @@ public function __construct(
private ?string $security = null,
private ?string $securityPostDenormalize = null,
private array|string|null $types = null,
/**
/*
* The related php types.
*/
private ?array $builtinTypes = null,
private ?array $schema = null,
private ?bool $initializable = null,
private $iris = null,
private ?bool $genId = null,
private array $extraProperties = []
private ?string $uriTemplate = null,
private array $extraProperties = [],
) {
if (\is_string($types)) {
$this->types = (array) $types;
Expand Down Expand Up @@ -464,4 +466,20 @@ public function withGenId(bool $genId): self

return $metadata;
}

/**
* Whether to return the subRessource collection IRI instead of an iterable of IRI.
*/
public function getUriTemplate(): ?string
{
return $this->uriTemplate;
}

public function withUriTemplate(?string $uriTemplate): self
{
$metadata = clone $this;
$metadata->uriTemplate = $uriTemplate;

return $metadata;
}
}
1 change: 1 addition & 0 deletions src/Metadata/Extractor/XmlPropertyExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ protected function extractPath(string $path): void
'extraProperties' => $this->buildExtraProperties($property, 'extraProperties'),
'iris' => $this->buildArrayValue($property, 'iri'),
'genId' => $this->phpize($property, 'genId', 'bool'),
'uriTemplate' => $this->phpize($property, 'uriTemplate', 'string'),
];
}
}
Expand Down
1 change: 1 addition & 0 deletions src/Metadata/Extractor/YamlPropertyExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ private function buildProperties(array $resourcesYaml): void
'builtinTypes' => $this->buildAttribute($propertyValues, 'builtinTypes'),
'schema' => $this->buildAttribute($propertyValues, 'schema'),
'genId' => $this->phpize($propertyValues, 'genId', 'bool'),
'uriTemplate' => $this->phpize($propertyValues, 'uriTemplate', 'string'),
];
}
}
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 @@ -44,6 +44,7 @@
<xsd:attribute name="securityPostDenormalize" type="xsd:string"/>
<xsd:attribute name="initializable" type="xsd:boolean"/>
<xsd:attribute name="genId" type="xsd:boolean"/>
<xsd:attribute name="uriTemplate" type="xsd:string"/>
</xsd:complexType>

<xsd:complexType name="types">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ final class XmlPropertyAdapter implements PropertyAdapterInterface
'initializable',
'iris',
'genId',
'uriTemplate',
];

/**
Expand Down
2 changes: 1 addition & 1 deletion src/Metadata/Tests/Extractor/Adapter/properties.xml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<properties xmlns="https://api-platform.com/schema/metadata/properties-3.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://api-platform.com/schema/metadata/properties-3.0 https://api-platform.com/schema/metadata/properties-3.0.xsd">
<property name="comment" resource="ApiPlatform\Metadata\Tests\Fixtures\ApiResource\Comment" description="Comment message" readable="true" writable="true" readableLink="true" writableLink="true" required="true" identifier="false" default="Plop" example="Lorem ipsum dolor sit amet" deprecationReason="Foo" fetchable="true" fetchEager="true" push="true" security="is_granted('IS_AUTHENTICATED_ANONYMOUSLY')" securityPostDenormalize="is_granted('ROLE_CUSTOM_ADMIN')" initializable="true" genId="true"><jsonldContext><values><value name="bar"><values><value name="foo"><values><value name="bar">baz</value></values></value></values></value></values></jsonldContext><openapiContext><values><value name="foo">bar</value></values></openapiContext><jsonSchemaContext><values><value name="lorem">ipsum</value></values></jsonSchemaContext><types><type>someirischema</type><type>anotheririschema</type></types><builtinTypes><builtinType>string</builtinType></builtinTypes><schema><values><value>https://schema.org/Thing</value></values></schema><iris><iri>https://schema.org/totalPrice</iri></iris><extraProperties><values><value name="custom_property">Lorem ipsum dolor sit amet</value></values></extraProperties></property></properties>
<property name="comment" resource="ApiPlatform\Metadata\Tests\Fixtures\ApiResource\Comment" description="Comment message" readable="true" writable="true" readableLink="true" writableLink="true" required="true" identifier="false" default="Plop" example="Lorem ipsum dolor sit amet" deprecationReason="Foo" fetchable="true" fetchEager="true" push="true" security="is_granted('IS_AUTHENTICATED_ANONYMOUSLY')" securityPostDenormalize="is_granted('ROLE_CUSTOM_ADMIN')" initializable="true" genId="true" uriTemplate="/sub-resource-get-collection"><jsonldContext><values><value name="bar"><values><value name="foo"><values><value name="bar">baz</value></values></value></values></value></values></jsonldContext><openapiContext><values><value name="foo">bar</value></values></openapiContext><jsonSchemaContext><values><value name="lorem">ipsum</value></values></jsonSchemaContext><types><type>someirischema</type><type>anotheririschema</type></types><builtinTypes><builtinType>string</builtinType></builtinTypes><schema><values><value>https://schema.org/Thing</value></values></schema><iris><iri>https://schema.org/totalPrice</iri></iris><extraProperties><values><value name="custom_property">Lorem ipsum dolor sit amet</value></values></extraProperties></property></properties>
1 change: 1 addition & 0 deletions src/Metadata/Tests/Extractor/Adapter/properties.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,4 @@ properties:
iris:
- 'https://schema.org/totalPrice'
genId: true
uriTemplate: /sub-resource-get-collection
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ final class PropertyMetadataCompatibilityTest extends TestCase
],
'iris' => ['https://schema.org/totalPrice'],
'genId' => true,
'uriTemplate' => '/sub-resource-get-collection',
];

/**
Expand Down
Loading