From 3233d543eba9cb658809979d9771a1e9982b4374 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 17 Jan 2026 20:04:29 +0100 Subject: [PATCH 1/7] Run PHPUnit on mysql and pgsql --- .github/workflows/ci.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c6e831f511..d2243214d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -479,7 +479,7 @@ jobs: continue-on-error: true postgresql: - name: Behat (PHP ${{ matrix.php }}) (PostgreSQL) + name: PHPUnit + Behat (PHP ${{ matrix.php }}) (PostgreSQL) runs-on: ubuntu-latest timeout-minutes: 20 strategy: @@ -524,12 +524,14 @@ jobs: composer global link . - name: Clear test app cache run: tests/Fixtures/app/console cache:clear --ansi + - name: Run PHPUnit tests + run: vendor/bin/phpunit - name: Run Behat tests run: | vendor/bin/behat --out=std --format=progress --profile=postgres --no-interaction -vv mysql: - name: Behat (PHP ${{ matrix.php }}) (MySQL) + name: PHPUnit + Behat (PHP ${{ matrix.php }}) (MySQL) runs-on: ubuntu-latest timeout-minutes: 20 strategy: @@ -575,6 +577,8 @@ jobs: composer global link . - name: Clear test app cache run: tests/Fixtures/app/console cache:clear --ansi + - name: Run PHPUnit tests + run: vendor/bin/phpunit - name: Run Behat tests run: vendor/bin/behat --out=std --format=progress --profile=default --no-interaction --tags '~@!mysql' From 2177de19cd42debbf6a3379b026acd4b2fed4880 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 17 Jan 2026 20:28:34 +0100 Subject: [PATCH 2/7] Try --- tests/RecreateSchemaTrait.php | 39 +++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/tests/RecreateSchemaTrait.php b/tests/RecreateSchemaTrait.php index cc6b9dfb53..08356c201b 100644 --- a/tests/RecreateSchemaTrait.php +++ b/tests/RecreateSchemaTrait.php @@ -37,15 +37,42 @@ private function recreateSchema(array $classes = []): void return; } - /** @var ClassMetadata[] $cl */ - $cl = []; - foreach ($classes as $c) { - $cl[] = $manager->getMetadataFactory()->getMetadataFor($c); + /** @var ClassMetadata[] $metadataCollection */ + $metadataCollection = []; + $processedClasses = []; + + foreach ($classes as $class) { + $this->addMetadataWithDependencies($manager, $class, $metadataCollection, $processedClasses); } $schemaTool = new SchemaTool($manager); - @$schemaTool->dropSchema($cl); - @$schemaTool->createSchema($cl); + + $manager->getConnection()->executeStatement('SET FOREIGN_KEY_CHECKS = 0'); + @$schemaTool->dropSchema($metadataCollection); + @$schemaTool->createSchema($metadataCollection); + $manager->getConnection()->executeStatement('SET FOREIGN_KEY_CHECKS = 1'); + } + + /** + * @param array $metadataCollection + * @param array $processedClasses + * + * @param-out array $metadataCollection + * @param-out array $processedClasses + */ + private function addMetadataWithDependencies(EntityManagerInterface $manager, string $class, array &$metadataCollection, array &$processedClasses): void + { + if (isset($processedClasses[$class])) { + return; + } + + $metadata = $manager->getMetadataFactory()->getMetadataFor($class); + $metadataCollection[] = $metadata; + $processedClasses[$class] = true; + + foreach ($metadata->getAssociationMappings() as $associationMapping) { + $this->addMetadataWithDependencies($manager, $associationMapping->targetEntity, $metadataCollection, $processedClasses); + } } private function isMongoDB(): bool From 6a74b863e7cb000b28717674e6fadee15f5f2543 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 17 Jan 2026 21:14:55 +0100 Subject: [PATCH 3/7] Try --- tests/RecreateSchemaTrait.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/RecreateSchemaTrait.php b/tests/RecreateSchemaTrait.php index 08356c201b..01032c769d 100644 --- a/tests/RecreateSchemaTrait.php +++ b/tests/RecreateSchemaTrait.php @@ -47,10 +47,8 @@ private function recreateSchema(array $classes = []): void $schemaTool = new SchemaTool($manager); - $manager->getConnection()->executeStatement('SET FOREIGN_KEY_CHECKS = 0'); - @$schemaTool->dropSchema($metadataCollection); + @$schemaTool->dropDatabase(); @$schemaTool->createSchema($metadataCollection); - $manager->getConnection()->executeStatement('SET FOREIGN_KEY_CHECKS = 1'); } /** From 12a13380ea81c44eb82bfcc693524f097380d97a Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 17 Jan 2026 21:27:11 +0100 Subject: [PATCH 4/7] Fix tests --- .../Fixtures/TestBundle/Entity/Issue6041/NumericValidated.php | 2 +- tests/Functional/JsonStreamerTest.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Fixtures/TestBundle/Entity/Issue6041/NumericValidated.php b/tests/Fixtures/TestBundle/Entity/Issue6041/NumericValidated.php index 5394a8eb43..6954e5b3d3 100644 --- a/tests/Fixtures/TestBundle/Entity/Issue6041/NumericValidated.php +++ b/tests/Fixtures/TestBundle/Entity/Issue6041/NumericValidated.php @@ -32,7 +32,7 @@ class NumericValidated private ?int $id = null; #[Assert\Range(min: 1, max: 10)] - #[ORM\Column] + #[ORM\Column(name: '_range')] public int $range; #[Assert\GreaterThan(value: 10)] diff --git a/tests/Functional/JsonStreamerTest.php b/tests/Functional/JsonStreamerTest.php index 7247e0e069..88d23e2cd9 100644 --- a/tests/Functional/JsonStreamerTest.php +++ b/tests/Functional/JsonStreamerTest.php @@ -229,7 +229,7 @@ public function testJsonStreamerWriteJsonLd(): void $this->assertSame(0, $res['views']); $this->assertSame(0, $res['rating']); $this->assertFalse($res['isFeatured']); - $this->assertSame('0', $res['price']); + $this->assertEquals('0.00', $res['price']); $this->assertStringStartsWith('/json_stream_resources/', $res['@id']); $this->assertSame('/contexts/JsonStreamResource', $res['@context']); @@ -273,7 +273,7 @@ public function testJsonStreamerWriteJson(): void $this->assertSame(0, $res['views']); $this->assertSame(0, $res['rating']); $this->assertFalse($res['isFeatured']); - $this->assertSame('0', $res['price']); + $this->assertEquals('0.00', $res['price']); $this->assertArrayNotHasKey('@id', $res); $this->assertArrayNotHasKey('@type', $res); $this->assertArrayNotHasKey('@context', $res); From ddd54ee4255a9dd6cf1cca13fc1a6420b9c18be9 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 17 Jan 2026 21:52:01 +0100 Subject: [PATCH 5/7] Fix --- .../Document/FilteredOrderParameter.php | 6 +++--- tests/Fixtures/TestBundle/Entity/Cart.php | 2 +- .../Entity/FilteredOrderParameter.php | 6 +++--- tests/Functional/JsonStreamerTest.php | 4 ++-- .../Functional/Parameters/OrderFilterTest.php | 18 +++++++++--------- tests/RecreateSchemaTrait.php | 4 ++-- 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/tests/Fixtures/TestBundle/Document/FilteredOrderParameter.php b/tests/Fixtures/TestBundle/Document/FilteredOrderParameter.php index ea6e5538de..a08313f57d 100644 --- a/tests/Fixtures/TestBundle/Document/FilteredOrderParameter.php +++ b/tests/Fixtures/TestBundle/Document/FilteredOrderParameter.php @@ -38,17 +38,17 @@ 'date_null_always_first' => new QueryParameter( filter: new OrderFilter(), property: 'createdAt', - filterContext: OrderFilterInterface::NULLS_ALWAYS_FIRST, + filterContext: ['nulls_comparison' => OrderFilterInterface::NULLS_ALWAYS_FIRST], nativeType: new BuiltinType(TypeIdentifier::STRING) ), 'date_null_always_first_old_way' => new QueryParameter( - filter: new OrderFilter(properties: ['createdAt' => OrderFilterInterface::NULLS_ALWAYS_FIRST]), + filter: new OrderFilter(properties: ['createdAt' => ['nulls_comparison' => OrderFilterInterface::NULLS_ALWAYS_FIRST]]), property: 'createdAt', nativeType: new BuiltinType(TypeIdentifier::STRING) ), 'order[:property]' => new QueryParameter( filter: new OrderFilter(), - filterContext: OrderFilterInterface::NULLS_ALWAYS_FIRST, + filterContext: ['nulls_comparison' => OrderFilterInterface::NULLS_ALWAYS_FIRST], ), ], )] diff --git a/tests/Fixtures/TestBundle/Entity/Cart.php b/tests/Fixtures/TestBundle/Entity/Cart.php index 844eb65d5d..1adf0cad56 100644 --- a/tests/Fixtures/TestBundle/Entity/Cart.php +++ b/tests/Fixtures/TestBundle/Entity/Cart.php @@ -62,7 +62,7 @@ public static function handleLinks(QueryBuilder $queryBuilder, array $uriVariabl ->addGroupBy(\sprintf('%s.id', $rootAlias)); } - public ?int $totalQuantity; + public int|string|null $totalQuantity; #[ORM\Id] #[ORM\GeneratedValue] diff --git a/tests/Fixtures/TestBundle/Entity/FilteredOrderParameter.php b/tests/Fixtures/TestBundle/Entity/FilteredOrderParameter.php index f431cb67b8..21bf7dbaa1 100644 --- a/tests/Fixtures/TestBundle/Entity/FilteredOrderParameter.php +++ b/tests/Fixtures/TestBundle/Entity/FilteredOrderParameter.php @@ -38,17 +38,17 @@ 'date_null_always_first' => new QueryParameter( filter: new OrderFilter(), property: 'createdAt', - filterContext: OrderFilterInterface::NULLS_ALWAYS_FIRST, + filterContext: ['nulls_comparison' => OrderFilterInterface::NULLS_ALWAYS_FIRST], nativeType: new BuiltinType(TypeIdentifier::STRING) ), 'date_null_always_first_old_way' => new QueryParameter( - filter: new OrderFilter(properties: ['createdAt' => OrderFilterInterface::NULLS_ALWAYS_FIRST]), + filter: new OrderFilter(properties: ['createdAt' => ['nulls_comparison' => OrderFilterInterface::NULLS_ALWAYS_FIRST]]), property: 'createdAt', nativeType: new BuiltinType(TypeIdentifier::STRING) ), 'order[:property]' => new QueryParameter( filter: new OrderFilter(), - filterContext: OrderFilterInterface::NULLS_ALWAYS_FIRST, + filterContext: ['nulls_comparison' => OrderFilterInterface::NULLS_ALWAYS_FIRST], ), ], )] diff --git a/tests/Functional/JsonStreamerTest.php b/tests/Functional/JsonStreamerTest.php index 88d23e2cd9..a261daeab1 100644 --- a/tests/Functional/JsonStreamerTest.php +++ b/tests/Functional/JsonStreamerTest.php @@ -229,7 +229,7 @@ public function testJsonStreamerWriteJsonLd(): void $this->assertSame(0, $res['views']); $this->assertSame(0, $res['rating']); $this->assertFalse($res['isFeatured']); - $this->assertEquals('0.00', $res['price']); + $this->assertContains($res['price'], ['0', '0.00']); // Depends on DB $this->assertStringStartsWith('/json_stream_resources/', $res['@id']); $this->assertSame('/contexts/JsonStreamResource', $res['@context']); @@ -273,7 +273,7 @@ public function testJsonStreamerWriteJson(): void $this->assertSame(0, $res['views']); $this->assertSame(0, $res['rating']); $this->assertFalse($res['isFeatured']); - $this->assertEquals('0.00', $res['price']); + $this->assertContains($res['price'], ['0', '0.00']); // Depends on DB $this->assertArrayNotHasKey('@id', $res); $this->assertArrayNotHasKey('@type', $res); $this->assertArrayNotHasKey('@context', $res); diff --git a/tests/Functional/Parameters/OrderFilterTest.php b/tests/Functional/Parameters/OrderFilterTest.php index c893caae55..0baa4e5b3d 100644 --- a/tests/Functional/Parameters/OrderFilterTest.php +++ b/tests/Functional/Parameters/OrderFilterTest.php @@ -79,29 +79,29 @@ public static function orderFilterScenariosProvider(): \Generator '/filtered_order_parameters?date=desc', ['2024-12-25T00:00:00+00:00', '2024-06-15T00:00:00+00:00', '2024-01-01T00:00:00+00:00', null], ]; - yield 'date_null_always_first_alias_nulls_first' => [ + yield 'date_null_always_first_alias_asc' => [ '/filtered_order_parameters?date_null_always_first=asc', [null, '2024-01-01T00:00:00+00:00', '2024-06-15T00:00:00+00:00', '2024-12-25T00:00:00+00:00'], ]; - yield 'date_null_always_first_alias_nulls_last' => [ + yield 'date_null_always_first_alias_desc' => [ '/filtered_order_parameters?date_null_always_first=desc', - ['2024-12-25T00:00:00+00:00', '2024-06-15T00:00:00+00:00', '2024-01-01T00:00:00+00:00', null], + [null, '2024-12-25T00:00:00+00:00', '2024-06-15T00:00:00+00:00', '2024-01-01T00:00:00+00:00'], ]; - yield 'date_null_always_first_old_way_alias_nulls_first' => [ + yield 'date_null_always_first_old_way_alias_asc' => [ '/filtered_order_parameters?date_null_always_first_old_way=asc', [null, '2024-01-01T00:00:00+00:00', '2024-06-15T00:00:00+00:00', '2024-12-25T00:00:00+00:00'], ]; - yield 'date_null_always_first_old_way_alias_nulls_last' => [ + yield 'date_null_always_first_old_way_alias_desc' => [ '/filtered_order_parameters?date_null_always_first_old_way=desc', - ['2024-12-25T00:00:00+00:00', '2024-06-15T00:00:00+00:00', '2024-01-01T00:00:00+00:00', null], + [null, '2024-12-25T00:00:00+00:00', '2024-06-15T00:00:00+00:00', '2024-01-01T00:00:00+00:00'], ]; - yield 'order_property_created_at_nulls_first' => [ + yield 'order_property_created_at_null_first_asc' => [ '/filtered_order_parameters?order[createdAt]=asc', [null, '2024-01-01T00:00:00+00:00', '2024-06-15T00:00:00+00:00', '2024-12-25T00:00:00+00:00'], ]; - yield 'order_property_created_at_nulls_last' => [ + yield 'order_property_created_at_null_first_desc' => [ '/filtered_order_parameters?order[createdAt]=desc', - ['2024-12-25T00:00:00+00:00', '2024-06-15T00:00:00+00:00', '2024-01-01T00:00:00+00:00', null], + [null, '2024-12-25T00:00:00+00:00', '2024-06-15T00:00:00+00:00', '2024-01-01T00:00:00+00:00'], ]; } diff --git a/tests/RecreateSchemaTrait.php b/tests/RecreateSchemaTrait.php index 01032c769d..0b2b8cae40 100644 --- a/tests/RecreateSchemaTrait.php +++ b/tests/RecreateSchemaTrait.php @@ -53,10 +53,10 @@ private function recreateSchema(array $classes = []): void /** * @param array $metadataCollection - * @param array $processedClasses + * @param array $processedClasses * * @param-out array $metadataCollection - * @param-out array $processedClasses + * @param-out array $processedClasses */ private function addMetadataWithDependencies(EntityManagerInterface $manager, string $class, array &$metadataCollection, array &$processedClasses): void { From b103036380a956e46a346483db084a8bff9885e1 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 17 Jan 2026 22:33:10 +0100 Subject: [PATCH 6/7] Fix --- .github/workflows/ci.yml | 4 ++++ .../Functional/Parameters/OrderFilterTest.php | 23 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d2243214d9..2b99e07176 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -526,6 +526,8 @@ jobs: run: tests/Fixtures/app/console cache:clear --ansi - name: Run PHPUnit tests run: vendor/bin/phpunit + - name: Clear test app cache + run: tests/Fixtures/app/console cache:clear --ansi - name: Run Behat tests run: | vendor/bin/behat --out=std --format=progress --profile=postgres --no-interaction -vv @@ -579,6 +581,8 @@ jobs: run: tests/Fixtures/app/console cache:clear --ansi - name: Run PHPUnit tests run: vendor/bin/phpunit + - name: Clear test app cache + run: tests/Fixtures/app/console cache:clear --ansi - name: Run Behat tests run: vendor/bin/behat --out=std --format=progress --profile=default --no-interaction --tags '~@!mysql' diff --git a/tests/Functional/Parameters/OrderFilterTest.php b/tests/Functional/Parameters/OrderFilterTest.php index 0baa4e5b3d..5bf908f783 100644 --- a/tests/Functional/Parameters/OrderFilterTest.php +++ b/tests/Functional/Parameters/OrderFilterTest.php @@ -13,6 +13,7 @@ namespace ApiPlatform\Tests\Functional\Parameters; +use ApiPlatform\Doctrine\Odm\Filter\OrderFilter; use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; use ApiPlatform\Tests\Fixtures\TestBundle\Document\FilteredOrderParameter as FilteredOrderParameterDocument; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\FilteredOrderParameter; @@ -79,6 +80,28 @@ public static function orderFilterScenariosProvider(): \Generator '/filtered_order_parameters?date=desc', ['2024-12-25T00:00:00+00:00', '2024-06-15T00:00:00+00:00', '2024-01-01T00:00:00+00:00', null], ]; + } + + #[DataProvider('orderFilterNullsComparisonScenariosProvider')] + public function testOrderFilterNullsComparisonResponses(string $url, array $expectedOrder): void + { + if ($this->isMongoDB()) { + $this->markTestSkipped(sprintf('Not implemented in %s', OrderFilter::class)); + } + + $response = self::createClient()->request('GET', $url); + $this->assertResponseIsSuccessful(); + + $responseData = $response->toArray(); + $orderedItems = $responseData['hydra:member']; + + $actualOrder = array_map(fn ($item) => $item['createdAt'] ?? null, $orderedItems); + + $this->assertSame($expectedOrder, $actualOrder, \sprintf('Expected order does not match for URL %s', $url)); + } + + public static function orderFilterNullsComparisonScenariosProvider(): \Generator + { yield 'date_null_always_first_alias_asc' => [ '/filtered_order_parameters?date_null_always_first=asc', [null, '2024-01-01T00:00:00+00:00', '2024-06-15T00:00:00+00:00', '2024-12-25T00:00:00+00:00'], From 2e30b2d83bcbea22a58b47aa9687decb8539f013 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 17 Jan 2026 22:41:19 +0100 Subject: [PATCH 7/7] Fix --- tests/Functional/Parameters/OrderFilterTest.php | 8 +++++++- tests/RecreateSchemaTrait.php | 5 +++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/Functional/Parameters/OrderFilterTest.php b/tests/Functional/Parameters/OrderFilterTest.php index 5bf908f783..f4812760fd 100644 --- a/tests/Functional/Parameters/OrderFilterTest.php +++ b/tests/Functional/Parameters/OrderFilterTest.php @@ -59,6 +59,12 @@ public function testOrderFilterResponses(string $url, array $expectedOrder): voi $actualOrder = array_map(fn ($item) => $item['createdAt'] ?? null, $orderedItems); + // Default NULL order is different in PostgreSQL. + if ($this->isPostgres()) { + $actualOrder = array_values(array_filter($actualOrder)); + $expectedOrder = array_values(array_filter($expectedOrder)); + } + $this->assertSame($expectedOrder, $actualOrder, \sprintf('Expected order does not match for URL %s', $url)); } @@ -86,7 +92,7 @@ public static function orderFilterScenariosProvider(): \Generator public function testOrderFilterNullsComparisonResponses(string $url, array $expectedOrder): void { if ($this->isMongoDB()) { - $this->markTestSkipped(sprintf('Not implemented in %s', OrderFilter::class)); + $this->markTestSkipped(\sprintf('Not implemented in %s', OrderFilter::class)); } $response = self::createClient()->request('GET', $url); diff --git a/tests/RecreateSchemaTrait.php b/tests/RecreateSchemaTrait.php index 0b2b8cae40..3808d04c89 100644 --- a/tests/RecreateSchemaTrait.php +++ b/tests/RecreateSchemaTrait.php @@ -78,6 +78,11 @@ private function isMongoDB(): bool return 'mongodb' === static::getContainer()->getParameter('kernel.environment'); } + private function isPostgres(): bool + { + return 'postgres' === static::getContainer()->getParameter('kernel.environment'); + } + private function getManager(): EntityManagerInterface|DocumentManager { return static::getContainer()->get($this->isMongoDB() ? 'doctrine_mongodb' : 'doctrine')->getManager();