diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 00a24cbcfc13c..f749de5e0d82a 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,6 @@ | Q | A | ------------- | --- -| Branch? | 7.1 for features / 5.4, 6.4, or 7.0 for bug fixes +| Branch? | 7.2 for features / 5.4, 6.4, 7.0, and 7.1 for bug fixes | Bug fix? | yes/no | New feature? | yes/no | Deprecations? | yes/no diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 258c37b37d388..30f4bfdd9ade6 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -105,11 +105,12 @@ jobs: ports: - 9092:9092 env: - KAFKA_AUTO_CREATE_TOPICS_ENABLE: false - KAFKA_CREATE_TOPICS: 'test-topic:1:1:compact' - KAFKA_ADVERTISED_HOST_NAME: 127.0.0.1 - KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181' - KAFKA_ADVERTISED_PORT: 9092 + KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE: false + ALLOW_PLAINTEXT_LISTENER: 'yes' + KAFKA_CFG_ADVERTISED_LISTENERS: 'PLAINTEXT://127.0.0.1:9092' + KAFKA_CFG_LISTENERS: 'PLAINTEXT://:9092' + KAFKA_CFG_ZOOKEEPER_CONNECT: 'zookeeper:2181' + options: --name=kafka frankenphp: image: dunglas/frankenphp:1.1.0 ports: @@ -127,6 +128,10 @@ jobs: with: fetch-depth: 0 + - name: Init Kafka topics + run: | + docker exec kafka /opt/bitnami/kafka/bin/kafka-topics.sh --create --topic test-topic --bootstrap-server kafka:9092 + - name: Install system dependencies run: | echo "::group::apt-get update" diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 0753dc03e2789..796590882f30f 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -6,7 +6,7 @@ on: schedule: - cron: '34 4 * * 6' push: - branches: [ "7.1" ] + branches: [ "7.2" ] # Declare default permissions as read only. permissions: read-all diff --git a/CHANGELOG-7.1.md b/CHANGELOG-7.1.md index 78fc0fb2f693d..3f492b322276e 100644 --- a/CHANGELOG-7.1.md +++ b/CHANGELOG-7.1.md @@ -7,6 +7,21 @@ in 7.1 minor versions. To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v7.1.0...v7.1.1 +* 7.1.0 (2024-05-31) + + * bug #57248 [DoctrineBridge] Revert deprecating by-{id} mapping of entities (nicolas-grekas) + * bug #54572 [Mailer] Fix sendmail transport failure handling and interactive mode (bobvandevijver) + * bug #57228 [Mime] fix PHP 7 compatibility (xabbuh) + * bug #57065 [Mime] Fixed `Mime\Message::ensureValidity()` when a required header is set, but has an empty body (rhertogh) + * bug #57069 [Config] gracefully handle cases when no resolver is set (xabbuh) + * bug #57109 [Notifier] keep boolean options when their value is false (xabbuh) + * bug #56848 [DoctrineBridge] Undeprecate DoctrineExtractor::getTypes() (derrabus) + * bug #54971 [Serializer] Cache readability/writability computation (mtarld) + * bug #54979 [Cache] Fix irrelevant deprecation when connecting to Couchbase (derrabus) + * bug #56827 Fix CharsetValidator with string encoding (alamirault) + * bug #54977 [DependencyInjection] Fix prepending strategy for php config loader (yceruto) + * bug #56488 [VarExporter] Fix exporting default values involving global constants (kylekatarnls) + * 7.1.0-RC1 (2024-05-17) * bug #54970 [DependencyInjection] Process PHP configs using the ContainerConfigurator (MatTheCat) diff --git a/UPGRADE-7.1.md b/UPGRADE-7.1.md index 51b5e994b3861..9d516fdbe01f6 100644 --- a/UPGRADE-7.1.md +++ b/UPGRADE-7.1.md @@ -30,7 +30,9 @@ Components * [Form](#Form) * [Intl](#Intl) * [HttpClient](#HttpClient) - * [PropertyInfo](#PropertyInfo) + * [HttpKernel](#HttpKernel) + * [Security](#Security) + * [Serializer](#Serializer) * [Translation](#Translation) * [Workflow](#Workflow) @@ -50,10 +52,47 @@ DependencyInjection * [BC BREAK] When used in the `prependExtension()` method, the `ContainerConfigurator::import()` method now prepends the configuration instead of appending it * Deprecate `#[TaggedIterator]` and `#[TaggedLocator]` attributes, use `#[AutowireIterator]` and `#[AutowireLocator]` instead + *Before* + ```php + use Symfony\Component\DependencyInjection\Attribute\TaggedIterator; + use Symfony\Component\DependencyInjection\Attribute\TaggedLocator; + + class HandlerCollection + { + public function __construct( + #[TaggedIterator('app.handler', indexAttribute: 'key')] + iterable $handlers, + + #[TaggedLocator('app.handler')] + private ContainerInterface $locator, + ) { + } + } + ``` + + *After* + ```php + use Symfony\Component\DependencyInjection\Attribute\AutowireIterator; + use Symfony\Component\DependencyInjection\Attribute\AutowireLocator; + + class HandlerCollection + { + public function __construct( + #[AutowireIterator('app.handler', indexAttribute: 'key')] + iterable $handlers, + + #[AutowireLocator('app.handler')] + private ContainerInterface $locator, + ) { + } + } + ``` + DoctrineBridge -------------- - * Deprecated `DoctrineExtractor::getTypes()`, use `DoctrineExtractor::getType()` instead + * Deprecate `DoctrineExtractor::getTypes()`, use `DoctrineExtractor::getType()` instead + * Mark class `ProxyCacheWarmer` as `final` ExpressionLanguage ------------------ @@ -61,6 +100,18 @@ ExpressionLanguage * Deprecate passing `null` as the allowed variable names to `ExpressionLanguage::lint()` and `Parser::lint()`, pass the `IGNORE_UNKNOWN_VARIABLES` flag instead to ignore unknown variables during linting + *Before* + ```php + $expressionLanguage->lint('a + 1', null); + ``` + + *After* + ```php + use Symfony\Component\ExpressionLanguage\Parser; + + $expressionLanguage->lint('a + 1', [], Parser::IGNORE_UNKNOWN_VARIABLES); + ``` + Form ---- @@ -69,6 +120,7 @@ Form FrameworkBundle --------------- + * [BC BREAK] Enabling `framework.rate_limiter` requires `symfony/rate-limiter` 7.1 or higher * Mark classes `ConfigBuilderCacheWarmer`, `Router`, `SerializerCacheWarmer`, `TranslationsCacheWarmer`, `Translator` and `ValidatorCacheWarmer` as `final` * Deprecate the `router.cache_dir` config option, the Router will always use the `kernel.build_dir` parameter * Reset env vars when resetting the container @@ -78,6 +130,37 @@ HttpClient * Deprecate the `setLogger()` methods of the `NoPrivateNetworkHttpClient`, `TraceableHttpClient` and `ScopingHttpClient` classes, configure the logger of the wrapped clients directly instead + *Before* + ```php + // ... + use Symfony\Component\HttpClient\HttpClient; + use Symfony\Component\HttpClient\NoPrivateNetworkHttpClient; + + $publicClient = new NoPrivateNetworkHttpClient(HttpClient::create()); + $publicClient->setLogger(new Logger()); + ``` + + *After* + ```php + // ... + use Symfony\Component\HttpClient\HttpClient; + use Symfony\Component\HttpClient\NoPrivateNetworkHttpClient; + + $client = HttpClient::create(); + $client->setLogger(new Logger()); + + $publicClient = new NoPrivateNetworkHttpClient($client); + ``` + +HttpKernel +---------- + + * The `Extension` class is marked as internal, extend the `Extension` class from the DependencyInjection component instead + * Deprecate `Extension::addAnnotatedClassesToCompile()` + * Deprecate `AddAnnotatedClassesToCachePass` + * Deprecate the `setAnnotatedClassCache()` and `getAnnotatedClassesToCompile()` methods of the `Kernel` class + * Deprecate the `addAnnotatedClassesToCompile()` and `getAnnotatedClassesToCompile()` methods of the `Extension` class + Intl ---- @@ -89,15 +172,49 @@ Mailer * Postmark's "406 - Inactive recipient" API error code now results in a `PostmarkDeliveryEvent` instead of throwing a `HttpTransportException` -HttpKernel ----------- +Security +-------- - * Deprecate `Extension::addAnnotatedClassesToCompile()` and related code infrastructure + * Change the first and second argument of `OidcTokenHandler` to `Jose\Component\Core\AlgorithmManager` and `Jose\Component\Core\JWKSet` respectively SecurityBundle -------------- * Mark class `ExpressionCacheWarmer` as `final` + * Deprecate options `algorithm` and `key` of `oidc` token handler, use + `algorithms` and `keyset` instead + + *Before* + ```yaml + security: + firewalls: + main: + access_token: + token_handler: + oidc: + algorithm: 'ES256' + key: '{"kty":"...","k":"..."}' + # ... + ``` + + *After* + ```yaml + security: + firewalls: + main: + access_token: + token_handler: + oidc: + algorithms: ['ES256'] + keyset: '{"keys":[{"kty":"...","k":"..."}]}' + # ... + ``` + * Deprecate the `security.access_token_handler.oidc.jwk` service, use `security.access_token_handler.oidc.jwkset` instead + +Serializer +---------- + + * Deprecate the `withDefaultContructorArguments()` method of `AbstractNormalizerContextBuilder`, use `withDefaultContructorArguments()` instead (note the typo in the old method name) Translation ----------- @@ -108,12 +225,13 @@ TwigBundle ---------- * Mark class `TemplateCacheWarmer` as `final` + * Deprecate the `base_template_class` config option, this option is no-op when using Twig 3+ Validator --------- * Deprecate not passing a value for the `requireTld` option to the `Url` constraint (the default value will become `true` in 8.0) - * Deprecate `Bic::INVALID_BANK_CODE_ERROR` + * Deprecate `Bic::INVALID_BANK_CODE_ERROR`, as ISO 9362 defines no restrictions on BIC bank code characters Workflow -------- diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 594b5fe37de12..e33df0fa31081 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -25,7 +25,7 @@ - + diff --git a/src/Symfony/Bridge/Doctrine/ArgumentResolver/EntityValueResolver.php b/src/Symfony/Bridge/Doctrine/ArgumentResolver/EntityValueResolver.php index d2ee05b42ce1d..5cbf1a088dbb2 100644 --- a/src/Symfony/Bridge/Doctrine/ArgumentResolver/EntityValueResolver.php +++ b/src/Symfony/Bridge/Doctrine/ArgumentResolver/EntityValueResolver.php @@ -156,9 +156,8 @@ private function getIdentifier(Request $request, MapEntity $options, ArgumentMet return $id ?? ($options->stripNull ? false : null); } - if ($request->attributes->has('id')) { - trigger_deprecation('symfony/doctrine-bridge', '7.1', 'Relying on auto-mapping for Doctrine entities is deprecated for argument $%s of "%s": declare the mapping using either the #[MapEntity] attribute or mapped route parameters.', $argument->getName(), method_exists($argument, 'getControllerName') ? $argument->getControllerName() : 'n/a'); + if ($request->attributes->has('id')) { return $request->attributes->get('id') ?? ($options->stripNull ? false : null); } diff --git a/src/Symfony/Bridge/Doctrine/CHANGELOG.md b/src/Symfony/Bridge/Doctrine/CHANGELOG.md index f4ca310235228..4c6e029b5d33c 100644 --- a/src/Symfony/Bridge/Doctrine/CHANGELOG.md +++ b/src/Symfony/Bridge/Doctrine/CHANGELOG.md @@ -4,7 +4,6 @@ CHANGELOG 7.1 --- - * Deprecate the `DoctrineExtractor::getTypes()` method, use `DoctrineExtractor::getType()` instead * Allow `EntityValueResolver` to return a list of entities * Add support for auto-closing idle connections * Allow validating every class against `UniqueEntity` constraint diff --git a/src/Symfony/Bridge/Doctrine/PropertyInfo/DoctrineExtractor.php b/src/Symfony/Bridge/Doctrine/PropertyInfo/DoctrineExtractor.php index 02b29ae9942d9..cf32c6c537b02 100644 --- a/src/Symfony/Bridge/Doctrine/PropertyInfo/DoctrineExtractor.php +++ b/src/Symfony/Bridge/Doctrine/PropertyInfo/DoctrineExtractor.php @@ -161,13 +161,8 @@ public function getType(string $class, string $property, array $context = []): ? }; } - /** - * @deprecated since Symfony 7.1, use "getType" instead - */ public function getTypes(string $class, string $property, array $context = []): ?array { - trigger_deprecation('symfony/property-info', '7.1', 'The "%s()" method is deprecated, use "%s::getType()" instead.', __METHOD__, self::class); - if (null === $metadata = $this->getMetadata($class)) { return null; } diff --git a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php index c0ab3e1c63783..90cd6b97b9237 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php @@ -108,8 +108,6 @@ public function testTestGetPropertiesWithEmbedded() } /** - * @group legacy - * * @dataProvider legacyTypesProvider */ public function testExtractLegacy(string $property, ?array $type = null) @@ -117,9 +115,6 @@ public function testExtractLegacy(string $property, ?array $type = null) $this->assertEquals($type, $this->createExtractor()->getTypes(DoctrineDummy::class, $property, [])); } - /** - * @group legacy - */ public function testExtractWithEmbeddedLegacy() { $expectedTypes = [new LegacyType( @@ -137,9 +132,6 @@ public function testExtractWithEmbeddedLegacy() $this->assertEquals($expectedTypes, $actualTypes); } - /** - * @group legacy - */ public function testExtractEnumLegacy() { $this->assertEquals([new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, EnumString::class)], $this->createExtractor()->getTypes(DoctrineEnum::class, 'enumString', [])); @@ -149,9 +141,6 @@ public function testExtractEnumLegacy() $this->assertNull($this->createExtractor()->getTypes(DoctrineEnum::class, 'enumCustom', [])); } - /** - * @group legacy - */ public static function legacyTypesProvider(): array { // DBAL 4 has a special fallback strategy for BINGINT (int -> string) @@ -251,9 +240,6 @@ public function testGetPropertiesCatchException() $this->assertNull($this->createExtractor()->getProperties('Not\Exist')); } - /** - * @group legacy - */ public function testGetTypesCatchExceptionLegacy() { $this->assertNull($this->createExtractor()->getTypes('Not\Exist', 'baz')); diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LoginThrottlingFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LoginThrottlingFactory.php index b62720bfd80d8..242affefd58d7 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LoginThrottlingFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LoginThrottlingFactory.php @@ -76,7 +76,7 @@ public function createAuthenticator(ContainerBuilder $container, string $firewal $container->register($config['limiter'] = 'security.login_throttling.'.$firewallName.'.limiter', DefaultLoginRateLimiter::class) ->addArgument(new Reference('limiter.'.$globalId)) ->addArgument(new Reference('limiter.'.$localId)) - ->addArgument('%kernel.secret%') + ->addArgument('%container.build_hash%') ; } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SignatureAlgorithmFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SignatureAlgorithmFactory.php deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php b/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php index 1d88fbf87ddeb..5d6336eedd73f 100644 --- a/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php @@ -118,7 +118,7 @@ public static function createConnection(#[\SensitiveParameter] string $dsn, arra return MemcachedAdapter::createConnection($dsn, $options); } if (str_starts_with($dsn, 'couchbase:')) { - if (CouchbaseBucketAdapter::isSupported()) { + if (class_exists('CouchbaseBucket') && CouchbaseBucketAdapter::isSupported()) { return CouchbaseBucketAdapter::createConnection($dsn, $options); } diff --git a/src/Symfony/Component/Cache/Tests/Adapter/CouchbaseCollectionAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/CouchbaseCollectionAdapterTest.php index 3ae00e06ae8ed..0e94de2b0d31b 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/CouchbaseCollectionAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/CouchbaseCollectionAdapterTest.php @@ -12,7 +12,6 @@ namespace Symfony\Component\Cache\Tests\Adapter; use Psr\Cache\CacheItemPoolInterface; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Cache\Adapter\AbstractAdapter; use Symfony\Component\Cache\Adapter\CouchbaseCollectionAdapter; @@ -20,22 +19,18 @@ * @requires extension couchbase <4.0.0 * @requires extension couchbase >=3.0.5 * - * @group legacy integration + * @group integration * * @author Antonio Jose Cerezo Aranda */ class CouchbaseCollectionAdapterTest extends AdapterTestCase { - use ExpectDeprecationTrait; - protected $skippedTests = [ 'testClearPrefix' => 'Couchbase cannot clear by prefix', ]; public function createCachePool($defaultLifetime = 0): CacheItemPoolInterface { - $this->expectDeprecation('Since symfony/cache 7.1: The "Symfony\Component\Cache\Adapter\CouchbaseBucketAdapter" class is deprecated, use "Symfony\Component\Cache\Adapter\CouchbaseCollectionAdapter" instead.'); - $client = AbstractAdapter::createConnection('couchbase://'.getenv('COUCHBASE_HOST').'/cache', ['username' => getenv('COUCHBASE_USER'), 'password' => getenv('COUCHBASE_PASS')] ); diff --git a/src/Symfony/Component/Cache/Traits/RelayProxy.php b/src/Symfony/Component/Cache/Traits/RelayProxy.php index 90af7a7d48ed4..ac839dfda9ca0 100644 --- a/src/Symfony/Component/Cache/Traits/RelayProxy.php +++ b/src/Symfony/Component/Cache/Traits/RelayProxy.php @@ -276,6 +276,11 @@ public function replicaof($host = null, $port = 0): \Relay\Relay|bool return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->replicaof(...\func_get_args()); } + public function waitaof($numlocal, $numremote, $timeout): \Relay\Relay|array|false + { + return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->waitaof(...\func_get_args()); + } + public function restore($key, $ttl, $value, $options = null): \Relay\Relay|bool { return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->restore(...\func_get_args()); diff --git a/src/Symfony/Component/Cache/phpunit.xml.dist b/src/Symfony/Component/Cache/phpunit.xml.dist index bdccab25ee56b..fb7c080562c45 100644 --- a/src/Symfony/Component/Cache/phpunit.xml.dist +++ b/src/Symfony/Component/Cache/phpunit.xml.dist @@ -15,7 +15,7 @@ - + diff --git a/src/Symfony/Component/Config/Exception/LogicException.php b/src/Symfony/Component/Config/Exception/LogicException.php new file mode 100644 index 0000000000000..d227aece4b3b2 --- /dev/null +++ b/src/Symfony/Component/Config/Exception/LogicException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Exception; + +class LogicException extends \LogicException +{ +} diff --git a/src/Symfony/Component/Config/Loader/Loader.php b/src/Symfony/Component/Config/Loader/Loader.php index dc65ecb528be4..28e5877bf22f0 100644 --- a/src/Symfony/Component/Config/Loader/Loader.php +++ b/src/Symfony/Component/Config/Loader/Loader.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Config\Loader; use Symfony\Component\Config\Exception\LoaderLoadException; +use Symfony\Component\Config\Exception\LogicException; /** * Loader is the abstract class used by all built-in loaders. @@ -20,7 +21,7 @@ */ abstract class Loader implements LoaderInterface { - protected LoaderResolverInterface $resolver; + protected ?LoaderResolverInterface $resolver = null; public function __construct( protected ?string $env = null, @@ -29,6 +30,10 @@ public function __construct( public function getResolver(): LoaderResolverInterface { + if (null === $this->resolver) { + throw new LogicException('Cannot get a resolver if none was set.'); + } + return $this->resolver; } diff --git a/src/Symfony/Component/Config/Tests/Loader/LoaderTest.php b/src/Symfony/Component/Config/Tests/Loader/LoaderTest.php index 70bfb8fc15005..263bde26de04a 100644 --- a/src/Symfony/Component/Config/Tests/Loader/LoaderTest.php +++ b/src/Symfony/Component/Config/Tests/Loader/LoaderTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Config\Exception\LoaderLoadException; +use Symfony\Component\Config\Exception\LogicException; use Symfony\Component\Config\Loader\Loader; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\Config\Loader\LoaderResolverInterface; @@ -29,6 +30,14 @@ public function testGetSetResolver() $this->assertSame($resolver, $loader->getResolver(), '->setResolver() sets the resolver loader'); } + public function testGetResolverWithoutSetResolver() + { + $this->expectException(LogicException::class); + + $loader = new ProjectLoader1(); + $loader->getResolver(); + } + public function testResolve() { $resolvedLoader = $this->createMock(LoaderInterface::class); @@ -46,6 +55,14 @@ public function testResolve() $this->assertSame($resolvedLoader, $loader->resolve('foo.xml'), '->resolve() finds a loader'); } + public function testResolveWithoutSetResolver() + { + $this->expectException(LoaderLoadException::class); + + $loader = new ProjectLoader1(); + $loader->resolve('foo.xml'); + } + public function testResolveWhenResolverCannotFindLoader() { $resolver = $this->createMock(LoaderResolverInterface::class); diff --git a/src/Symfony/Component/DependencyInjection/Attribute/Autoconfigure.php b/src/Symfony/Component/DependencyInjection/Attribute/Autoconfigure.php index be492e4fda8f1..637a24e3ba1b6 100644 --- a/src/Symfony/Component/DependencyInjection/Attribute/Autoconfigure.php +++ b/src/Symfony/Component/DependencyInjection/Attribute/Autoconfigure.php @@ -20,16 +20,16 @@ class Autoconfigure { /** - * @param array>|null $tags The tags to add to the service - * @param array>|null $calls The calls to be made when instantiating the service - * @param array|null $bind The bindings to declare for the service - * @param bool|string|null $lazy Whether the service is lazy-loaded - * @param bool|null $public Whether to declare the service as public - * @param bool|null $shared Whether to declare the service as shared - * @param bool|null $autowire Whether to declare the service as autowired - * @param array|null $properties The properties to define when creating the service - * @param array|string|null $configurator A PHP function, reference or an array containing a class/Reference and a method to call after the service is fully initialized - * @param string|null $constructor The public static method to use to instantiate the service + * @param array>|string[]|null $tags The tags to add to the service + * @param array>|null $calls The calls to be made when instantiating the service + * @param array|null $bind The bindings to declare for the service + * @param bool|string|null $lazy Whether the service is lazy-loaded + * @param bool|null $public Whether to declare the service as public + * @param bool|null $shared Whether to declare the service as shared + * @param bool|null $autowire Whether to declare the service as autowired + * @param array|null $properties The properties to define when creating the service + * @param array|string|null $configurator A PHP function, reference or an array containing a class/Reference and a method to call after the service is fully initialized + * @param string|null $constructor The public static method to use to instantiate the service */ public function __construct( public ?array $tags = null, diff --git a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php index dcdec0ac25788..dcbf1b36f519d 100644 --- a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php +++ b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php @@ -880,7 +880,7 @@ public function getAlias(string $id): Alias /** * Registers a service definition. * - * This methods allows for simple registration of service definition + * This method allows for simple registration of service definition * with a fluid interface. */ public function register(string $id, ?string $class = null): Definition diff --git a/src/Symfony/Component/DependencyInjection/Loader/PhpFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/PhpFileLoader.php index 35173cf3a031e..f6d032a4a0473 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/PhpFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/PhpFileLoader.php @@ -146,7 +146,7 @@ class_exists(ContainerConfigurator::class); $callback(...$arguments); foreach ($configBuilders as $configBuilder) { - $containerConfigurator->extension($configBuilder->getExtensionAlias(), $configBuilder->toArray(), $this->prepend); + $this->loadExtensionConfig($configBuilder->getExtensionAlias(), ContainerConfigurator::processValue($configBuilder->toArray())); } $this->loadExtensionConfigs(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/config_builder.expected.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/config_builder.expected.yml index efe9667c0c7fd..b34e58227de11 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/config_builder.expected.yml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/config_builder.expected.yml @@ -1,5 +1,5 @@ parameters: - acme.configs: [{ color: blue }] + acme.configs: [{ color: red }, { color: blue }] services: service_container: diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/config_builder.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/config_builder.php index 02772e64ccb99..4b99622eeff1e 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/config_builder.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/config_builder.php @@ -1,11 +1,14 @@ import('nested_config_builder.php'); + $config->color('blue'); }; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/nested_config_builder.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/nested_config_builder.php new file mode 100644 index 0000000000000..7b475b200783d --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/nested_config_builder.php @@ -0,0 +1,11 @@ +color('red'); +}; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php index d3721286cd12a..d7df9b6f11875 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php @@ -56,6 +56,7 @@ public function testPrependExtensionConfig() $loader->load('config/config_builder.php'); $expected = [ + ['color' => 'red'], ['color' => 'blue'], ['foo' => 'bar'], ]; diff --git a/src/Symfony/Component/ExpressionLanguage/ExpressionLanguage.php b/src/Symfony/Component/ExpressionLanguage/ExpressionLanguage.php index 978a068ec6c0b..22112e973a46b 100644 --- a/src/Symfony/Component/ExpressionLanguage/ExpressionLanguage.php +++ b/src/Symfony/Component/ExpressionLanguage/ExpressionLanguage.php @@ -101,7 +101,7 @@ public function parse(Expression|string $expression, array $names, int $flags = public function lint(Expression|string $expression, ?array $names, int $flags = 0): void { if (null === $names) { - trigger_deprecation('symfony/expression-language', '7.1', 'Passing "null" as the second argument of "%s()" is deprecated, pass "self::IGNORE_UNKNOWN_VARIABLES" instead as a third argument.', __METHOD__); + trigger_deprecation('symfony/expression-language', '7.1', 'Passing "null" as the second argument of "%s()" is deprecated, pass "%s\Parser::IGNORE_UNKNOWN_VARIABLES" instead as a third argument.', __METHOD__, __NAMESPACE__); $flags |= Parser::IGNORE_UNKNOWN_VARIABLES; $names = []; diff --git a/src/Symfony/Component/ExpressionLanguage/Parser.php b/src/Symfony/Component/ExpressionLanguage/Parser.php index 1708d18d4e12b..d34e742906dfc 100644 --- a/src/Symfony/Component/ExpressionLanguage/Parser.php +++ b/src/Symfony/Component/ExpressionLanguage/Parser.php @@ -112,7 +112,7 @@ public function parse(TokenStream $stream, array $names = [], int $flags = 0): N public function lint(TokenStream $stream, ?array $names = [], int $flags = 0): void { if (null === $names) { - trigger_deprecation('symfony/expression-language', '7.1', 'Passing "null" as the second argument of "%s()" is deprecated, pass "self::IGNORE_UNKNOWN_VARIABLES" instead as a third argument.', __METHOD__); + trigger_deprecation('symfony/expression-language', '7.1', 'Passing "null" as the second argument of "%s()" is deprecated, pass "%s::IGNORE_UNKNOWN_VARIABLES" instead as a third argument.', __METHOD__, __CLASS__); $flags |= self::IGNORE_UNKNOWN_VARIABLES; $names = []; diff --git a/src/Symfony/Component/Form/FormRegistryInterface.php b/src/Symfony/Component/Form/FormRegistryInterface.php index b1e77898e2234..5c76b5c67843c 100644 --- a/src/Symfony/Component/Form/FormRegistryInterface.php +++ b/src/Symfony/Component/Form/FormRegistryInterface.php @@ -21,7 +21,7 @@ interface FormRegistryInterface /** * Returns a form type by name. * - * This methods registers the type extensions from the form extensions. + * This method registers the type extensions from the form extensions. * * @throws Exception\InvalidArgumentException if the type cannot be retrieved from any extension */ diff --git a/src/Symfony/Component/HttpFoundation/Response.php b/src/Symfony/Component/HttpFoundation/Response.php index 0ebe65f43dc01..22c09a01fc9db 100644 --- a/src/Symfony/Component/HttpFoundation/Response.php +++ b/src/Symfony/Component/HttpFoundation/Response.php @@ -777,7 +777,7 @@ public function getMaxAge(): ?int /** * Sets the number of seconds after which the response should no longer be considered fresh. * - * This methods sets the Cache-Control max-age directive. + * This method sets the Cache-Control max-age directive. * * @return $this * @@ -825,7 +825,7 @@ public function setStaleWhileRevalidate(int $value): static /** * Sets the number of seconds after which the response should no longer be considered fresh by shared caches. * - * This methods sets the Cache-Control s-maxage directive. + * This method sets the Cache-Control s-maxage directive. * * @return $this * diff --git a/src/Symfony/Component/HttpKernel/CHANGELOG.md b/src/Symfony/Component/HttpKernel/CHANGELOG.md index 81daf2166f708..eb35d73955e80 100644 --- a/src/Symfony/Component/HttpKernel/CHANGELOG.md +++ b/src/Symfony/Component/HttpKernel/CHANGELOG.md @@ -9,7 +9,10 @@ CHANGELOG * Add `$validationFailedStatusCode` argument to `#[MapQueryParameter]` that allows setting a custom HTTP status code when validation fails * Add `NearMissValueResolverException` to let value resolvers report when an argument could be under their watch but failed to be resolved * Add `$type` argument to `#[MapRequestPayload]` that allows mapping a list of items - * Deprecate `Extension::addAnnotatedClassesToCompile()` and related code infrastructure + * The `Extension` class is marked as internal, extend the `Extension` class from the DependencyInjection component instead + * Deprecate `Extension::addAnnotatedClassesToCompile()` + * Deprecate `AddAnnotatedClassesToCachePass` + * Deprecate the `setAnnotatedClassCache()` and `getAnnotatedClassesToCompile()` methods of the `Kernel` class * Add `#[MapUploadedFile]` attribute to fetch, validate, and inject uploaded files into controller arguments 7.0 diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index f50a3b42938de..a3269806200b8 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -73,12 +73,12 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl */ private static array $freshCache = []; - public const VERSION = '7.1.0-RC1'; + public const VERSION = '7.1.0'; public const VERSION_ID = 70100; public const MAJOR_VERSION = 7; public const MINOR_VERSION = 1; public const RELEASE_VERSION = 0; - public const EXTRA_VERSION = 'RC1'; + public const EXTRA_VERSION = ''; public const END_OF_MAINTENANCE = '01/2025'; public const END_OF_LIFE = '01/2025'; diff --git a/src/Symfony/Component/Mailer/Tests/Transport/Fixtures/fake-failing-sendmail.php b/src/Symfony/Component/Mailer/Tests/Transport/Fixtures/fake-failing-sendmail.php index 920b980e0f714..1ce987202d3d6 100755 --- a/src/Symfony/Component/Mailer/Tests/Transport/Fixtures/fake-failing-sendmail.php +++ b/src/Symfony/Component/Mailer/Tests/Transport/Fixtures/fake-failing-sendmail.php @@ -1,4 +1,8 @@ #!/usr/bin/env php markTestSkipped('Windows does not support shebangs nor non-blocking standard streams'); - } + $this->skipOnWindows(); $mail = new Email(); $mail @@ -68,20 +72,9 @@ public function testToIsUsedWhenRecipientsAreNotSet() public function testRecipientsAreUsedWhenSet() { - if ('\\' === \DIRECTORY_SEPARATOR) { - $this->markTestSkipped('Windows does not support shebangs nor non-blocking standard streams'); - } + $this->skipOnWindows(); - $mail = new Email(); - $mail - ->from('from@mail.com') - ->to('to@mail.com') - ->subject('Subject') - ->text('Some text') - ; - - $envelope = new DelayedEnvelope($mail); - $envelope->setRecipients([new Address('recipient@mail.com')]); + [$mail, $envelope] = $this->defaultMailAndEnvelope(); $sendmailTransport = new SendmailTransport(self::FAKE_SENDMAIL); $sendmailTransport->send($mail, $envelope); @@ -90,11 +83,90 @@ public function testRecipientsAreUsedWhenSet() } public function testThrowsTransportExceptionOnFailure() + { + $this->skipOnWindows(); + + [$mail, $envelope] = $this->defaultMailAndEnvelope(); + + $sendmailTransport = new SendmailTransport(self::FAKE_FAILING_SENDMAIL); + $this->expectException(TransportException::class); + $this->expectExceptionMessage('Process failed with exit code 42: Sending failed'); + $sendmailTransport->send($mail, $envelope); + + $streamProperty = new \ReflectionProperty(SendmailTransport::class, 'stream'); + $streamProperty->setAccessible(true); + $stream = $streamProperty->getValue($sendmailTransport); + $this->assertNull($stream->stream); + } + + public function testStreamIsClearedOnFailure() + { + $this->skipOnWindows(); + + [$mail, $envelope] = $this->defaultMailAndEnvelope(); + + $sendmailTransport = new SendmailTransport(self::FAKE_FAILING_SENDMAIL); + try { + $sendmailTransport->send($mail, $envelope); + } catch (TransportException $e) { + } + + $streamProperty = new \ReflectionProperty(SendmailTransport::class, 'stream'); + $streamProperty->setAccessible(true); + $stream = $streamProperty->getValue($sendmailTransport); + $innerStreamProperty = new \ReflectionProperty(ProcessStream::class, 'stream'); + $innerStreamProperty->setAccessible(true); + $this->assertNull($innerStreamProperty->getValue($stream)); + } + + public function testDoesNotThrowWhenInteractive() + { + $this->skipOnWindows(); + + [$mail, $envelope] = $this->defaultMailAndEnvelope(); + + $sendmailTransport = new SendmailTransport(self::FAKE_INTERACTIVE_SENDMAIL); + $transportProperty = new \ReflectionProperty(SendmailTransport::class, 'transport'); + $transportProperty->setAccessible(true); + + // Replace the transport with an anonymous consumer that trigger the stream methods + $transportProperty->setValue($sendmailTransport, new class($transportProperty->getValue($sendmailTransport)->getStream()) implements TransportInterface { + private $stream; + + public function __construct(ProcessStream $stream) + { + $this->stream = $stream; + } + + public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMessage + { + $this->stream->initialize(); + $this->stream->write('SMTP'); + $this->stream->terminate(); + + return new SentMessage($message, $envelope); + } + + public function __toString(): string + { + return 'Interactive mode test'; + } + }); + + $sendmailTransport->send($mail, $envelope); + + $this->assertStringEqualsFile($this->argsPath, __DIR__.'/Fixtures/fake-failing-sendmail.php -bs'); + } + + private function skipOnWindows() { if ('\\' === \DIRECTORY_SEPARATOR) { $this->markTestSkipped('Windows does not support shebangs nor non-blocking standard streams'); } + } + private function defaultMailAndEnvelope(): array + { $mail = new Email(); $mail ->from('from@mail.com') @@ -106,9 +178,6 @@ public function testThrowsTransportExceptionOnFailure() $envelope = new DelayedEnvelope($mail); $envelope->setRecipients([new Address('recipient@mail.com')]); - $sendmailTransport = new SendmailTransport(self::FAKE_FAILING_SENDMAIL); - $this->expectException(TransportException::class); - $this->expectExceptionMessage('Process failed with exit code 42: Sending failed'); - $sendmailTransport->send($mail, $envelope); + return [$mail, $envelope]; } } diff --git a/src/Symfony/Component/Mailer/Transport/SendmailTransport.php b/src/Symfony/Component/Mailer/Transport/SendmailTransport.php index 36bb93802effb..3add460ebcf89 100644 --- a/src/Symfony/Component/Mailer/Transport/SendmailTransport.php +++ b/src/Symfony/Component/Mailer/Transport/SendmailTransport.php @@ -64,6 +64,7 @@ public function __construct(?string $command = null, ?EventDispatcherInterface $ $this->stream = new ProcessStream(); if (str_contains($this->command, ' -bs')) { $this->stream->setCommand($this->command); + $this->stream->setInteractive(true); $this->transport = new SmtpTransport($this->stream, $dispatcher, $logger); } } diff --git a/src/Symfony/Component/Mailer/Transport/Smtp/Stream/ProcessStream.php b/src/Symfony/Component/Mailer/Transport/Smtp/Stream/ProcessStream.php index 8644d7dad7296..96a6c0fe85b33 100644 --- a/src/Symfony/Component/Mailer/Transport/Smtp/Stream/ProcessStream.php +++ b/src/Symfony/Component/Mailer/Transport/Smtp/Stream/ProcessStream.php @@ -24,12 +24,18 @@ final class ProcessStream extends AbstractStream { private string $command; + private bool $interactive = false; public function setCommand(string $command): void { $this->command = $command; } + public function setInteractive(bool $interactive) + { + $this->interactive = $interactive; + } + public function initialize(): void { $descriptorSpec = [ @@ -57,11 +63,15 @@ public function terminate(): void $err = stream_get_contents($this->err); fclose($this->err); if (0 !== $exitCode = proc_close($this->stream)) { - throw new TransportException('Process failed with exit code '.$exitCode.': '.$out.$err); + $errorMessage = 'Process failed with exit code '.$exitCode.': '.$out.$err; } } parent::terminate(); + + if (!$this->interactive && isset($errorMessage)) { + throw new TransportException($errorMessage); + } } protected function getReadConnectionDescription(): string diff --git a/src/Symfony/Component/Mime/Message.php b/src/Symfony/Component/Mime/Message.php index dea746ad42c29..0374ae7a07068 100644 --- a/src/Symfony/Component/Mime/Message.php +++ b/src/Symfony/Component/Mime/Message.php @@ -124,11 +124,11 @@ public function toIterable(): iterable public function ensureValidity(): void { - if (!$this->headers->has('To') && !$this->headers->has('Cc') && !$this->headers->has('Bcc')) { + if (!$this->headers->get('To')?->getBody() && !$this->headers->get('Cc')?->getBody() && !$this->headers->get('Bcc')?->getBody()) { throw new LogicException('An email must have a "To", "Cc", or "Bcc" header.'); } - if (!$this->headers->has('From') && !$this->headers->has('Sender')) { + if (!$this->headers->get('From')?->getBody() && !$this->headers->get('Sender')?->getBody()) { throw new LogicException('An email must have a "From" or a "Sender" header.'); } diff --git a/src/Symfony/Component/Mime/Tests/MessageTest.php b/src/Symfony/Component/Mime/Tests/MessageTest.php index 4f7c2a67ce396..1d01fa4519e93 100644 --- a/src/Symfony/Component/Mime/Tests/MessageTest.php +++ b/src/Symfony/Component/Mime/Tests/MessageTest.php @@ -276,4 +276,71 @@ public function testSymfonySerialize() $serialized = $serializer->serialize($e, 'json'); $this->assertStringMatchesFormat($expectedJson, json_encode(json_decode($serialized), \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES)); } + + /** + * @dataProvider ensureValidityProvider + */ + public function testEnsureValidity(array $headers, ?string $exceptionClass, ?string $exceptionMessage) + { + if ($exceptionClass) { + $this->expectException($exceptionClass); + $this->expectExceptionMessage($exceptionMessage); + } else { + $this->expectNotToPerformAssertions(); + } + + $m = new Message(); + foreach ($headers as $headerName => $headerValue) { + $m->getHeaders()->addMailboxListHeader($headerName, $headerValue); + } + $m->ensureValidity(); + } + + public function ensureValidityProvider() + { + return [ + 'Valid address fields' => [ + [ + 'To' => ['dummy@symfony.com'], + 'From' => ['test@symfony.com'], + ], + null, + null, + ], + + 'No destination address fields' => [ + [ + 'From' => ['test@symfony.com'], + ], + LogicException::class, + 'An email must have a "To", "Cc", or "Bcc" header.', + ], + + 'Empty destination address fields' => [ + [ + 'To' => [], + 'From' => ['test@symfony.com'], + ], + LogicException::class, + 'An email must have a "To", "Cc", or "Bcc" header.', + ], + + 'No originator fields' => [ + [ + 'To' => ['dummy@symfony.com'], + ], + LogicException::class, + 'An email must have a "From" or a "Sender" header.', + ], + + 'Empty originator fields' => [ + [ + 'To' => ['dummy@symfony.com'], + 'From' => [], + ], + LogicException::class, + 'An email must have a "From" or a "Sender" header.', + ], + ]; + } } diff --git a/src/Symfony/Component/Notifier/Bridge/Slack/SlackTransport.php b/src/Symfony/Component/Notifier/Bridge/Slack/SlackTransport.php index 7b28412434603..18063efadd1cd 100644 --- a/src/Symfony/Component/Notifier/Bridge/Slack/SlackTransport.php +++ b/src/Symfony/Component/Notifier/Bridge/Slack/SlackTransport.php @@ -78,7 +78,7 @@ protected function doSend(MessageInterface $message): SlackSentMessage } $response = $this->client->request('POST', 'https://'.$this->getEndpoint().'/api/'.$apiMethod, [ - 'json' => array_filter($options), + 'json' => array_filter($options, function ($value): bool { return !\in_array($value, ['', [], null], true); }), 'auth_bearer' => $this->accessToken, 'headers' => [ 'Content-Type' => 'application/json; charset=utf-8', diff --git a/src/Symfony/Component/Notifier/Bridge/Slack/Tests/SlackTransportTest.php b/src/Symfony/Component/Notifier/Bridge/Slack/Tests/SlackTransportTest.php index 0b9a17fc08493..b0a54d5e0c611 100644 --- a/src/Symfony/Component/Notifier/Bridge/Slack/Tests/SlackTransportTest.php +++ b/src/Symfony/Component/Notifier/Bridge/Slack/Tests/SlackTransportTest.php @@ -167,6 +167,56 @@ public function testSendWithNotification() $this->assertSame('1503435956.000247', $sentMessage->getMessageId()); } + /** + * @testWith [true] + * [false] + */ + public function testSendWithBooleanOptionValue(bool $value) + { + $channel = 'testChannel'; + $message = 'testMessage'; + + $response = $this->createMock(ResponseInterface::class); + + $response->expects($this->exactly(2)) + ->method('getStatusCode') + ->willReturn(200); + + $response->expects($this->once()) + ->method('getContent') + ->willReturn(json_encode(['ok' => true, 'ts' => '1503435956.000247', 'channel' => 'C123456'])); + + $options = new SlackOptions(); + $options->asUser($value); + $options->linkNames($value); + $options->mrkdwn($value); + $options->unfurlLinks($value); + $options->unfurlMedia($value); + $notification = new Notification($message); + $chatMessage = ChatMessage::fromNotification($notification); + $chatMessage->options($options); + + $expectedBody = json_encode([ + 'as_user' => $value, + 'channel' => $channel, + 'link_names' => $value, + 'mrkdwn' => $value, + 'text' => $message, + 'unfurl_links' => $value, + 'unfurl_media' => $value, + ]); + + $client = new MockHttpClient(function (string $method, string $url, array $options = []) use ($response, $expectedBody): ResponseInterface { + $this->assertJsonStringEqualsJsonString($expectedBody, $options['body']); + + return $response; + }); + + $transport = self::createTransport($client, $channel); + + $transport->send($chatMessage); + } + public function testSendWith200ResponseButNotOk() { $channel = 'testChannel'; diff --git a/src/Symfony/Component/PropertyInfo/Extractor/ConstructorArgumentTypeExtractorInterface.php b/src/Symfony/Component/PropertyInfo/Extractor/ConstructorArgumentTypeExtractorInterface.php index 17ec478c39555..571b6fa6f014a 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/ConstructorArgumentTypeExtractorInterface.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/ConstructorArgumentTypeExtractorInterface.php @@ -28,8 +28,6 @@ interface ConstructorArgumentTypeExtractorInterface * * @return LegacyType[]|null * - * @deprecated since Symfony 7.1, use "getTypeFromConstructor" instead - * * @internal */ public function getTypesFromConstructor(string $class, string $property): ?array; diff --git a/src/Symfony/Component/PropertyInfo/PropertyInfoCacheExtractor.php b/src/Symfony/Component/PropertyInfo/PropertyInfoCacheExtractor.php index 83c7fc3fa801e..38b9c68a2e29e 100644 --- a/src/Symfony/Component/PropertyInfo/PropertyInfoCacheExtractor.php +++ b/src/Symfony/Component/PropertyInfo/PropertyInfoCacheExtractor.php @@ -64,9 +64,6 @@ public function getType(string $class, string $property, array $context = []): ? return $this->extract('getType', [$class, $property, $context]); } - /** - * @deprecated since Symfony 7.1, use "getType" instead - */ public function getTypes(string $class, string $property, array $context = []): ?array { return $this->extract('getTypes', [$class, $property, $context]); diff --git a/src/Symfony/Component/Routing/Exception/LogicException.php b/src/Symfony/Component/Routing/Exception/LogicException.php new file mode 100644 index 0000000000000..16ed58eefe379 --- /dev/null +++ b/src/Symfony/Component/Routing/Exception/LogicException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Exception; + +class LogicException extends \LogicException +{ +} diff --git a/src/Symfony/Component/Routing/Loader/AttributeClassLoader.php b/src/Symfony/Component/Routing/Loader/AttributeClassLoader.php index 3b895a9f8006e..8372d90ae9b49 100644 --- a/src/Symfony/Component/Routing/Loader/AttributeClassLoader.php +++ b/src/Symfony/Component/Routing/Loader/AttributeClassLoader.php @@ -15,6 +15,7 @@ use Symfony\Component\Config\Loader\LoaderResolverInterface; use Symfony\Component\Config\Resource\FileResource; use Symfony\Component\Routing\Attribute\Route as RouteAnnotation; +use Symfony\Component\Routing\Exception\LogicException; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; @@ -226,6 +227,7 @@ public function setResolver(LoaderResolverInterface $resolver): void public function getResolver(): LoaderResolverInterface { + throw new LogicException(sprintf('The "%s()" method must not be called.', __METHOD__)); } /** diff --git a/src/Symfony/Component/Routing/Tests/Loader/AttributeClassLoaderTest.php b/src/Symfony/Component/Routing/Tests/Loader/AttributeClassLoaderTest.php index 9b8c5d27397ed..ad65f09c253cc 100644 --- a/src/Symfony/Component/Routing/Tests/Loader/AttributeClassLoaderTest.php +++ b/src/Symfony/Component/Routing/Tests/Loader/AttributeClassLoaderTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Routing\Alias; +use Symfony\Component\Routing\Exception\LogicException; use Symfony\Component\Routing\Tests\Fixtures\AttributeFixtures\AbstractClassController; use Symfony\Component\Routing\Tests\Fixtures\AttributeFixtures\ActionPathController; use Symfony\Component\Routing\Tests\Fixtures\AttributeFixtures\BazClass; @@ -54,6 +55,14 @@ protected function setUp(?string $env = null): void $this->loader = new TraceableAttributeClassLoader($env); } + public function testGetResolver() + { + $this->expectException(LogicException::class); + + $loader = new TraceableAttributeClassLoader(); + $loader->getResolver(); + } + /** * @dataProvider provideTestSupportsChecksResource */ diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.el.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.el.xlf index 383bfc2cf4f17..25cfb43bdf474 100644 --- a/src/Symfony/Component/Security/Core/Resources/translations/security.el.xlf +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.el.xlf @@ -76,7 +76,7 @@ Too many failed login attempts, please try again in %minutes% minutes. - Πολλές αποτυχημένες προσπάθειες σύνδεσης, δοκιμάστε ξανά σε %minutes% λεπτά. + Πολλές αποτυχημένες προσπάθειες σύνδεσης, δοκιμάστε ξανά σε %minutes% λεπτά. diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.fr.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.fr.xlf index b3793bc7a25a1..058ad9473b96a 100644 --- a/src/Symfony/Component/Security/Core/Resources/translations/security.fr.xlf +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.fr.xlf @@ -76,7 +76,7 @@ Too many failed login attempts, please try again in %minutes% minutes. - Trop de tentatives de connexion échouées, veuillez réessayer dans %minutes% minutes. + Trop de tentatives de connexion échouées, veuillez réessayer dans %minutes% minutes. diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.hr.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.hr.xlf index a8f0066d8a4e5..f3b5a257e5f28 100644 --- a/src/Symfony/Component/Security/Core/Resources/translations/security.hr.xlf +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.hr.xlf @@ -76,7 +76,7 @@ Too many failed login attempts, please try again in %minutes% minutes. - Previše neuspjelih pokušaja prijave, pokušajte ponovo za %minutes% minutu.|Previše neuspjelih pokušaja prijave, pokušajte ponovo za %minutes% minuta. + Previše neuspjelih pokušaja prijave, pokušajte ponovo za %minutes% minutu.|Previše neuspjelih pokušaja prijave, pokušajte ponovo za %minutes% minute.|Previše neuspjelih pokušaja prijave, pokušajte ponovo za %minutes% minuta. diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.id.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.id.xlf index 0bfd6474f61e4..4c1cd9965e1af 100644 --- a/src/Symfony/Component/Security/Core/Resources/translations/security.id.xlf +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.id.xlf @@ -4,7 +4,7 @@ An authentication exception occurred. - Terjadi sebuah pengecualian otentikasi. + Terjadi kesalahan otentikasi. Authentication credentials could not be found. @@ -16,7 +16,7 @@ Invalid credentials. - Kredensial salah. + Kredensial tidak valid. Cookie has already been used by someone else. @@ -28,7 +28,7 @@ Invalid CSRF token. - Token CSRF salah. + Token CSRF tidak valid. No authentication provider found to support the authentication token. @@ -64,19 +64,19 @@ Too many failed login attempts, please try again later. - Terlalu banyak percobaan login yang salah, silahkan coba lagi nanti. + Terlalu banyak percobaan login yang gagal, silahkan coba lagi nanti. Invalid or expired login link. - Link login salah atau sudah kedaluwarsa. + Link login tidak valid atau sudah kedaluwarsa. Too many failed login attempts, please try again in %minutes% minute. - Terlalu banyak percobaan login yang salah, silahkan coba lagi dalam %minutes% menit. + Terlalu banyak percobaan login yang gagal, silahkan coba lagi dalam %minutes% menit. Too many failed login attempts, please try again in %minutes% minutes. - Terlalu banyak upaya login yang gagal, silakan coba lagi dalam %minutes% menit.|Terlalu banyak upaya login yang gagal, silakan coba lagi dalam %minutes% menit. + Terlalu banyak upaya login yang gagal, silakan coba lagi dalam beberapa %minutes% menit. diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.ja.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.ja.xlf index 2d9590a8c3474..bc3a18aefd8b2 100644 --- a/src/Symfony/Component/Security/Core/Resources/translations/security.ja.xlf +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.ja.xlf @@ -76,7 +76,7 @@ Too many failed login attempts, please try again in %minutes% minutes. - ログイン試行の失敗が多すぎます。%minutes% 分後に再試行してください。|ログイン試行の失敗が多すぎます。%minutes% 分後に再試行してください。 + ログイン試行回数が多すぎます。%minutes%分後に再度お試しください。 diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.lv.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.lv.xlf index 0a3164caf7985..fdf0a09698887 100644 --- a/src/Symfony/Component/Security/Core/Resources/translations/security.lv.xlf +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.lv.xlf @@ -76,7 +76,7 @@ Too many failed login attempts, please try again in %minutes% minutes. - Pārāk daudz neveiksmīgu pieteikšanās mēģinājumu, lūdzu, mēģiniet vēlreiz pēc %minutes% minūtes.|Pārāk daudz neveiksmīgu pieteikšanās mēģinājumu, lūdzu, mēģiniet vēlreiz pēc %minutes% minūtēm. + Pārāk daudz neveiksmīgu autentifikācijas mēģinājumu, lūdzu, mēģiniet vēlreiz pēc %minutes% minūtes.|Pārāk daudz neveiksmīgu autentifikācijas mēģinājumu, lūdzu, mēģiniet vēlreiz pēc %minutes% minūtēm. diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.pl.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.pl.xlf index f842a2169ee64..0cfc58b35bf2d 100644 --- a/src/Symfony/Component/Security/Core/Resources/translations/security.pl.xlf +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.pl.xlf @@ -76,7 +76,7 @@ Too many failed login attempts, please try again in %minutes% minutes. - Zbyt wiele nieudanych prób logowania, spróbuj ponownie za %minutes% minutę.|Zbyt wiele nieudanych prób logowania, spróbuj ponownie za %minutes% minuty.|Zbyt wiele nieudanych prób logowania, spróbuj ponownie za %minutes% minut. + Zbyt wiele nieudanych prób logowania, spróbuj ponownie za %minutes% minutę.|Zbyt wiele nieudanych prób logowania, spróbuj ponownie za %minutes% minuty.|Zbyt wiele nieudanych prób logowania, spróbuj ponownie za %minutes% minut. diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.ro.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.ro.xlf index c46b9b50738a0..3316275fdec13 100644 --- a/src/Symfony/Component/Security/Core/Resources/translations/security.ro.xlf +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.ro.xlf @@ -76,7 +76,7 @@ Too many failed login attempts, please try again in %minutes% minutes. - Prea multe încercări eșuate de autentificare, vă rugăm să încercați din nou peste %minutes% minut.|Prea multe încercări eșuate de autentificare, vă rugăm să încercați din nou peste %minutes% minute. + Prea multe încercări eșuate de autentificare, vă rugăm să încercați din nou peste %minutes% minut.|Prea multe încercări eșuate de autentificare, vă rugăm să încercați din nou peste %minutes% minute. diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.ru.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.ru.xlf index 4ff423f0e7c3d..8705a24cff2e3 100644 --- a/src/Symfony/Component/Security/Core/Resources/translations/security.ru.xlf +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.ru.xlf @@ -72,11 +72,11 @@ Too many failed login attempts, please try again in %minutes% minute. - Слишком много неудачных попыток входа в систему, повторите попытку через %minutes% минуту. + Слишком много неудачных попыток входа, повторите попытку через %minutes% минуту. Too many failed login attempts, please try again in %minutes% minutes. - Слишком много неудачных попыток входа в систему, попробуйте снова через %minutes% минуту.|Слишком много неудачных попыток входа в систему, попробуйте снова через %minutes% минуты.|Слишком много неудачных попыток входа в систему, попробуйте снова через %minutes% минут. + Слишком много неудачных попыток входа, повторите попытку через %minutes% минуту.|Слишком много неудачных попыток входа, повторите попытку через %minutes% минуты.|Слишком много неудачных попыток входа, повторите попытку через %minutes% минут. diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.uk.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.uk.xlf index 3d6a561355e82..6b27de7caed99 100644 --- a/src/Symfony/Component/Security/Core/Resources/translations/security.uk.xlf +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.uk.xlf @@ -76,7 +76,7 @@ Too many failed login attempts, please try again in %minutes% minutes. - Занадто багато невдалих спроб входу, будь ласка, спробуйте ще раз через %minutes% хвилину.|Занадто багато невдалих спроб входу, будь ласка, спробуйте ще раз через %minutes% хвилини.|Занадто багато невдалих спроб входу, будь ласка, спробуйте ще раз через %minutes% хвилин. + Забагато невдалих спроб входу, будь ласка, спробуйте ще раз через %minutes% хвилину.|Забагато невдалих спроб входу, будь ласка, спробуйте ще раз через %minutes% хвилини.|Забагато невдалих спроб входу, будь ласка, спробуйте ще раз через %minutes% хвилин. diff --git a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php index c52e9e8382a30..1b51b729c660b 100644 --- a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php @@ -33,6 +33,8 @@ final class ObjectNormalizer extends AbstractObjectNormalizer { private static $reflectionCache = []; + private static $isReadableCache = []; + private static $isWritableCache = []; protected PropertyAccessorInterface $propertyAccessor; protected $propertyInfoExtractor; @@ -170,21 +172,23 @@ protected function isAllowedAttribute($classOrObject, string $attribute, ?string if (!parent::isAllowedAttribute($classOrObject, $attribute, $format, $context)) { return false; } + $class = \is_object($classOrObject) ? \get_class($classOrObject) : $classOrObject; if ($context['_read_attributes'] ?? true) { - return $this->propertyInfoExtractor->isReadable($class, $attribute) || $this->hasAttributeAccessorMethod($class, $attribute); - } + if (!isset(self::$isReadableCache[$class.$attribute])) { + self::$isReadableCache[$class.$attribute] = $this->propertyInfoExtractor->isReadable($class, $attribute) || $this->hasAttributeAccessorMethod($class, $attribute); + } - if ($this->propertyInfoExtractor->isWritable($class, $attribute)) { - return true; + return self::$isReadableCache[$class.$attribute]; } - if (($writeInfo = $this->writeInfoExtractor->getWriteInfo($class, $attribute)) && PropertyWriteInfo::TYPE_NONE !== $writeInfo->getType()) { - return true; + if (!isset(self::$isWritableCache[$class.$attribute])) { + self::$isWritableCache[$class.$attribute] = $this->propertyInfoExtractor->isWritable($class, $attribute) + || (($writeInfo = $this->writeInfoExtractor->getWriteInfo($class, $attribute)) && PropertyWriteInfo::TYPE_NONE !== $writeInfo->getType()); } - return false; + return self::$isWritableCache[$class.$attribute]; } private function hasAttributeAccessorMethod(string $class, string $attribute): bool diff --git a/src/Symfony/Component/Validator/Constraints/CharsetValidator.php b/src/Symfony/Component/Validator/Constraints/CharsetValidator.php index 5cde6ae90fd96..63259a45d28c2 100644 --- a/src/Symfony/Component/Validator/Constraints/CharsetValidator.php +++ b/src/Symfony/Component/Validator/Constraints/CharsetValidator.php @@ -38,7 +38,7 @@ public function validate(mixed $value, Constraint $constraint): void if (!\in_array(mb_detect_encoding($value, $constraint->encodings, true), (array) $constraint->encodings, true)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ detected }}', mb_detect_encoding($value, strict: true)) - ->setParameter('{{ encodings }}', implode(', ', $constraint->encodings)) + ->setParameter('{{ encodings }}', implode(', ', (array) $constraint->encodings)) ->setCode(Charset::BAD_ENCODING_ERROR) ->addViolation(); } diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.ja.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.ja.xlf index c977df4a3e186..9ec98ebbd47c4 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.ja.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.ja.xlf @@ -136,7 +136,7 @@ This value is not a valid IP address. - この値は有効なIPアドレスではありません。 + 有効なIPアドレスではありません。 This value is not a valid language. @@ -192,7 +192,7 @@ No temporary folder was configured in php.ini, or the configured folder does not exist. - php.iniに一時フォルダが設定されていないか、設定されたフォルダが存在しません。 + php.iniに一時フォルダが設定されていないか、設定されたフォルダが存在しません。 Cannot write temporary file to disk. @@ -224,7 +224,7 @@ This value is not a valid International Bank Account Number (IBAN). - この値は有効な国際銀行口座番号(IBAN)ではありません。 + 有効な国際銀行勘定番号(IBAN)ではありません。 This value is not a valid ISBN-10. @@ -312,7 +312,7 @@ This value is not a valid Business Identifier Code (BIC). - この値は有効なビジネス識別コード(BIC)ではありません。 + 有効なSWIFTコードではありません。 Error @@ -320,7 +320,7 @@ This value is not a valid UUID. - この値は有効なUUIDではありません。 + 有効なUUIDではありません。 This value should be a multiple of {{ compared_value }}. @@ -436,11 +436,11 @@ This value is not a valid MAC address. - この値は有効なMACアドレスではありません。 + 有効なMACアドレスではありません。 This URL is missing a top-level domain. - このURLはトップレベルドメインがありません。 + このURLはトップレベルドメインがありません。 diff --git a/src/Symfony/Component/Validator/Tests/Constraints/CharsetValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/CharsetValidatorTest.php index 88998dcdbcad0..5c2f2c884f96f 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/CharsetValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/CharsetValidatorTest.php @@ -37,13 +37,13 @@ public function testEncodingIsValid(string|\Stringable $value, array|string $enc /** * @dataProvider provideInvalidValues */ - public function testInvalidValues(string $value, array $encodings) + public function testInvalidValues(string $value, array|string $encodings) { $this->validator->validate($value, new Charset(encodings: $encodings)); $this->buildViolation('The detected character encoding is invalid ({{ detected }}). Allowed encodings are {{ encodings }}.') ->setParameter('{{ detected }}', 'UTF-8') - ->setParameter('{{ encodings }}', implode(', ', $encodings)) + ->setParameter('{{ encodings }}', implode(', ', (array) $encodings)) ->setCode(Charset::BAD_ENCODING_ERROR) ->assertRaised(); } @@ -73,6 +73,7 @@ public static function provideValidValues() public static function provideInvalidValues() { + yield ['my non-Äscîi string', 'ASCII']; yield ['my non-Äscîi string', ['ASCII']]; yield ['😊', ['7bit']]; } diff --git a/src/Symfony/Component/VarDumper/Caster/ReflectionCaster.php b/src/Symfony/Component/VarDumper/Caster/ReflectionCaster.php index c7ccaf97aac62..e7bd9a152a006 100644 --- a/src/Symfony/Component/VarDumper/Caster/ReflectionCaster.php +++ b/src/Symfony/Component/VarDumper/Caster/ReflectionCaster.php @@ -83,13 +83,13 @@ public static function castGenerator(\Generator $c, array $a, Stub $stub, bool $ // Cannot create ReflectionGenerator based on a terminated Generator try { $reflectionGenerator = new \ReflectionGenerator($c); + + return self::castReflectionGenerator($reflectionGenerator, $a, $stub, $isNested); } catch (\Exception) { $a[Caster::PREFIX_VIRTUAL.'closed'] = true; return $a; } - - return self::castReflectionGenerator($reflectionGenerator, $a, $stub, $isNested); } public static function castType(\ReflectionType $c, array $a, Stub $stub, bool $isNested): array diff --git a/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php b/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php index b7a260e230f92..522b9ae34398d 100644 --- a/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php +++ b/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php @@ -460,6 +460,84 @@ class: "Symfony\Component\VarDumper\Tests\Caster\ReflectionCasterTest" ); } + /** + * @requires PHP < 8.4 + */ + public function testGeneratorPriorTo84() + { + if (\extension_loaded('xdebug')) { + $this->markTestSkipped('xdebug is active'); + } + + $generator = new GeneratorDemo(); + $generator = $generator->baz(); + + $expectedDump = <<<'EODUMP' +Generator { + this: Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo { …} + %s: { + %sGeneratorDemo.php:14 { + Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo->baz() + › { + › yield from bar(); + › } + } +%A} + closed: false +} +EODUMP; + + $this->assertDumpMatchesFormat($expectedDump, $generator); + + foreach ($generator as $v) { + break; + } + + $expectedDump = <<<'EODUMP' +array:2 [ + 0 => ReflectionGenerator { + this: Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo { …} + %s: { + %s%eTests%eFixtures%eGeneratorDemo.php:%d { + Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo::foo() +%A › yield 1; +%A } + %s%eTests%eFixtures%eGeneratorDemo.php:20 { …} + %s%eTests%eFixtures%eGeneratorDemo.php:14 { …} +%A } + closed: false + } + 1 => Generator { + %s: { + %s%eTests%eFixtures%eGeneratorDemo.php:%d { + Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo::foo() + › yield 1; + › } + › + } +%A } + closed: false + } +] +EODUMP; + + $r = new \ReflectionGenerator($generator); + $this->assertDumpMatchesFormat($expectedDump, [$r, $r->getExecutingGenerator()]); + + foreach ($generator as $v) { + } + + $expectedDump = <<<'EODUMP' +Generator { + closed: true +} +EODUMP; + $this->assertDumpMatchesFormat($expectedDump, $generator); + } + + /** + * @requires PHP 8.4 + */ public function testGenerator() { if (\extension_loaded('xdebug')) { @@ -471,6 +549,7 @@ public function testGenerator() $expectedDump = <<<'EODUMP' Generator { + function: "Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo::baz" this: Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo { …} %s: { %sGeneratorDemo.php:14 { @@ -479,6 +558,7 @@ public function testGenerator() › yield from bar(); › } } + Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo->baz() {} %A} closed: false } @@ -505,6 +585,7 @@ public function testGenerator() closed: false } 1 => Generator { + function: "Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo::foo" %s: { %s%eTests%eFixtures%eGeneratorDemo.php:%d { Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo::foo() @@ -526,6 +607,7 @@ public function testGenerator() $expectedDump = <<<'EODUMP' Generator { + function: "Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo::baz" closed: true } EODUMP; diff --git a/src/Symfony/Component/VarExporter/ProxyHelper.php b/src/Symfony/Component/VarExporter/ProxyHelper.php index 77805d2e8e28b..c5978ee06820f 100644 --- a/src/Symfony/Component/VarExporter/ProxyHelper.php +++ b/src/Symfony/Component/VarExporter/ProxyHelper.php @@ -219,12 +219,14 @@ public static function exportSignature(\ReflectionFunctionAbstract $function, bo $args = ''; $param = null; $parameters = []; + $namespace = $function instanceof \ReflectionMethod ? $function->class : $function->getNamespaceName().'\\'; + $namespace = substr($namespace, 0, strrpos($namespace, '\\') ?: 0); foreach ($function->getParameters() as $param) { $parameters[] = ($param->getAttributes(\SensitiveParameter::class) ? '#[\SensitiveParameter] ' : '') .($withParameterTypes && $param->hasType() ? self::exportType($param).' ' : '') .($param->isPassedByReference() ? '&' : '') .($param->isVariadic() ? '...' : '').'$'.$param->name - .($param->isOptional() && !$param->isVariadic() ? ' = '.self::exportDefault($param) : ''); + .($param->isOptional() && !$param->isVariadic() ? ' = '.self::exportDefault($param, $namespace) : ''); if ($param->isPassedByReference()) { $byRefIndex = 1 + $param->getPosition(); } @@ -333,7 +335,7 @@ private static function exportPropertyScopes(string $parent): string return $propertyScopes; } - private static function exportDefault(\ReflectionParameter $param): string + private static function exportDefault(\ReflectionParameter $param, $namespace): string { $default = rtrim(substr(explode('$'.$param->name.' = ', (string) $param, 2)[1] ?? '', 0, -2)); @@ -347,7 +349,7 @@ private static function exportDefault(\ReflectionParameter $param): string $regexp = "/(\"(?:[^\"\\\\]*+(?:\\\\.)*+)*+\"|'(?:[^'\\\\]*+(?:\\\\.)*+)*+')/"; $parts = preg_split($regexp, $default, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY); - $regexp = '/([\[\( ]|^)([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\\\\[a-zA-Z0-9_\x7f-\xff]++)*+)(?!: )/'; + $regexp = '/([\[\( ]|^)([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\\\\[a-zA-Z0-9_\x7f-\xff]++)*+)(\(?)(?!: )/'; $callback = (false !== strpbrk($default, "\\:('") && $class = $param->getDeclaringClass()) ? fn ($m) => $m[1].match ($m[2]) { 'new', 'false', 'true', 'null' => $m[2], @@ -355,13 +357,13 @@ private static function exportDefault(\ReflectionParameter $param): string 'self' => '\\'.$class->name, 'namespace\\parent', 'parent' => ($parent = $class->getParentClass()) ? '\\'.$parent->name : 'parent', - default => '\\'.$m[2], - } + default => self::exportSymbol($m[2], '(' !== $m[3], $namespace), + }.$m[3] : fn ($m) => $m[1].match ($m[2]) { 'new', 'false', 'true', 'null', 'self', 'parent' => $m[2], 'NULL' => 'null', - default => '\\'.$m[2], - }; + default => self::exportSymbol($m[2], '(' !== $m[3], $namespace), + }.$m[3]; return implode('', array_map(fn ($part) => match ($part[0]) { '"' => $part, // for internal classes only @@ -369,4 +371,18 @@ private static function exportDefault(\ReflectionParameter $param): string default => preg_replace_callback($regexp, $callback, $part), }, $parts)); } + + private static function exportSymbol(string $symbol, bool $mightBeRootConst, string $namespace): string + { + if (!$mightBeRootConst + || false === ($ns = strrpos($symbol, '\\')) + || substr($symbol, 0, $ns) !== $namespace + || \defined($symbol) + || !\defined(substr($symbol, $ns + 1)) + ) { + return '\\'.$symbol; + } + + return '\\'.substr($symbol, $ns + 1); + } } diff --git a/src/Symfony/Component/VarExporter/Tests/ProxyHelperTest.php b/src/Symfony/Component/VarExporter/Tests/ProxyHelperTest.php index c10f47d495d82..c97bd75efdf43 100644 --- a/src/Symfony/Component/VarExporter/Tests/ProxyHelperTest.php +++ b/src/Symfony/Component/VarExporter/Tests/ProxyHelperTest.php @@ -37,6 +37,7 @@ public static function provideExportSignature() $expected = str_replace(['.', ' . . . ', '\'$a\', \'$a\n\', "\$a\n"'], [' . ', '...', '\'$a\', "\$a\\\n", "\$a\n"'], $expected); $expected = str_replace('Bar', '\\'.Bar::class, $expected); $expected = str_replace('self', '\\'.TestForProxyHelper::class, $expected); + $expected = str_replace('= [namespace\M_PI, new M_PI()]', '= [\M_PI, new \Symfony\Component\VarExporter\Tests\M_PI()]', $expected); yield [$expected, $method]; } @@ -237,6 +238,10 @@ public static function foo8() public function foo9($a = self::BOB, $b = ['$a', '$a\n', "\$a\n"], $c = ['$a', '$a\n', "\$a\n", new \stdClass()]) { } + + public function foo10($a = [namespace\M_PI, new M_PI()]) + { + } } interface TestForProxyHelperInterface1