diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index c8458c0b78e..301d1eca0f9 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -18,6 +18,7 @@ 'tests/Fixtures/app/var', 'docs/guides', 'docs/var', + '**vendor**' ]) ->notPath('src/Symfony/Bundle/DependencyInjection/Configuration.php') ->notPath('src/Annotation/ApiFilter.php') // temporary diff --git a/CHANGELOG.md b/CHANGELOG.md index f0a9f55f9a4..d895178dbb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ # Changelog +## v3.2.2 + +### Bug fixes + +* [3d0dfc148](https://github.com/api-platform/core/commit/3d0dfc148ec864364d1c36dfaa2690e1fc58dfc5) fix(symfony): swagger ui should use base url (#5918) +* [4f51b5198](https://github.com/api-platform/core/commit/4f51b519853cf972070db79a8b82c824afa000fc) fix(symfony): use http exception headers (#5932) +* [547c4e605](https://github.com/api-platform/core/commit/547c4e605c60c54642abc06c37462f5e47fbe25d) fix(graphql): item resolver inheritance error (#5910) +* [6b5df95ca](https://github.com/api-platform/core/commit/6b5df95caf2e3c6f807f2083ea3526fcd2ae473a) fix(doctrine): odm order filter should use a left join on nullable fields (#5911) +* [ae090c7c4](https://github.com/api-platform/core/commit/ae090c7c4ec9619655ae95534b87a07aa7b2b061) fix(graphql): use normalization context to get item from IRI (#5915) +* [b2d9ce40c](https://github.com/api-platform/core/commit/b2d9ce40cf27ee9743aafff4f163e195ae47b880) fix(serializer): pass $context to IriConverter (#5908) +* [c2824c1d7](https://github.com/api-platform/core/commit/c2824c1d72f04a0d05b902b08a475a95db18e69f) fix(jsonschema): restore type factory usage (#5897) +* [cd6f5834b](https://github.com/api-platform/core/commit/cd6f5834b7458798054fb4c7b3ea94f193246405) fix(serializer): use error normalizers (#5931) +* [d9f77402d](https://github.com/api-platform/core/commit/d9f77402d55c40a867edf8fa15cee67c2801574f) fix(graphql): service missing in debug mode (#5930) + +Note: + +`extra_properties.skip_deprecated_exception_normalizers` is set to `false` so that decorating Error normalizers works. Set it to `true` to avoid deprecations and decorate the corresponding `ItemNormalizer` instead. + +## v3.2.1 + +### Bug fixes + +* [05363d98f](https://github.com/api-platform/core/commit/05363d98f54babff49119a1fb55a17bb1550f21a) fix(symfony): force json format with GraphQL +* [0c50d4ceb](https://github.com/api-platform/core/commit/0c50d4ceba9d83a2212771f21e2d1de4442c1456) fix(state): add link header processor without links (#5888) +* [51b818304](https://github.com/api-platform/core/commit/51b818304874ec60ebab914455adc8f50402ca9d) fix: error traces without arguments (#5891) +* [b7c094aca](https://github.com/api-platform/core/commit/b7c094acae3ac3271f42443ea2f62f22d019bea6) fix(metadata): interface breaking in 3.2 (#5883) +* [dbd4f64de](https://github.com/api-platform/core/commit/dbd4f64debab876ab556ec87c8c973f0c38ada10) fix(graphql): docs should answer text/html + ## v3.2.0 ### Bug fixes @@ -147,6 +175,19 @@ Notes: * [92a81f024](https://github.com/api-platform/core/commit/92a81f024541054b9322e7457b75c721261e14e0) feat(graphql): allow to disable the introspection query (#5711) * [d793ffb92](https://github.com/api-platform/core/commit/d793ffb9228a21655ee35f0b90a959f93281a4cf) feat: union/intersect types (#5470) +## v3.1.22 + +### Bug fixes + +* [157faafd5](https://github.com/api-platform/core/commit/157faafd54db75214b39fc8c7c6a97a171513c67) fix(state): wrong variable name +* [b2d9ce40c](https://github.com/api-platform/core/commit/b2d9ce40cf27ee9743aafff4f163e195ae47b880) fix(serializer): pass $context to IriConverter (#5908) + +## v3.1.21 + +### Bug fixes + +* [364732d83](https://github.com/api-platform/core/commit/364732d838f2fba05887fd24c75c4fb302c7af04) fix(serializer): missing parenthesis fixes #5773 + ## v3.1.20 ### Bug fixes @@ -158,7 +199,7 @@ Notes: ### Bug fixes * [6a62a53f8](https://github.com/api-platform/core/commit/6a62a53f854ec93947d1c4a5a32007df09e55d06) fix(hydra): add xxx[] hydra:search iexact -* [7f0e00cd2](https://github.com/api-platform/core/commit/7f0e00cd2d838037f716e0b8588a6529ef9f158c) fix(mercure): custom topics on newly created entities causes error #5074 +* [7f0e00cd2](https://github.com/api-platform/core/commit/7f0e00cd2d838037f716e0b8588a6529ef9f158c) fix(mercure): custom topics on newly created entities causes error #5074 * [1fccb8413](https://github.com/api-platform/core/commit/1fccb8413a902a1011f049d0f8ddcd8d5456d335) fix(doctrine): add SearchFilter case-insensitive strategies constants ## v3.1.18 @@ -543,7 +584,7 @@ Breaking changes: * Metadata: `Patch` is added to the automatic CRUD * Symfony: generated route names and operation names changed, route naming can be changed directly within metadata * Doctrine: remove `@final` annotation from filters and mark them as `final` - + ## v2.7.14 ### Bug fixes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a84e92f2950..ae32f9a5bbc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -118,6 +118,8 @@ We strongly recommend the use of a scope on API Platform core. On `api-platform/core` there are two kinds of tests: unit (`phpunit` through `simple-phpunit`) and integration tests (`behat`). +Note that we stopped using `prophesize` for new tests since 3.2, use `phpunit` stub system. + Both `simple-phpunit` and `behat` are development dependencies and should be available in the `vendor` directory. #### PHPUnit and Coverage Generation diff --git a/features/graphql/docs.feature b/features/graphql/docs.feature new file mode 100644 index 00000000000..7c54a7343f0 --- /dev/null +++ b/features/graphql/docs.feature @@ -0,0 +1,10 @@ +Feature: Documentation support + In order to play with GraphQL + As a client software developer + I want to reach the GraphQL documentation + + Scenario: Retrieve the OpenAPI documentation + Given I add "Accept" header equal to "text/html" + And I send a "GET" request to "/graphql" + Then the response status code should be 200 + And the header "Content-Type" should be equal to "text/html; charset=utf-8" diff --git a/features/main/exception_to_status.feature b/features/main/exception_to_status.feature index d95a6112d4e..a182ea848a8 100644 --- a/features/main/exception_to_status.feature +++ b/features/main/exception_to_status.feature @@ -38,3 +38,10 @@ Feature: Using exception_to_status config And I send a "DELETE" request to "/error_with_overriden_status/1" Then the response status code should be 403 And the JSON node "status" should be equal to 403 + + @!mongodb + Scenario: Get HTTP Exception headers + When I add "Accept" header equal to "application/ld+json" + And I send a "GET" request to "/issue5924" + Then the response status code should be 429 + Then the header "retry-after" should be equal to 32 diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 714dee130ed..d6dee67d939 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -26,6 +26,7 @@ parameters: - src/Symfony/Bundle/DependencyInjection/Configuration.php # Templates for Maker - src/Symfony/Maker/Resources/skeleton + - **vendor** earlyTerminatingMethodCalls: PHPUnit\Framework\Constraint\Constraint: - fail diff --git a/src/Api/FilterInterface.php b/src/Api/FilterInterface.php index 01b9ec589d9..967147ce336 100644 --- a/src/Api/FilterInterface.php +++ b/src/Api/FilterInterface.php @@ -13,6 +13,44 @@ namespace ApiPlatform\Api; -interface FilterInterface extends \ApiPlatform\Metadata\FilterInterface +/** + * Filters applicable on a resource. + * + * @author Kévin Dunglas + */ +interface FilterInterface { + /** + * Gets the description of this filter for the given resource. + * + * Returns an array with the filter parameter names as keys and array with the following data as values: + * - property: the property where the filter is applied + * - type: the type of the filter + * - required: if this filter is required + * - strategy (optional): the used strategy + * - is_collection (optional): if this filter is for collection + * - swagger (optional): additional parameters for the path operation, + * e.g. 'swagger' => [ + * 'description' => 'My Description', + * 'name' => 'My Name', + * 'type' => 'integer', + * ] + * - openapi (optional): additional parameters for the path operation in the version 3 spec, + * e.g. 'openapi' => [ + * 'description' => 'My Description', + * 'name' => 'My Name', + * 'schema' => [ + * 'type' => 'integer', + * ] + * ] + * - schema (optional): schema definition, + * e.g. 'schema' => [ + * 'type' => 'string', + * 'enum' => ['value_1', 'value_2'], + * ] + * The description can contain additional data specific to a filter. + * + * @see \ApiPlatform\OpenApi\Factory\OpenApiFactory::getFiltersParameters + */ + public function getDescription(string $resourceClass): array; } diff --git a/src/Api/FilterLocatorTrait.php b/src/Api/FilterLocatorTrait.php index 5119390bf3c..cb9f5edbb0e 100644 --- a/src/Api/FilterLocatorTrait.php +++ b/src/Api/FilterLocatorTrait.php @@ -14,7 +14,7 @@ namespace ApiPlatform\Api; use ApiPlatform\Exception\InvalidArgumentException; -use ApiPlatform\Metadata\FilterInterface; +use ApiPlatform\Metadata\FilterInterface as MetadataFilterInterface; use Psr\Container\ContainerInterface; /** @@ -45,7 +45,7 @@ private function setFilterLocator(?ContainerInterface $filterLocator, bool $allo /** * Gets a filter with a backward compatibility. */ - private function getFilter(string $filterId): ?FilterInterface + private function getFilter(string $filterId): null|FilterInterface|MetadataFilterInterface { if ($this->filterLocator && $this->filterLocator->has($filterId)) { return $this->filterLocator->get($filterId); diff --git a/src/Api/IdentifiersExtractorInterface.php b/src/Api/IdentifiersExtractorInterface.php index ff0f428c0a3..d0e7566dc45 100644 --- a/src/Api/IdentifiersExtractorInterface.php +++ b/src/Api/IdentifiersExtractorInterface.php @@ -13,15 +13,20 @@ namespace ApiPlatform\Api; -class_exists(\ApiPlatform\Metadata\IdentifiersExtractorInterface::class); +use ApiPlatform\Metadata\Exception\RuntimeException; +use ApiPlatform\Metadata\Operation; -class_alias( - \ApiPlatform\Metadata\IdentifiersExtractorInterface::class, - __NAMESPACE__.'\IdentifiersExtractorInterface' -); - -if (false) { // @phpstan-ignore-line - interface IdentifiersExtractorInterface extends \ApiPlatform\Metadata\IdentifiersExtractorInterface - { - } +/** + * Extracts identifiers for a given Resource according to the retrieved Metadata. + * + * @author Antoine Bluchet + */ +interface IdentifiersExtractorInterface +{ + /** + * Finds identifiers from an Item (object). + * + * @throws RuntimeException + */ + public function getIdentifiersFromItem(object $item, Operation $operation = null, array $context = []): array; } diff --git a/src/Api/IriConverterInterface.php b/src/Api/IriConverterInterface.php index e1f39780324..8a319f370cb 100644 --- a/src/Api/IriConverterInterface.php +++ b/src/Api/IriConverterInterface.php @@ -13,15 +13,33 @@ namespace ApiPlatform\Api; -class_exists(\ApiPlatform\Metadata\IriConverterInterface::class); +use ApiPlatform\Metadata\Exception\InvalidArgumentException; +use ApiPlatform\Metadata\Exception\ItemNotFoundException; +use ApiPlatform\Metadata\Exception\RuntimeException; +use ApiPlatform\Metadata\Operation; -class_alias( - \ApiPlatform\Metadata\IriConverterInterface::class, - __NAMESPACE__.'\IriConverterInterface' -); +/** + * Converts item and resources to IRI and vice versa. + * + * @author Kévin Dunglas + */ +interface IriConverterInterface +{ + /** + * Retrieves an item from its IRI. + * + * @throws InvalidArgumentException + * @throws ItemNotFoundException + */ + public function getResourceFromIri(string $iri, array $context = [], Operation $operation = null): object; -if (false) { // @phpstan-ignore-line - interface IriConverterInterface extends \ApiPlatform\Metadata\IriConverterInterface - { - } + /** + * Gets the IRI associated with the given item. + * + * @param object|class-string $resource + * + * @throws InvalidArgumentException + * @throws RuntimeException + */ + public function getIriFromResource(object|string $resource, int $referenceType = UrlGeneratorInterface::ABS_PATH, Operation $operation = null, array $context = []): ?string; } diff --git a/src/Api/ResourceClassResolverInterface.php b/src/Api/ResourceClassResolverInterface.php index 0dd636478b9..683b6618368 100644 --- a/src/Api/ResourceClassResolverInterface.php +++ b/src/Api/ResourceClassResolverInterface.php @@ -13,10 +13,27 @@ namespace ApiPlatform\Api; -class_exists(\ApiPlatform\Metadata\ResourceClassResolverInterface::class); +use ApiPlatform\Metadata\Exception\InvalidArgumentException; -if (false) { // @phpstan-ignore-line - interface ResourceClassResolverInterface extends \ApiPlatform\Metadata\ResourceClassResolverInterface - { - } +/** + * Guesses which resource is associated with a given object. + * + * @author Kévin Dunglas + */ +interface ResourceClassResolverInterface +{ + /** + * Guesses the associated resource. + * + * @param string $resourceClass The expected resource class + * @param bool $strict If true, value must match the expected resource class + * + * @throws InvalidArgumentException + */ + public function getResourceClass(mixed $value, string $resourceClass = null, bool $strict = false): string; + + /** + * Is the given class a resource class? + */ + public function isResourceClass(string $type): bool; } diff --git a/src/Api/UriVariableTransformerInterface.php b/src/Api/UriVariableTransformerInterface.php index 544e5c1c989..c3a0013244e 100644 --- a/src/Api/UriVariableTransformerInterface.php +++ b/src/Api/UriVariableTransformerInterface.php @@ -13,6 +13,27 @@ namespace ApiPlatform\Api; -interface UriVariableTransformerInterface extends \ApiPlatform\Metadata\UriVariableTransformerInterface +use ApiPlatform\Exception\InvalidUriVariableException; + +interface UriVariableTransformerInterface { + /** + * Transforms the value of a URI variable (identifier) to its type. + * + * @param mixed $value The URI variable value to transform + * @param array $types The guessed type behind the URI variable + * @param array $context Options available to the transformer + * + * @throws InvalidUriVariableException Occurs when the URI variable could not be transformed + */ + public function transform(mixed $value, array $types, array $context = []); + + /** + * Checks whether the value of a URI variable can be transformed to its type by this transformer. + * + * @param mixed $value The URI variable value to transform + * @param array $types The types to which the URI variable value should be transformed + * @param array $context Options available to the transformer + */ + public function supportsTransformation(mixed $value, array $types, array $context = []): bool; } diff --git a/src/Api/UriVariablesConverterInterface.php b/src/Api/UriVariablesConverterInterface.php index 45dd27a07ed..67c49092221 100644 --- a/src/Api/UriVariablesConverterInterface.php +++ b/src/Api/UriVariablesConverterInterface.php @@ -13,15 +13,24 @@ namespace ApiPlatform\Api; -class_exists(\ApiPlatform\Metadata\UriVariablesConverterInterface::class); +use ApiPlatform\Metadata\Exception\InvalidIdentifierException; -class_alias( - \ApiPlatform\Metadata\UriVariablesConverterInterface::class, - __NAMESPACE__.'\UriVariablesConverterInterface' -); - -if (false) { // @phpstan-ignore-line - interface UriVariablesConverterInterface extends \ApiPlatform\Metadata\UriVariablesConverterInterface - { - } +/** + * Identifier converter. + * + * @author Antoine Bluchet + */ +interface UriVariablesConverterInterface +{ + /** + * Takes an array of strings representing URI variables (identifiers) and transform their values to the expected type. + * + * @param array $data URI variables to convert to PHP values + * @param string $class The class to which the URI variables belong to + * + * @throws InvalidIdentifierException + * + * @return array Array indexed by identifiers properties with their values denormalized + */ + public function convert(array $data, string $class, array $context = []): array; } diff --git a/src/Api/UrlGeneratorInterface.php b/src/Api/UrlGeneratorInterface.php index 5b199a684c1..b6ee97cf0cd 100644 --- a/src/Api/UrlGeneratorInterface.php +++ b/src/Api/UrlGeneratorInterface.php @@ -13,13 +13,72 @@ namespace ApiPlatform\Api; -class_alias( - \ApiPlatform\Metadata\UrlGeneratorInterface::class, - __NAMESPACE__.'\UrlGeneratorInterface' -); - -if (false) { // @phpstan-ignore-line - interface UrlGeneratorInterface extends \ApiPlatform\Metadata\UrlGeneratorInterface - { - } +use Symfony\Component\Routing\Exception\InvalidParameterException; +use Symfony\Component\Routing\Exception\MissingMandatoryParametersException; +use Symfony\Component\Routing\Exception\RouteNotFoundException; + +/** + * UrlGeneratorInterface is the interface that all URL generator classes must implement. + * + * This interface has been imported and adapted from the Symfony project. + * + * The constants in this interface define the different types of resource references that + * are declared in RFC 3986: http://tools.ietf.org/html/rfc3986 + * We are using the term "URL" instead of "URI" as this is more common in web applications + * and we do not need to distinguish them as the difference is mostly semantical and + * less technical. Generating URIs, i.e. representation-independent resource identifiers, + * is also possible. + * + * @author Fabien Potencier + * @author Tobias Schultze + * @copyright Fabien Potencier + */ +interface UrlGeneratorInterface +{ + /** + * Generates an absolute URL, e.g. "http://example.com/dir/file". + */ + public const ABS_URL = 0; + + /** + * Generates an absolute path, e.g. "/dir/file". + */ + public const ABS_PATH = 1; + + /** + * Generates a relative path based on the current request path, e.g. "../parent-file". + * + * @see UrlGenerator::getRelativePath() + */ + public const REL_PATH = 2; + + /** + * Generates a network path, e.g. "//example.com/dir/file". + * Such reference reuses the current scheme but specifies the host. + */ + public const NET_PATH = 3; + + /** + * Generates a URL or path for a specific route based on the given parameters. + * + * Parameters that reference placeholders in the route pattern will substitute them in the + * path or host. Extra params are added as query string to the URL. + * + * When the passed reference type cannot be generated for the route because it requires a different + * host or scheme than the current one, the method will return a more comprehensive reference + * that includes the required params. For example, when you call this method with $referenceType = ABSOLUTE_PATH + * but the route requires the https scheme whereas the current scheme is http, it will instead return an + * ABSOLUTE_URL with the https scheme and the current host. This makes sure the generated URL matches + * the route in any case. + * + * If there is no route with the given name, the generator must throw the RouteNotFoundException. + * + * The special parameter _fragment will be used as the document fragment suffixed to the final URL. + * + * @throws RouteNotFoundException If the named route doesn't exist + * @throws MissingMandatoryParametersException When some parameters are missing that are mandatory for the route + * @throws InvalidParameterException When a parameter value for a placeholder is not correct because + * it does not match the requirement + */ + public function generate(string $name, array $parameters = [], int $referenceType = self::ABS_PATH): string; } diff --git a/src/ApiResource/Error.php b/src/ApiResource/Error.php index 65699de1752..a859049d403 100644 --- a/src/ApiResource/Error.php +++ b/src/ApiResource/Error.php @@ -61,14 +61,28 @@ public function __construct( private readonly string $title, private readonly string $detail, #[ApiProperty(identifier: true)] private int $status, - private readonly array $originalTrace, + array $originalTrace = null, private ?string $instance = null, private string $type = 'about:blank', private array $headers = [] ) { parent::__construct(); + + if (!$originalTrace) { + return; + } + + $this->originalTrace = []; + foreach ($originalTrace as $i => $t) { + unset($t['args']); // we don't want arguments in our JSON traces, especially with xdebug + $this->originalTrace[$i] = $t; + } } + #[SerializedName('trace')] + #[Groups(['trace'])] + public ?array $originalTrace = null; + #[SerializedName('hydra:title')] #[Groups(['jsonld', 'legacy_jsonld'])] public function getHydraTitle(): string @@ -76,13 +90,6 @@ public function getHydraTitle(): string return $this->title; } - #[SerializedName('trace')] - #[Groups(['trace'])] - public function getOriginalTrace(): array - { - return $this->originalTrace; - } - #[SerializedName('hydra:description')] #[Groups(['jsonld', 'legacy_jsonld'])] public function getHydraDescription(): string diff --git a/src/Doctrine/EventListener/PurgeHttpCacheListener.php b/src/Doctrine/EventListener/PurgeHttpCacheListener.php index a8f07a20c76..1c1ef5718c3 100644 --- a/src/Doctrine/EventListener/PurgeHttpCacheListener.php +++ b/src/Doctrine/EventListener/PurgeHttpCacheListener.php @@ -126,6 +126,11 @@ private function gatherRelationTags(EntityManagerInterface $em, object $entity): { $associationMappings = $em->getClassMetadata(ClassUtils::getClass($entity))->getAssociationMappings(); foreach (array_keys($associationMappings) as $property) { + if ( + \array_key_exists('targetEntity', $associationMappings[$property]) + && !$this->resourceClassResolver->isResourceClass($associationMappings[$property]['targetEntity'])) { + return; + } if ($this->propertyAccessor->isReadable($entity, $property)) { $this->addTagsFor($this->propertyAccessor->getValue($entity, $property)); } diff --git a/src/Doctrine/Odm/Extension/OrderExtension.php b/src/Doctrine/Odm/Extension/OrderExtension.php index 77cd255d69b..884149c975f 100644 --- a/src/Doctrine/Odm/Extension/OrderExtension.php +++ b/src/Doctrine/Odm/Extension/OrderExtension.php @@ -64,7 +64,7 @@ public function applyToCollection(Builder $aggregationBuilder, string $resourceC } if ($this->isPropertyNested($field, $resourceClass)) { - [$field] = $this->addLookupsForNestedProperty($field, $aggregationBuilder, $resourceClass); + [$field] = $this->addLookupsForNestedProperty($field, $aggregationBuilder, $resourceClass, true); } $aggregationBuilder->sort( $context['mongodb_odm_sort_fields'] = ($context['mongodb_odm_sort_fields'] ?? []) + [$field => $order] diff --git a/src/Doctrine/Odm/Filter/OrderFilter.php b/src/Doctrine/Odm/Filter/OrderFilter.php index f34dcb25b95..3058fbcccd4 100644 --- a/src/Doctrine/Odm/Filter/OrderFilter.php +++ b/src/Doctrine/Odm/Filter/OrderFilter.php @@ -253,7 +253,7 @@ protected function filterProperty(string $property, $direction, Builder $aggrega $matchField = $property; if ($this->isPropertyNested($property, $resourceClass)) { - [$matchField] = $this->addLookupsForNestedProperty($property, $aggregationBuilder, $resourceClass); + [$matchField] = $this->addLookupsForNestedProperty($property, $aggregationBuilder, $resourceClass, true); } $aggregationBuilder->sort( diff --git a/src/Doctrine/Odm/Filter/SearchFilter.php b/src/Doctrine/Odm/Filter/SearchFilter.php index 6193d776d9c..7b0a680307f 100644 --- a/src/Doctrine/Odm/Filter/SearchFilter.php +++ b/src/Doctrine/Odm/Filter/SearchFilter.php @@ -149,7 +149,7 @@ public function __construct(ManagerRegistry $managerRegistry, IriConverterInterf $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); } - protected function getIriConverter(): IriConverterInterface + protected function getIriConverter(): LegacyIriConverterInterface|IriConverterInterface { return $this->iriConverter; } diff --git a/src/Doctrine/Odm/PropertyHelperTrait.php b/src/Doctrine/Odm/PropertyHelperTrait.php index c288ccf60db..449168b281b 100644 --- a/src/Doctrine/Odm/PropertyHelperTrait.php +++ b/src/Doctrine/Odm/PropertyHelperTrait.php @@ -60,7 +60,7 @@ protected function getClassMetadata(string $resourceClass): ClassMetadata * the second element is the $field name * the third element is the $associations array */ - protected function addLookupsForNestedProperty(string $property, Builder $aggregationBuilder, string $resourceClass): array + protected function addLookupsForNestedProperty(string $property, Builder $aggregationBuilder, string $resourceClass, bool $preserveNullAndEmptyArrays = false): array { $propertyParts = $this->splitPropertyParts($property, $resourceClass); $alias = ''; @@ -98,7 +98,8 @@ protected function addLookupsForNestedProperty(string $property, Builder $aggreg ->localField($isOwningSide ? $localField : '_id') ->foreignField($isOwningSide ? '_id' : $referenceMapping['mappedBy']) ->alias($alias); - $aggregationBuilder->unwind("\$$alias"); + $aggregationBuilder->unwind("\$$alias") + ->preserveNullAndEmptyArrays($preserveNullAndEmptyArrays); // association.property => association_lkup.property $property = substr_replace($property, $propertyAlias, strpos($property, (string) $association), \strlen((string) $association)); diff --git a/src/Doctrine/Orm/Filter/SearchFilter.php b/src/Doctrine/Orm/Filter/SearchFilter.php index 7094a823f12..3f7480e9fb2 100644 --- a/src/Doctrine/Orm/Filter/SearchFilter.php +++ b/src/Doctrine/Orm/Filter/SearchFilter.php @@ -148,7 +148,7 @@ public function __construct(ManagerRegistry $managerRegistry, IriConverterInterf $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); } - protected function getIriConverter(): IriConverterInterface + protected function getIriConverter(): IriConverterInterface|LegacyIriConverterInterface { return $this->iriConverter; } diff --git a/src/GraphQl/Action/GraphQlPlaygroundAction.php b/src/GraphQl/Action/GraphQlPlaygroundAction.php index ff00e970160..cc03e4a4a06 100644 --- a/src/GraphQl/Action/GraphQlPlaygroundAction.php +++ b/src/GraphQl/Action/GraphQlPlaygroundAction.php @@ -37,7 +37,7 @@ public function __invoke(Request $request): Response 'title' => $this->title, 'graphql_playground_data' => ['entrypoint' => $this->router->generate('api_graphql_entrypoint')], 'assetPackage' => $this->assetPackage, - ])); + ]), 200, ['content-type' => 'text/html']); } throw new BadRequestHttpException('GraphQL Playground is not enabled.'); diff --git a/src/GraphQl/Action/GraphiQlAction.php b/src/GraphQl/Action/GraphiQlAction.php index cd8dc288123..dc6c0119483 100644 --- a/src/GraphQl/Action/GraphiQlAction.php +++ b/src/GraphQl/Action/GraphiQlAction.php @@ -37,7 +37,7 @@ public function __invoke(Request $request): Response 'title' => $this->title, 'graphiql_data' => ['entrypoint' => $this->router->generate('api_graphql_entrypoint')], 'assetPackage' => $this->assetPackage, - ])); + ]), 200, ['content-type' => 'text/html']); } throw new BadRequestHttpException('GraphiQL is not enabled.'); diff --git a/src/GraphQl/Resolver/Factory/ItemResolverFactory.php b/src/GraphQl/Resolver/Factory/ItemResolverFactory.php index a08f909bf31..28169adb6ff 100644 --- a/src/GraphQl/Resolver/Factory/ItemResolverFactory.php +++ b/src/GraphQl/Resolver/Factory/ItemResolverFactory.php @@ -106,7 +106,7 @@ private function getResourceClass(?object $item, ?string $resourceClass, string return $itemClass; } - if ($resourceClass !== $itemClass) { + if ($resourceClass !== $itemClass && !$item instanceof $resourceClass) { throw new \UnexpectedValueException(sprintf($errorMessage, (new \ReflectionClass($resourceClass))->getShortName(), (new \ReflectionClass($itemClass))->getShortName())); } diff --git a/src/GraphQl/Tests/Fixtures/ApiResource/ChildFoo.php b/src/GraphQl/Tests/Fixtures/ApiResource/ChildFoo.php new file mode 100644 index 00000000000..79d678b7c06 --- /dev/null +++ b/src/GraphQl/Tests/Fixtures/ApiResource/ChildFoo.php @@ -0,0 +1,18 @@ + + * + * 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\GraphQl\Tests\Fixtures\ApiResource; + +class ChildFoo extends ParentFoo +{ +} diff --git a/src/GraphQl/Tests/Fixtures/ApiResource/ParentFoo.php b/src/GraphQl/Tests/Fixtures/ApiResource/ParentFoo.php new file mode 100644 index 00000000000..37ec3e5e39d --- /dev/null +++ b/src/GraphQl/Tests/Fixtures/ApiResource/ParentFoo.php @@ -0,0 +1,18 @@ + + * + * 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\GraphQl\Tests\Fixtures\ApiResource; + +class ParentFoo +{ +} diff --git a/src/GraphQl/Tests/Resolver/Factory/ItemResolverFactoryTest.php b/src/GraphQl/Tests/Resolver/Factory/ItemResolverFactoryTest.php index c57e49f218b..7ab86f63f5f 100644 --- a/src/GraphQl/Tests/Resolver/Factory/ItemResolverFactoryTest.php +++ b/src/GraphQl/Tests/Resolver/Factory/ItemResolverFactoryTest.php @@ -18,7 +18,9 @@ use ApiPlatform\GraphQl\Resolver\Stage\SecurityPostDenormalizeStageInterface; use ApiPlatform\GraphQl\Resolver\Stage\SecurityStageInterface; use ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface; +use ApiPlatform\GraphQl\Tests\Fixtures\ApiResource\ChildFoo; use ApiPlatform\GraphQl\Tests\Fixtures\ApiResource\Dummy; +use ApiPlatform\GraphQl\Tests\Fixtures\ApiResource\ParentFoo; use ApiPlatform\Metadata\GraphQl\Query; use GraphQL\Type\Definition\ResolveInfo; use PHPUnit\Framework\TestCase; @@ -245,4 +247,22 @@ public function testResolveCustomBadItem(): void ($this->itemResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info); } + + public function testResolveInheritedClass(): void + { + $resourceClass = ParentFoo::class; + $rootClass = $resourceClass; + $operationName = 'custom_query'; + $operation = (new Query())->withName($operationName); + $source = ['source']; + $args = ['args']; + $info = $this->prophesize(ResolveInfo::class)->reveal(); + $info->fieldName = 'field'; + $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => false]; + + $readStageItem = new ChildFoo(); + $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); + + ($this->itemResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info); + } } diff --git a/src/Hydra/Serializer/CollectionFiltersNormalizer.php b/src/Hydra/Serializer/CollectionFiltersNormalizer.php index 1552e1cb167..2b58890249d 100644 --- a/src/Hydra/Serializer/CollectionFiltersNormalizer.php +++ b/src/Hydra/Serializer/CollectionFiltersNormalizer.php @@ -13,7 +13,9 @@ namespace ApiPlatform\Hydra\Serializer; +use ApiPlatform\Api\FilterInterface as LegacyFilterInterface; use ApiPlatform\Api\FilterLocatorTrait; +use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; use ApiPlatform\Doctrine\Odm\State\Options as ODMOptions; use ApiPlatform\Doctrine\Orm\State\Options; use ApiPlatform\Metadata\FilterInterface; @@ -40,7 +42,7 @@ final class CollectionFiltersNormalizer implements NormalizerInterface, Normaliz /** * @param ContainerInterface $filterLocator The new filter locator or the deprecated filter collection */ - public function __construct(private readonly NormalizerInterface $collectionNormalizer, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly ResourceClassResolverInterface $resourceClassResolver, ContainerInterface $filterLocator) + public function __construct(private readonly NormalizerInterface $collectionNormalizer, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly LegacyResourceClassResolverInterface|ResourceClassResolverInterface $resourceClassResolver, ContainerInterface $filterLocator) { $this->setFilterLocator($filterLocator); } @@ -142,7 +144,7 @@ public function setNormalizer(NormalizerInterface $normalizer): void /** * Returns the content of the Hydra search property. * - * @param FilterInterface[] $filters + * @param LegacyFilterInterface[]|FilterInterface[] $filters */ private function getSearch(string $resourceClass, array $parts, array $filters): array { diff --git a/src/Hydra/Serializer/ErrorNormalizer.php b/src/Hydra/Serializer/ErrorNormalizer.php index af59a428958..febf2908d99 100644 --- a/src/Hydra/Serializer/ErrorNormalizer.php +++ b/src/Hydra/Serializer/ErrorNormalizer.php @@ -24,7 +24,7 @@ /** * Converts {@see \Exception} or {@see FlattenException} to a Hydra error representation. * - * @deprecated + * @deprecated we use ItemNormalizer instead * * @author Kévin Dunglas * @author Samuel ROZE @@ -37,7 +37,7 @@ final class ErrorNormalizer implements NormalizerInterface, CacheableSupportsMet public const TITLE = 'title'; private array $defaultContext = [self::TITLE => 'An error occurred']; - public function __construct(private readonly UrlGeneratorInterface|LegacyUrlGeneratorInterface $urlGenerator, private readonly bool $debug = false, array $defaultContext = []) + public function __construct(private readonly UrlGeneratorInterface|LegacyUrlGeneratorInterface $urlGenerator, private readonly bool $debug = false, array $defaultContext = [], private readonly ?NormalizerInterface $itemNormalizer = null) { $this->defaultContext = array_merge($this->defaultContext, $defaultContext); } @@ -47,7 +47,12 @@ public function __construct(private readonly UrlGeneratorInterface|LegacyUrlGene */ public function normalize(mixed $object, string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null { - trigger_deprecation('api-platform', '3.2', sprintf('The class "%s" is deprecated in favor of using an Error resource.', __CLASS__)); + trigger_deprecation('api-platform', '3.2', sprintf('The class "%s" is deprecated in favor of using an Error resource. We fallback on "api_platform.serializer.normalizer.item".', __CLASS__)); + + if ($this->itemNormalizer) { + return $this->itemNormalizer->normalize($object, $format, $context); + } + $data = [ '@context' => $this->urlGenerator->generate('api_jsonld_context', ['shortName' => 'Error']), '@type' => 'hydra:Error', diff --git a/src/JsonApi/Serializer/ErrorNormalizer.php b/src/JsonApi/Serializer/ErrorNormalizer.php index 32ca637faa9..835a9ec0da4 100644 --- a/src/JsonApi/Serializer/ErrorNormalizer.php +++ b/src/JsonApi/Serializer/ErrorNormalizer.php @@ -22,6 +22,8 @@ /** * Converts {@see \Exception} or {@see FlattenException} or to a JSON API error representation. * + * @deprecated we use ItemNormalizer instead + * * @author Héctor Hurtarte */ final class ErrorNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface @@ -34,7 +36,7 @@ final class ErrorNormalizer implements NormalizerInterface, CacheableSupportsMet self::TITLE => 'An error occurred', ]; - public function __construct(private readonly bool $debug = false, array $defaultContext = []) + public function __construct(private readonly bool $debug = false, array $defaultContext = [], private readonly ?NormalizerInterface $itemNormalizer = null) { $this->defaultContext = array_merge($this->defaultContext, $defaultContext); } @@ -44,7 +46,12 @@ public function __construct(private readonly bool $debug = false, array $default */ public function normalize(mixed $object, string $format = null, array $context = []): array { - trigger_deprecation('api-platform', '3.2', sprintf('The class "%s" is deprecated in favor of using an Error resource.', __CLASS__)); + trigger_deprecation('api-platform', '3.2', sprintf('The class "%s" is deprecated in favor of using an Error resource. We fallback on "api_platform.serializer.normalizer.item".', __CLASS__)); + + if ($this->itemNormalizer) { + return $this->itemNormalizer->normalize($object, $format, $context); + } + $data = [ 'title' => $context[self::TITLE] ?? $this->defaultContext[self::TITLE], 'description' => $this->getErrorMessage($object, $context, $this->debug), diff --git a/src/JsonSchema/SchemaFactory.php b/src/JsonSchema/SchemaFactory.php index 179a7cb11c7..2874c9ac9f7 100644 --- a/src/JsonSchema/SchemaFactory.php +++ b/src/JsonSchema/SchemaFactory.php @@ -37,7 +37,7 @@ final class SchemaFactory implements SchemaFactoryInterface { use ResourceClassInfoTrait; private array $distinctFormats = []; - + private ?TypeFactoryInterface $typeFactory = null; // Edge case where the related resource is not readable (for example: NotExposed) but we have groups to read the whole related object public const FORCE_SUBSCHEMA = '_api_subschema_force_readable_link'; public const OPENAPI_DEFINITION_NAME = 'openapi_definition_name'; @@ -45,7 +45,7 @@ final class SchemaFactory implements SchemaFactoryInterface public function __construct(?TypeFactoryInterface $typeFactory, ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ?NameConverterInterface $nameConverter = null, ResourceClassResolverInterface $resourceClassResolver = null) { if ($typeFactory) { - trigger_deprecation('api-platform/core', '3.2', sprintf('The "%s" is not needed anymore and will not be used anymore.', TypeFactoryInterface::class)); + $this->typeFactory = $typeFactory; } $this->resourceMetadataFactory = $resourceMetadataFactory; @@ -198,6 +198,12 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str $subSchema->setDefinitions($schema->getDefinitions()); // Populate definitions of the main schema foreach ($types as $type) { + // TODO: in 3.3 add trigger_deprecation() as type factories are not used anymore, we moved this logic to SchemaPropertyMetadataFactory so that it gets cached + if ($typeFromFactory = $this->typeFactory?->getType($type, 'jsonschema', $propertyMetadata->isReadableLink(), $serializerContext)) { + $propertySchema = $typeFromFactory; + break; + } + $isCollection = $type->isCollection(); if ($isCollection) { $valueType = $type->getCollectionValueTypes()[0] ?? null; diff --git a/src/JsonSchema/TypeFactory.php b/src/JsonSchema/TypeFactory.php index 05e695a707b..872737ce547 100644 --- a/src/JsonSchema/TypeFactory.php +++ b/src/JsonSchema/TypeFactory.php @@ -23,6 +23,8 @@ /** * {@inheritdoc} * + * @deprecated since 3.3 https://github.com/api-platform/core/pull/5470 + * * @author Kévin Dunglas */ final class TypeFactory implements TypeFactoryInterface @@ -46,6 +48,11 @@ public function setSchemaFactory(SchemaFactoryInterface $schemaFactory): void */ public function getType(Type $type, string $format = 'json', bool $readableLink = null, array $serializerContext = null, Schema $schema = null): array { + if ('jsonschema' === $format) { + return []; + } + + // TODO: OpenApiFactory uses this to compute filter types if ($type->isCollection()) { $keyType = $type->getCollectionKeyTypes()[0] ?? null; $subType = ($type->getCollectionValueTypes()[0] ?? null) ?? new Type($type->getBuiltinType(), false, $type->getClassName(), false); diff --git a/src/Metadata/FilterInterface.php b/src/Metadata/FilterInterface.php index 3a8b8036513..51ccea3521f 100644 --- a/src/Metadata/FilterInterface.php +++ b/src/Metadata/FilterInterface.php @@ -13,44 +13,57 @@ namespace ApiPlatform\Metadata; -/** - * Filters applicable on a resource. - * - * @author Kévin Dunglas - */ -interface FilterInterface -{ +if (interface_exists(\ApiPlatform\Api\FilterInterface::class)) { + class_alias( + \ApiPlatform\Api\FilterInterface::class, + __NAMESPACE__.'\FilterInterface' + ); + + if (false) { // @phpstan-ignore-line + interface FilterInterface extends \ApiPlatform\Api\FilterInterface + { + } + } +} else { /** - * Gets the description of this filter for the given resource. - * - * Returns an array with the filter parameter names as keys and array with the following data as values: - * - property: the property where the filter is applied - * - type: the type of the filter - * - required: if this filter is required - * - strategy (optional): the used strategy - * - is_collection (optional): if this filter is for collection - * - swagger (optional): additional parameters for the path operation, - * e.g. 'swagger' => [ - * 'description' => 'My Description', - * 'name' => 'My Name', - * 'type' => 'integer', - * ] - * - openapi (optional): additional parameters for the path operation in the version 3 spec, - * e.g. 'openapi' => [ - * 'description' => 'My Description', - * 'name' => 'My Name', - * 'schema' => [ - * 'type' => 'integer', - * ] - * ] - * - schema (optional): schema definition, - * e.g. 'schema' => [ - * 'type' => 'string', - * 'enum' => ['value_1', 'value_2'], - * ] - * The description can contain additional data specific to a filter. + * Filters applicable on a resource. * - * @see \ApiPlatform\OpenApi\Factory\OpenApiFactory::getFiltersParameters + * @author Kévin Dunglas */ - public function getDescription(string $resourceClass): array; + interface FilterInterface + { + /** + * Gets the description of this filter for the given resource. + * + * Returns an array with the filter parameter names as keys and array with the following data as values: + * - property: the property where the filter is applied + * - type: the type of the filter + * - required: if this filter is required + * - strategy (optional): the used strategy + * - is_collection (optional): if this filter is for collection + * - swagger (optional): additional parameters for the path operation, + * e.g. 'swagger' => [ + * 'description' => 'My Description', + * 'name' => 'My Name', + * 'type' => 'integer', + * ] + * - openapi (optional): additional parameters for the path operation in the version 3 spec, + * e.g. 'openapi' => [ + * 'description' => 'My Description', + * 'name' => 'My Name', + * 'schema' => [ + * 'type' => 'integer', + * ] + * ] + * - schema (optional): schema definition, + * e.g. 'schema' => [ + * 'type' => 'string', + * 'enum' => ['value_1', 'value_2'], + * ] + * The description can contain additional data specific to a filter. + * + * @see \ApiPlatform\OpenApi\Factory\OpenApiFactory::getFiltersParameters + */ + public function getDescription(string $resourceClass): array; + } } diff --git a/src/Metadata/IdentifiersExtractorInterface.php b/src/Metadata/IdentifiersExtractorInterface.php index 5ef91fde783..c8ac46d9929 100644 --- a/src/Metadata/IdentifiersExtractorInterface.php +++ b/src/Metadata/IdentifiersExtractorInterface.php @@ -15,17 +15,30 @@ use ApiPlatform\Exception\RuntimeException; -/** - * Extracts identifiers for a given Resource according to the retrieved Metadata. - * - * @author Antoine Bluchet - */ -interface IdentifiersExtractorInterface -{ +if (interface_exists(\ApiPlatform\Api\IdentifiersExtractorInterface::class)) { + class_alias( + \ApiPlatform\Api\IdentifiersExtractorInterface::class, + __NAMESPACE__.'\IdentifiersExtractorInterface' + ); + + if (false) { // @phpstan-ignore-line + interface IdentifiersExtractorInterface extends \ApiPlatform\Api\IdentifiersExtractorInterface + { + } + } +} else { /** - * Finds identifiers from an Item (object). + * Extracts identifiers for a given Resource according to the retrieved Metadata. * - * @throws RuntimeException + * @author Antoine Bluchet */ - public function getIdentifiersFromItem(object $item, Operation $operation = null, array $context = []): array; + interface IdentifiersExtractorInterface + { + /** + * Finds identifiers from an Item (object). + * + * @throws RuntimeException + */ + public function getIdentifiersFromItem(object $item, Operation $operation = null, array $context = []): array; + } } diff --git a/src/Metadata/IriConverterInterface.php b/src/Metadata/IriConverterInterface.php index b55bb75b984..56fd9618fba 100644 --- a/src/Metadata/IriConverterInterface.php +++ b/src/Metadata/IriConverterInterface.php @@ -17,28 +17,41 @@ use ApiPlatform\Metadata\Exception\ItemNotFoundException; use ApiPlatform\Metadata\Exception\RuntimeException; -/** - * Converts item and resources to IRI and vice versa. - * - * @author Kévin Dunglas - */ -interface IriConverterInterface -{ - /** - * Retrieves an item from its IRI. - * - * @throws InvalidArgumentException - * @throws ItemNotFoundException - */ - public function getResourceFromIri(string $iri, array $context = [], Operation $operation = null): object; +if (interface_exists(\ApiPlatform\Api\IriConverterInterface::class)) { + class_alias( + \ApiPlatform\Api\IriConverterInterface::class, + __NAMESPACE__.'\IriConverterInterface' + ); + if (false) { // @phpstan-ignore-line + interface IriConverterInterface extends \ApiPlatform\Api\IriConverterInterface + { + } + } +} else { /** - * Gets the IRI associated with the given item. - * - * @param object|class-string $resource + * Converts item and resources to IRI and vice versa. * - * @throws InvalidArgumentException - * @throws RuntimeException + * @author Kévin Dunglas */ - public function getIriFromResource(object|string $resource, int $referenceType = UrlGeneratorInterface::ABS_PATH, Operation $operation = null, array $context = []): ?string; + interface IriConverterInterface + { + /** + * Retrieves an item from its IRI. + * + * @throws InvalidArgumentException + * @throws ItemNotFoundException + */ + public function getResourceFromIri(string $iri, array $context = [], Operation $operation = null): object; + + /** + * Gets the IRI associated with the given item. + * + * @param object|class-string $resource + * + * @throws InvalidArgumentException + * @throws RuntimeException + */ + public function getIriFromResource(object|string $resource, int $referenceType = UrlGeneratorInterface::ABS_PATH, Operation $operation = null, array $context = []): ?string; + } } diff --git a/src/Metadata/Resource/Factory/DeprecationResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/DeprecationResourceMetadataCollectionFactory.php index eebbaf4aaca..30df81e872c 100644 --- a/src/Metadata/Resource/Factory/DeprecationResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/DeprecationResourceMetadataCollectionFactory.php @@ -14,6 +14,7 @@ namespace ApiPlatform\Metadata\Resource\Factory; use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Put; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; @@ -37,12 +38,22 @@ public function create(string $resourceClass): ResourceMetadataCollection { $resourceMetadataCollection = $this->decorated->create($resourceClass); - foreach ($resourceMetadataCollection as $resourceMetadata) { - foreach ($resourceMetadata->getOperations() as $operation) { - if ($operation instanceof Put && null === ($operation->getExtraProperties()['standard_put'] ?? null)) { + foreach ($resourceMetadataCollection as $i => $resourceMetadata) { + $newOperations = []; + foreach ($resourceMetadata->getOperations() as $operationName => $operation) { + $extraProperties = $operation->getExtraProperties(); + if ($operation instanceof Put && null === ($extraProperties['standard_put'] ?? null)) { $this->triggerDeprecationOnce($operation, 'extraProperties["standard_put"]', 'In API Platform 4 PUT will always replace the data, use extraProperties["standard_put"] to "true" on every operation to avoid breaking PUT\'s behavior. Use PATCH to use the old behavior.'); } + + if (null === ($extraProperties['skip_deprecated_exception_normalizers'] ?? null)) { + $operation = $operation->withExtraProperties(['skip_deprecated_exception_normalizers' => false] + $extraProperties); + } + + $newOperations[$operationName] = $operation; } + + $resourceMetadataCollection[$i] = $resourceMetadata->withOperations(new Operations($newOperations)); } return $resourceMetadataCollection; diff --git a/src/Metadata/ResourceClassResolverInterface.php b/src/Metadata/ResourceClassResolverInterface.php index 8dabc533502..afb460bb252 100644 --- a/src/Metadata/ResourceClassResolverInterface.php +++ b/src/Metadata/ResourceClassResolverInterface.php @@ -15,27 +15,40 @@ use ApiPlatform\Metadata\Exception\InvalidArgumentException; -/** - * Guesses which resource is associated with a given object. - * - * @author Kévin Dunglas - */ -interface ResourceClassResolverInterface -{ +if (interface_exists(\ApiPlatform\Api\ResourceClassResolverInterface::class)) { + class_alias( + \ApiPlatform\Api\ResourceClassResolverInterface::class, + __NAMESPACE__.'\ResourceClassResolverInterface' + ); + + if (false) { // @phpstan-ignore-line + interface ResourceClassResolverInterface extends \ApiPlatform\Api\ResourceClassResolverInterface + { + } + } +} else { /** - * Guesses the associated resource. - * - * @param string $resourceClass The expected resource class - * @param bool $strict If true, value must match the expected resource class + * Guesses which resource is associated with a given object. * - * @throws InvalidArgumentException + * @author Kévin Dunglas */ - public function getResourceClass(mixed $value, string $resourceClass = null, bool $strict = false): string; + interface ResourceClassResolverInterface + { + /** + * Guesses the associated resource. + * + * @param string $resourceClass The expected resource class + * @param bool $strict If true, value must match the expected resource class + * + * @throws InvalidArgumentException + */ + public function getResourceClass(mixed $value, string $resourceClass = null, bool $strict = false): string; - /** - * Is the given class a resource class? - */ - public function isResourceClass(string $type): bool; -} + /** + * Is the given class a resource class? + */ + public function isResourceClass(string $type): bool; + } -class_alias(ResourceClassResolverInterface::class, \ApiPlatform\Api\ResourceClassResolverInterface::class); + class_alias(ResourceClassResolverInterface::class, \ApiPlatform\Api\ResourceClassResolverInterface::class); +} diff --git a/src/Metadata/UriVariableTransformerInterface.php b/src/Metadata/UriVariableTransformerInterface.php index e91bfc6d7f6..8411cb4e8ee 100644 --- a/src/Metadata/UriVariableTransformerInterface.php +++ b/src/Metadata/UriVariableTransformerInterface.php @@ -15,25 +15,38 @@ use ApiPlatform\Exception\InvalidUriVariableException; -interface UriVariableTransformerInterface -{ - /** - * Transforms the value of a URI variable (identifier) to its type. - * - * @param mixed $value The URI variable value to transform - * @param array $types The guessed type behind the URI variable - * @param array $context Options available to the transformer - * - * @throws InvalidUriVariableException Occurs when the URI variable could not be transformed - */ - public function transform(mixed $value, array $types, array $context = []); +if (class_exists(\ApiPlatform\Api\UriVariableTransformerInterface::class)) { + class_alias( + \ApiPlatform\Api\UriVariableTransformerInterface::class, + __NAMESPACE__.'\UriVariableTransformerInterface' + ); - /** - * Checks whether the value of a URI variable can be transformed to its type by this transformer. - * - * @param mixed $value The URI variable value to transform - * @param array $types The types to which the URI variable value should be transformed - * @param array $context Options available to the transformer - */ - public function supportsTransformation(mixed $value, array $types, array $context = []): bool; + if (false) { // @phpstan-ignore-line + interface UriVariableTransformerInterface extends \ApiPlatform\Api\UriVariableTransformerInterface + { + } + } +} else { + interface UriVariableTransformerInterface + { + /** + * Transforms the value of a URI variable (identifier) to its type. + * + * @param mixed $value The URI variable value to transform + * @param array $types The guessed type behind the URI variable + * @param array $context Options available to the transformer + * + * @throws InvalidUriVariableException Occurs when the URI variable could not be transformed + */ + public function transform(mixed $value, array $types, array $context = []); + + /** + * Checks whether the value of a URI variable can be transformed to its type by this transformer. + * + * @param mixed $value The URI variable value to transform + * @param array $types The types to which the URI variable value should be transformed + * @param array $context Options available to the transformer + */ + public function supportsTransformation(mixed $value, array $types, array $context = []): bool; + } } diff --git a/src/Metadata/UriVariablesConverterInterface.php b/src/Metadata/UriVariablesConverterInterface.php index 22ba1e7214e..8320ba26746 100644 --- a/src/Metadata/UriVariablesConverterInterface.php +++ b/src/Metadata/UriVariablesConverterInterface.php @@ -15,22 +15,35 @@ use ApiPlatform\Metadata\Exception\InvalidIdentifierException; -/** - * Identifier converter. - * - * @author Antoine Bluchet - */ -interface UriVariablesConverterInterface -{ +if (interface_exists(\ApiPlatform\Api\UriVariablesConverterInterface::class)) { + class_alias( + \ApiPlatform\Api\UriVariablesConverterInterface::class, + __NAMESPACE__.'\UriVariablesConverterInterface' + ); + + if (false) { // @phpstan-ignore-line + interface UriVariablesConverterInterface extends \ApiPlatform\Api\UriVariablesConverterInterface + { + } + } +} else { /** - * Takes an array of strings representing URI variables (identifiers) and transform their values to the expected type. - * - * @param array $data URI variables to convert to PHP values - * @param string $class The class to which the URI variables belong to - * - * @throws InvalidIdentifierException + * Identifier converter. * - * @return array Array indexed by identifiers properties with their values denormalized + * @author Antoine Bluchet */ - public function convert(array $data, string $class, array $context = []): array; + interface UriVariablesConverterInterface + { + /** + * Takes an array of strings representing URI variables (identifiers) and transform their values to the expected type. + * + * @param array $data URI variables to convert to PHP values + * @param string $class The class to which the URI variables belong to + * + * @throws InvalidIdentifierException + * + * @return array Array indexed by identifiers properties with their values denormalized + */ + public function convert(array $data, string $class, array $context = []): array; + } } diff --git a/src/Metadata/UrlGeneratorInterface.php b/src/Metadata/UrlGeneratorInterface.php index 5df27ef16e4..82a412bbc41 100644 --- a/src/Metadata/UrlGeneratorInterface.php +++ b/src/Metadata/UrlGeneratorInterface.php @@ -17,68 +17,81 @@ use Symfony\Component\Routing\Exception\MissingMandatoryParametersException; use Symfony\Component\Routing\Exception\RouteNotFoundException; -/** - * UrlGeneratorInterface is the interface that all URL generator classes must implement. - * - * This interface has been imported and adapted from the Symfony project. - * - * The constants in this interface define the different types of resource references that - * are declared in RFC 3986: http://tools.ietf.org/html/rfc3986 - * We are using the term "URL" instead of "URI" as this is more common in web applications - * and we do not need to distinguish them as the difference is mostly semantical and - * less technical. Generating URIs, i.e. representation-independent resource identifiers, - * is also possible. - * - * @author Fabien Potencier - * @author Tobias Schultze - * @copyright Fabien Potencier - */ -interface UrlGeneratorInterface -{ - /** - * Generates an absolute URL, e.g. "http://example.com/dir/file". - */ - public const ABS_URL = 0; - - /** - * Generates an absolute path, e.g. "/dir/file". - */ - public const ABS_PATH = 1; - - /** - * Generates a relative path based on the current request path, e.g. "../parent-file". - * - * @see UrlGenerator::getRelativePath() - */ - public const REL_PATH = 2; - - /** - * Generates a network path, e.g. "//example.com/dir/file". - * Such reference reuses the current scheme but specifies the host. - */ - public const NET_PATH = 3; +if (interface_exists(\ApiPlatform\Api\UrlGeneratorInterface::class)) { + class_alias( + \ApiPlatform\Api\UrlGeneratorInterface::class, + __NAMESPACE__.'\UrlGeneratorInterface' + ); + if (false) { // @phpstan-ignore-line + interface UrlGeneratorInterface extends \ApiPlatform\Api\UrlGeneratorInterface + { + } + } +} else { /** - * Generates a URL or path for a specific route based on the given parameters. - * - * Parameters that reference placeholders in the route pattern will substitute them in the - * path or host. Extra params are added as query string to the URL. + * UrlGeneratorInterface is the interface that all URL generator classes must implement. * - * When the passed reference type cannot be generated for the route because it requires a different - * host or scheme than the current one, the method will return a more comprehensive reference - * that includes the required params. For example, when you call this method with $referenceType = ABSOLUTE_PATH - * but the route requires the https scheme whereas the current scheme is http, it will instead return an - * ABSOLUTE_URL with the https scheme and the current host. This makes sure the generated URL matches - * the route in any case. + * This interface has been imported and adapted from the Symfony project. * - * If there is no route with the given name, the generator must throw the RouteNotFoundException. + * The constants in this interface define the different types of resource references that + * are declared in RFC 3986: http://tools.ietf.org/html/rfc3986 + * We are using the term "URL" instead of "URI" as this is more common in web applications + * and we do not need to distinguish them as the difference is mostly semantical and + * less technical. Generating URIs, i.e. representation-independent resource identifiers, + * is also possible. * - * The special parameter _fragment will be used as the document fragment suffixed to the final URL. - * - * @throws RouteNotFoundException If the named route doesn't exist - * @throws MissingMandatoryParametersException When some parameters are missing that are mandatory for the route - * @throws InvalidParameterException When a parameter value for a placeholder is not correct because - * it does not match the requirement + * @author Fabien Potencier + * @author Tobias Schultze + * @copyright Fabien Potencier */ - public function generate(string $name, array $parameters = [], int $referenceType = self::ABS_PATH): string; + interface UrlGeneratorInterface + { + /** + * Generates an absolute URL, e.g. "http://example.com/dir/file". + */ + public const ABS_URL = 0; + + /** + * Generates an absolute path, e.g. "/dir/file". + */ + public const ABS_PATH = 1; + + /** + * Generates a relative path based on the current request path, e.g. "../parent-file". + * + * @see UrlGenerator::getRelativePath() + */ + public const REL_PATH = 2; + + /** + * Generates a network path, e.g. "//example.com/dir/file". + * Such reference reuses the current scheme but specifies the host. + */ + public const NET_PATH = 3; + + /** + * Generates a URL or path for a specific route based on the given parameters. + * + * Parameters that reference placeholders in the route pattern will substitute them in the + * path or host. Extra params are added as query string to the URL. + * + * When the passed reference type cannot be generated for the route because it requires a different + * host or scheme than the current one, the method will return a more comprehensive reference + * that includes the required params. For example, when you call this method with $referenceType = ABSOLUTE_PATH + * but the route requires the https scheme whereas the current scheme is http, it will instead return an + * ABSOLUTE_URL with the https scheme and the current host. This makes sure the generated URL matches + * the route in any case. + * + * If there is no route with the given name, the generator must throw the RouteNotFoundException. + * + * The special parameter _fragment will be used as the document fragment suffixed to the final URL. + * + * @throws RouteNotFoundException If the named route doesn't exist + * @throws MissingMandatoryParametersException When some parameters are missing that are mandatory for the route + * @throws InvalidParameterException When a parameter value for a placeholder is not correct because + * it does not match the requirement + */ + public function generate(string $name, array $parameters = [], int $referenceType = self::ABS_PATH): string; + } } diff --git a/src/OpenApi/Factory/OpenApiFactory.php b/src/OpenApi/Factory/OpenApiFactory.php index d6014da2063..6f18de73814 100644 --- a/src/OpenApi/Factory/OpenApiFactory.php +++ b/src/OpenApi/Factory/OpenApiFactory.php @@ -573,7 +573,7 @@ private function getFiltersParameters(CollectionOperationInterface|HttpOperation } foreach ($filter->getDescription($entityClass) as $name => $data) { - $schema = $data['schema'] ?? (\in_array($data['type'], Type::$builtinTypes, true) ? $this->jsonSchemaTypeFactory->getType(new Type($data['type'], false, null, $data['is_collection'] ?? false)) : ['type' => 'string']); + $schema = $data['schema'] ?? (\in_array($data['type'], Type::$builtinTypes, true) ? $this->jsonSchemaTypeFactory->getType(new Type($data['type'], false, null, $data['is_collection'] ?? false), 'openapi') : ['type' => 'string']); $parameters[] = new Parameter( $name, diff --git a/src/Problem/Serializer/ErrorNormalizer.php b/src/Problem/Serializer/ErrorNormalizer.php index 39093aec65f..c1da53e12c1 100644 --- a/src/Problem/Serializer/ErrorNormalizer.php +++ b/src/Problem/Serializer/ErrorNormalizer.php @@ -22,6 +22,7 @@ * Normalizes errors according to the API Problem spec (RFC 7807). * * @see https://tools.ietf.org/html/rfc7807 + * @deprecated we use ItemNormalizer instead * * @author Kévin Dunglas */ @@ -36,7 +37,7 @@ final class ErrorNormalizer implements NormalizerInterface, CacheableSupportsMet self::TITLE => 'An error occurred', ]; - public function __construct(private readonly bool $debug = false, array $defaultContext = []) + public function __construct(private readonly bool $debug = false, array $defaultContext = [], private readonly ?NormalizerInterface $itemNormalizer = null) { $this->defaultContext = array_merge($this->defaultContext, $defaultContext); } @@ -46,7 +47,12 @@ public function __construct(private readonly bool $debug = false, array $default */ public function normalize(mixed $object, string $format = null, array $context = []): array { - trigger_deprecation('api-platform', '3.2', sprintf('The class "%s" is deprecated in favor of using an Error resource.', __CLASS__)); + trigger_deprecation('api-platform', '3.2', sprintf('The class "%s" is deprecated in favor of using an Error resource. We fallback on "api_platform.serializer.normalizer.item".', __CLASS__)); + + if ($this->itemNormalizer) { + return $this->itemNormalizer->normalize($object, $format, $context); + } + $data = [ 'type' => $context[self::TYPE] ?? $this->defaultContext[self::TYPE], 'title' => $context[self::TITLE] ?? $this->defaultContext[self::TITLE], @@ -69,12 +75,12 @@ public function supportsNormalization(mixed $data, string $format = null, array return false; } - return self::FORMAT === $format && ($data instanceof \Exception || $data instanceof FlattenException); + return (self::FORMAT === $format || 'json' === $format) && ($data instanceof \Exception || $data instanceof FlattenException); } public function getSupportedTypes($format): array { - if (self::FORMAT === $format) { + if (self::FORMAT === $format || 'json' === $format) { return [ \Exception::class => false, FlattenException::class => false, diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php index fb25fa780b9..b3f694a3b04 100644 --- a/src/Serializer/AbstractItemNormalizer.php +++ b/src/Serializer/AbstractItemNormalizer.php @@ -13,6 +13,8 @@ namespace ApiPlatform\Serializer; +use ApiPlatform\Api\IriConverterInterface as LegacyIriConverterInterface; +use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; use ApiPlatform\Exception\InvalidArgumentException; use ApiPlatform\Exception\ItemNotFoundException; use ApiPlatform\Metadata\ApiProperty; @@ -61,7 +63,7 @@ abstract class AbstractItemNormalizer extends AbstractObjectNormalizer protected array $localCache = []; protected array $localFactoryOptionsCache = []; - public function __construct(protected PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, protected PropertyMetadataFactoryInterface $propertyMetadataFactory, protected IriConverterInterface $iriConverter, protected ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null, array $defaultContext = [], ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, protected ?ResourceAccessCheckerInterface $resourceAccessChecker = null) + public function __construct(protected PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, protected PropertyMetadataFactoryInterface $propertyMetadataFactory, protected LegacyIriConverterInterface|IriConverterInterface $iriConverter, protected LegacyResourceClassResolverInterface|ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null, array $defaultContext = [], ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, protected ?ResourceAccessCheckerInterface $resourceAccessChecker = null) { if (!isset($defaultContext['circular_reference_handler'])) { $defaultContext['circular_reference_handler'] = fn ($object): ?string => $this->iriConverter->getIriFromResource($object); @@ -787,7 +789,7 @@ protected function normalizeRelation(ApiProperty $propertyMetadata, ?object $rel return $normalizedRelatedObject; } - $iri = $this->iriConverter->getIriFromResource($relatedObject); + $iri = $this->iriConverter->getIriFromResource(resource: $relatedObject, context: $context); if (isset($context['resources'])) { $context['resources'][$iri] = $iri; diff --git a/src/Serializer/SerializerContextBuilder.php b/src/Serializer/SerializerContextBuilder.php index b77f40dec0a..02cace61ab1 100644 --- a/src/Serializer/SerializerContextBuilder.php +++ b/src/Serializer/SerializerContextBuilder.php @@ -62,7 +62,7 @@ public function createFromRequest(Request $request, bool $normalization, array $ $context['uri'] = $request->getUri(); $context['input'] = $operation->getInput(); $context['output'] = $operation->getOutput(); - $context['skip_deprecated_exception_normalizers'] = true; + $context['skip_deprecated_exception_normalizers'] = $operation->getExtraProperties()['skip_deprecated_exception_normalizers'] ?? false; // Special case as this is usually handled by our OperationContextTrait, here we want to force the IRI in the response if (!$operation instanceof CollectionOperationInterface && method_exists($operation, 'getItemUriTemplate') && $operation->getItemUriTemplate()) { diff --git a/src/Serializer/Tests/SerializerContextBuilderTest.php b/src/Serializer/Tests/SerializerContextBuilderTest.php index b95ed38bc8b..04a0bb929ac 100644 --- a/src/Serializer/Tests/SerializerContextBuilderTest.php +++ b/src/Serializer/Tests/SerializerContextBuilderTest.php @@ -67,42 +67,42 @@ public function testCreateFromRequest(): void { $request = Request::create('/foos/1'); $request->attributes->replace(['_api_resource_class' => 'Foo', '_api_operation_name' => 'get', '_api_format' => 'xml', '_api_mime_type' => 'text/xml']); - $expected = ['foo' => 'bar', 'operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => '/foos/1', 'uri' => 'http://localhost/foos/1', 'output' => null, 'input' => null, 'iri_only' => false, 'skip_null_values' => true, 'operation' => $this->operation, 'exclude_from_cache_key' => ['root_operation', 'operation'], 'skip_deprecated_exception_normalizers' => true]; + $expected = ['foo' => 'bar', 'operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => '/foos/1', 'uri' => 'http://localhost/foos/1', 'output' => null, 'input' => null, 'iri_only' => false, 'skip_null_values' => true, 'operation' => $this->operation, 'exclude_from_cache_key' => ['root_operation', 'operation'], 'skip_deprecated_exception_normalizers' => false]; $this->assertEquals($expected, $this->builder->createFromRequest($request, true)); $request = Request::create('/foos'); $request->attributes->replace(['_api_resource_class' => 'Foo', '_api_operation_name' => 'get_collection', '_api_format' => 'xml', '_api_mime_type' => 'text/xml']); - $expected = ['foo' => 'bar', 'operation_name' => 'get_collection', 'resource_class' => 'Foo', 'request_uri' => '/foos', 'uri' => 'http://localhost/foos', 'output' => null, 'input' => null, 'iri_only' => false, 'skip_null_values' => true, 'operation' => $this->operation->withName('get_collection'), 'exclude_from_cache_key' => ['root_operation', 'operation'], 'skip_deprecated_exception_normalizers' => true]; + $expected = ['foo' => 'bar', 'operation_name' => 'get_collection', 'resource_class' => 'Foo', 'request_uri' => '/foos', 'uri' => 'http://localhost/foos', 'output' => null, 'input' => null, 'iri_only' => false, 'skip_null_values' => true, 'operation' => $this->operation->withName('get_collection'), 'exclude_from_cache_key' => ['root_operation', 'operation'], 'skip_deprecated_exception_normalizers' => false]; $this->assertEquals($expected, $this->builder->createFromRequest($request, true)); $request = Request::create('/foos/1'); $request->attributes->replace(['_api_resource_class' => 'Foo', '_api_operation_name' => 'get', '_api_format' => 'xml', '_api_mime_type' => 'text/xml']); - $expected = ['bar' => 'baz', 'operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => '/foos/1', 'api_allow_update' => false, 'uri' => 'http://localhost/foos/1', 'output' => null, 'input' => null, 'iri_only' => false, 'skip_null_values' => true, 'operation' => $this->operation, 'exclude_from_cache_key' => ['root_operation', 'operation'], 'skip_deprecated_exception_normalizers' => true]; + $expected = ['bar' => 'baz', 'operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => '/foos/1', 'api_allow_update' => false, 'uri' => 'http://localhost/foos/1', 'output' => null, 'input' => null, 'iri_only' => false, 'skip_null_values' => true, 'operation' => $this->operation, 'exclude_from_cache_key' => ['root_operation', 'operation'], 'skip_deprecated_exception_normalizers' => false]; $this->assertEquals($expected, $this->builder->createFromRequest($request, false)); $request = Request::create('/foos', 'POST'); $request->attributes->replace(['_api_resource_class' => 'Foo', '_api_operation_name' => 'post', '_api_format' => 'xml', '_api_mime_type' => 'text/xml']); - $expected = ['bar' => 'baz', 'operation_name' => 'post', 'resource_class' => 'Foo', 'request_uri' => '/foos', 'api_allow_update' => false, 'uri' => 'http://localhost/foos', 'output' => null, 'input' => null, 'iri_only' => false, 'skip_null_values' => true, 'operation' => $this->operation->withName('post'), 'exclude_from_cache_key' => ['root_operation', 'operation'], 'skip_deprecated_exception_normalizers' => true]; + $expected = ['bar' => 'baz', 'operation_name' => 'post', 'resource_class' => 'Foo', 'request_uri' => '/foos', 'api_allow_update' => false, 'uri' => 'http://localhost/foos', 'output' => null, 'input' => null, 'iri_only' => false, 'skip_null_values' => true, 'operation' => $this->operation->withName('post'), 'exclude_from_cache_key' => ['root_operation', 'operation'], 'skip_deprecated_exception_normalizers' => false]; $this->assertEquals($expected, $this->builder->createFromRequest($request, false)); $request = Request::create('/foos', 'PUT'); $request->attributes->replace(['_api_resource_class' => 'Foo', '_api_operation_name' => 'put', '_api_format' => 'xml', '_api_mime_type' => 'text/xml']); - $expected = ['bar' => 'baz', 'operation_name' => 'put', 'resource_class' => 'Foo', 'request_uri' => '/foos', 'api_allow_update' => true, 'uri' => 'http://localhost/foos', 'output' => null, 'input' => null, 'iri_only' => false, 'skip_null_values' => true, 'operation' => (new Put(name: 'put'))->withOperation($this->operation), 'exclude_from_cache_key' => ['root_operation', 'operation'], 'skip_deprecated_exception_normalizers' => true]; + $expected = ['bar' => 'baz', 'operation_name' => 'put', 'resource_class' => 'Foo', 'request_uri' => '/foos', 'api_allow_update' => true, 'uri' => 'http://localhost/foos', 'output' => null, 'input' => null, 'iri_only' => false, 'skip_null_values' => true, 'operation' => (new Put(name: 'put'))->withOperation($this->operation), 'exclude_from_cache_key' => ['root_operation', 'operation'], 'skip_deprecated_exception_normalizers' => false]; $this->assertEquals($expected, $this->builder->createFromRequest($request, false)); $request = Request::create('/bars/1/foos'); $request->attributes->replace(['_api_resource_class' => 'Foo', '_api_operation_name' => 'get', '_api_format' => 'xml', '_api_mime_type' => 'text/xml']); - $expected = ['bar' => 'baz', 'operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => '/bars/1/foos', 'api_allow_update' => false, 'uri' => 'http://localhost/bars/1/foos', 'output' => null, 'input' => null, 'iri_only' => false, 'skip_null_values' => true, 'operation' => $this->operation, 'exclude_from_cache_key' => ['root_operation', 'operation'], 'skip_deprecated_exception_normalizers' => true]; + $expected = ['bar' => 'baz', 'operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => '/bars/1/foos', 'api_allow_update' => false, 'uri' => 'http://localhost/bars/1/foos', 'output' => null, 'input' => null, 'iri_only' => false, 'skip_null_values' => true, 'operation' => $this->operation, 'exclude_from_cache_key' => ['root_operation', 'operation'], 'skip_deprecated_exception_normalizers' => false]; $this->assertEquals($expected, $this->builder->createFromRequest($request, false)); $request = Request::create('/foowithpatch/1', 'PATCH'); $request->attributes->replace(['_api_resource_class' => 'FooWithPatch', '_api_operation_name' => 'patch', '_api_format' => 'json', '_api_mime_type' => 'application/json']); - $expected = ['operation_name' => 'patch', 'resource_class' => 'FooWithPatch', 'request_uri' => '/foowithpatch/1', 'api_allow_update' => true, 'uri' => 'http://localhost/foowithpatch/1', 'output' => null, 'input' => null, 'deep_object_to_populate' => true, 'skip_null_values' => true, 'iri_only' => false, 'operation' => $this->patchOperation, 'exclude_from_cache_key' => ['root_operation', 'operation'], 'skip_deprecated_exception_normalizers' => true]; + $expected = ['operation_name' => 'patch', 'resource_class' => 'FooWithPatch', 'request_uri' => '/foowithpatch/1', 'api_allow_update' => true, 'uri' => 'http://localhost/foowithpatch/1', 'output' => null, 'input' => null, 'deep_object_to_populate' => true, 'skip_null_values' => true, 'iri_only' => false, 'operation' => $this->patchOperation, 'exclude_from_cache_key' => ['root_operation', 'operation'], 'skip_deprecated_exception_normalizers' => false]; $this->assertEquals($expected, $this->builder->createFromRequest($request, false)); $request = Request::create('/bars/1/foos'); $request->attributes->replace(['_api_resource_class' => 'Foo', '_api_operation_name' => 'get', '_api_format' => 'xml', '_api_mime_type' => 'text/xml', 'id' => '1']); - $expected = ['bar' => 'baz', 'operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => '/bars/1/foos', 'api_allow_update' => false, 'uri' => 'http://localhost/bars/1/foos', 'output' => null, 'input' => null, 'iri_only' => false, 'operation' => $this->operation, 'skip_null_values' => true, 'exclude_from_cache_key' => ['root_operation', 'operation'], 'skip_deprecated_exception_normalizers' => true]; + $expected = ['bar' => 'baz', 'operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => '/bars/1/foos', 'api_allow_update' => false, 'uri' => 'http://localhost/bars/1/foos', 'output' => null, 'input' => null, 'iri_only' => false, 'operation' => $this->operation, 'skip_null_values' => true, 'exclude_from_cache_key' => ['root_operation', 'operation'], 'skip_deprecated_exception_normalizers' => false]; $this->assertEquals($expected, $this->builder->createFromRequest($request, false)); } @@ -115,7 +115,7 @@ public function testThrowExceptionOnInvalidRequest(): void public function testReuseExistingAttributes(): void { - $expected = ['bar' => 'baz', 'operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => '/foos/1', 'api_allow_update' => false, 'uri' => 'http://localhost/foos/1', 'output' => null, 'input' => null, 'iri_only' => false, 'skip_null_values' => true, 'operation' => $this->operation, 'exclude_from_cache_key' => ['root_operation', 'operation'], 'skip_deprecated_exception_normalizers' => true]; + $expected = ['bar' => 'baz', 'operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => '/foos/1', 'api_allow_update' => false, 'uri' => 'http://localhost/foos/1', 'output' => null, 'input' => null, 'iri_only' => false, 'skip_null_values' => true, 'operation' => $this->operation, 'exclude_from_cache_key' => ['root_operation', 'operation'], 'skip_deprecated_exception_normalizers' => false]; $this->assertEquals($expected, $this->builder->createFromRequest(Request::create('/foos/1'), false, ['resource_class' => 'Foo', 'operation_name' => 'get'])); } diff --git a/src/State/Processor/AddLinkHeaderProcessor.php b/src/State/Processor/AddLinkHeaderProcessor.php index 27b7330d8c8..de738486d63 100644 --- a/src/State/Processor/AddLinkHeaderProcessor.php +++ b/src/State/Processor/AddLinkHeaderProcessor.php @@ -37,7 +37,7 @@ public function process(mixed $data, Operation $operation, array $uriVariables = // We add our header here as Symfony does it only for the main Request and we want it to be done on errors (sub-request) as well $linksProvider = $request->attributes->get('_api_platform_links'); - if ($this->serializer && ($links = $linksProvider->getLinks())) { + if ($this->serializer && ($links = $linksProvider?->getLinks())) { $response->headers->set('Link', $this->serializer->serialize($links)); } diff --git a/src/State/Processor/RespondProcessor.php b/src/State/Processor/RespondProcessor.php index 99e68360e03..b58f97239c0 100644 --- a/src/State/Processor/RespondProcessor.php +++ b/src/State/Processor/RespondProcessor.php @@ -13,6 +13,7 @@ namespace ApiPlatform\State\Processor; +use ApiPlatform\Metadata\Exception\HttpExceptionInterface; use ApiPlatform\Metadata\HttpOperation; use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Metadata\Operation; @@ -24,6 +25,7 @@ use ApiPlatform\Metadata\Util\CloneTrait; use ApiPlatform\State\ProcessorInterface; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface as SymfonyHttpExceptionInterface; /** * Serializes data. @@ -64,6 +66,11 @@ public function process(mixed $data, Operation $operation, array $uriVariables = 'X-Frame-Options' => 'deny', ]; + $exception = $request->attributes->get('exception'); + if (($exception instanceof HttpExceptionInterface || $exception instanceof SymfonyHttpExceptionInterface) && $exceptionHeaders = $exception->getHeaders()) { + $headers = array_merge($headers, $exceptionHeaders); + } + $status = $operation->getStatus(); if ($sunset = $operation->getSunset()) { diff --git a/src/State/Tests/Processor/AddLinkHeaderProcessorTest.php b/src/State/Tests/Processor/AddLinkHeaderProcessorTest.php new file mode 100644 index 00000000000..a18d7980f44 --- /dev/null +++ b/src/State/Tests/Processor/AddLinkHeaderProcessorTest.php @@ -0,0 +1,32 @@ + + * + * 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\State\Tests\Processor; + +use ApiPlatform\Metadata\Get; +use ApiPlatform\State\Processor\AddLinkHeaderProcessor; +use ApiPlatform\State\ProcessorInterface; +use PHPUnit\Framework\TestCase; + +class AddLinkHeaderProcessorTest extends TestCase +{ + public function testWithoutLinks(): void + { + $data = new \stdClass(); + $operation = new Get(); + $decorated = $this->createStub(ProcessorInterface::class); + $decorated->method('process')->willReturn($data); + $processor = new AddLinkHeaderProcessor($decorated); + $this->assertEquals($data, $processor->process($data, $operation)); + } +} diff --git a/src/State/UriVariablesResolverTrait.php b/src/State/UriVariablesResolverTrait.php index b9e222aab0a..b67695b92a9 100644 --- a/src/State/UriVariablesResolverTrait.php +++ b/src/State/UriVariablesResolverTrait.php @@ -13,6 +13,7 @@ namespace ApiPlatform\State; +use ApiPlatform\Api\UriVariablesConverterInterface as LegacyUriVariablesConverterInterface; use ApiPlatform\Metadata\Exception\InvalidIdentifierException; use ApiPlatform\Metadata\HttpOperation; use ApiPlatform\Metadata\UriVariablesConverterInterface; @@ -20,7 +21,7 @@ trait UriVariablesResolverTrait { - private ?UriVariablesConverterInterface $uriVariablesConverter = null; + private null|LegacyUriVariablesConverterInterface|UriVariablesConverterInterface $uriVariablesConverter = null; /** * Resolves an operation's UriVariables to their identifiers values. @@ -52,14 +53,14 @@ private function getOperationUriVariables(HttpOperation $operation = null, array foreach ($currentIdentifiers as $key => $value) { $identifiers[$key] = $value; - $uriVariableMap[$key] = $uriVariableDefinition; + $uriVariablesMap[$key] = $uriVariableDefinition; } continue; } $identifiers[$parameterName] = $parameters[$parameterName]; - $uriVariableMap[$parameterName] = $uriVariableDefinition; + $uriVariablesMap[$parameterName] = $uriVariableDefinition; } if ($this->uriVariablesConverter) { diff --git a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index f292edbe19c..c677474d393 100644 --- a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -127,6 +127,15 @@ public function load(array $configs, ContainerBuilder $container): void $errorFormats['json'] = ['application/problem+json', 'application/json']; } + if (!isset($errorFormats['jsonproblem'])) { + $errorFormats['jsonproblem'] = ['application/problem+json']; + } + + if ($this->isConfigEnabled($container, $config['graphql']) && !isset($formats['json'])) { + trigger_deprecation('api-platform/core', '3.2', 'Add the "json" format to the configuration to use GraphQL.'); + $formats['json'] = ['application/json']; + } + // Backward Compatibility layer if (isset($formats['jsonapi']) && !isset($patchFormats['jsonapi'])) { $patchFormats['jsonapi'] = ['application/vnd.api+json']; @@ -578,13 +587,16 @@ private function registerGraphQlConfiguration(ContainerBuilder $container, array $container->registerForAutoconfiguration(ErrorHandlerInterface::class) ->addTag('api_platform.graphql.error_handler'); - if (!$container->getParameter('kernel.debug')) { - return; - } - /* TODO: remove these in 4.x only one resolver factory is used and we're using providers/processors */ if ($config['event_listeners_backward_compatibility_layer'] ?? true) { + // @TODO: API Platform 3.3 trigger_deprecation('api-platform/core', '3.3', 'In API Platform 4 only one factory "api_platform.graphql.resolver.factory.item" will remain. Stages are deprecated in favor of using a provider/processor.'); + // + deprecate every service from legacy/graphql.xml $loader->load('legacy/graphql.xml'); + + if (!$container->getParameter('kernel.debug')) { + return; + } + $requestStack = new Reference('request_stack', ContainerInterface::NULL_ON_INVALID_REFERENCE); $collectionDataCollectorResolverFactory = (new Definition(DataCollectorResolverFactory::class)) ->setDecoratedService('api_platform.graphql.resolver.factory.collection') diff --git a/src/Symfony/Bundle/Resources/config/hydra.xml b/src/Symfony/Bundle/Resources/config/hydra.xml index d568ef82693..c6dbdda6827 100644 --- a/src/Symfony/Bundle/Resources/config/hydra.xml +++ b/src/Symfony/Bundle/Resources/config/hydra.xml @@ -49,6 +49,8 @@ %kernel.debug% + + diff --git a/src/Symfony/Bundle/Resources/config/json_schema.xml b/src/Symfony/Bundle/Resources/config/json_schema.xml index 4081591327e..2ad6fe11746 100644 --- a/src/Symfony/Bundle/Resources/config/json_schema.xml +++ b/src/Symfony/Bundle/Resources/config/json_schema.xml @@ -16,7 +16,7 @@ - null + diff --git a/src/Symfony/Bundle/Resources/config/jsonapi.xml b/src/Symfony/Bundle/Resources/config/jsonapi.xml index bb2796046ac..b17ca9e3ce8 100644 --- a/src/Symfony/Bundle/Resources/config/jsonapi.xml +++ b/src/Symfony/Bundle/Resources/config/jsonapi.xml @@ -73,6 +73,8 @@ %kernel.debug% + + diff --git a/src/Symfony/Bundle/Resources/config/problem.xml b/src/Symfony/Bundle/Resources/config/problem.xml index c253f393dba..549d7f5ab10 100644 --- a/src/Symfony/Bundle/Resources/config/problem.xml +++ b/src/Symfony/Bundle/Resources/config/problem.xml @@ -22,6 +22,8 @@ %kernel.debug% + + diff --git a/src/Symfony/Bundle/Resources/public/fonts/open-sans/400.css b/src/Symfony/Bundle/Resources/public/fonts/open-sans/400.css index 4a3ee47e66f..9e07f0b1b0b 100644 --- a/src/Symfony/Bundle/Resources/public/fonts/open-sans/400.css +++ b/src/Symfony/Bundle/Resources/public/fonts/open-sans/400.css @@ -2,9 +2,9 @@ @font-face { font-family: 'Open Sans'; font-style: normal; - font-display: swap; + font-display: var(--fontsource-display, swap); font-weight: 400; - src: url(./files/open-sans-cyrillic-ext-400-normal.woff2) format('woff2'), url(./files/open-sans-cyrillic-ext-400-normal.woff) format('woff'); + src: url(./files/open-sans-cyrillic-ext-400-normal.woff2) format('woff2'); unicode-range: U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F; } @@ -12,9 +12,9 @@ @font-face { font-family: 'Open Sans'; font-style: normal; - font-display: swap; + font-display: var(--fontsource-display, swap); font-weight: 400; - src: url(./files/open-sans-cyrillic-400-normal.woff2) format('woff2'), url(./files/open-sans-cyrillic-400-normal.woff) format('woff'); + src: url(./files/open-sans-cyrillic-400-normal.woff2) format('woff2'); unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116; } @@ -22,9 +22,9 @@ @font-face { font-family: 'Open Sans'; font-style: normal; - font-display: swap; + font-display: var(--fontsource-display, swap); font-weight: 400; - src: url(./files/open-sans-greek-ext-400-normal.woff2) format('woff2'), url(./files/open-sans-greek-ext-400-normal.woff) format('woff'); + src: url(./files/open-sans-greek-ext-400-normal.woff2) format('woff2'); unicode-range: U+1F00-1FFF; } @@ -32,9 +32,9 @@ @font-face { font-family: 'Open Sans'; font-style: normal; - font-display: swap; + font-display: var(--fontsource-display, swap); font-weight: 400; - src: url(./files/open-sans-greek-400-normal.woff2) format('woff2'), url(./files/open-sans-greek-400-normal.woff) format('woff'); + src: url(./files/open-sans-greek-400-normal.woff2) format('woff2'); unicode-range: U+0370-03FF; } @@ -42,9 +42,9 @@ @font-face { font-family: 'Open Sans'; font-style: normal; - font-display: swap; + font-display: var(--fontsource-display, swap); font-weight: 400; - src: url(./files/open-sans-hebrew-400-normal.woff2) format('woff2'), url(./files/open-sans-hebrew-400-normal.woff) format('woff'); + src: url(./files/open-sans-hebrew-400-normal.woff2) format('woff2'); unicode-range: U+0590-05FF,U+200C-2010,U+20AA,U+25CC,U+FB1D-FB4F; } @@ -52,9 +52,9 @@ @font-face { font-family: 'Open Sans'; font-style: normal; - font-display: swap; + font-display: var(--fontsource-display, swap); font-weight: 400; - src: url(./files/open-sans-vietnamese-400-normal.woff2) format('woff2'), url(./files/open-sans-vietnamese-400-normal.woff) format('woff'); + src: url(./files/open-sans-vietnamese-400-normal.woff2) format('woff2'); unicode-range: U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB; } @@ -62,18 +62,18 @@ @font-face { font-family: 'Open Sans'; font-style: normal; - font-display: swap; + font-display: var(--fontsource-display, swap); font-weight: 400; - src: url(./files/open-sans-latin-ext-400-normal.woff2) format('woff2'), url(./files/open-sans-latin-ext-400-normal.woff) format('woff'); - unicode-range: U+0100-02AF,U+0304,U+0308,U+0329,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF; + src: url(./files/open-sans-latin-ext-400-normal.woff2) format('woff2'); + unicode-range: U+0100-02AF,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF; } /* open-sans-latin-400-normal */ @font-face { font-family: 'Open Sans'; font-style: normal; - font-display: swap; + font-display: var(--fontsource-display, swap); font-weight: 400; - src: url(./files/open-sans-latin-400-normal.woff2) format('woff2'), url(./files/open-sans-latin-400-normal.woff) format('woff'); - unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; -} \ No newline at end of file + src: url(./files/open-sans-latin-400-normal.woff2) format('woff2'); + unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; +} diff --git a/src/Symfony/Bundle/Resources/public/fonts/open-sans/700.css b/src/Symfony/Bundle/Resources/public/fonts/open-sans/700.css index f20bbcf0684..bc61ca38165 100644 --- a/src/Symfony/Bundle/Resources/public/fonts/open-sans/700.css +++ b/src/Symfony/Bundle/Resources/public/fonts/open-sans/700.css @@ -2,9 +2,9 @@ @font-face { font-family: 'Open Sans'; font-style: normal; - font-display: swap; + font-display: var(--fontsource-display, swap); font-weight: 700; - src: url(./files/open-sans-cyrillic-ext-700-normal.woff2) format('woff2'), url(./files/open-sans-cyrillic-ext-700-normal.woff) format('woff'); + src: url(./files/open-sans-cyrillic-ext-700-normal.woff2) format('woff2'); unicode-range: U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F; } @@ -12,9 +12,9 @@ @font-face { font-family: 'Open Sans'; font-style: normal; - font-display: swap; + font-display: var(--fontsource-display, swap); font-weight: 700; - src: url(./files/open-sans-cyrillic-700-normal.woff2) format('woff2'), url(./files/open-sans-cyrillic-700-normal.woff) format('woff'); + src: url(./files/open-sans-cyrillic-700-normal.woff2) format('woff2'); unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116; } @@ -22,9 +22,9 @@ @font-face { font-family: 'Open Sans'; font-style: normal; - font-display: swap; + font-display: var(--fontsource-display, swap); font-weight: 700; - src: url(./files/open-sans-greek-ext-700-normal.woff2) format('woff2'), url(./files/open-sans-greek-ext-700-normal.woff) format('woff'); + src: url(./files/open-sans-greek-ext-700-normal.woff2) format('woff2'); unicode-range: U+1F00-1FFF; } @@ -32,9 +32,9 @@ @font-face { font-family: 'Open Sans'; font-style: normal; - font-display: swap; + font-display: var(--fontsource-display, swap); font-weight: 700; - src: url(./files/open-sans-greek-700-normal.woff2) format('woff2'), url(./files/open-sans-greek-700-normal.woff) format('woff'); + src: url(./files/open-sans-greek-700-normal.woff2) format('woff2'); unicode-range: U+0370-03FF; } @@ -42,9 +42,9 @@ @font-face { font-family: 'Open Sans'; font-style: normal; - font-display: swap; + font-display: var(--fontsource-display, swap); font-weight: 700; - src: url(./files/open-sans-hebrew-700-normal.woff2) format('woff2'), url(./files/open-sans-hebrew-700-normal.woff) format('woff'); + src: url(./files/open-sans-hebrew-700-normal.woff2) format('woff2'); unicode-range: U+0590-05FF,U+200C-2010,U+20AA,U+25CC,U+FB1D-FB4F; } @@ -52,9 +52,9 @@ @font-face { font-family: 'Open Sans'; font-style: normal; - font-display: swap; + font-display: var(--fontsource-display, swap); font-weight: 700; - src: url(./files/open-sans-vietnamese-700-normal.woff2) format('woff2'), url(./files/open-sans-vietnamese-700-normal.woff) format('woff'); + src: url(./files/open-sans-vietnamese-700-normal.woff2) format('woff2'); unicode-range: U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB; } @@ -62,18 +62,18 @@ @font-face { font-family: 'Open Sans'; font-style: normal; - font-display: swap; + font-display: var(--fontsource-display, swap); font-weight: 700; - src: url(./files/open-sans-latin-ext-700-normal.woff2) format('woff2'), url(./files/open-sans-latin-ext-700-normal.woff) format('woff'); - unicode-range: U+0100-02AF,U+0304,U+0308,U+0329,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF; + src: url(./files/open-sans-latin-ext-700-normal.woff2) format('woff2'); + unicode-range: U+0100-02AF,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF; } /* open-sans-latin-700-normal */ @font-face { font-family: 'Open Sans'; font-style: normal; - font-display: swap; + font-display: var(--fontsource-display, swap); font-weight: 700; - src: url(./files/open-sans-latin-700-normal.woff2) format('woff2'), url(./files/open-sans-latin-700-normal.woff) format('woff'); - unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; -} \ No newline at end of file + src: url(./files/open-sans-latin-700-normal.woff2) format('woff2'); + unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; +} diff --git a/src/Symfony/Bundle/SwaggerUi/SwaggerUiProvider.php b/src/Symfony/Bundle/SwaggerUi/SwaggerUiProvider.php index e93914b3e8a..773d1daec33 100644 --- a/src/Symfony/Bundle/SwaggerUi/SwaggerUiProvider.php +++ b/src/Symfony/Bundle/SwaggerUi/SwaggerUiProvider.php @@ -72,6 +72,6 @@ class: OpenApi::class, // save our operation $request->attributes->set('_api_operation', $swaggerUiOperation); - return $this->openApiFactory->__invoke($context); + return $this->openApiFactory->__invoke(['base_url' => $request->getBaseUrl() ?: '/']); } } diff --git a/src/Symfony/EventListener/ErrorListener.php b/src/Symfony/EventListener/ErrorListener.php index 7ff6c33fee3..d5ae4bd090b 100644 --- a/src/Symfony/EventListener/ErrorListener.php +++ b/src/Symfony/EventListener/ErrorListener.php @@ -67,7 +67,13 @@ public function __construct( protected function duplicateRequest(\Throwable $exception, Request $request): Request { $dup = parent::duplicateRequest($exception, $request); + $apiOperation = $this->initializeOperation($request); + + if ($this->debug) { + $this->logger?->error('An exception occured, transforming to an Error resource.', ['exception' => $exception, 'operation' => $apiOperation]); + } + $format = $this->getRequestFormat($request, $this->errorFormats, false); if ($this->resourceMetadataCollectionFactory) { diff --git a/src/Symfony/EventListener/ReadListener.php b/src/Symfony/EventListener/ReadListener.php index 5e41061aa5e..7cad346945f 100644 --- a/src/Symfony/EventListener/ReadListener.php +++ b/src/Symfony/EventListener/ReadListener.php @@ -13,11 +13,12 @@ namespace ApiPlatform\Symfony\EventListener; -use ApiPlatform\Api\UriVariablesConverterInterface; +use ApiPlatform\Api\UriVariablesConverterInterface as LegacyUriVariablesConverterInterface; use ApiPlatform\Exception\InvalidIdentifierException; use ApiPlatform\Exception\InvalidUriVariableException; use ApiPlatform\Metadata\Put; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\UriVariablesConverterInterface; use ApiPlatform\Metadata\Util\CloneTrait; use ApiPlatform\Serializer\SerializerContextBuilderInterface; use ApiPlatform\State\Exception\ProviderNotFoundException; @@ -44,7 +45,7 @@ public function __construct( private readonly ProviderInterface $provider, ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, private readonly ?SerializerContextBuilderInterface $serializerContextBuilder = null, - UriVariablesConverterInterface $uriVariablesConverter = null, + LegacyUriVariablesConverterInterface|UriVariablesConverterInterface $uriVariablesConverter = null, ) { $this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory; $this->uriVariablesConverter = $uriVariablesConverter; diff --git a/src/Symfony/EventListener/WriteListener.php b/src/Symfony/EventListener/WriteListener.php index 08c5db56ae5..88559a323d7 100644 --- a/src/Symfony/EventListener/WriteListener.php +++ b/src/Symfony/EventListener/WriteListener.php @@ -13,6 +13,9 @@ namespace ApiPlatform\Symfony\EventListener; +use ApiPlatform\Api\IriConverterInterface as LegacyIriConverterInterface; +use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; +use ApiPlatform\Api\UriVariablesConverterInterface as LegacyUriVariablesConverterInterface; use ApiPlatform\Exception\InvalidIdentifierException; use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; @@ -41,10 +44,10 @@ final class WriteListener public function __construct( private readonly ProcessorInterface $processor, - private readonly IriConverterInterface $iriConverter, - private readonly ResourceClassResolverInterface $resourceClassResolver, + private readonly LegacyIriConverterInterface|IriConverterInterface $iriConverter, + private readonly ResourceClassResolverInterface|LegacyResourceClassResolverInterface $resourceClassResolver, ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, - UriVariablesConverterInterface $uriVariablesConverter = null, + LegacyUriVariablesConverterInterface|UriVariablesConverterInterface $uriVariablesConverter = null, ) { $this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory; $this->uriVariablesConverter = $uriVariablesConverter; diff --git a/tests/Doctrine/Common/Filter/OrderFilterTestTrait.php b/tests/Doctrine/Common/Filter/OrderFilterTestTrait.php index 85d432f9e91..f6d1ae833a2 100644 --- a/tests/Doctrine/Common/Filter/OrderFilterTestTrait.php +++ b/tests/Doctrine/Common/Filter/OrderFilterTestTrait.php @@ -325,6 +325,14 @@ private static function provideApplyTestArguments(): array ], ], ], + 'nullable field in relation will be a LEFT JOIN' => [ + [ + 'relatedDummy.name' => null, + ], + [ + 'order' => ['relatedDummy.name' => 'ASC'], + ], + ], ]; } } diff --git a/tests/Doctrine/EventListener/PublishMercureUpdatesListenerTest.php b/tests/Doctrine/EventListener/PublishMercureUpdatesListenerTest.php index 4e9c87b2644..78a5a1680ba 100644 --- a/tests/Doctrine/EventListener/PublishMercureUpdatesListenerTest.php +++ b/tests/Doctrine/EventListener/PublishMercureUpdatesListenerTest.php @@ -13,17 +13,17 @@ namespace ApiPlatform\Tests\Doctrine\EventListener; -use ApiPlatform\Api\IriConverterInterface; -use ApiPlatform\Api\ResourceClassResolverInterface; -use ApiPlatform\Api\UrlGeneratorInterface; use ApiPlatform\Doctrine\EventListener\PublishMercureUpdatesListener; use ApiPlatform\GraphQl\Subscription\MercureSubscriptionIriGeneratorInterface as GraphQlMercureSubscriptionIriGeneratorInterface; use ApiPlatform\GraphQl\Subscription\SubscriptionManagerInterface as GraphQlSubscriptionManagerInterface; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use ApiPlatform\Metadata\ResourceClassResolverInterface; +use ApiPlatform\Metadata\UrlGeneratorInterface; use ApiPlatform\Tests\Fixtures\NotAResource; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyCar; diff --git a/tests/Doctrine/EventListener/PurgeHttpCacheListenerTest.php b/tests/Doctrine/EventListener/PurgeHttpCacheListenerTest.php index 726ecc5dfbe..0d5650dbb01 100644 --- a/tests/Doctrine/EventListener/PurgeHttpCacheListenerTest.php +++ b/tests/Doctrine/EventListener/PurgeHttpCacheListenerTest.php @@ -13,14 +13,14 @@ namespace ApiPlatform\Tests\Doctrine\EventListener; -use ApiPlatform\Api\IriConverterInterface; -use ApiPlatform\Api\ResourceClassResolverInterface; -use ApiPlatform\Api\UrlGeneratorInterface; use ApiPlatform\Doctrine\EventListener\PurgeHttpCacheListener; use ApiPlatform\Exception\InvalidArgumentException; use ApiPlatform\Exception\ItemNotFoundException; use ApiPlatform\HttpCache\PurgerInterface; use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\IriConverterInterface; +use ApiPlatform\Metadata\ResourceClassResolverInterface; +use ApiPlatform\Metadata\UrlGeneratorInterface; use ApiPlatform\Tests\Fixtures\NotAResource; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ContainNonResource; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; @@ -201,13 +201,16 @@ public function testNotAResourceClass(): void // @phpstan-ignore-next-line $dummyClassMetadata->associationMappings = [ 'notAResource' => [], + 'collectionOfNotAResource' => ['targetEntity' => NotAResource::class], ]; $emProphecy->getClassMetadata(ContainNonResource::class)->willReturn($dummyClassMetadata); $eventArgs = new OnFlushEventArgs($emProphecy->reveal()); $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); $propertyAccessorProphecy->isReadable(Argument::type(ContainNonResource::class), 'notAResource')->willReturn(true); + $propertyAccessorProphecy->isReadable(Argument::type(ContainNonResource::class), 'collectionOfNotAResource')->shouldNotBeCalled(); $propertyAccessorProphecy->getValue(Argument::type(ContainNonResource::class), 'notAResource')->shouldBeCalled()->willReturn($nonResource); + $propertyAccessorProphecy->getValue(Argument::type(ContainNonResource::class), 'collectionOfNotAResource')->shouldNotBeCalled(); $listener = new PurgeHttpCacheListener($purgerProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal()); $listener->onFlush($eventArgs); diff --git a/tests/Doctrine/Odm/Extension/OrderExtensionTest.php b/tests/Doctrine/Odm/Extension/OrderExtensionTest.php index ce722cdc0c4..62181535282 100644 --- a/tests/Doctrine/Odm/Extension/OrderExtensionTest.php +++ b/tests/Doctrine/Odm/Extension/OrderExtensionTest.php @@ -19,6 +19,7 @@ use Doctrine\ODM\MongoDB\Aggregation\Builder; use Doctrine\ODM\MongoDB\Aggregation\Stage\Lookup; use Doctrine\ODM\MongoDB\Aggregation\Stage\Sort; +use Doctrine\ODM\MongoDB\Aggregation\Stage\Unwind; use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; use Doctrine\Persistence\ManagerRegistry; @@ -128,7 +129,9 @@ public function testApplyToCollectionWithOrderOverriddenWithAssociation(): void $lookupProphecy->foreignField('_id')->shouldBeCalled()->willReturn($lookupProphecy); $lookupProphecy->alias('author_lkup')->shouldBeCalled(); $aggregationBuilderProphecy->lookup(Dummy::class)->shouldBeCalled()->willReturn($lookupProphecy->reveal()); - $aggregationBuilderProphecy->unwind('$author_lkup')->shouldBeCalled(); + $unwindProphecy = $this->prophesize(Unwind::class); + $unwindProphecy->preserveNullAndEmptyArrays(true)->shouldBeCalled(); + $aggregationBuilderProphecy->unwind('$author_lkup')->shouldBeCalled()->willReturn($unwindProphecy->reveal()); $aggregationBuilderProphecy->getStage(0)->willThrow(new \OutOfRangeException('message')); $aggregationBuilderProphecy->sort(['author_lkup.name' => 'ASC'])->shouldBeCalled(); diff --git a/tests/Doctrine/Odm/Filter/OrderFilterTest.php b/tests/Doctrine/Odm/Filter/OrderFilterTest.php index a1722d8861f..43ceb21de3e 100644 --- a/tests/Doctrine/Odm/Filter/OrderFilterTest.php +++ b/tests/Doctrine/Odm/Filter/OrderFilterTest.php @@ -343,7 +343,10 @@ public static function provideApplyTestData(): array ], ], [ - '$unwind' => '$relatedDummy_lkup', + '$unwind' => [ + 'path' => '$relatedDummy_lkup', + 'preserveNullAndEmptyArrays' => true, + ], ], [ '$sort' => [ @@ -514,7 +517,10 @@ public static function provideApplyTestData(): array ], ], [ - '$unwind' => '$relatedDummy_lkup', + '$unwind' => [ + 'path' => '$relatedDummy_lkup', + 'preserveNullAndEmptyArrays' => true, + ], ], [ '$sort' => [ @@ -546,6 +552,30 @@ public static function provideApplyTestData(): array $orderFilterFactory, EmbeddedDummy::class, ], + 'nullable field in relation will be a LEFT JOIN' => [ + [ + [ + '$lookup' => [ + 'from' => 'RelatedDummy', + 'localField' => 'relatedDummy', + 'foreignField' => '_id', + 'as' => 'relatedDummy_lkup', + ], + ], + [ + '$unwind' => [ + 'path' => '$relatedDummy_lkup', + 'preserveNullAndEmptyArrays' => true, + ], + ], + [ + '$sort' => [ + 'relatedDummy_lkup.name' => 1, + ], + ], + ], + $orderFilterFactory, + ], ] ); } diff --git a/tests/Doctrine/Odm/Filter/SearchFilterTest.php b/tests/Doctrine/Odm/Filter/SearchFilterTest.php index 746db94d55a..845e3a3d11c 100644 --- a/tests/Doctrine/Odm/Filter/SearchFilterTest.php +++ b/tests/Doctrine/Odm/Filter/SearchFilterTest.php @@ -13,9 +13,9 @@ namespace ApiPlatform\Tests\Doctrine\Odm\Filter; -use ApiPlatform\Api\IriConverterInterface; use ApiPlatform\Doctrine\Odm\Filter\SearchFilter; use ApiPlatform\Exception\InvalidArgumentException; +use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Test\DoctrineMongoDbOdmFilterTestCase; use ApiPlatform\Tests\Doctrine\Common\Filter\SearchFilterTestTrait; use ApiPlatform\Tests\Fixtures\TestBundle\Document\Dummy; diff --git a/tests/Doctrine/Orm/Filter/OrderFilterTest.php b/tests/Doctrine/Orm/Filter/OrderFilterTest.php index c33fff16c3c..76454322505 100644 --- a/tests/Doctrine/Orm/Filter/OrderFilterTest.php +++ b/tests/Doctrine/Orm/Filter/OrderFilterTest.php @@ -414,6 +414,11 @@ public static function provideApplyTestData(): array $orderFilterFactory, EmbeddedDummy::class, ], + 'nullable field in relation will be a LEFT JOIN' => [ + sprintf('SELECT o FROM %s o LEFT JOIN o.relatedDummy relatedDummy_a1 ORDER BY relatedDummy_a1.name ASC', Dummy::class), + null, + $orderFilterFactory, + ], ] ); } diff --git a/tests/Doctrine/Orm/Filter/SearchFilterTest.php b/tests/Doctrine/Orm/Filter/SearchFilterTest.php index 37e74519aab..8644ec7a289 100644 --- a/tests/Doctrine/Orm/Filter/SearchFilterTest.php +++ b/tests/Doctrine/Orm/Filter/SearchFilterTest.php @@ -13,11 +13,11 @@ namespace ApiPlatform\Tests\Doctrine\Orm\Filter; -use ApiPlatform\Api\IriConverterInterface; use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; use ApiPlatform\Doctrine\Orm\Util\QueryNameGenerator; use ApiPlatform\Exception\InvalidArgumentException; use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Test\DoctrineOrmFilterTestCase; use ApiPlatform\Tests\Doctrine\Common\Filter\SearchFilterTestTrait; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; diff --git a/tests/Fixtures/TestBundle/ApiResource/Issue5896/Foo.php b/tests/Fixtures/TestBundle/ApiResource/Issue5896/Foo.php new file mode 100644 index 00000000000..59d70e6e924 --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/Issue5896/Foo.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\Issue5896; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\Get; + +#[Get] +class Foo +{ + #[ApiProperty(readable: false, writable: false, identifier: true)] + public ?int $id = null; + public ?LocalDate $expiration; +} diff --git a/tests/Fixtures/TestBundle/ApiResource/Issue5896/LocalDate.php b/tests/Fixtures/TestBundle/ApiResource/Issue5896/LocalDate.php new file mode 100644 index 00000000000..5a63f34d1ac --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/Issue5896/LocalDate.php @@ -0,0 +1,18 @@ + + * + * 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\Issue5896; + +class LocalDate +{ +} diff --git a/tests/Fixtures/TestBundle/ApiResource/Issue5896/TypeFactoryDecorator.php b/tests/Fixtures/TestBundle/ApiResource/Issue5896/TypeFactoryDecorator.php new file mode 100644 index 00000000000..ff28992f826 --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/Issue5896/TypeFactoryDecorator.php @@ -0,0 +1,38 @@ + + * + * 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\Issue5896; + +use ApiPlatform\JsonSchema\Schema; +use ApiPlatform\JsonSchema\TypeFactoryInterface; +use Symfony\Component\PropertyInfo\Type; + +class TypeFactoryDecorator implements TypeFactoryInterface +{ + public function __construct( + private readonly TypeFactoryInterface $decorated, + ) { + } + + public function getType(Type $type, string $format = 'json', bool $readableLink = null, array $serializerContext = null, Schema $schema = null): array + { + if (is_a($type->getClassName(), LocalDate::class, true)) { + return [ + 'type' => 'string', + 'format' => 'date', + ]; + } + + return $this->decorated->getType($type, $format, $readableLink, $serializerContext, $schema); + } +} diff --git a/tests/Fixtures/TestBundle/ApiResource/Issue5921/ExceptionResource.php b/tests/Fixtures/TestBundle/ApiResource/Issue5921/ExceptionResource.php new file mode 100644 index 00000000000..b12a9a3c377 --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/Issue5921/ExceptionResource.php @@ -0,0 +1,27 @@ + + * + * 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\Issue5921; + +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\Tests\Fixtures\TestBundle\Exception\TestException; + +#[Get(uriTemplate: 'issue5921{._format}', read: true, provider: [ExceptionResource::class, 'provide'])] +class ExceptionResource +{ + public static function provide(Operation $operation, array $uriVariables = [], array $context = []): void + { + throw new TestException(); + } +} diff --git a/tests/Fixtures/TestBundle/ApiResource/Issue5924/TooManyRequests.php b/tests/Fixtures/TestBundle/ApiResource/Issue5924/TooManyRequests.php new file mode 100644 index 00000000000..757e282da32 --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/Issue5924/TooManyRequests.php @@ -0,0 +1,27 @@ + + * + * 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\Issue5924; + +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Operation; +use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException; + +#[Get(uriTemplate: 'issue5924{._format}', read: true, provider: [TooManyRequests::class, 'provide'])] +class TooManyRequests +{ + public static function provide(Operation $operation, array $uriVariables = [], array $context = []): void + { + throw new TooManyRequestsHttpException(32); + } +} diff --git a/tests/Fixtures/TestBundle/Entity/UriVariableMask.php b/tests/Fixtures/TestBundle/Entity/UriVariableMask.php new file mode 100644 index 00000000000..f50d611b43c --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/UriVariableMask.php @@ -0,0 +1,32 @@ + + * + * 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; + +use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Post; +use Doctrine\ORM\Mapping as ORM; + +#[Post(uriTemplate: '/uri_variable_mask/{id}/{direction}', processor: [UriVariableMask::class, 'process'])] +#[ORM\Entity] +class UriVariableMask +{ + #[ORM\Column(type: 'string')] + #[ORM\Id] + public ?string $id; + + public static function process($data, Operation $operation, array $uriVariables = []) + { + return $data; + } +} diff --git a/tests/Fixtures/TestBundle/Exception/TestException.php b/tests/Fixtures/TestBundle/Exception/TestException.php new file mode 100644 index 00000000000..141719a15db --- /dev/null +++ b/tests/Fixtures/TestBundle/Exception/TestException.php @@ -0,0 +1,18 @@ + + * + * 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\Exception; + +class TestException extends \Exception +{ +} diff --git a/tests/Fixtures/TestBundle/Serializer/Denormalizer/RelatedDummyPlainIdentifierDenormalizer.php b/tests/Fixtures/TestBundle/Serializer/Denormalizer/RelatedDummyPlainIdentifierDenormalizer.php index 28f9ef27228..1b9c42ff1d8 100644 --- a/tests/Fixtures/TestBundle/Serializer/Denormalizer/RelatedDummyPlainIdentifierDenormalizer.php +++ b/tests/Fixtures/TestBundle/Serializer/Denormalizer/RelatedDummyPlainIdentifierDenormalizer.php @@ -13,8 +13,8 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\Serializer\Denormalizer; +use ApiPlatform\Api\IriConverterInterface; use ApiPlatform\Metadata\Get; -use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Metadata\UrlGeneratorInterface; use ApiPlatform\Tests\Fixtures\TestBundle\Document\RelatedDummy as RelatedDummyDocument; use ApiPlatform\Tests\Fixtures\TestBundle\Document\ThirdLevel as ThirdLevelDocument; diff --git a/tests/Fixtures/TestBundle/Serializer/ErrorNormalizer.php b/tests/Fixtures/TestBundle/Serializer/ErrorNormalizer.php new file mode 100644 index 00000000000..1871a869fd7 --- /dev/null +++ b/tests/Fixtures/TestBundle/Serializer/ErrorNormalizer.php @@ -0,0 +1,52 @@ + + * + * 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\Serializer; + +use ApiPlatform\Tests\Fixtures\TestBundle\Exception\TestException; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + +final class ErrorNormalizer implements NormalizerInterface +{ + public function __construct(private readonly NormalizerInterface $decorated) + { + } + + public function normalize(mixed $object, string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject + { + $a = $this->decorated->normalize($object, $format, $context); + $a['hello'] = 'world'; + + return $a; + } + + public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool + { + if (\is_object($data) && $data instanceof TestException) { + return true; + } + + return $this->decorated->supportsNormalization($data, $format); + } + + public function hasCacheableSupportsMethod(): bool + { + return false; + } + + public function getSupportedTypes(?string $format): array + { + // @phpstan-ignore-next-line + return $this->decorated->getSupportedTypes($format); + } +} diff --git a/tests/Fixtures/app/config/config_common.yml b/tests/Fixtures/app/config/config_common.yml index 449c1d268f3..9e0fb6597a2 100644 --- a/tests/Fixtures/app/config/config_common.yml +++ b/tests/Fixtures/app/config/config_common.yml @@ -457,3 +457,12 @@ services: ApiPlatform\Tests\Fixtures\TestBundle\State\ODMLinkHandledDummyLinksHandler: tags: - {name: 'api_platform.doctrine.odm.links_handler'} + + ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue5896\TypeFactoryDecorator: + decorates: 'api_platform.json_schema.type_factory' + arguments: + $decorated: '@.inner' + + ApiPlatform\Tests\Fixtures\TestBundle\Serializer\ErrorNormalizer: + decorates: 'api_platform.problem.normalizer.error' + arguments: [ '@.inner' ] diff --git a/tests/Hal/Serializer/EntrypointNormalizerTest.php b/tests/Hal/Serializer/EntrypointNormalizerTest.php index eee21751bde..a1af82bda2e 100644 --- a/tests/Hal/Serializer/EntrypointNormalizerTest.php +++ b/tests/Hal/Serializer/EntrypointNormalizerTest.php @@ -14,16 +14,16 @@ namespace ApiPlatform\Tests\Hal\Serializer; use ApiPlatform\Api\Entrypoint; -use ApiPlatform\Api\IriConverterInterface; -use ApiPlatform\Api\UrlGeneratorInterface; use ApiPlatform\Documentation\Entrypoint as DocumentationEntrypoint; use ApiPlatform\Hal\Serializer\EntrypointNormalizer; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use ApiPlatform\Metadata\Resource\ResourceNameCollection; +use ApiPlatform\Metadata\UrlGeneratorInterface; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; diff --git a/tests/Hal/Serializer/ItemNormalizerTest.php b/tests/Hal/Serializer/ItemNormalizerTest.php index 04001d90dbd..c3d5266d39e 100644 --- a/tests/Hal/Serializer/ItemNormalizerTest.php +++ b/tests/Hal/Serializer/ItemNormalizerTest.php @@ -13,13 +13,13 @@ namespace ApiPlatform\Tests\Hal\Serializer; -use ApiPlatform\Api\IriConverterInterface; -use ApiPlatform\Api\ResourceClassResolverInterface; use ApiPlatform\Hal\Serializer\ItemNormalizer; use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\Property\PropertyNameCollection; +use ApiPlatform\Metadata\ResourceClassResolverInterface; use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue5452\ActivableInterface; use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue5452\Author; use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue5452\Book; @@ -132,7 +132,7 @@ public function testNormalize(): void $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); $iriConverterProphecy->getIriFromResource($dummy, Argument::cetera())->willReturn('/dummies/1'); - $iriConverterProphecy->getIriFromResource($relatedDummy)->willReturn('/related-dummies/2'); + $iriConverterProphecy->getIriFromResource($relatedDummy, Argument::cetera())->willReturn('/related-dummies/2'); $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $resourceClassResolverProphecy->isResourceClass(RelatedDummy::class)->willReturn(true); @@ -260,7 +260,7 @@ public function testNormalizeWithoutCache(): void $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); $iriConverterProphecy->getIriFromResource($dummy, Argument::cetera())->willReturn('/dummies/1'); - $iriConverterProphecy->getIriFromResource($relatedDummy)->willReturn('/related-dummies/2'); + $iriConverterProphecy->getIriFromResource($relatedDummy, Argument::cetera())->willReturn('/related-dummies/2'); $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $resourceClassResolverProphecy->getResourceClass($dummy, null)->willReturn(Dummy::class); diff --git a/tests/Hal/Serializer/ObjectNormalizerTest.php b/tests/Hal/Serializer/ObjectNormalizerTest.php index c503e3ead2b..5215714635f 100644 --- a/tests/Hal/Serializer/ObjectNormalizerTest.php +++ b/tests/Hal/Serializer/ObjectNormalizerTest.php @@ -13,8 +13,8 @@ namespace ApiPlatform\Tests\Hal\Serializer; -use ApiPlatform\Api\IriConverterInterface; use ApiPlatform\Hal\Serializer\ObjectNormalizer; +use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; diff --git a/tests/Hydra/Serializer/CollectionNormalizerTest.php b/tests/Hydra/Serializer/CollectionNormalizerTest.php index a479a5601c9..2ba7d9e1eb8 100644 --- a/tests/Hydra/Serializer/CollectionNormalizerTest.php +++ b/tests/Hydra/Serializer/CollectionNormalizerTest.php @@ -13,11 +13,11 @@ namespace ApiPlatform\Tests\Hydra\Serializer; -use ApiPlatform\Api\IriConverterInterface; -use ApiPlatform\Api\ResourceClassResolverInterface; -use ApiPlatform\Api\UrlGeneratorInterface; use ApiPlatform\Hydra\Serializer\CollectionNormalizer; use ApiPlatform\JsonLd\ContextBuilderInterface; +use ApiPlatform\Metadata\IriConverterInterface; +use ApiPlatform\Metadata\ResourceClassResolverInterface; +use ApiPlatform\Metadata\UrlGeneratorInterface; use ApiPlatform\Serializer\AbstractItemNormalizer; use ApiPlatform\State\Pagination\PaginatorInterface; use ApiPlatform\State\Pagination\PartialPaginatorInterface; diff --git a/tests/Hydra/Serializer/EntrypointNormalizerTest.php b/tests/Hydra/Serializer/EntrypointNormalizerTest.php index da009ff2afc..69b0f7501c9 100644 --- a/tests/Hydra/Serializer/EntrypointNormalizerTest.php +++ b/tests/Hydra/Serializer/EntrypointNormalizerTest.php @@ -14,16 +14,16 @@ namespace ApiPlatform\Tests\Hydra\Serializer; use ApiPlatform\Api\Entrypoint; -use ApiPlatform\Api\IriConverterInterface; -use ApiPlatform\Api\UrlGeneratorInterface; use ApiPlatform\Documentation\Entrypoint as DocumentationEntrypoint; use ApiPlatform\Hydra\Serializer\EntrypointNormalizer; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use ApiPlatform\Metadata\Resource\ResourceNameCollection; +use ApiPlatform\Metadata\UrlGeneratorInterface; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\FooDummy; use PHPUnit\Framework\TestCase; diff --git a/tests/JsonApi/Serializer/EntrypointNormalizerTest.php b/tests/JsonApi/Serializer/EntrypointNormalizerTest.php index 2bcfa0b1941..eb035c848cc 100644 --- a/tests/JsonApi/Serializer/EntrypointNormalizerTest.php +++ b/tests/JsonApi/Serializer/EntrypointNormalizerTest.php @@ -14,19 +14,19 @@ namespace ApiPlatform\Tests\JsonApi\Serializer; use ApiPlatform\Api\Entrypoint; -use ApiPlatform\Api\IriConverterInterface; -use ApiPlatform\Api\UrlGeneratorInterface; use ApiPlatform\Documentation\Entrypoint as DocumentationEntrypoint; use ApiPlatform\JsonApi\Serializer\EntrypointNormalizer; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Post; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use ApiPlatform\Metadata\Resource\ResourceNameCollection; +use ApiPlatform\Metadata\UrlGeneratorInterface; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyCar; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy; diff --git a/tests/JsonApi/Serializer/ItemNormalizerTest.php b/tests/JsonApi/Serializer/ItemNormalizerTest.php index 802537cd711..e8e40f06c04 100644 --- a/tests/JsonApi/Serializer/ItemNormalizerTest.php +++ b/tests/JsonApi/Serializer/ItemNormalizerTest.php @@ -13,19 +13,19 @@ namespace ApiPlatform\Tests\JsonApi\Serializer; -use ApiPlatform\Api\IriConverterInterface; -use ApiPlatform\Api\ResourceClassResolverInterface; use ApiPlatform\JsonApi\Serializer\ItemNormalizer; use ApiPlatform\JsonApi\Serializer\ReservedAttributeNameConverter; use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\Property\PropertyNameCollection; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use ApiPlatform\Metadata\ResourceClassResolverInterface; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\CircularReference; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy; diff --git a/tests/JsonLd/ContextBuilderTest.php b/tests/JsonLd/ContextBuilderTest.php index 7671ba6284e..466210fb0c2 100644 --- a/tests/JsonLd/ContextBuilderTest.php +++ b/tests/JsonLd/ContextBuilderTest.php @@ -13,12 +13,11 @@ namespace ApiPlatform\Tests\JsonLd; -use ApiPlatform\Api\IriConverterInterface; -use ApiPlatform\Api\UrlGeneratorInterface; use ApiPlatform\JsonLd\ContextBuilder; use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; @@ -27,6 +26,7 @@ use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use ApiPlatform\Metadata\Resource\ResourceNameCollection; +use ApiPlatform\Metadata\UrlGeneratorInterface; use ApiPlatform\Tests\Fixtures\TestBundle\Dto\OutputDto; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; use PHPUnit\Framework\TestCase; diff --git a/tests/JsonLd/Serializer/ItemNormalizerTest.php b/tests/JsonLd/Serializer/ItemNormalizerTest.php index be607b30d84..9f062bac54d 100644 --- a/tests/JsonLd/Serializer/ItemNormalizerTest.php +++ b/tests/JsonLd/Serializer/ItemNormalizerTest.php @@ -13,20 +13,20 @@ namespace ApiPlatform\Tests\JsonLd\Serializer; -use ApiPlatform\Api\IriConverterInterface; -use ApiPlatform\Api\ResourceClassResolverInterface; -use ApiPlatform\Api\UrlGeneratorInterface; use ApiPlatform\JsonLd\ContextBuilderInterface; use ApiPlatform\JsonLd\Serializer\ItemNormalizer; use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\Property\PropertyNameCollection; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use ApiPlatform\Metadata\ResourceClassResolverInterface; +use ApiPlatform\Metadata\UrlGeneratorInterface; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; use PHPUnit\Framework\TestCase; use Prophecy\Argument; diff --git a/tests/JsonLd/Serializer/ObjectNormalizerTest.php b/tests/JsonLd/Serializer/ObjectNormalizerTest.php index 99e8da1d576..65dcfe619b4 100644 --- a/tests/JsonLd/Serializer/ObjectNormalizerTest.php +++ b/tests/JsonLd/Serializer/ObjectNormalizerTest.php @@ -13,9 +13,9 @@ namespace ApiPlatform\Tests\JsonLd\Serializer; -use ApiPlatform\Api\IriConverterInterface; use ApiPlatform\JsonLd\AnonymousContextBuilderInterface; use ApiPlatform\JsonLd\Serializer\ObjectNormalizer; +use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; use PHPUnit\Framework\TestCase; use Prophecy\Argument; diff --git a/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php b/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php index 1c338717d5c..75ed827f8fb 100644 --- a/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php +++ b/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php @@ -124,4 +124,18 @@ public function testArraySchemaWithReference(): void '$ref' => '#/definitions/TestEntity.jsonld-write', ]); } + + /** + * TODO: add deprecation (TypeFactory will be deprecated in api platform 3.3). + * + * @group legacy + */ + public function testArraySchemaWithTypeFactory(): void + { + $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => 'ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue5896\Foo', '--type' => 'output']); + $result = $this->tester->getDisplay(); + $json = json_decode($result, associative: true); + + $this->assertEquals($json['definitions']['Foo.jsonld']['properties']['expiration'], ['type' => 'string', 'format' => 'date']); + } } diff --git a/tests/Problem/Serializer/ErrorNormalizerTest.php b/tests/Problem/Serializer/ErrorNormalizerTest.php index 07c1e3ef8ac..db477fe584d 100644 --- a/tests/Problem/Serializer/ErrorNormalizerTest.php +++ b/tests/Problem/Serializer/ErrorNormalizerTest.php @@ -38,11 +38,14 @@ public function testSupportNormalization(): void $this->assertTrue($normalizer->supportsNormalization(new FlattenException(), ErrorNormalizer::FORMAT)); $this->assertFalse($normalizer->supportsNormalization(new FlattenException(), 'xml')); $this->assertFalse($normalizer->supportsNormalization(new \stdClass(), ErrorNormalizer::FORMAT)); - $this->assertEmpty($normalizer->getSupportedTypes('json')); $this->assertSame([ \Exception::class => false, FlattenException::class => false, ], $normalizer->getSupportedTypes($normalizer::FORMAT)); + $this->assertSame([ + \Exception::class => false, + FlattenException::class => false, + ], $normalizer->getSupportedTypes('json')); // note: jsonproblem is the default for json if (!method_exists(Serializer::class, 'getSupportedTypes')) { $this->assertFalse($normalizer->hasCacheableSupportsMethod()); diff --git a/tests/State/RespondProcessorTest.php b/tests/State/RespondProcessorTest.php index 4ccb52445a4..935d4109640 100644 --- a/tests/State/RespondProcessorTest.php +++ b/tests/State/RespondProcessorTest.php @@ -13,8 +13,8 @@ namespace ApiPlatform\Tests\State; -use ApiPlatform\Api\IriConverterInterface; use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Metadata\Operation\Factory\OperationMetadataFactoryInterface; use ApiPlatform\Metadata\ResourceClassResolverInterface; use ApiPlatform\State\Processor\RespondProcessor; @@ -25,6 +25,7 @@ use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException; class RespondProcessorTest extends TestCase { @@ -97,4 +98,19 @@ public function testRedirectToOperation(): void $this->assertSame(200, $response->getStatusCode()); $this->assertNull($response->headers->get('Location')); } + + public function testAddsExceptionHeaders(): void + { + $operation = new Get(); + + /** @var ProcessorInterface $respondProcessor */ + $respondProcessor = new RespondProcessor(); + $req = new Request(); + $req->attributes->set('exception', new TooManyRequestsHttpException(32)); + $response = $respondProcessor->process('content', new Get(), context: [ + 'request' => $req, + ]); + + $this->assertSame('32', $response->headers->get('retry-after')); + } } diff --git a/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php index 3458994a647..0c355e9edef 100644 --- a/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ b/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php @@ -88,6 +88,7 @@ class ApiPlatformExtensionTest extends TestCase 'description' => 'description', 'version' => 'version', 'formats' => [ + 'json' => ['mime_types' => ['json']], 'jsonld' => ['mime_types' => ['application/ld+json']], 'jsonhal' => ['mime_types' => ['application/hal+json']], ], @@ -1260,4 +1261,28 @@ public function testHasClassMetadataCache(): void $this->assertFalse($this->container->hasDefinition('api_platform.serializer.mapping.cache_class_metadata_factory')); } + + /** + * @group legacy + */ + public function testLegacyGraphQlConfigurationWithoutJsonFormat(): void + { + $this->expectDeprecation('Since api-platform/core 3.2: Add the "json" format to the configuration to use GraphQL.'); + $config = self::DEFAULT_CONFIG; + unset($config['api_platform']['formats']['json']); + + (new ApiPlatformExtension())->load($config, $this->container); + $this->assertArrayHasKey('json', $this->container->getParameter('api_platform.formats')); + } + + /** + * @see https://github.com/api-platform/core/issues/5919 + */ + public function testGraphQlLegacyConfigurationInDebugMode(): void + { + $config = self::DEFAULT_CONFIG; + + (new ApiPlatformExtension())->load($config, $this->container); + $this->assertTrue($this->container->hasDefinition('api_platform.graphql.resolver.factory.item')); + } } diff --git a/tests/Symfony/Bundle/SwaggerUi/SwaggerUiProviderTest.php b/tests/Symfony/Bundle/SwaggerUi/SwaggerUiProviderTest.php new file mode 100644 index 00000000000..0f88a4af50f --- /dev/null +++ b/tests/Symfony/Bundle/SwaggerUi/SwaggerUiProviderTest.php @@ -0,0 +1,42 @@ + + * + * 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\Symfony\Bundle\SwaggerUi; + +use ApiPlatform\Documentation\Documentation; +use ApiPlatform\Metadata\Get; +use ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface; +use ApiPlatform\OpenApi\Model\Info; +use ApiPlatform\OpenApi\Model\Paths; +use ApiPlatform\OpenApi\OpenApi; +use ApiPlatform\State\ProviderInterface; +use ApiPlatform\Symfony\Bundle\SwaggerUi\SwaggerUiProvider; +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\ParameterBag; +use Symfony\Component\HttpFoundation\Request; + +class SwaggerUiProviderTest extends TestCase +{ + public function testProvideWithBaseUrl(): void + { + $openapiFactory = $this->createMock(OpenApiFactoryInterface::class); + $request = $this->createStub(Request::class); + $request->attributes = new ParameterBag(); + $request->method('getRequestFormat')->willReturn('html'); + $request->method('getBaseUrl')->willReturn('test'); + $decorated = $this->createStub(ProviderInterface::class); + $provider = new SwaggerUiProvider($decorated, $openapiFactory); + $openapiFactory->expects($this->once())->method('__invoke')->with(['base_url' => 'test'])->willReturn(new OpenApi(new Info('test', '1'), [], new Paths())); + $provider->provide(new Get(class: Documentation::class), [], ['request' => $request]); + } +} diff --git a/tests/Symfony/Bundle/Test/ApiTestCaseTest.php b/tests/Symfony/Bundle/Test/ApiTestCaseTest.php index 5a2984a59c5..4143e7385cf 100644 --- a/tests/Symfony/Bundle/Test/ApiTestCaseTest.php +++ b/tests/Symfony/Bundle/Test/ApiTestCaseTest.php @@ -24,9 +24,12 @@ use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Tools\SchemaTool; use PHPUnit\Framework\ExpectationFailedException; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; class ApiTestCaseTest extends ApiTestCase { + use ExpectDeprecationTrait; + public function testAssertJsonContains(): void { self::createClient()->request('GET', '/'); @@ -268,6 +271,24 @@ public function testGetMercureMessages(): void ); } + /** + * @group legacy + */ + public function testExceptionNormalizer(): void + { + $this->expectDeprecation('Since api-platform 3.2: The class "ApiPlatform\Problem\Serializer\ErrorNormalizer" is deprecated in favor of using an Error resource. We fallback on "api_platform.serializer.normalizer.item".'); + + $response = self::createClient()->request('GET', '/issue5921', [ + 'headers' => [ + 'accept' => 'application/json', + ], + ]); + + $data = $response->toArray(false); + $this->assertArrayHasKey('hello', $data); + $this->assertEquals($data['hello'], 'world'); + } + private function recreateSchema(array $options = []): void { self::bootKernel($options); diff --git a/tests/Symfony/Bundle/Twig/ApiPlatformProfilerPanelTest.php b/tests/Symfony/Bundle/Twig/ApiPlatformProfilerPanelTest.php index 27636deb23b..81a8989181f 100644 --- a/tests/Symfony/Bundle/Twig/ApiPlatformProfilerPanelTest.php +++ b/tests/Symfony/Bundle/Twig/ApiPlatformProfilerPanelTest.php @@ -115,7 +115,7 @@ public function testProfilerGeneralLayoutNotResourceClass(): void $this->assertSame(200, $client->getResponse()->getStatusCode()); // Check that the Api-Platform sidebar link is active - $this->assertNotEmpty($menuLink = $crawler->filter('a[href$="panel=api_platform.data_collector.request"]')); + $this->assertNotEmpty($menuLink = $crawler->filter('a[href*="panel=api_platform.data_collector.request"]')); $this->assertNotEmpty($menuLink->filter('.disabled'), 'The sidebar menu should be disabled.'); $metrics = $crawler->filter('.metrics'); diff --git a/tests/Symfony/EventListener/WriteListenerTest.php b/tests/Symfony/EventListener/WriteListenerTest.php index 957ec3f1e0d..9c9992599d7 100644 --- a/tests/Symfony/EventListener/WriteListenerTest.php +++ b/tests/Symfony/EventListener/WriteListenerTest.php @@ -13,11 +13,10 @@ namespace ApiPlatform\Tests\Symfony\EventListener; -use ApiPlatform\Api\IriConverterInterface; -use ApiPlatform\Api\ResourceClassResolverInterface; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Delete; use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Metadata\Link; use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Operations; @@ -26,6 +25,7 @@ use ApiPlatform\Metadata\Put; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use ApiPlatform\Metadata\ResourceClassResolverInterface; use ApiPlatform\State\ProcessorInterface; use ApiPlatform\Symfony\EventListener\WriteListener; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\AttributeResource;