Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## v3.0.7

### Bug fixes

* [27af3216f](https://github.com/api-platform/core/commit/27af3216f2beac654acb7881b52b3e2e29bf9078) fix(symfony): wire Symfony JsonEncoder if it exists (#5240)
* [31215c623](https://github.com/api-platform/core/commit/31215c62365c6b9095486c307d29837e53c0357a) ci: fix mongod startup (#5248)
* [55be4ca41](https://github.com/api-platform/core/commit/55be4ca41b6a97004d4be623d55bd5e7a3004b16) fix: get back return phpdoc on ProviderInterface
* [6d38cd941](https://github.com/api-platform/core/commit/6d38cd94140edd573ef9b09997204ef345360880) fix(metadata): include routePrefix in default operation name (#5203) (#5252)
* [b52161f](https://github.com/api-platform/core/commit/b52161f75cbfb8fd42b79db8b62e38747c84f089) perf(symfony): use default cache pool config in development environment (#5242)

## v3.0.6

### Bug fixes
Expand Down
48 changes: 48 additions & 0 deletions features/graphql/query.feature
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,54 @@ Feature: GraphQL query support
And the JSON node "data.dummy.name" should be equal to "Dummy #1"
And the JSON node "data.dummy.name_converted" should be equal to "Converted 1"

@createSchema
Scenario: Retrieve an item with different relations to the same resource
Given there are 2 multiRelationsDummy objects having each a manyToOneRelation, 2 manyToManyRelations and 3 oneToManyRelations
When I send the following GraphQL request:
"""
{
multiRelationsDummy(id: "/multi_relations_dummies/2") {
id
name
manyToOneRelation {
id
name
}
manyToManyRelations {
edges{
node {
id
name
}
}
}
oneToManyRelations {
edges{
node {
id
name
}
}
}
}
}
"""
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/json"
And the JSON node "data.multiRelationsDummy.id" should be equal to "/multi_relations_dummies/2"
And the JSON node "data.multiRelationsDummy.name" should be equal to "Dummy #2"
And the JSON node "data.multiRelationsDummy.manyToOneRelation.id" should not be null
And the JSON node "data.multiRelationsDummy.manyToOneRelation.name" should be equal to "RelatedManyToOneDummy #2"
And the JSON node "data.multiRelationsDummy.manyToManyRelations.edges" should have 2 element
And the JSON node "data.multiRelationsDummy.manyToManyRelations.edges[1].node.id" should not be null
And the JSON node "data.multiRelationsDummy.manyToManyRelations.edges[0].node.name" should be equal to "RelatedManyToManyDummy12"
And the JSON node "data.multiRelationsDummy.manyToManyRelations.edges[1].node.name" should be equal to "RelatedManyToManyDummy22"
And the JSON node "data.multiRelationsDummy.oneToManyRelations.edges" should have 3 element
And the JSON node "data.multiRelationsDummy.oneToManyRelations.edges[1].node.id" should not be null
And the JSON node "data.multiRelationsDummy.oneToManyRelations.edges[0].node.name" should be equal to "RelatedOneToManyDummy12"
And the JSON node "data.multiRelationsDummy.oneToManyRelations.edges[2].node.name" should be equal to "RelatedOneToManyDummy32"

@createSchema
Scenario: Retrieve a Relay Node
Given there are 2 dummy objects with relatedDummy
Expand Down
141 changes: 141 additions & 0 deletions features/hal/table_inheritance.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
Feature: Table inheritance
In order to use the api with Doctrine table inheritance
As a client software developer
I need to be able to create resources and fetch them on the upper entity

Background:
Given I add "Accept" header equal to "application/hal+json"
And I add "Content-Type" header equal to "application/json"

@createSchema
Scenario: Create a table inherited resource
And I send a "POST" request to "/dummy_table_inheritance_children" with body:
"""
{
"name": "foo",
"nickname": "bar"
}
"""
Then the response status code should be 201
And the response should be in JSON
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": "/dummy_table_inheritance_children/1"
}
},
"nickname": "bar",
"id": 1,
"name": "foo"
}
"""

Scenario: Get the parent entity collection
When some dummy table inheritance data but not api resource child are created
When I send a "GET" request to "/dummy_table_inheritances"
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/hal+json; charset=utf-8"
And the JSON should be equal to:
"""
{
"_links": {
"self": {
"href": "/dummy_table_inheritances"
},
"item": [
{
"href": "/dummy_table_inheritance_children/1"
},
{
"href": "/dummy_table_inheritances/2"
}
]
},
"totalItems": 2,
"itemsPerPage": 3,
"_embedded": {
"item": [
{
"_links": {
"self": {
"href": "/dummy_table_inheritance_children/1"
}
},
"nickname": "bar",
"id": 1,
"name": "foo"
},
{
"_links": {
"self": {
"href": "/dummy_table_inheritances/2"
}
},
"id": 2,
"name": "Foobarbaz inheritance"
}
]
}
}
"""


Scenario: Get related entity with multiple inherited children types
And I send a "POST" request to "/dummy_table_inheritance_relateds" with body:
"""
{
"children": [
"/dummy_table_inheritance_children/1",
"/dummy_table_inheritances/2"
]
}
"""
Then the response status code should be 201
And the response should be in JSON
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": "/dummy_table_inheritance_relateds/1"
},
"children": [
{
"href": "/dummy_table_inheritance_children/1"
},
{
"href": "/dummy_table_inheritances/2"
}
]
},
"_embedded": {
"children": [
{
"_links": {
"self": {
"href": "/dummy_table_inheritance_children/1"
}
},
"nickname": "bar",
"id": 1,
"name": "foo"
},
{
"_links": {
"self": {
"href": "/dummy_table_inheritances/2"
}
},
"id": 2,
"name": "Foobarbaz inheritance"
}
]
},
"id": 1
}
"""
21 changes: 14 additions & 7 deletions src/Doctrine/Common/State/LinksHandlerTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,20 @@ private function getLinks(string $resourceClass, Operation $operation, array $co
return $links;
}

$newLinks = [];
$newLink = null;
$linkProperty = $context['linkProperty'] ?? null;

foreach ($links as $link) {
if ($linkClass === $link->getFromClass()) {
$newLinks[] = $link;
if ($linkClass === $link->getFromClass() && $linkProperty === $link->getFromProperty()) {
$newLink = $link;
break;
}
}

if ($newLink) {
return [$newLink];
}

// Using GraphQL, it's possible that we won't find a GraphQL Operation of the same type (e.g. it is disabled).
try {
$resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($linkClass);
Expand All @@ -62,16 +68,17 @@ private function getLinks(string $resourceClass, Operation $operation, array $co
}

foreach ($this->getOperationLinks($linkedOperation ?? null) as $link) {
if ($resourceClass === $link->getToClass()) {
$newLinks[] = $link;
if ($resourceClass === $link->getToClass() && $linkProperty === $link->getFromProperty()) {
$newLink = $link;
break;
}
}

if (!$newLinks) {
if (!$newLink) {
throw new RuntimeException(sprintf('The class "%s" cannot be retrieved from "%s".', $resourceClass, $linkClass));
}

return $newLinks;
return [$newLink];
}

private function getIdentifierValue(array &$identifiers, string $name = null): mixed
Expand Down
1 change: 1 addition & 0 deletions src/GraphQl/Resolver/Stage/ReadStage.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ public function __invoke(?string $resourceClass, ?string $rootClass, Operation $
if (isset($source[$info->fieldName], $source[ItemNormalizer::ITEM_IDENTIFIERS_KEY], $source[ItemNormalizer::ITEM_RESOURCE_CLASS_KEY])) {
$uriVariables = $source[ItemNormalizer::ITEM_IDENTIFIERS_KEY];
$normalizationContext['linkClass'] = $source[ItemNormalizer::ITEM_RESOURCE_CLASS_KEY];
$normalizationContext['linkProperty'] = $info->fieldName;
}

return $this->provider->provide($operation, $uriVariables, $normalizationContext);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ final class ExtractorResourceMetadataCollectionFactory implements ResourceMetada
{
use OperationDefaultsTrait;

public function __construct(private readonly ResourceExtractorInterface $extractor, private readonly ?ResourceMetadataCollectionFactoryInterface $decorated = null, array $defaults = [], LoggerInterface $logger = null)
public function __construct(private readonly ResourceExtractorInterface $extractor, private readonly ?ResourceMetadataCollectionFactoryInterface $decorated = null, array $defaults = [], LoggerInterface $logger = null, private readonly bool $graphQlEnabled = false)
{
$this->logger = $logger ?? new NullLogger();
$this->defaults = $defaults;
Expand Down Expand Up @@ -85,7 +85,9 @@ private function buildResources(array $nodes, string $resourceClass): array
}
}

$resource = $this->addGraphQlOperations($node['graphQlOperations'] ?? null, $resource);
if ($this->graphQlEnabled) {
$resource = $this->addGraphQlOperations($node['graphQlOperations'] ?? null, $resource);
}

$resources[] = $this->addOperations($node['operations'] ?? null, $resource);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,16 +68,16 @@ private function mergeLinks(array $links, array $toMergeLinks): array
{
$classLinks = [];
foreach ($links as $link) {
$classLinks[$link->getToClass()] = $link;
$classLinks[$link->getToClass().'#'.$link->getFromProperty()] = $link;
}

foreach ($toMergeLinks as $link) {
if (isset($classLinks[$link->getToClass()])) {
$classLinks[$link->getToClass()] = $classLinks[$link->getToClass()]->withLink($link);
if (null !== $prevLink = $classLinks[$link->getToClass().'#'.$link->getFromProperty()] ?? null) {
$classLinks[$link->getToClass().'#'.$link->getFromProperty()] = $prevLink->withLink($link);

continue;
}
$classLinks[$link->getToClass()] = $link;
$classLinks[$link->getToClass().'#'.$link->getFromProperty()] = $link;
}

return array_values($classLinks);
Expand Down
24 changes: 18 additions & 6 deletions src/Metadata/Resource/Factory/OperationDefaultsTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -187,16 +187,28 @@ private function getOperationWithDefaults(ApiResource $resource, Operation $oper
$operation = $operation->withName($operation->getRouteName());
}

$operationName = $operation->getName() ?? sprintf(
'_api_%s_%s%s',
$operation->getUriTemplate() ?: $operation->getShortName(),
strtolower($operation->getMethod() ?? HttpOperation::METHOD_GET),
$operation instanceof CollectionOperationInterface ? '_collection' : '',
);
$path = ($operation->getRoutePrefix() ?? '').($operation->getUriTemplate() ?? '');
$operationName = $operation->getName() ?? $this->getDefaultOperationName($operation, $resource->getClass());

return [
$operationName,
$operation,
];
}

private function getDefaultShortname(string $resourceClass): string
{
return (false !== $pos = strrpos($resourceClass, '\\')) ? substr($resourceClass, $pos + 1) : $resourceClass;
}

private function getDefaultOperationName(HttpOperation $operation, string $resourceClass): string
{
$path = ($operation->getRoutePrefix() ?? '').($operation->getUriTemplate() ?? '');

return sprintf(
'_api_%s_%s%s',
$path ?: ($operation->getShortName() ?? $this->getDefaultShortname($resourceClass)),
strtolower($operation->getMethod() ?? HttpOperation::METHOD_GET),
$operation instanceof CollectionOperationInterface ? '_collection' : '');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@

namespace ApiPlatform\Metadata\Resource\Factory;

use ApiPlatform\Metadata\CollectionOperationInterface;
use ApiPlatform\Metadata\HttpOperation;
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;

/**
Expand All @@ -24,6 +22,8 @@
*/
final class OperationNameResourceMetadataCollectionFactory implements ResourceMetadataCollectionFactoryInterface
{
use OperationDefaultsTrait;

public function __construct(private readonly ?ResourceMetadataCollectionFactoryInterface $decorated = null)
{
}
Expand Down Expand Up @@ -52,8 +52,7 @@ public function create(string $resourceClass): ResourceMetadataCollection
continue;
}

$path = ($operation->getRoutePrefix() ?? '').($operation->getUriTemplate() ?? '');
$newOperationName = sprintf('_api_%s_%s%s', $path ?: ($operation->getShortName() ?? $this->getDefaultShortname($resourceClass)), strtolower($operation->getMethod() ?? HttpOperation::METHOD_GET), $operation instanceof CollectionOperationInterface ? '_collection' : '');
$newOperationName = $this->getDefaultOperationName($operation, $resourceClass);
$operations->remove($operationName)->add($newOperationName, $operation->withName($newOperationName));
}

Expand All @@ -62,9 +61,4 @@ public function create(string $resourceClass): ResourceMetadataCollection

return $resourceMetadataCollection;
}

private function getDefaultShortname(string $resourceClass): string
{
return (false !== $pos = strrpos($resourceClass, '\\')) ? substr($resourceClass, $pos + 1) : $resourceClass;
}
}
Loading