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
4 changes: 3 additions & 1 deletion .github/workflows/guides.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
6 changes: 3 additions & 3 deletions docs/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -43,6 +44,5 @@
},
"require-dev": {
"phpunit/phpunit": "^10"
},
"minimum-stability": "dev"
}
}
1 change: 0 additions & 1 deletion docs/config/packages/framework.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
12 changes: 12 additions & 0 deletions features/doctrine/issue6039/entity_class_option.feature
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion src/Doctrine/Odm/Filter/AbstractFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
Expand Down
2 changes: 1 addition & 1 deletion src/Doctrine/Orm/Filter/AbstractFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
Expand Down
6 changes: 6 additions & 0 deletions src/Hydra/Serializer/DocumentationNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
12 changes: 7 additions & 5 deletions src/Metadata/Extractor/XmlResourceExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
6 changes: 4 additions & 2 deletions src/Metadata/Extractor/YamlResourceExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/Metadata/Resource/Factory/OperationDefaultsTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public function create(string $resourceClass): ResourceMetadataCollection
$operation = $operation->withName($routeName);
}

$operations->add($routeName, $operation);
$operations->add($operation->getName(), $operation);
continue;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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)));
Expand Down
4 changes: 4 additions & 0 deletions src/Serializer/AbstractCollectionNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
9 changes: 9 additions & 0 deletions src/State/ApiResource/Error.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
operations: [
new Operation(
name: '_api_errors_problem',
routeName: 'api_errors',
outputFormats: ['json' => ['application/problem+json']],
normalizationContext: [
'groups' => ['jsonproblem'],
Expand All @@ -41,6 +42,7 @@
),
new Operation(
name: '_api_errors_hydra',
routeName: 'api_errors',
outputFormats: ['jsonld' => ['application/problem+json']],
normalizationContext: [
'groups' => ['jsonld'],
Expand All @@ -51,13 +53,18 @@
),
new Operation(
name: '_api_errors_jsonapi',
routeName: 'api_errors',
outputFormats: ['jsonapi' => ['application/vnd.api+json']],
normalizationContext: [
'groups' => ['jsonapi'],
'skip_null_values' => true,
'rfc_7807_compliant_errors' => true,
],
),
new Operation(
name: '_api_errors',
routeName: 'api_errors'
),
],
provider: 'api_platform.state.error_provider',
graphQlOperations: []
Expand Down Expand Up @@ -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;
Expand Down
11 changes: 11 additions & 0 deletions src/Symfony/Bundle/Resources/config/routing/api.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,15 @@
<requirement key="index">index</requirement>
</route>

<route id="api_errors" path="/errors/{status}">
<default key="_controller">api_platform.action.not_exposed</default>
<default key="status">500</default>

<requirement key="status">\d+</requirement>
</route>

<route id="api_validation_errors" path="/validation_errors/{id}">
<default key="_controller">api_platform.action.not_exposed</default>
</route>

</routes>
4 changes: 3 additions & 1 deletion src/Symfony/Routing/IriConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
7 changes: 7 additions & 0 deletions src/Symfony/Validator/Exception/ValidationException.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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: [
Expand All @@ -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: []
)]
Expand Down
17 changes: 17 additions & 0 deletions tests/Behat/DoctrineContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -37,5 +38,8 @@ class DummyResource

public string $name;

/**
* @var RelatedDummy[]
*/
public array $relatedDummies;
}
25 changes: 25 additions & 0 deletions tests/Fixtures/TestBundle/ApiResource/Issue6039/UserApi.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* 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;
}
Loading