diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f5bf1ba326c..fd7271bc7f2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -933,6 +933,57 @@ jobs: - name: Run PHPUnit tests run: vendor/bin/phpunit + phpunit-symfony-64: + name: PHPUnit (PHP ${{ matrix.php }}) (Symfony 6.4) + runs-on: ubuntu-latest + timeout-minutes: 20 + continue-on-error: true + strategy: + matrix: + php: + - '8.2' + fail-fast: false + steps: + - name: Checkout + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + - name: Setup PHP + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + with: + php-version: ${{ matrix.php }} + tools: pecl, composer:2.9.8 + extensions: intl, bcmath, curl, openssl, mbstring + coverage: none + ini-values: memory_limit=-1 + - name: Get composer cache directory + id: composercache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + # Every package below only exists from Symfony 7.x (or requires it), no other job ever resolves 6.4 without dropping them + - name: Remove dependencies that require Symfony 7+ + run: composer remove --dev --no-update --no-interaction symfony/mcp-bundle mcp/sdk laravel/framework illuminate/config illuminate/contracts illuminate/database illuminate/http illuminate/pagination illuminate/routing illuminate/support orchestra/testbench symfony/json-streamer symfony/object-mapper symfony/var-exporter + # symfony/type-info has no 6.x release, 7.4 is the highest line compatible with the 6.4 components + - name: Force Symfony 6.4 + run: | + jq '(.["require"], .["require-dev"]) |= with_entries(if (.key | startswith("symfony/")) and (.value | contains("^6.4")) then .value = "6.4.*" elif .key == "symfony/type-info" then .value = "7.4.*" else . end)' composer.json > composer.json.tmp + mv composer.json.tmp composer.json + - name: Cache dependencies + uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 + with: + path: ${{ steps.composercache.outputs.dir }} + key: ${{ runner.os }}-composer-symfony-64-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer-symfony-64- + - name: Remove cache + run: rm -Rf tests/Fixtures/app/var/cache/* + - name: Update project dependencies + run: | + composer global require "soyuka/pmu:$PMU_VERSION" + composer global config allow-plugins.soyuka/pmu true --no-interaction + composer global link . + composer update + - name: Clear test app cache + run: tests/Fixtures/app/console cache:clear --ansi + - name: Run PHPUnit tests + run: vendor/bin/phpunit + phpunit-symfony-lowest: name: PHPUnit (PHP ${{ matrix.php }}) (Symfony lowest) runs-on: ubuntu-latest diff --git a/src/Metadata/Tests/Util/PropertyInfoToTypeInfoHelperTest.php b/src/Metadata/Tests/Util/PropertyInfoToTypeInfoHelperTest.php index 0725ed3ad9d..79ffcc83e1e 100644 --- a/src/Metadata/Tests/Util/PropertyInfoToTypeInfoHelperTest.php +++ b/src/Metadata/Tests/Util/PropertyInfoToTypeInfoHelperTest.php @@ -62,6 +62,20 @@ public function testConvertLegacyTypesToType(): void } } + public function testConvertLegacyCollectionWithoutKeyTypeToType(): void + { + if (!class_exists(LegacyType::class)) { + $this->markTestSkipped(); + } + + $legacyTypes = [new LegacyType('array', false, null, true, null, new LegacyType('int'))]; + + $this->assertEquals( + Type::collection(Type::builtin(TypeIdentifier::ARRAY), Type::int(), Type::union(Type::int(), Type::string())), // @phpstan-ignore-line + PropertyInfoToTypeInfoHelper::convertLegacyTypesToType($legacyTypes) + ); + } + public function testConvertTypeToLegacyTypes(): void { if (!class_exists(LegacyType::class)) { diff --git a/src/Metadata/Util/PropertyInfoToTypeInfoHelper.php b/src/Metadata/Util/PropertyInfoToTypeInfoHelper.php index bda642848f8..a30e352d7a2 100644 --- a/src/Metadata/Util/PropertyInfoToTypeInfoHelper.php +++ b/src/Metadata/Util/PropertyInfoToTypeInfoHelper.php @@ -97,7 +97,7 @@ public static function createTypeFromLegacyValues(string $builtinType, bool $nul if ($collectionValueTypes) { if (!$collectionKeyTypes) { - $variableTypes[] = \is_array($collectionKeyTypes) ? Type::mixed() : Type::union(Type::int(), Type::string()); // @phpstan-ignore-line + $variableTypes[] = Type::union(Type::int(), Type::string()); } $collectionValueTypes = array_unique(array_map(self::convertLegacyTypeToType(...), $collectionValueTypes)); diff --git a/tests/Fixtures/app/AppKernel.php b/tests/Fixtures/app/AppKernel.php index 6cb1bb86951..8a01b9a351b 100644 --- a/tests/Fixtures/app/AppKernel.php +++ b/tests/Fixtures/app/AppKernel.php @@ -113,6 +113,10 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load $loader->load(__DIR__."/config/config_{$this->getEnvironment()}.yml"); + if (class_exists(McpBundle::class)) { + $loader->load(__DIR__.'/config/config_mcp.yml'); + } + $messengerConfig = [ 'default_bus' => 'messenger.bus.default', 'buses' => [ diff --git a/tests/Fixtures/app/config/config_common.yml b/tests/Fixtures/app/config/config_common.yml index 531eb2b30de..574522f6ed1 100644 --- a/tests/Fixtures/app/config/config_common.yml +++ b/tests/Fixtures/app/config/config_common.yml @@ -93,17 +93,6 @@ api_platform: mercure: include_type: true -mcp: - servers: - default: - http: - path: '/mcp' - session: - store: 'file' - directory: '%kernel.cache_dir%/mcp' - ttl: 3600 - registry: '*' - services: test.client: class: ApiPlatform\Tests\Fixtures\TestBundle\BrowserKit\Client diff --git a/tests/Fixtures/app/config/config_mcp.yml b/tests/Fixtures/app/config/config_mcp.yml new file mode 100644 index 00000000000..6abd39f8f50 --- /dev/null +++ b/tests/Fixtures/app/config/config_mcp.yml @@ -0,0 +1,10 @@ +mcp: + servers: + default: + http: + path: '/mcp' + session: + store: 'file' + directory: '%kernel.cache_dir%/mcp' + ttl: 3600 + registry: '*' diff --git a/tests/Fixtures/app/config/routing_test.php b/tests/Fixtures/app/config/routing_test.php index e9f7fbc01f3..3b56fe7bec1 100644 --- a/tests/Fixtures/app/config/routing_test.php +++ b/tests/Fixtures/app/config/routing_test.php @@ -11,6 +11,7 @@ declare(strict_types=1); +use Symfony\AI\McpBundle\McpBundle; use Symfony\Bundle\WebProfilerBundle\WebProfilerBundle; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; @@ -18,7 +19,9 @@ $routes->import('routing_common.yml'); $routes->import('@TestBundle/Controller/Orm', 'attribute'); - $routes->import('.', 'mcp'); + if (class_exists(McpBundle::class)) { + $routes->import('.', 'mcp'); + } if (class_exists(WebProfilerBundle::class)) { $reflection = new ReflectionClass(WebProfilerBundle::class); diff --git a/tests/Functional/Doctrine/PartialPaginationMongoDbTest.php b/tests/Functional/Doctrine/PartialPaginationMongoDbTest.php index 2b96b7691e9..dd627f4b49a 100644 --- a/tests/Functional/Doctrine/PartialPaginationMongoDbTest.php +++ b/tests/Functional/Doctrine/PartialPaginationMongoDbTest.php @@ -17,8 +17,6 @@ use ApiPlatform\Tests\Fixtures\TestBundle\Document\PartialPaginationMongo\PartialPaginationMongoDummy; use ApiPlatform\Tests\RecreateSchemaTrait; use ApiPlatform\Tests\SetupClassResourcesTrait; -use Illuminate\Foundation\Testing\RefreshDatabase; -use Orchestra\Testbench\Concerns\WithWorkbench; use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface; use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface; @@ -28,9 +26,7 @@ class PartialPaginationMongoDbTest extends ApiTestCase { use RecreateSchemaTrait; - use RefreshDatabase; use SetupClassResourcesTrait; - use WithWorkbench; /** * @return class-string[] diff --git a/tests/Functional/Parameters/ValidationTest.php b/tests/Functional/Parameters/ValidationTest.php index 7c243edd679..edffdde4327 100644 --- a/tests/Functional/Parameters/ValidationTest.php +++ b/tests/Functional/Parameters/ValidationTest.php @@ -19,6 +19,7 @@ use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\WithParameter; use ApiPlatform\Tests\SetupClassResourcesTrait; use PHPUnit\Framework\Attributes\DataProvider; +use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; final class ValidationTest extends ApiTestCase { @@ -190,6 +191,10 @@ public function testValidatePattern(): void public function testTranslationValidation(): void { + if (class_exists(AnnotationLoader::class)) { + $this->markTestSkipped('Parameter validation messages are not translated on Symfony 6.4.'); + } + $res = self::createClient()->request('GET', 'translate_validation_error', ['headers' => ['accept-language' => 'es']]); $this->assertSame([ ['propertyPath' => 'name', 'message' => 'Este valor no debería estar vacío.', 'code' => 'c1051bb4-d103-4f74-8988-acbcafc7fdc3'], diff --git a/tests/State/Provider/ObjectMapperProviderTest.php b/tests/State/Provider/ObjectMapperProviderTest.php index 672134c7d39..29436fd75ab 100644 --- a/tests/State/Provider/ObjectMapperProviderTest.php +++ b/tests/State/Provider/ObjectMapperProviderTest.php @@ -25,6 +25,13 @@ class ObjectMapperProviderTest extends TestCase { + protected function setUp(): void + { + if (!interface_exists(ObjectMapperInterface::class)) { + $this->markTestSkipped('ObjectMapper not installed'); + } + } + public function testProvideBypassesWhenNoObjectMapper(): void { $data = new SourceEntity(); diff --git a/tests/Symfony/Bundle/DependencyInjection/Compiler/ErrorResourceAttributeLoaderPassTest.php b/tests/Symfony/Bundle/DependencyInjection/Compiler/ErrorResourceAttributeLoaderPassTest.php index 5705624b126..649b2f07c44 100644 --- a/tests/Symfony/Bundle/DependencyInjection/Compiler/ErrorResourceAttributeLoaderPassTest.php +++ b/tests/Symfony/Bundle/DependencyInjection/Compiler/ErrorResourceAttributeLoaderPassTest.php @@ -22,6 +22,7 @@ use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Mapping\ClassMetadataInterface; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; +use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; use Symfony\Component\Serializer\Mapping\Loader\LoaderChain; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; @@ -32,8 +33,17 @@ */ final class ErrorResourceAttributeLoaderPassTest extends TestCase { + private function skipOnSymfony64(): void + { + if (class_exists(AnnotationLoader::class)) { + $this->markTestSkipped('Symfony 6.4 AttributeLoader has a different constructor.'); + } + } + public function testRegistersAttributeLoaderForErrorClassesInChainLoader(): void { + $this->skipOnSymfony64(); + $container = new ContainerBuilder(); $container->setDefinition('serializer.mapping.chain_loader', new Definition(LoaderChain::class, [[]])); $container->setDefinition('serializer.mapping.cache_warmer', new Definition(\stdClass::class, [[]])); @@ -58,6 +68,8 @@ public function testRegistersAttributeLoaderForErrorClassesInChainLoader(): void public function testAlsoUpdatesCacheWarmer(): void { + $this->skipOnSymfony64(); + $container = new ContainerBuilder(); $container->setDefinition('serializer.mapping.chain_loader', new Definition(LoaderChain::class, [[]])); $container->setDefinition('serializer.mapping.cache_warmer', new Definition(\stdClass::class, [[]])); @@ -99,7 +111,7 @@ public function testDoesNothingWhenChainAlreadyContainsAnAttributeLoader(): void */ public function testSkipsOnSymfony64SerializerSignature(): void { - if (!class_exists(\Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader::class)) { + if (!class_exists(AnnotationLoader::class)) { $this->markTestSkipped('Only relevant when running against symfony/serializer 6.4 (AnnotationLoader still present).'); } @@ -122,6 +134,8 @@ public function testSkipsOnSymfony64SerializerSignature(): void */ public function testErrorNormalizationStaysPopulatedWhenAttributesAreDisabled(): void { + $this->skipOnSymfony64(); + // Simulates `enable_attributes: false`: Symfony's attribute loader rejects every class. $disabledAttributeLoader = new AttributeLoader(allowAnyClass: false, mappedClasses: []); @@ -169,6 +183,8 @@ public function testErrorNormalizationStaysPopulatedWhenAttributesAreDisabled(): public function testTheCompilerPassDefinitionMatchesTheRuntimeExpectation(): void { + $this->skipOnSymfony64(); + $container = new ContainerBuilder(); $container->setDefinition('serializer.mapping.chain_loader', new Definition(LoaderChain::class, [[]])); $container->setDefinition('serializer.mapping.cache_warmer', new Definition(\stdClass::class, [[]])); diff --git a/tests/Symfony/Bundle/Test/ApiTestCaseTest.php b/tests/Symfony/Bundle/Test/ApiTestCaseTest.php index fd86e580eaa..578348f95ef 100644 --- a/tests/Symfony/Bundle/Test/ApiTestCaseTest.php +++ b/tests/Symfony/Bundle/Test/ApiTestCaseTest.php @@ -427,6 +427,11 @@ public function testExplicitContentTypeIsPreserved(): void public function testBrowserKitAssertionsStayVerboseByDefault(): void { + // @phpstan-ignore function.alreadyNarrowedType (the setter only exists since Symfony 7.3) + if (!method_exists(self::class, 'setBrowserKitAssertionsAsVerbose')) { + $this->markTestSkipped('BrowserKit assertions verbosity requires Symfony 7.3+.'); + } + // The trait's static property is flattened into ApiTestCase (which directly uses the assertions trait) // and shared with subclasses, so read it there rather than on the trait or this subclass. $verboseMode = new \ReflectionProperty(ApiTestCase::class, 'defaultVerboseMode');