diff --git a/.github/workflows/guides.yaml b/.github/workflows/guides.yaml
index 41413e23626..1b2b483b915 100644
--- a/.github/workflows/guides.yaml
+++ b/.github/workflows/guides.yaml
@@ -42,7 +42,9 @@ jobs:
restore-keys: ${{ runner.os }}-composer-
- name: Install project dependencies
working-directory: docs
- run: composer install --no-interaction --no-progress --ansi && composer require webonyx/graphql-php
+ run: |
+ composer update --no-interaction --no-progress --ansi
+ cp -r ../src ./vendor/api-platform/core/
- name: Test guides
working-directory: docs
env:
diff --git a/docs/composer.json b/docs/composer.json
index 1d8298be592..c72780bbf30 100644
--- a/docs/composer.json
+++ b/docs/composer.json
@@ -34,7 +34,8 @@
"zenstruck/foundry": "^1.31",
"symfony/http-client": "^7.0",
"symfony/browser-kit": "^7.0",
- "justinrainbow/json-schema": "^5.2"
+ "justinrainbow/json-schema": "^5.2",
+ "webonyx/graphql-php": "^15.11"
},
"config": {
"allow-plugins": {
@@ -43,6 +44,5 @@
},
"require-dev": {
"phpunit/phpunit": "^10"
- },
- "minimum-stability": "dev"
+ }
}
diff --git a/docs/config/packages/framework.yaml b/docs/config/packages/framework.yaml
index 582a3b5972c..c0aac2ebbb3 100644
--- a/docs/config/packages/framework.yaml
+++ b/docs/config/packages/framework.yaml
@@ -7,7 +7,6 @@ api_platform:
json: ['application/json']
docs_formats:
jsonopenapi: ['application/vnd.openapi+json']
- event_listeners_backward_compatibility_layer: false
keep_legacy_inflector: false
defaults:
extra_properties:
diff --git a/features/doctrine/issue6039/entity_class_option.feature b/features/doctrine/issue6039/entity_class_option.feature
new file mode 100644
index 00000000000..18609626a2e
--- /dev/null
+++ b/features/doctrine/issue6039/entity_class_option.feature
@@ -0,0 +1,12 @@
+Feature: Test entity class option on collections
+ In order to retrieve a collections of resources mapped to a DTO automatically
+ As a client software developer
+
+ @createSchema
+ @!mongodb
+ Scenario: Get collection
+ Given there are issue6039 users
+ And I add "Accept" header equal to "application/ld+json"
+ When I send a "GET" request to "/issue6039_user_apis"
+ Then the response status code should be 200
+ And the JSON node "hydra:member[0].bar" should not exist
diff --git a/src/Doctrine/Odm/Filter/AbstractFilter.php b/src/Doctrine/Odm/Filter/AbstractFilter.php
index 40cecd1400f..d1b30add62a 100644
--- a/src/Doctrine/Odm/Filter/AbstractFilter.php
+++ b/src/Doctrine/Odm/Filter/AbstractFilter.php
@@ -86,7 +86,7 @@ protected function isPropertyEnabled(string $property, string $resourceClass): b
protected function denormalizePropertyName(string|int $property): string
{
if (!$this->nameConverter instanceof NameConverterInterface) {
- return $property;
+ return (string) $property;
}
return implode('.', array_map($this->nameConverter->denormalize(...), explode('.', (string) $property)));
diff --git a/src/Doctrine/Orm/Filter/AbstractFilter.php b/src/Doctrine/Orm/Filter/AbstractFilter.php
index c1deb1c0a90..0ad569e5145 100644
--- a/src/Doctrine/Orm/Filter/AbstractFilter.php
+++ b/src/Doctrine/Orm/Filter/AbstractFilter.php
@@ -80,7 +80,7 @@ protected function isPropertyEnabled(string $property, string $resourceClass): b
protected function denormalizePropertyName(string|int $property): string
{
if (!$this->nameConverter instanceof NameConverterInterface) {
- return $property;
+ return (string) $property;
}
return implode('.', array_map($this->nameConverter->denormalize(...), explode('.', (string) $property)));
diff --git a/src/Hydra/Serializer/DocumentationNormalizer.php b/src/Hydra/Serializer/DocumentationNormalizer.php
index fac663c78b8..e7b35f9b3da 100644
--- a/src/Hydra/Serializer/DocumentationNormalizer.php
+++ b/src/Hydra/Serializer/DocumentationNormalizer.php
@@ -20,6 +20,7 @@
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\CollectionOperationInterface;
+use ApiPlatform\Metadata\ErrorResource;
use ApiPlatform\Metadata\HttpOperation;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
@@ -29,6 +30,7 @@
use ApiPlatform\Metadata\ResourceClassResolverInterface;
use ApiPlatform\Metadata\UrlGeneratorInterface;
use ApiPlatform\Serializer\CacheableSupportsMethodInterface;
+use ApiPlatform\Symfony\Validator\Exception\ValidationException;
use Symfony\Component\PropertyInfo\Type;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
@@ -60,6 +62,10 @@ public function normalize(mixed $object, ?string $format = null, array $context
$resourceMetadataCollection = $this->resourceMetadataFactory->create($resourceClass);
$resourceMetadata = $resourceMetadataCollection[0];
+ if ($resourceMetadata instanceof ErrorResource && ValidationException::class === $resourceMetadata->getClass()) {
+ continue;
+ }
+
$shortName = $resourceMetadata->getShortName();
$prefixedShortName = $resourceMetadata->getTypes()[0] ?? "#$shortName";
$this->populateEntrypointProperties($resourceMetadata, $shortName, $prefixedShortName, $entrypointProperties, $resourceMetadataCollection);
diff --git a/src/Metadata/Extractor/XmlResourceExtractor.php b/src/Metadata/Extractor/XmlResourceExtractor.php
index e92785331a9..3211518cc3f 100644
--- a/src/Metadata/Extractor/XmlResourceExtractor.php
+++ b/src/Metadata/Extractor/XmlResourceExtractor.php
@@ -13,12 +13,12 @@
namespace ApiPlatform\Metadata\Extractor;
+use ApiPlatform\Elasticsearch\State\Options;
use ApiPlatform\Metadata\Exception\InvalidArgumentException;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\HeaderParameter;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\QueryParameter;
-use ApiPlatform\Metadata\Tests\Fixtures\StateOptions;
use ApiPlatform\OpenApi\Model\ExternalDocumentation;
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter;
@@ -455,10 +455,12 @@ private function buildStateOptions(\SimpleXMLElement $resource): ?OptionsInterfa
}
$elasticsearchOptions = $stateOptions->elasticsearchOptions ?? null;
if ($elasticsearchOptions) {
- return new StateOptions(
- isset($elasticsearchOptions['index']) ? (string) $elasticsearchOptions['index'] : null,
- isset($elasticsearchOptions['type']) ? (string) $elasticsearchOptions['type'] : null,
- );
+ if (class_exists(Options::class)) {
+ return new Options(
+ isset($elasticsearchOptions['index']) ? (string) $elasticsearchOptions['index'] : null,
+ isset($elasticsearchOptions['type']) ? (string) $elasticsearchOptions['type'] : null,
+ );
+ }
}
return null;
diff --git a/src/Metadata/Extractor/YamlResourceExtractor.php b/src/Metadata/Extractor/YamlResourceExtractor.php
index da539bea856..476ff6a7cb7 100644
--- a/src/Metadata/Extractor/YamlResourceExtractor.php
+++ b/src/Metadata/Extractor/YamlResourceExtractor.php
@@ -13,12 +13,12 @@
namespace ApiPlatform\Metadata\Extractor;
+use ApiPlatform\Elasticsearch\State\Options;
use ApiPlatform\Metadata\Exception\InvalidArgumentException;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\HeaderParameter;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\QueryParameter;
-use ApiPlatform\Metadata\Tests\Fixtures\StateOptions;
use ApiPlatform\OpenApi\Model\ExternalDocumentation;
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
use ApiPlatform\OpenApi\Model\Parameter;
@@ -414,7 +414,9 @@ private function buildStateOptions(array $resource): ?OptionsInterface
$configuration = reset($stateOptions);
switch (key($stateOptions)) {
case 'elasticsearchOptions':
- return new StateOptions($configuration['index'] ?? null, $configuration['type'] ?? null);
+ if (class_exists(Options::class)) {
+ return new Options($configuration['index'] ?? null, $configuration['type'] ?? null);
+ }
}
return null;
diff --git a/src/Metadata/Resource/Factory/OperationDefaultsTrait.php b/src/Metadata/Resource/Factory/OperationDefaultsTrait.php
index 1abf1b1c6d6..8814c5b9214 100644
--- a/src/Metadata/Resource/Factory/OperationDefaultsTrait.php
+++ b/src/Metadata/Resource/Factory/OperationDefaultsTrait.php
@@ -202,7 +202,7 @@ private function getOperationWithDefaults(ApiResource $resource, Operation $oper
throw new RuntimeException(sprintf('Operation should be an instance of "%s"', HttpOperation::class));
}
- if ($operation->getRouteName()) {
+ if (!$operation->getName() && $operation->getRouteName()) {
/** @var HttpOperation $operation */
$operation = $operation->withName($operation->getRouteName());
}
diff --git a/src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php
index 67db22b49cc..35530b33ba8 100644
--- a/src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php
+++ b/src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php
@@ -75,7 +75,7 @@ public function create(string $resourceClass): ResourceMetadataCollection
$operation = $operation->withName($routeName);
}
- $operations->add($routeName, $operation);
+ $operations->add($operation->getName(), $operation);
continue;
}
diff --git a/src/Metadata/Tests/Extractor/ResourceMetadataCompatibilityTest.php b/src/Metadata/Tests/Extractor/ResourceMetadataCompatibilityTest.php
index 7d79549ade9..bb0ac51dd73 100644
--- a/src/Metadata/Tests/Extractor/ResourceMetadataCompatibilityTest.php
+++ b/src/Metadata/Tests/Extractor/ResourceMetadataCompatibilityTest.php
@@ -37,12 +37,10 @@
use ApiPlatform\Metadata\Tests\Extractor\Adapter\XmlResourceAdapter;
use ApiPlatform\Metadata\Tests\Extractor\Adapter\YamlResourceAdapter;
use ApiPlatform\Metadata\Tests\Fixtures\ApiResource\Comment;
-use ApiPlatform\Metadata\Tests\Fixtures\StateOptions;
use ApiPlatform\Metadata\Util\CamelCaseToSnakeCaseNameConverter;
use ApiPlatform\OpenApi\Model\ExternalDocumentation;
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
use ApiPlatform\OpenApi\Model\RequestBody;
-use ApiPlatform\State\OptionsInterface;
use PHPUnit\Framework\AssertionFailedError;
use PHPUnit\Framework\TestCase;
use Symfony\Component\WebLink\Link;
@@ -723,7 +721,7 @@ private function withGraphQlOperations(array $values, ?array $fixtures): array
return $operations;
}
- private function withStateOptions(array $values): ?OptionsInterface
+ private function withStateOptions(array $values)
{
if (!$values) {
return null;
@@ -736,7 +734,7 @@ private function withStateOptions(array $values): ?OptionsInterface
$configuration = reset($values);
switch (key($values)) {
case 'elasticsearchOptions':
- return new StateOptions($configuration['index'] ?? null, $configuration['type'] ?? null);
+ return null;
}
throw new \LogicException(sprintf('Unsupported "%s" state options.', key($values)));
diff --git a/src/Serializer/AbstractCollectionNormalizer.php b/src/Serializer/AbstractCollectionNormalizer.php
index 96d2eb06c7f..ff1aa7f1a07 100644
--- a/src/Serializer/AbstractCollectionNormalizer.php
+++ b/src/Serializer/AbstractCollectionNormalizer.php
@@ -103,6 +103,10 @@ public function normalize(mixed $object, ?string $format = null, array $context
$paginationData = $this->getPaginationData($object, $collectionContext);
$childContext = $this->createOperationContext($collectionContext, $resourceClass);
+ if (isset($collectionContext['force_resource_class'])) {
+ $childContext['force_resource_class'] = $collectionContext['force_resource_class'];
+ }
+
$itemsData = $this->getItemsData($object, $format, $childContext);
return array_merge_recursive($data, $paginationData, $itemsData);
diff --git a/src/State/ApiResource/Error.php b/src/State/ApiResource/Error.php
index 62970a078f6..834a6f7a70c 100644
--- a/src/State/ApiResource/Error.php
+++ b/src/State/ApiResource/Error.php
@@ -32,6 +32,7 @@
operations: [
new Operation(
name: '_api_errors_problem',
+ routeName: 'api_errors',
outputFormats: ['json' => ['application/problem+json']],
normalizationContext: [
'groups' => ['jsonproblem'],
@@ -41,6 +42,7 @@
),
new Operation(
name: '_api_errors_hydra',
+ routeName: 'api_errors',
outputFormats: ['jsonld' => ['application/problem+json']],
normalizationContext: [
'groups' => ['jsonld'],
@@ -51,6 +53,7 @@
),
new Operation(
name: '_api_errors_jsonapi',
+ routeName: 'api_errors',
outputFormats: ['jsonapi' => ['application/vnd.api+json']],
normalizationContext: [
'groups' => ['jsonapi'],
@@ -58,6 +61,10 @@
'rfc_7807_compliant_errors' => true,
],
),
+ new Operation(
+ name: '_api_errors',
+ routeName: 'api_errors'
+ ),
],
provider: 'api_platform.state.error_provider',
graphQlOperations: []
@@ -119,12 +126,14 @@ public static function createFromException(\Exception|\Throwable $exception, int
}
#[Ignore]
+ #[ApiProperty(readable: false)]
public function getHeaders(): array
{
return $this->headers;
}
#[Ignore]
+ #[ApiProperty(readable: false)]
public function getStatusCode(): int
{
return $this->status;
diff --git a/src/Symfony/Bundle/Resources/config/routing/api.xml b/src/Symfony/Bundle/Resources/config/routing/api.xml
index 7c9cbdd7f26..e4524f69a2c 100644
--- a/src/Symfony/Bundle/Resources/config/routing/api.xml
+++ b/src/Symfony/Bundle/Resources/config/routing/api.xml
@@ -14,4 +14,15 @@
index
+
+ api_platform.action.not_exposed
+ 500
+
+ \d+
+
+
+
+ api_platform.action.not_exposed
+
+
diff --git a/src/Symfony/Routing/IriConverter.php b/src/Symfony/Routing/IriConverter.php
index 266774c2bff..9de95f35f87 100644
--- a/src/Symfony/Routing/IriConverter.php
+++ b/src/Symfony/Routing/IriConverter.php
@@ -186,7 +186,9 @@ private function generateSymfonyRoute(object|string $resource, int $referenceTyp
}
try {
- return $this->router->generate($operation->getName(), $identifiers, $operation->getUrlGenerationStrategy() ?? $referenceType);
+ $routeName = $operation instanceof HttpOperation ? ($operation->getRouteName() ?? $operation->getName()) : $operation->getName();
+
+ return $this->router->generate($routeName, $identifiers, $operation->getUrlGenerationStrategy() ?? $referenceType);
} catch (RoutingExceptionInterface $e) {
throw new InvalidArgumentException(sprintf('Unable to generate an IRI for the item of type "%s"', $operation->getClass()), $e->getCode(), $e);
}
diff --git a/src/Symfony/Validator/Exception/ValidationException.php b/src/Symfony/Validator/Exception/ValidationException.php
index 3e4d4a3aeba..41faa0d1ac3 100644
--- a/src/Symfony/Validator/Exception/ValidationException.php
+++ b/src/Symfony/Validator/Exception/ValidationException.php
@@ -40,6 +40,7 @@
operations: [
new ErrorOperation(
name: '_api_validation_errors_problem',
+ routeName: 'api_validation_errors',
outputFormats: ['json' => ['application/problem+json']],
normalizationContext: ['groups' => ['json'],
'skip_null_values' => true,
@@ -48,6 +49,7 @@
),
new ErrorOperation(
name: '_api_validation_errors_hydra',
+ routeName: 'api_validation_errors',
outputFormats: ['jsonld' => ['application/problem+json']],
links: [new Link(rel: ContextBuilderInterface::JSONLD_NS.'error', href: 'http://www.w3.org/ns/hydra/error')],
normalizationContext: [
@@ -58,9 +60,14 @@
),
new ErrorOperation(
name: '_api_validation_errors_jsonapi',
+ routeName: 'api_validation_errors',
outputFormats: ['jsonapi' => ['application/vnd.api+json']],
normalizationContext: ['groups' => ['jsonapi'], 'skip_null_values' => true, 'rfc_7807_compliant_errors' => true]
),
+ new ErrorOperation(
+ name: '_api_validation_errors',
+ routeName: 'api_validation_errors'
+ ),
],
graphQlOperations: []
)]
diff --git a/tests/Behat/DoctrineContext.php b/tests/Behat/DoctrineContext.php
index d8dbd441a4c..d27b1b219e1 100644
--- a/tests/Behat/DoctrineContext.php
+++ b/tests/Behat/DoctrineContext.php
@@ -158,6 +158,7 @@
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue5722\Event;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue5722\ItemLog;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue5735\Group;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue6039\Issue6039EntityUser;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\LinkHandledDummy;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MaxDepthDummy;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MultiRelationsDummy;
@@ -2296,6 +2297,22 @@ public function thereIsADummyEntityWithAMappedSuperclass(): void
$this->manager->flush();
}
+ /**
+ * @Given there are issue6039 users
+ */
+ public function thereAreIssue6039Users(): void
+ {
+ $entity = new Issue6039EntityUser();
+ $entity->name = 'test';
+ $entity->bar = 'test';
+ $this->manager->persist($entity);
+ $entity = new Issue6039EntityUser();
+ $entity->name = 'test2';
+ $entity->bar = 'test';
+ $this->manager->persist($entity);
+ $this->manager->flush();
+ }
+
private function isOrm(): bool
{
return null !== $this->schemaTool;
diff --git a/tests/Fixtures/TestBundle/ApiResource/Issue5648/DummyResource.php b/tests/Fixtures/TestBundle/ApiResource/Issue5648/DummyResource.php
index 21674757fc3..62b86ff4684 100644
--- a/tests/Fixtures/TestBundle/ApiResource/Issue5648/DummyResource.php
+++ b/tests/Fixtures/TestBundle/ApiResource/Issue5648/DummyResource.php
@@ -21,10 +21,11 @@
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Link;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy;
#[ApiResource(
operations: [
- new GetCollection(uriTemplate: '/dummy_resource_with_custom_filter', itemUriTemplate: '/dummy_resource_with_custom_filter/{id}'),
+ new GetCollection(uriTemplate: '/dummy_resource_with_custom_filter{._format}', itemUriTemplate: '/dummy_resource_with_custom_filter/{id}'),
new Get(uriTemplate: '/dummy_resource_with_custom_filter/{id}', uriVariables: ['id' => new Link(fromClass: Dummy::class)]),
],
stateOptions: new Options(entityClass: Dummy::class)
@@ -37,5 +38,8 @@ class DummyResource
public string $name;
+ /**
+ * @var RelatedDummy[]
+ */
public array $relatedDummies;
}
diff --git a/tests/Fixtures/TestBundle/ApiResource/Issue6039/UserApi.php b/tests/Fixtures/TestBundle/ApiResource/Issue6039/UserApi.php
new file mode 100644
index 00000000000..42174b7f3f0
--- /dev/null
+++ b/tests/Fixtures/TestBundle/ApiResource/Issue6039/UserApi.php
@@ -0,0 +1,25 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue6039;
+
+use ApiPlatform\Doctrine\Orm\State\Options;
+use ApiPlatform\Metadata\GetCollection;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue6039\Issue6039EntityUser;
+
+#[GetCollection(shortName: 'Issue6039UserApi', stateOptions: new Options(entityClass: Issue6039EntityUser::class))]
+class UserApi
+{
+ public string $id;
+ public string $name;
+}
diff --git a/tests/Fixtures/TestBundle/Entity/Issue6039/Issue6039EntityUser.php b/tests/Fixtures/TestBundle/Entity/Issue6039/Issue6039EntityUser.php
new file mode 100644
index 00000000000..0b00bfa5064
--- /dev/null
+++ b/tests/Fixtures/TestBundle/Entity/Issue6039/Issue6039EntityUser.php
@@ -0,0 +1,31 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue6039;
+
+use Doctrine\ORM\Mapping as ORM;
+
+#[ORM\Entity]
+class Issue6039EntityUser
+{
+ #[ORM\Column(type: 'integer')]
+ #[ORM\Id]
+ #[ORM\GeneratedValue(strategy: 'AUTO')]
+ public ?int $id = null;
+
+ #[ORM\Column]
+ public string $name;
+
+ #[ORM\Column]
+ public string $bar;
+}