From e96623ebfd8691ba943bdb56a4d91e160497a311 Mon Sep 17 00:00:00 2001 From: Antoine Bluchet Date: Mon, 14 Oct 2024 10:59:27 +0200 Subject: [PATCH 1/8] fix(jsonld): prefix error @type with hydra: (#6721) --- features/main/relation.feature | 2 +- features/serializer/vo_relations.feature | 2 +- src/JsonLd/Serializer/ErrorNormalizer.php | 7 +++- .../JsonLd/Serializer/ErrorNormalizerTest.php | 35 +++++++++++++++++++ 4 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 tests/JsonLd/Serializer/ErrorNormalizerTest.php diff --git a/features/main/relation.feature b/features/main/relation.feature index 206a4789ffe..0f16560dfbe 100644 --- a/features/main/relation.feature +++ b/features/main/relation.feature @@ -493,7 +493,7 @@ Feature: Relations support "properties": { "@type": { "type": "string", - "pattern": "^Error$" + "pattern": "^hydra:Error$" }, "title": { "type": "string", diff --git a/features/serializer/vo_relations.feature b/features/serializer/vo_relations.feature index ccf49439814..63600d7b8a1 100644 --- a/features/serializer/vo_relations.feature +++ b/features/serializer/vo_relations.feature @@ -148,7 +148,7 @@ Feature: Value object as ApiResource "properties": { "@type": { "type": "string", - "pattern": "^Error$" + "pattern": "^hydra:Error$" }, "title": { "type": "string", diff --git a/src/JsonLd/Serializer/ErrorNormalizer.php b/src/JsonLd/Serializer/ErrorNormalizer.php index b5bf3eec6c6..d1b27e9d3f0 100644 --- a/src/JsonLd/Serializer/ErrorNormalizer.php +++ b/src/JsonLd/Serializer/ErrorNormalizer.php @@ -28,12 +28,17 @@ public function __construct(private readonly NormalizerInterface $inner, private public function normalize(mixed $object, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null { + $context += $this->defaultContext; $normalized = $this->inner->normalize($object, $format, $context); - $hydraPrefix = $this->getHydraPrefix($context + $this->defaultContext); + $hydraPrefix = $this->getHydraPrefix($context); if (!$hydraPrefix) { return $normalized; } + if ('Error' === $normalized['@type']) { + $normalized['@type'] = 'hydra:Error'; + } + if (isset($normalized['description'])) { $normalized['hydra:description'] = $normalized['description']; } diff --git a/tests/JsonLd/Serializer/ErrorNormalizerTest.php b/tests/JsonLd/Serializer/ErrorNormalizerTest.php new file mode 100644 index 00000000000..916217d409d --- /dev/null +++ b/tests/JsonLd/Serializer/ErrorNormalizerTest.php @@ -0,0 +1,35 @@ + + * + * 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\JsonLd\Serializer; + +use ApiPlatform\JsonLd\ContextBuilder; +use ApiPlatform\JsonLd\Serializer\ErrorNormalizer; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + +final class ErrorNormalizerTest extends TestCase +{ + public function testAddHydraPrefix(): void + { + $provider = $this->createMock(NormalizerInterface::class); + $provider->method('normalize')->willReturn(['@type' => 'Error', 'title' => 'foo', 'description' => 'bar']); + $errorNormalizer = new ErrorNormalizer($provider, ['hydra_prefix' => ContextBuilder::HYDRA_CONTEXT_HAS_PREFIX]); + $res = $errorNormalizer->normalize(new \stdClass()); + $this->assertEquals('hydra:Error', $res['@type']); + $this->assertArrayHasKey('hydra:description', $res); + $this->assertEquals($res['hydra:description'], $res['description']); + $this->assertArrayHasKey('hydra:title', $res); + $this->assertEquals($res['hydra:title'], $res['title']); + } +} From 99262dce739800bd841c95e026848b587ba25801 Mon Sep 17 00:00:00 2001 From: Martin Chudoba Date: Mon, 14 Oct 2024 11:09:36 +0200 Subject: [PATCH 2/8] fix(jsonschema): handle @id when genId is false (#6716) Co-authored-by: soyuka --- src/JsonSchema/SchemaFactory.php | 8 ++++++++ .../Command/JsonSchemaGenerateCommandTest.php | 11 +++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/JsonSchema/SchemaFactory.php b/src/JsonSchema/SchemaFactory.php index 610c242d175..9c6c6a38a35 100644 --- a/src/JsonSchema/SchemaFactory.php +++ b/src/JsonSchema/SchemaFactory.php @@ -232,6 +232,14 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str continue; } + if (false === $propertyMetadata->getGenId()) { + $subDefinitionName = $this->definitionNameFactory->create($className, $format, $className, null, $serializerContext); + + if (isset($subSchema->getDefinitions()[$subDefinitionName])) { + unset($subSchema->getDefinitions()[$subDefinitionName]['properties']['@id']); + } + } + if ($isCollection) { $propertySchema['items']['$ref'] = $subSchema['$ref']; unset($propertySchema['items']['type']); diff --git a/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php b/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php index c84baa4aafc..3f64481dc6a 100644 --- a/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php +++ b/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php @@ -334,4 +334,15 @@ public function testResourceWithEnumPropertiesSchema(): void $properties['genders'] ); } + + /** + * Test feature #6716. + */ + public function testGenId(): void + { + $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => 'ApiPlatform\Tests\Fixtures\TestBundle\Entity\DisableIdGeneration', '--type' => 'output', '--format' => 'jsonld']); + $result = $this->tester->getDisplay(); + $json = json_decode($result, associative: true); + $this->assertArrayNotHasKey('@id', $json['definitions']['DisableIdGenerationItem.jsonld']['properties']); + } } From 550347867f30611b673d8df99f65186d013919dd Mon Sep 17 00:00:00 2001 From: Antoine Bluchet Date: Wed, 16 Oct 2024 08:37:42 +0200 Subject: [PATCH 3/8] fix(graphql): register query parameter arguments with filters (#6727) --- src/GraphQl/Type/FieldsBuilder.php | 95 +++++++++++++++++------------- 1 file changed, 55 insertions(+), 40 deletions(-) diff --git a/src/GraphQl/Type/FieldsBuilder.php b/src/GraphQl/Type/FieldsBuilder.php index dfcf19492e8..438602ee70d 100644 --- a/src/GraphQl/Type/FieldsBuilder.php +++ b/src/GraphQl/Type/FieldsBuilder.php @@ -19,6 +19,7 @@ use ApiPlatform\GraphQl\Resolver\Factory\ResolverFactory; use ApiPlatform\GraphQl\Resolver\Factory\ResolverFactoryInterface; use ApiPlatform\GraphQl\Type\Definition\TypeInterface; +use ApiPlatform\Metadata\FilterInterface; use ApiPlatform\Metadata\GraphQl\Mutation; use ApiPlatform\Metadata\GraphQl\Operation; use ApiPlatform\Metadata\GraphQl\Query; @@ -292,41 +293,6 @@ public function resolveResourceArgs(array $args, Operation $operation): array $args[$id]['type'] = $this->typeConverter->resolveType($arg['type']); } - /* - * This is @experimental, read the comment on the parameterToObjectType function as additional information. - */ - foreach ($operation->getParameters() ?? [] as $parameter) { - $key = $parameter->getKey(); - - if (str_contains($key, ':property')) { - if (!($filterId = $parameter->getFilter()) || !$this->filterLocator->has($filterId)) { - continue; - } - - $parsedKey = explode('[:property]', $key); - $flattenFields = []; - foreach ($this->filterLocator->get($filterId)->getDescription($operation->getClass()) as $key => $value) { - $values = []; - parse_str($key, $values); - if (isset($values[$parsedKey[0]])) { - $values = $values[$parsedKey[0]]; - } - - $name = key($values); - $flattenFields[] = ['name' => $name, 'required' => $value['required'] ?? null, 'description' => $value['description'] ?? null, 'leafs' => $values[$name], 'type' => $value['type'] ?? 'string']; - } - - $args[$parsedKey[0]] = $this->parameterToObjectType($flattenFields, $parsedKey[0]); - continue; - } - - $args[$key] = ['type' => GraphQLType::string()]; - - if ($parameter->getRequired()) { - $args[$key]['type'] = GraphQLType::nonNull($args[$key]['type']); - } - } - return $args; } @@ -448,12 +414,15 @@ private function getResourceFieldConfiguration(?string $property, ?string $field $args = []; - if (!$input && !$rootOperation instanceof Mutation && !$rootOperation instanceof Subscription && !$isStandardGraphqlType && $isCollectionType) { - if (!$this->isEnumClass($resourceClass) && $this->pagination->isGraphQlEnabled($resourceOperation)) { - $args = $this->getGraphQlPaginationArgs($resourceOperation); - } + if (!$input && !$rootOperation instanceof Mutation && !$rootOperation instanceof Subscription && !$isStandardGraphqlType) { + if ($isCollectionType) { + if (!$this->isEnumClass($resourceClass) && $this->pagination->isGraphQlEnabled($resourceOperation)) { + $args = $this->getGraphQlPaginationArgs($resourceOperation); + } - $args = $this->getFilterArgs($args, $resourceClass, $rootResource, $resourceOperation, $rootOperation, $property, $depth); + $args = $this->getFilterArgs($args, $resourceClass, $rootResource, $resourceOperation, $rootOperation, $property, $depth); + $args = $this->getParameterArgs($rootOperation, $args); + } } if ($this->itemResolverFactory instanceof ResolverFactory) { @@ -488,6 +457,52 @@ private function getResourceFieldConfiguration(?string $property, ?string $field return null; } + /* + * This function is @experimental, read the comment on the parameterToObjectType function for additional information. + * @experimental + */ + private function getParameterArgs(Operation $operation, array $args = []): array + { + foreach ($operation->getParameters() ?? [] as $parameter) { + $key = $parameter->getKey(); + + if (!str_contains($key, ':property')) { + $args[$key] = ['type' => GraphQLType::string()]; + + if ($parameter->getRequired()) { + $args[$key]['type'] = GraphQLType::nonNull($args[$key]['type']); + } + + continue; + } + + if (!($filterId = $parameter->getFilter()) || !$this->filterLocator->has($filterId)) { + continue; + } + + $filter = $this->filterLocator->get($filterId); + $parsedKey = explode('[:property]', $key); + $flattenFields = []; + + if ($filter instanceof FilterInterface) { + foreach ($filter->getDescription($operation->getClass()) as $name => $value) { + $values = []; + parse_str($name, $values); + if (isset($values[$parsedKey[0]])) { + $values = $values[$parsedKey[0]]; + } + + $name = key($values); + $flattenFields[] = ['name' => $name, 'required' => $value['required'] ?? null, 'description' => $value['description'] ?? null, 'leafs' => $values[$name], 'type' => $value['type'] ?? 'string']; + } + + $args[$parsedKey[0]] = $this->parameterToObjectType($flattenFields, $parsedKey[0]); + } + } + + return $args; + } + private function getGraphQlPaginationArgs(Operation $queryOperation): array { $paginationType = $this->pagination->getGraphQlPaginationType($queryOperation); From d34cd7be8e7a12fd08a8b10270a614c06c10aa89 Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Wed, 16 Oct 2024 10:45:47 +0200 Subject: [PATCH 4/8] fix: use stateOptions when retrieving a Parameter filter (#6728) Co-authored-by: soyuka --- ...meterResourceMetadataCollectionFactory.php | 29 +++++- .../TestBundle/ApiResource/AgentApi.php | 90 ++++++++++++++++++ .../TestBundle/Document/AgentDocument.php | 95 +++++++++++++++++++ tests/Fixtures/TestBundle/Entity/Agent.php | 82 ++++++++++++++++ .../QueryParameterStateOptionsTest.php | 91 ++++++++++++++++++ 5 files changed, 382 insertions(+), 5 deletions(-) create mode 100644 tests/Fixtures/TestBundle/ApiResource/AgentApi.php create mode 100644 tests/Fixtures/TestBundle/Document/AgentDocument.php create mode 100644 tests/Fixtures/TestBundle/Entity/Agent.php create mode 100644 tests/Functional/Parameters/QueryParameterStateOptionsTest.php diff --git a/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php index c9f8500847d..c422696a95b 100644 --- a/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php @@ -13,9 +13,12 @@ namespace ApiPlatform\Metadata\Resource\Factory; +use ApiPlatform\Doctrine\Odm\State\Options as DoctrineOdmOptions; +use ApiPlatform\Doctrine\Orm\State\Options as DoctrineOrmOptions; use ApiPlatform\Metadata\FilterInterface; use ApiPlatform\Metadata\HeaderParameterInterface; use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Parameter; use ApiPlatform\Metadata\Parameters; use ApiPlatform\Metadata\QueryParameter; @@ -54,6 +57,7 @@ public function create(string $resourceClass): ResourceMetadataCollection $resourceMetadataCollection = $this->decorated?->create($resourceClass) ?? new ResourceMetadataCollection($resourceClass); foreach ($resourceMetadataCollection as $i => $resource) { + $resourceClass = $resource->getClass(); $operations = $resource->getOperations(); $internalPriority = -1; @@ -61,7 +65,7 @@ public function create(string $resourceClass): ResourceMetadataCollection $parameters = $operation->getParameters() ?? new Parameters(); foreach ($parameters as $key => $parameter) { $key = $parameter->getKey() ?? $key; - $parameter = $this->setDefaults($key, $parameter, $resourceClass); + $parameter = $this->setDefaults($key, $parameter, $resourceClass, $operation); $priority = $parameter->getPriority() ?? $internalPriority--; $parameters->add($key, $parameter->withPriority($priority)); } @@ -87,7 +91,7 @@ public function create(string $resourceClass): ResourceMetadataCollection $parameters = $operation->getParameters() ?? new Parameters(); foreach ($operation->getParameters() ?? [] as $key => $parameter) { $key = $parameter->getKey() ?? $key; - $parameter = $this->setDefaults($key, $parameter, $resourceClass); + $parameter = $this->setDefaults($key, $parameter, $resourceClass, $operation); $priority = $parameter->getPriority() ?? $internalPriority--; $parameters->add($key, $parameter->withPriority($priority)); } @@ -101,7 +105,7 @@ public function create(string $resourceClass): ResourceMetadataCollection return $resourceMetadataCollection; } - private function setDefaults(string $key, Parameter $parameter, string $resourceClass): Parameter + private function setDefaults(string $key, Parameter $parameter, string $resourceClass, Operation $operation): Parameter { if (null === $parameter->getKey()) { $parameter = $parameter->withKey($key); @@ -117,7 +121,7 @@ private function setDefaults(string $key, Parameter $parameter, string $resource } // Read filter description to populate the Parameter - $description = $filter instanceof FilterInterface ? $filter->getDescription($resourceClass) : []; + $description = $filter instanceof FilterInterface ? $filter->getDescription($this->getFilterClass($operation)) : []; if (($schema = $description[$key]['schema'] ?? null) && null === $parameter->getSchema()) { $parameter = $parameter->withSchema($schema); } @@ -242,7 +246,7 @@ private function addFilterValidation(HttpOperation $operation): Parameters } $filter = $this->filterLocator->get($filter); - foreach ($filter->getDescription($operation->getClass()) as $parameterName => $definition) { + foreach ($filter->getDescription($this->getFilterClass($operation)) as $parameterName => $definition) { $key = $parameterName; $required = $definition['required'] ?? false; $schema = $definition['schema'] ?? null; @@ -296,4 +300,19 @@ private function addFilterValidation(HttpOperation $operation): Parameters return $parameters; } + + private function getFilterClass(Operation $operation): ?string + { + $stateOptions = $operation->getStateOptions(); + + if ($stateOptions instanceof DoctrineOrmOptions) { + return $stateOptions->getEntityClass(); + } + + if ($stateOptions instanceof DoctrineOdmOptions) { + return $stateOptions->getDocumentClass(); + } + + return $operation->getClass(); + } } diff --git a/tests/Fixtures/TestBundle/ApiResource/AgentApi.php b/tests/Fixtures/TestBundle/ApiResource/AgentApi.php new file mode 100644 index 00000000000..9d040921bb1 --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/AgentApi.php @@ -0,0 +1,90 @@ + + * + * 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; + +use ApiPlatform\Doctrine\Odm\Filter\DateFilter as OdmDateFilter; +use ApiPlatform\Doctrine\Odm\State\Options as OdmOptions; +use ApiPlatform\Doctrine\Orm\Filter\DateFilter; +use ApiPlatform\Doctrine\Orm\State\Options; +use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\QueryParameter; +use ApiPlatform\Tests\Fixtures\TestBundle\Document\AgentDocument; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Agent; + +#[ApiFilter(DateFilter::class, properties: ['birthday'], alias: 'app_filter_date')] +#[ApiResource( + shortName: 'Agent', + operations: [ + new GetCollection(parameters: [ + 'birthday' => new QueryParameter(filter: 'app_filter_date'), + ]), + ], + stateOptions: new Options(entityClass: Agent::class) +)] +#[ApiFilter(OdmDateFilter::class, properties: ['birthday'], alias: 'app_filter_date_odm')] +#[ApiResource( + shortName: 'AgentDocument', + operations: [ + new GetCollection(parameters: [ + 'birthday' => new QueryParameter(filter: 'app_filter_date_odm'), + ]), + ], + stateOptions: new OdmOptions(documentClass: AgentDocument::class) +)] +class AgentApi +{ + private ?int $id = null; + + private ?string $name = null; + + private ?\DateTimeInterface $birthday = null; + + public function getId(): ?int + { + return $this->id; + } + + public function setId(?int $id): self + { + $this->id = $id; + + return $this; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(?string $name): self + { + $this->name = $name; + + return $this; + } + + public function getBirthday(): ?\DateTimeInterface + { + return $this->birthday; + } + + public function setBirthday(?\DateTimeInterface $birthday): self + { + $this->birthday = $birthday; + + return $this; + } +} diff --git a/tests/Fixtures/TestBundle/Document/AgentDocument.php b/tests/Fixtures/TestBundle/Document/AgentDocument.php new file mode 100644 index 00000000000..6605e9cac7c --- /dev/null +++ b/tests/Fixtures/TestBundle/Document/AgentDocument.php @@ -0,0 +1,95 @@ + + * + * 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\Document; + +use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; + +#[ODM\Document] +class AgentDocument +{ + #[ODM\Id(strategy: 'INCREMENT', type: 'int')] + public ?int $id = null; + + #[ODM\Field] + public ?string $name = null; + + #[ODM\Field] + public ?string $apiKey = null; + + #[ODM\Field] + public ?\DateTimeImmutable $createdAt = null; + + #[ODM\Field] + public ?\DateTimeImmutable $birthday = null; + + public function getId(): ?int + { + return $this->id; + } + + public function setId(int $id): static + { + $this->id = $id; + + return $this; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(string $name): static + { + $this->name = $name; + + return $this; + } + + public function getApiKey(): ?string + { + return $this->apiKey; + } + + public function setApiKey(string $apiKey): static + { + $this->apiKey = $apiKey; + + return $this; + } + + public function getCreatedAt(): ?\DateTimeImmutable + { + return $this->createdAt; + } + + public function setCreatedAt(\DateTimeImmutable $createdAt): static + { + $this->createdAt = $createdAt; + + return $this; + } + + public function getBirthday(): ?\DateTimeImmutable + { + return $this->birthday; + } + + public function setBirthday(\DateTimeImmutable $birthday): static + { + $this->birthday = $birthday; + + return $this; + } +} diff --git a/tests/Fixtures/TestBundle/Entity/Agent.php b/tests/Fixtures/TestBundle/Entity/Agent.php new file mode 100644 index 00000000000..2b32c4aee7a --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/Agent.php @@ -0,0 +1,82 @@ + + * + * 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 Doctrine\ORM\Mapping as ORM; + +#[ORM\Entity] +class Agent +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + public ?int $id = null; + + #[ORM\Column(length: 255)] + public ?string $name = null; + + #[ORM\Column] + public ?\DateTimeImmutable $createdAt = null; + + #[ORM\Column] + public ?\DateTimeImmutable $birthday = null; + + public function getId(): ?int + { + return $this->id; + } + + public function setId(int $id): static + { + $this->id = $id; + + return $this; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(string $name): static + { + $this->name = $name; + + return $this; + } + + public function getCreatedAt(): ?\DateTimeImmutable + { + return $this->createdAt; + } + + public function setCreatedAt(\DateTimeImmutable $createdAt): static + { + $this->createdAt = $createdAt; + + return $this; + } + + public function getBirthday(): ?\DateTimeImmutable + { + return $this->birthday; + } + + public function setBirthday(\DateTimeImmutable $birthday): static + { + $this->birthday = $birthday; + + return $this; + } +} diff --git a/tests/Functional/Parameters/QueryParameterStateOptionsTest.php b/tests/Functional/Parameters/QueryParameterStateOptionsTest.php new file mode 100644 index 00000000000..ba60820b6f8 --- /dev/null +++ b/tests/Functional/Parameters/QueryParameterStateOptionsTest.php @@ -0,0 +1,91 @@ + + * + * 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\Functional\Parameters; + +use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; +use ApiPlatform\Tests\Fixtures\TestBundle\Document\AgentDocument; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Agent; +use Doctrine\ODM\MongoDB\DocumentManager; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Tools\SchemaTool; + +final class QueryParameterStateOptionsTest extends ApiTestCase +{ + public function testQueryParameterStateOptions(): void + { + $this->recreateSchema(); + $response = self::createClient()->request('GET', ($this->isMongoDb() ? 'agent_documents' : 'agents').'?birthday[before]=2000-01-01&birthday[after]=1990-01-01'); + $this->assertResponseIsSuccessful(); + + $data = $response->toArray(); + $agents = $data['hydra:member']; + $this->assertCount(1, $agents); + + $validBirthdays = array_column($agents, 'birthday'); + $this->assertValidBirthdayRange($validBirthdays); + } + + /** + * @param array $birthdays + */ + private function assertValidBirthdayRange(array $birthdays): void + { + foreach ($birthdays as $birthday) { + $this->assertLessThanOrEqual('2000-01-01T00:00:00+00:00', $birthday, "The birthday date {$birthday} exceeds the upper limit."); + $this->assertGreaterThanOrEqual('1990-01-01T00:00:00+00:00', $birthday, "The birthday date {$birthday} is below the lower limit."); + } + } + + /** + * @param array $options kernel options + */ + private function recreateSchema(array $options = []): void + { + self::bootKernel($options); + $container = static::getContainer(); + $isMongoDb = $this->isMongoDb(); + $registry = $container->get($isMongoDb ? 'doctrine_mongodb' : 'doctrine'); + $resourceClass = $isMongoDb ? AgentDocument::class : Agent::class; + $manager = $registry->getManager(); + + if ($manager instanceof EntityManagerInterface) { + $classes = $manager->getClassMetadata($resourceClass); + $schemaTool = new SchemaTool($manager); + + @$schemaTool->dropSchema([$classes]); + @$schemaTool->createSchema([$classes]); + } elseif ($manager instanceof DocumentManager) { + @$manager->getSchemaManager()->dropCollections(); + } + + $birthdays = [new \DateTimeImmutable('2002-01-01'), new \DateTimeImmutable(), new \DateTimeImmutable('1990-12-31')]; + foreach ($birthdays as $birthday) { + $agent = (new $resourceClass()) + ->setName('Agent '.$birthday->format('Y')) + ->setBirthday($birthday) + ->setCreatedAt(new \DateTimeImmutable()); + + $manager->persist($agent); + } + + $manager->flush(); + } + + private function isMongoDb(): bool + { + $container = static::getContainer(); + + return 'mongodb' === $container->getParameter('kernel.environment'); + } +} From e7fb04fab05bc077e2dbeb0fa0fc2c1d28c96105 Mon Sep 17 00:00:00 2001 From: Antoine Bluchet Date: Wed, 16 Oct 2024 11:13:53 +0200 Subject: [PATCH 5/8] fix(symfony): fetch api-platform/symfony version debug bar (#6722) fixes #6709 --- .../DataCollector/RequestDataCollector.php | 29 +++++++++++++++++-- .../RequestDataCollectorTest.php | 7 ++++- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Bundle/DataCollector/RequestDataCollector.php b/src/Symfony/Bundle/DataCollector/RequestDataCollector.php index ddde1871a0e..f409b5cd362 100644 --- a/src/Symfony/Bundle/DataCollector/RequestDataCollector.php +++ b/src/Symfony/Bundle/DataCollector/RequestDataCollector.php @@ -16,6 +16,7 @@ use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\State\Util\RequestAttributesExtractor; +use Composer\InstalledVersions; use PackageVersions\Versions; use Psr\Container\ContainerInterface; use Symfony\Component\HttpFoundation\Request; @@ -67,16 +68,38 @@ private function setFilters(ApiResource $resourceMetadata, int $index, array &$f } } + // TODO: 4.1 remove Versions as its deprecated public function getVersion(): ?string { + if (class_exists(InstalledVersions::class)) { + return InstalledVersions::getPrettyVersion('api-platform/symfony') ?? InstalledVersions::getPrettyVersion('api-platform/core'); + } + if (!class_exists(Versions::class)) { return null; } - $version = Versions::getVersion('api-platform/core'); - preg_match('/^v(.*?)@/', (string) $version, $output); + try { + $version = strtok(Versions::getVersion('api-platform/symfony'), '@'); + } catch (\OutOfBoundsException) { + $version = false; + } + + if (false === $version) { + try { + $version = strtok(Versions::getVersion('api-platform/core'), '@'); + } catch (\OutOfBoundsException) { + $version = false; + } + } + + if (false === $version) { + return null; + } + + preg_match('/^v(.*?)$/', $version, $output); - return $output[1] ?? strtok($version, '@'); + return $output[1] ?? $version; } /** diff --git a/tests/Symfony/Bundle/DataCollector/RequestDataCollectorTest.php b/tests/Symfony/Bundle/DataCollector/RequestDataCollectorTest.php index c9af7f056eb..9a5a35a00f8 100644 --- a/tests/Symfony/Bundle/DataCollector/RequestDataCollectorTest.php +++ b/tests/Symfony/Bundle/DataCollector/RequestDataCollectorTest.php @@ -20,6 +20,7 @@ use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use ApiPlatform\Symfony\Bundle\DataCollector\RequestDataCollector; use ApiPlatform\Tests\Fixtures\DummyEntity; +use Composer\InstalledVersions; use PackageVersions\Versions; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -163,7 +164,11 @@ public function testVersionCollection(): void $this->response ); - $this->assertSame(null !== $dataCollector->getVersion(), class_exists(Versions::class)); + if (class_exists(InstalledVersions::class)) { + $this->assertTrue(null !== $dataCollector->getVersion()); + } else { + $this->assertSame(null !== $dataCollector->getVersion(), class_exists(Versions::class)); + } } public function testWithPreviousData(): void From ad5efa535a4dcbaad64ecff89514eaa6e07f5b7c Mon Sep 17 00:00:00 2001 From: Antoine Bluchet Date: Thu, 17 Oct 2024 12:05:01 +0200 Subject: [PATCH 6/8] fix: multiple parameter provider #6673 (#6732) --- src/State/Provider/ParameterProvider.php | 2 +- .../Issue6673/MutlipleParameterProvider.php | 63 +++++++++++++++++++ .../Parameters/ParameterProviderTest.php | 25 ++++++++ 3 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 tests/Fixtures/TestBundle/ApiResource/Issue6673/MutlipleParameterProvider.php create mode 100644 tests/Functional/Parameters/ParameterProviderTest.php diff --git a/src/State/Provider/ParameterProvider.php b/src/State/Provider/ParameterProvider.php index 7ffa0c1b923..548d364b7e7 100644 --- a/src/State/Provider/ParameterProvider.php +++ b/src/State/Provider/ParameterProvider.php @@ -49,13 +49,13 @@ public function provide(Operation $operation, array $uriVariables = [], array $c $request->attributes->set('_api_header_parameters', $request->headers->all()); } - $context = ['operation' => $operation] + $context; $parameters = $operation->getParameters(); foreach ($parameters ?? [] as $parameter) { $extraProperties = $parameter->getExtraProperties(); unset($extraProperties['_api_values']); $parameters->add($parameter->getKey(), $parameter = $parameter->withExtraProperties($extraProperties)); + $context = ['operation' => $operation] + $context; $values = $this->getParameterValues($parameter, $request, $context); $value = $this->extractParameterValues($parameter, $values); diff --git a/tests/Fixtures/TestBundle/ApiResource/Issue6673/MutlipleParameterProvider.php b/tests/Fixtures/TestBundle/ApiResource/Issue6673/MutlipleParameterProvider.php new file mode 100644 index 00000000000..83ef0829302 --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/Issue6673/MutlipleParameterProvider.php @@ -0,0 +1,63 @@ + + * + * 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\Issue6673; + +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Parameter; +use ApiPlatform\Metadata\QueryParameter; + +#[GetCollection( + uriTemplate: 'issue6673_multiple_parameter_provider', + shortName: 'multiple_parameter_provider', + outputFormats: ['json'], + parameters: [ + 'a' => new QueryParameter( + provider: [self::class, 'parameterOneProvider'], + ), + 'b' => new QueryParameter( + provider: [self::class, 'parameterTwoProvider'], + ), + ], + provider: [self::class, 'provide'] +)] +final class MutlipleParameterProvider +{ + public function __construct(public readonly string $id) + { + } + + public static function provide(Operation $operation): ?array + { + return $operation->getNormalizationContext(); + } + + public static function parameterOneProvider(Parameter $parameter, array $parameters = [], array $context = []): ?Operation + { + $operation = $context['operation']; + $context = $operation->getNormalizationContext() ?? []; + $context['a'] = $parameter->getValue(); + + return $operation->withNormalizationContext($context); + } + + public static function parameterTwoProvider(Parameter $parameter, array $parameters = [], array $context = []): ?Operation + { + $operation = $context['operation']; + $context = $operation->getNormalizationContext() ?? []; + $context['b'] = $parameter->getValue(); + + return $operation->withNormalizationContext($context); + } +} diff --git a/tests/Functional/Parameters/ParameterProviderTest.php b/tests/Functional/Parameters/ParameterProviderTest.php new file mode 100644 index 00000000000..b04bfa72527 --- /dev/null +++ b/tests/Functional/Parameters/ParameterProviderTest.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\Functional\Parameters; + +use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; + +final class ParameterProviderTest extends ApiTestCase +{ + public function testMultipleParameterProviderShouldChangeTheOperation(): void + { + $response = self::createClient()->request('GET', 'issue6673_multiple_parameter_provider?a=1&b=2', ['headers' => ['accept' => 'application/json']]); + $this->assertArraySubset(['a' => '1', 'b' => '2'], $response->toArray()); + } +} From 77d3ff3e8ddf88546eaf288a399d11f130553606 Mon Sep 17 00:00:00 2001 From: soyuka Date: Sat, 19 Oct 2024 12:05:07 +0200 Subject: [PATCH 7/8] doc: changelog 3.4.4 --- CHANGELOG.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b32c7f019c4..8577f255225 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## v3.4.4 + +### Bug fixes + +* [550347867](https://github.com/api-platform/core/commit/550347867f30611b673d8df99f65186d013919dd) fix(graphql): register query parameter arguments with filters (#6727) +* [99262dce7](https://github.com/api-platform/core/commit/99262dce739800bd841c95e026848b587ba25801) fix(jsonschema): handle @id when genId is false (#6716) +* [ad5efa535](https://github.com/api-platform/core/commit/ad5efa535a4dcbaad64ecff89514eaa6e07f5b7c) fix: multiple parameter provider #6673 (#6732) +* [d34cd7be8](https://github.com/api-platform/core/commit/d34cd7be8e7a12fd08a8b10270a614c06c10aa89) fix: use stateOptions when retrieving a Parameter filter (#6728) +* [e7fb04fab](https://github.com/api-platform/core/commit/e7fb04fab05bc077e2dbeb0fa0fc2c1d28c96105) fix(symfony): fetch api-platform/symfony version debug bar (#6722) +* [e96623ebf](https://github.com/api-platform/core/commit/e96623ebfd8691ba943bdb56a4d91e160497a311) fix(jsonld): prefix error @type with hydra: (#6721) + ## v3.4.3 ### Bug fixes @@ -2621,4 +2632,4 @@ Please read #2825 if you have issues with the behavior of Readable/Writable Link ## 1.0.0 beta 2 * Preserve indexes when normalizing and denormalizing associative arrays -* Allow setting default order for property when registering a `Doctrine\Orm\Filter\OrderFilter` instance +* Allow setting default order for property when registering a `Doctrine\Orm\Filter\OrderFilter` instance \ No newline at end of file From 58c43b8a4590e361fae65c8e95d03c9c2cbe1c4a Mon Sep 17 00:00:00 2001 From: soyuka Date: Tue, 22 Oct 2024 07:48:23 +0200 Subject: [PATCH 8/8] chore: phpstan fixes --- .../Tests/Eloquent/Metadata/ModelMetadataTest.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Laravel/Tests/Eloquent/Metadata/ModelMetadataTest.php b/src/Laravel/Tests/Eloquent/Metadata/ModelMetadataTest.php index e306c731b8c..c76babf0703 100644 --- a/src/Laravel/Tests/Eloquent/Metadata/ModelMetadataTest.php +++ b/src/Laravel/Tests/Eloquent/Metadata/ModelMetadataTest.php @@ -37,9 +37,9 @@ public function testHiddenAttributesAreCorrectlyIdentified(): void /** * @return HasMany */ - public function secret(): HasMany + public function secret(): HasMany // @phpstan-ignore-line { - return $this->hasMany(Book::class); + return $this->hasMany(Book::class); // @phpstan-ignore-line } }; @@ -55,9 +55,9 @@ public function testVisibleAttributesAreCorrectlyIdentified(): void /** * @return HasMany */ - public function secret(): HasMany + public function secret(): HasMany // @phpstan-ignore-line { - return $this->hasMany(Book::class); + return $this->hasMany(Book::class); // @phpstan-ignore-line } }; @@ -71,9 +71,9 @@ public function testAllAttributesVisibleByDefault(): void /** * @return HasMany */ - public function secret(): HasMany + public function secret(): HasMany // @phpstan-ignore-line { - return $this->hasMany(Book::class); + return $this->hasMany(Book::class); // @phpstan-ignore-line } };