diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 4b8b6eb2a5610..1171d75845cba 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -3,3 +3,4 @@ f4118e110a46de3ffb799e7d79bf15128d1646ea 9519b54417c09c49496a4a6be238e63be9a73465 ae0a783425b80b78376488619bf9106e69193fa4 9c1e36257c4df0929179462d6b2bdd00453ac8aa +6ae74d38e3d20d0ffcc66c7c3d28767fab76bdfb diff --git a/.gitattributes b/.gitattributes index cf8890eefbda8..c699c63193d23 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,5 +5,6 @@ /src/Symfony/Component/Notifier/Bridge export-ignore /src/Symfony/Component/Runtime export-ignore /src/Symfony/Component/Translation/Bridge export-ignore +/src/Symfony/Component/Emoji/Resources/data/* linguist-generated=true /src/Symfony/Component/Intl/Resources/data/*/* linguist-generated=true /.git* export-ignore diff --git a/.github/get-modified-packages.php b/.github/get-modified-packages.php index 990aa35f038f6..11478cbe935c0 100644 --- a/.github/get-modified-packages.php +++ b/.github/get-modified-packages.php @@ -19,31 +19,15 @@ function getPackageType(string $packageDir): string { - if (preg_match('@Symfony/Bridge/@', $packageDir)) { - return 'bridge'; - } - - if (preg_match('@Symfony/Bundle/@', $packageDir)) { - return 'bundle'; - } - - if (preg_match('@Symfony/Component/[^/]+/Bridge/@', $packageDir)) { - return 'component_bridge'; - } - - if (preg_match('@Symfony/Component/@', $packageDir)) { - return 'component'; - } - - if (preg_match('@Symfony/Contracts/@', $packageDir)) { - return 'contract'; - } - - if (preg_match('@Symfony/Contracts$@', $packageDir)) { - return 'contracts'; - } - - throw new \LogicException(); + return match (true) { + str_contains($packageDir, 'Symfony/Bridge/') => 'bridge', + str_contains($packageDir, 'Symfony/Bundle/') => 'bundle', + preg_match('@Symfony/Component/[^/]+/Bridge/@', $packageDir) => 'component_bridge', + str_contains($packageDir, 'Symfony/Component/') => 'component', + str_contains($packageDir, 'Symfony/Contracts/') => 'contract', + str_ends_with($packageDir, 'Symfony/Contracts') => 'contracts', + default => throw new \LogicException(), + }; } $newPackage = []; diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 1a0dd8c254f57..482ea42869d98 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -152,10 +152,10 @@ jobs: - name: Configure Couchbase run: | - curl -s -u 'username=Administrator&password=111111' -X POST http://localhost:8091/node/controller/setupServices -d 'services=kv%2Cn1ql%2Cindex%2Cfts' - curl -s -X POST http://localhost:8091/settings/web -d 'username=Administrator&password=111111&port=SAME' - curl -s -u Administrator:111111 -X POST http://localhost:8091/pools/default/buckets -d 'ramQuotaMB=100&bucketType=ephemeral&name=cache' - curl -s -u Administrator:111111 -X POST http://localhost:8091/pools/default -d 'memoryQuota=256' + curl -s -u 'username=Administrator&password=111111@' -X POST http://localhost:8091/node/controller/setupServices -d 'services=kv%2Cn1ql%2Cindex%2Cfts' + curl -s -X POST http://localhost:8091/settings/web -d 'username=Administrator&password=111111%40&port=SAME' + curl -s -u Administrator:111111@ -X POST http://localhost:8091/pools/default/buckets -d 'ramQuotaMB=100&bucketType=ephemeral&name=cache' + curl -s -u Administrator:111111@ -X POST http://localhost:8091/pools/default -d 'memoryQuota=256' - name: Setup PHP uses: shivammathur/setup-php@v2 diff --git a/.github/workflows/intl-data-tests.yml b/.github/workflows/intl-data-tests.yml index 01401fedc232f..a02bd73ac5b8f 100644 --- a/.github/workflows/intl-data-tests.yml +++ b/.github/workflows/intl-data-tests.yml @@ -1,8 +1,11 @@ -name: Intl data +name: Intl/Emoji data on: push: paths: + - 'src/Symfony/Component/Emoji/*.php' + - 'src/Symfony/Component/Emoji/Resources/data/**' + - 'src/Symfony/Component/Emoji/Tests/*Test.php' - 'src/Symfony/Component/Intl/*.php' - 'src/Symfony/Component/Intl/Util/GitRepository.php' - 'src/Symfony/Component/Intl/Resources/data/**' @@ -10,6 +13,9 @@ on: - 'src/Symfony/Component/Intl/Tests/Util/GitRepositoryTest.php' pull_request: paths: + - 'src/Symfony/Component/Emoji/*.php' + - 'src/Symfony/Component/Emoji/Resources/data/**' + - 'src/Symfony/Component/Emoji/Tests/*Test.php' - 'src/Symfony/Component/Intl/*.php' - 'src/Symfony/Component/Intl/Util/GitRepository.php' - 'src/Symfony/Component/Intl/Resources/data/**' @@ -29,7 +35,7 @@ permissions: jobs: tests: - name: Intl data + name: Intl/Emoji data runs-on: Ubuntu-20.04 steps: @@ -80,15 +86,23 @@ jobs: - name: Run intl-data tests run: ./phpunit --group intl-data -v - - name: Test with compressed data + - name: Test intl-data with compressed data run: | [ -f src/Symfony/Component/Intl/Resources/data/locales/en.php ] [ ! -f src/Symfony/Component/Intl/Resources/data/locales/en.php.gz ] - [ -f src/Symfony/Component/Intl/Resources/data/transliterator/emoji/emoji-en.php ] - [ ! -f src/Symfony/Component/Intl/Resources/data/transliterator/emoji/emoji-en.php.gz ] src/Symfony/Component/Intl/Resources/bin/compress [ ! -f src/Symfony/Component/Intl/Resources/data/locales/en.php ] [ -f src/Symfony/Component/Intl/Resources/data/locales/en.php.gz ] - [ ! -f src/Symfony/Component/Intl/Resources/data/transliterator/emoji/emoji-en.php ] - [ -f src/Symfony/Component/Intl/Resources/data/transliterator/emoji/emoji-en.php.gz ] ./phpunit src/Symfony/Component/Intl + + - name: Run Emoji tests + run: ./phpunit src/Symfony/Component/Emoji -v + + - name: Test Emoji with compressed data + run: | + [ -f src/Symfony/Component/Emoji/Resources/data/emoji-en.php ] + [ ! -f src/Symfony/Component/Emoji/Resources/data/emoji-en.php.gz ] + src/Symfony/Component/Emoji/Resources/bin/compress + [ ! -f src/Symfony/Component/Emoji/Resources/data/emoji-en.php ] + [ -f src/Symfony/Component/Emoji/Resources/data/emoji-en.php.gz ] + ./phpunit src/Symfony/Component/Emoji diff --git a/.github/workflows/package-tests.yml b/.github/workflows/package-tests.yml index 0792f4e8d6da5..9aa5dae976bfe 100644 --- a/.github/workflows/package-tests.yml +++ b/.github/workflows/package-tests.yml @@ -21,7 +21,7 @@ jobs: - name: Find packages id: find-packages - run: echo "packages=$(php .github/get-modified-packages.php $(find src/Symfony -mindepth 2 -type f -name composer.json -printf '%h\n' | grep -v src/Symfony/Component/Intl/Resources/emoji |jq -R -s -c 'split("\n")[:-1]') $(git diff --name-only origin/${{ github.base_ref }} HEAD | grep src/ | jq -R -s -c 'split("\n")[:-1]'))" >> $GITHUB_OUTPUT + run: echo "packages=$(php .github/get-modified-packages.php $(find src/Symfony -mindepth 2 -type f -name composer.json -printf '%h\n' | grep -v src/Symfony/Component/Emoji/Resources/bin |jq -R -s -c 'split("\n")[:-1]') $(git diff --name-only origin/${{ github.base_ref }} HEAD | grep src/ | jq -R -s -c 'split("\n")[:-1]'))" >> $GITHUB_OUTPUT - name: Verify meta files are correct run: | diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index c9deba395c405..dfc5b0e63728f 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -96,7 +96,7 @@ jobs: echo SYMFONY_DEPRECATIONS_HELPER=weak >> $GITHUB_ENV cp composer.json composer.json.orig echo -e '{\n"require":{'"$(grep phpunit-bridge composer.json)"'"php":"*"},"minimum-stability":"dev"}' > composer.json - php .github/build-packages.php HEAD^ $SYMFONY_VERSION $(find src/Symfony -mindepth 2 -type f -name composer.json -printf '%h\n' | grep -v src/Symfony/Component/Intl/Resources/emoji) + php .github/build-packages.php HEAD^ $SYMFONY_VERSION $(find src/Symfony -mindepth 2 -type f -name composer.json -printf '%h\n' | grep -v src/Symfony/Component/Emoji/Resources/bin) mv composer.json composer.json.phpunit mv composer.json.orig composer.json fi diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 290e3f20cad0b..f422cf3c55fab 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -29,16 +29,9 @@ '@Symfony' => true, '@Symfony:risky' => true, 'protected_to_private' => false, - 'native_constant_invocation' => ['strict' => false], - 'no_superfluous_phpdoc_tags' => [ - 'remove_inheritdoc' => true, - 'allow_unused_params' => true, // for future-ready params, to be replaced with https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues/7377 - ], - 'nullable_type_declaration_for_default_null_value' => true, 'header_comment' => ['header' => $fileHeaderComment], - 'modernize_strpos' => true, - 'get_class_to_class_keyword' => true, 'nullable_type_declaration' => true, + 'trailing_comma_in_multiline' => ['elements' => ['arrays', 'match', 'parameters']], ]) ->setRiskyAllowed(true) ->setFinder( @@ -47,13 +40,9 @@ ->append([__FILE__]) ->notPath('#/Fixtures/#') ->exclude([ - // directories containing files with content that is autogenerated by `var_export`, which breaks CS in output code - // fixture templates - 'Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Custom', - // resource templates - 'Symfony/Bundle/FrameworkBundle/Resources/views/Form', // explicit trigger_error tests 'Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/', + 'Symfony/Component/Emoji/Resources/', 'Symfony/Component/Intl/Resources/data/', ]) // explicit tests for ommited @param type, against `no_superfluous_phpdoc_tags` @@ -63,12 +52,6 @@ ->notPath('Symfony/Bridge/PhpUnit/SymfonyTestsListener.php') ->notPath('#Symfony/Bridge/PhpUnit/.*Mock\.php#') ->notPath('#Symfony/Bridge/PhpUnit/.*Legacy#') - // file content autogenerated by `var_export` - ->notPath('Symfony/Component/Translation/Tests/Fixtures/resources.php') - // file content autogenerated by `VarExporter::export` - ->notPath('Symfony/Component/Serializer/Tests/Fixtures/serializer.class.metadata.php') - // test template - ->notPath('Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Custom/_name_entry_label.html.php') // explicit trigger_error tests ->notPath('Symfony/Component/ErrorHandler/Tests/DebugClassLoaderTest.php') // stop removing spaces on the end of the line in strings @@ -79,6 +62,10 @@ ->notPath('Symfony/Component/Cache/Traits/Redis6Proxy.php') ->notPath('Symfony/Component/Cache/Traits/RedisCluster5Proxy.php') ->notPath('Symfony/Component/Cache/Traits/RedisCluster6Proxy.php') + // svg + ->notPath('Symfony/Component/ErrorHandler/Resources/assets/images/symfony-ghost.svg.php') + // HTML templates + ->notPath('#Symfony/.*\.html\.php#') ) ->setCacheFile('.php-cs-fixer.cache') ; diff --git a/CHANGELOG-7.1.md b/CHANGELOG-7.1.md new file mode 100644 index 0000000000000..a855cd6c406ea --- /dev/null +++ b/CHANGELOG-7.1.md @@ -0,0 +1,172 @@ +CHANGELOG for 7.1.x +=================== + +This changelog references the relevant changes (bug and security fixes) done +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-BETA1 (2024-05-02) + + * feature #54818 [Translation] Crowdin is backing its translation bridge, thanks to them! \o/ (nicolas-grekas) + * feature #54806 [HttpClient]  deprecate setLogger() methods of decorating clients (xabbuh) + * feature #54809 JoliCode is sponsoring Symfony 7.1, thanks to them! \o/ (nicolas-grekas) + * feature #54657 [TwigBundle] Deprecate `base_template_class` option (Steveb-p) + * feature #54663 [Serializer] Add `XmlEncoder::CDATA_WRAPPING_PATTERN` context option (alexpozzi) + * feature #54666 [DependencyInjection] Reset env vars when resetting the container (faizanakram99) + * feature #54455 [DoctrineBridge] Deprecate auto-mapping of entities in favor of mapped route parameters (nicolas-grekas) + * feature #52820 [DependencyInjection] Add `#[AutowireInline]` attribute to allow service definition at the class level (DaDeather, nicolas-grekas) + * feature #54720 [Routing] Add `{foo:bar}` syntax to define a mapping between a route parameter and its corresponding request attribute (nicolas-grekas) + * feature #38662 [DoctrineBridge][Validator] Allow validating every class against unique entity constraint (wkania) + * feature #54371 [DependencyInjection] Deprecate `#[TaggedIterator]` and `#[TaggedLocator]` (GromNaN) + * feature #54674 [FrameworkBundle] revert implementing LoggerAwareInterface in HTTP client decorators (xabbuh) + * feature #54670 [TypeInfo] Ease getting base type on nullable types (mtarld) + * feature #54789 [PropertyInfo] remove deprecations, mark TypeInfo as experimental (soyuka) + * feature #54788 [TypeInfo] mark classes as experimental (soyuka) + * feature #54661 [TypeInfo] Handle custom collection objects properly (mtarld) + * feature #53214 [DoctrineBridge] Idle connection listener for long running runtime (alli83) + * feature #49978 [HttpKernel] Introduce `#[MapUploadedFile]` controller argument attribute (renedelima) + * feature #53365 [DoctrineBridge] Allow `EntityValueResolver` to return a list of entities (HypeMC) + * feature #54525 [Mailer] [Resend] Add Resend webhook signature verification (welcoMattic) + * feature #54589 [Messenger] forward a Clock instance to the created InMemoryTransport (xabbuh) + * feature #54604 [Ldap] Improve error reporting during LDAP bind (RoSk0) + * feature #54356 [Notifier] LOX24 SMS bridge (alebedev80) + * feature #54473 [Validator] Add support for types (`ALL*`, `LOCAL_*`, `UNIVERSAL_*`, `UNICAST_*`, `MULTICAST_*`, `BROADCAST`) in `MacAddress` constraint (Ninos) + * feature #53971 Cast env vars to null or bool when referencing them using `#[Autowire(env: '...')]` depending on the signature of the corresponding parameter (ruudk) + * feature #54535 [Validator] Deprecate `Bic::INVALID_BANK_CODE_ERROR` (MatTheCat) + * feature #54044 [Mailer] Add support for allowing some users even if `recipients` is defined in `EnvelopeListener` (lyrixx) + * feature #54442 [Clock] Add a polyfill for DateTimeImmutable::createFromTimestamp() (derrabus) + * feature #53801 [HttpKernel] Deprecate `AddAnnotatedClassesToCachePass` and related code infrastructure (nicolas-grekas) + * feature #54510 [Console] Handle SIGQUIT signal (ostrolucky) + * feature #54385 [HttpKernel] Map a list of items with `MapRequestPayload` attribute (yceruto) + * feature #54432 [TwigBridge] Add `emojify` twig filter (lyrixx) + * feature #54496 [Contracts] Rename ServiceSubscriberTrait to ServiceMethodsSubscriberTrait (nicolas-grekas) + * feature #54443 [Security] Add support for dynamic CSRF id with Expression in `#[IsCsrfTokenValid]` (yguedidi) + * feature #53968 [Process] allow to ignore signals when executing a process (joelwurtz) + * feature #54346 [Serializer] Fix: Report Xml warning/error instead of silently returning a wrong xml (VincentLanglet) + * feature #54423 [WebProfilerBundle] Update the search links in the profiler layout (javiereguiluz) + * feature #52986 [HttpFoundation] Similar locale selection (Spomky) + * feature #53160 [PropertyInfo] Deprecate PropertyInfo Type (mtarld) + * feature #54408 [Validator] Add a `requireTld` option to `Url` constraint (javiereguiluz) + * feature #54470 [Emoji] Add the "text" locale (nicolas-grekas) + * feature #54414 [DependencyInjection] Improve the error message when there is no extension to load some configuration (javiereguiluz) + * feature #54479 [Validator] set the password strength as a violation parameter (xabbuh) + * feature #52843 [DependencyInjection] Prepending extension configs with file loaders (yceruto) + * feature #53682 [Security] Support RSA algorithm signature for OIDC tokens (Spomky) + * feature #54441 [Emoji] Add the gitlab locale (alamirault) + * feature #54420 [WebProfilerBundle] Update main menu to display active panels first (javiereguiluz) + * feature #54381 [Messenger] Allow extending attribute class `AsMessageHandler` (GromNaN) + * feature #54364 [WebProfilerBundle] [WebProfilerPanel] Update the design of the workflow profiler panel (javiereguiluz) + * feature #54365 [DependencyInjection] Apply attribute configurator to child classes (GromNaN) + * feature #54320 [VarDumper] Add support for new DOM extension classes in `DOMCaster` (alexandre-daubois) + * feature #54344 [Workflow] Add EventNameTrait to compute event name strings in subscribers (squrious) + * feature #54347 [Console] Allow to return all tokens after the command name (lyrixx) + * feature #54135 [Console] Add a way to use custom lock factory in lockableTrait (VincentLanglet) + * feature #51502 [HttpKernel] Add temporary URI signed (BaptisteContreras) + * feature #53898 [Serializer] Add context for `CamelCaseToSnakeCaseNameConverter` (AurelienPillevesse) + * feature #49184 [FrameworkBundle][HttpFoundation] reduce response constraints verbosity (Nicolas Appriou, Nicals) + * feature #53901 [Mailer] [Amazon] Add support for X-SES-LIST-MANAGEMENT-OPTIONS header (sebschaefer) + * feature #54153 [HttpKernel] allow boolean argument support for MapQueryString (Jean-Beru) + * feature #53885 [WebProfilerBundle] Allow to search inside profiler tables (javiereguiluz) + * feature #52950 [WebProfilerBundle] Set `XDEBUG_IGNORE` option for all XHR (adrolter) + * feature #54173 [Filesystem] Add the `readFile()` method (derrabus) + * feature #54313 [Mime] Update mime types (smnandre) + * feature #54016 [DependencyInjection] Add `#[AutowireMethodOf]` attribute to autowire a method of a service as a callable (nicolas-grekas) + * feature #54272 [Workflow] Add support for workflows that need to store many tokens in the marking (lyrixx) + * feature #54238 [Console] Add `ArgvInput::getRawTokens()` (lyrixx) + * feature #51227 [FrameworkBundle][Workflow] Attach the workflow's configuration to the `workflow` tag (lyrixx) + * feature #54107 [HttpKernel] Improve error reporting when requiring the wrong Request class (ilyachase) + * feature #53851 [Security] Ignore empty username or password login attempts (llupa) + * feature #54125 [AssetMapper] Deprecate unused method `splitPackageNameAndFilePath` (smnandre) + * feature #54084 [Lock] Make NoLock implement the SharedLockInterface (mbabker) + * feature #53892 [Messenger][AMQP] Automatically reconnect on connection loss (ostrolucky) + * feature #53734 [Notifier] Add SMSense bridge (jimiero) + * feature #53942 [Clock] Add get/setMicroseconds() (nicolas-grekas) + * feature #53986 [Console] Add descriptions to Fish completion output (adriaanzon) + * feature #53927 [Notifier] Add new Pushy notifier bridge (stloyd) + * feature #53866 [Workflow] determines places from transitions (lyrixx) + * feature #53868 [Config] Allow custom meta location in `ConfigCache` (alamirault) + * feature #48603 [Messenger][Amqp] Add config option 'arguments' for delay queues (Thomas Beaujean) + * feature #50745 [DependencyInjection] Add `CheckAliasValidityPass` to check interface compatibility (n-valverde) + * feature #53806 [ExpressionLanguage] Add more configurability to the parsing/linting methods (fabpot) + * feature #49144 [HttpFoundation] Add support for `\SplTempFileObject` in `BinaryFileResponse` (alexandre-daubois) + * feature #53706 [Mailer] Add timestamp to SMTP debug log (bytestream) + * feature #53747 [Yaml] Revert "feature #48022 Fix Yaml Parser with quote end in a newline (maxbeckers)" (xabbuh) + * feature #50864 [TwigBridge] Allow `twig:lint` to excludes dirs (94noni) + * feature #48022 [Yaml] Fix Yaml Parser with quote end in a new line (maxbeckers) + * feature #48803 [CssSelector] add support for :is() and :where() (Jean-Beru) + * feature #52510 [TypeInfo] Introduce component (mtarld) + * feature #52043 [Config] Allow custom meta location in `ResourceCheckerConfigCache` (ruudk) + * feature #51343 [HttpFoundation] Add `HeaderRequestMatcher` (alexandre-daubois) + * feature #51324 [HttpFoundation] Add `QueryParameterRequestMatcher` (alexandre-daubois) + * feature #51514 [Serializer] Add Default and "class name" default groups (mtarld) + * feature #52658 [Validator] Add additional versions (`*_NO_PUBLIC`, `*_ONLY_PRIV` & `*_ONLY_RES`) in IP address & CIDR constraint (Ninos) + * feature #52230 [Yaml] Allow to get all the enum cases (phansys) + * feature #48276 [Security] add CAS 2.0 AccessToken handler (nacorp) + * feature #52922 [DependencyInjection] Add Lazy attribute for classes and arguments (Tiriel) + * feature #53056 [Serializer] Add `DateTimeNormalizer::CAST_KEY` context option (norkunas) + * feature #53096 [Intl] [Emoji] Move emoji data in a new component (smnandre) + * feature #53362 [PropertyInfo] Restrict access to `PhpStanExtractor` based on visibility (nikophil) + * feature #53550 [FrameworkBundle][HttpClient] Add `ThrottlingHttpClient` to limit requests within a timeframe (HypeMC) + * feature #53466 Add `SecretsRevealCommand` (danielburger1337) + * feature #53740 Mailersend webhook remote event (doobas, fabpot) + * feature #53728 [ExpressionLanguage] Add ``min`` and ``max`` php functions (maxbeckers) + * feature #51562 [DoctrineBridge] Add `message` to #[MapEntity] for NotFoundHttpException (moesoha) + * feature #53680 [DependencyInjection][Yaml] dump enums with the !php/enum tag (xabbuh) + * feature #53621 [Mailer] [Smtp] Add DSN param 'auto_tls' to disable automatic STARTTLS (srsbiz) + * feature #53554 [Mailer] Add Resend bridge (welcoMattic) + * feature #53448 [Cache] Add support for using DSN with `PDOAdapter` (HypeMC) + * feature #52447 [Form] Add option `separator` to `ChoiceType` to use a custom separator after preferred choices (mboultoureau) + * feature #53080 [Clock] Return Symfony ClockInterface in ClockSensitiveTrait (ruudk) + * feature #53328 [Messenger] Add jitter parameter to MultiplierRetryStrategy (rmikalkenas) + * feature #53382 [Uid] Add `AbstractUid::toString()` (fancyweb) + * feature #53374 [Validator] support `\Stringable` instances in all constraints (xabbuh) + * feature #53163 [Contracts][DependencyInjection] Add `ServiceCollectionInterface` (kbond) + * feature #52638 [Dotenv] Add `SYMFONY_DOTENV_PATH`, consumed by `debug:dotenv` for custom `.env` path (GromNaN) + * feature #51862 [Validator] Add `MacAddress` constraint for validating MAC address (Ninos) + * feature #52924 [FrameworkBundle] add a private_ranges shortcut for trusted_proxies (xabbuh) + * feature #53209 [HttpKernel] Add support for custom HTTP status code for the `#[MapQueryParameter]` attribute (ovidiuenache) + * feature #53151 mark classes implementing the `WarmableInterface` as `final` (xabbuh) + * feature #53262 [Notifier] [OneSignal] Add support for sending to external user ids (KDederichs) + * feature #51884 [Form] add "model_type" option to MoneyType (garak) + * feature #52936 [Notifier] Add Seven.io bridge (NeoBlack) + * feature #53249 [Validator] support `Stringable` instances in `CharsetValidator` (xabbuh) + * feature #53251 [AssetMapper] Add integrity hash to the default es-module-shims script (smnandre) + * feature #52976 [Notifier] Add sms-sluzba.cz bridge (dfridrich) + * feature #53154 [Validator] Add the `Charset` constraint (alexandre-daubois) + * feature #53191 [HttpKernel] Allow `#[WithHttpStatus]` and `#[WithLogLevel]` to take effect on interfaces (priyadi) + * feature #53212 [HttpKernel] Add `HttpException::fromStatusCode()` (nicolas-grekas) + * feature #53060 [Uid] Add `UuidV1::toV6()`, `UuidV1::toV7()` and `UuidV6::toV7()` (fancyweb, nicolas-grekas) + * feature #53148 [Notifier] Add Smsbox notifier bridge (Alan ZARLI) + * feature #52954 [Validator] Add `list` and `associative_array` types to `Type` constraint (Florian Hermann) + * feature #53091 [FrameworkBundle][RateLimiter] add `rate_limiter` tag to rate limiter services (kbond) + * feature #53123 [VarDumper] Added default message for dd function (Shamimul Alam) + * feature #52948 Use faster hashing algorithms when possible (javiereguiluz) + * feature #52970 [HttpClient] Add `JsonMockResponse::fromFile()` and `MockResponse::fromFile()` shortcuts (fancyweb) + * feature #52989 allow Twig 4 (xabbuh) + * feature #53092 [Notifier] Add Unifonic notifier bridge (seferov) + * feature #53083 [Notifier] Add Bluesky notifier bridge (Nyholm) + * feature #52775 [HttpClient] Allow mocking `start_time` info in `MockResponse` (fancyweb) + * feature #52962 [FrameworkBundle] Move Router cache directory to `kernel.build_dir` (Okhoshi) + * feature #52974 [Cache] Deprecate `CouchbaseBucketAdapter`, use `CouchbaseCollectionAdapter` (alexandre-daubois) + * feature #52971 [Messenger] Make `#[AsMessageHandler]` final (Valmonzo) + * feature #52961 [Security][SecurityBundle] Add `#[IsCsrfTokenValid]` attribute (yguedidi) + * feature #39438 [Form] Errors Property Paths mismatch CollectionType children when removing an entry (cristoforocervino) + * feature #52842 [Mailer] Add Azure bridge (hafael) + * feature #52946 [HttpClient] Add HttpOptions->addHeader as a shortcut to add an header in an existing options object (Dean151) + * feature #52893 [Cache][Messenger]  make both options redis_sentinel and sentinel_master available everywhere (xabbuh) + * feature #52854 [Workflow] Add `getEnabledTransition()` method annotation to WorkflowInterface (alexandre-daubois) + * feature #52916 [Mailer] Dispatch event for Postmark's "inactive recipient" API error (vicdelfant) + * feature #52879 [PropertyAccess][Serializer] Fix "type unknown" on denormalize (seferov) + * feature #52411 [Messenger] Add `--all` option to `messenger:consume` (javaDeveloperKid) + * feature #52493 [HttpFoundation] Add `UploadedFile::getClientOriginalPath()` to support directory uploads (danielburger1337) + * feature #52632 [PropertyInfo] Make `PhpDocExtractor::getDocBlock()` public (Nyholm) + * feature #52730 [Serializer] Consider SerializedPath in debug command output (jschaedl) + * feature #52128 [HttpKernel] Introduce `ExceptionEvent::isKernelTerminating()` to skip error rendering when kernel is terminating (VincentLanglet) + * feature #52636 [DependencyInjection] Prepend extension config with `ContainerConfigurator` (yceruto) + * feature #52198 [String] New locale aware casing methods (bram123) + * feature #52369 [DependencyInjection] Add `urlencode` function to `EnvVarProcessor` (crtl) + * feature #50922 [Form] Deprecate not configuring the `default_protocol` option of the `UrlType` (MatTheCat) + * feature #52605 [Console] Support `ProgressBar::iterate()` on empty array (GromNaN) + diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 04ba9eca15947..2c65442650d09 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -14,8 +14,8 @@ The Symfony Connect username in parenthesis allows to get more information - Grégoire Pineau (lyrixx) - Thomas Calvet (fancyweb) - Christophe Coevoet (stof) - - Wouter de Jong (wouterj) - Alexandre Daubois (alexandre-daubois) + - Wouter de Jong (wouterj) - Jordi Boggiano (seldaek) - Maxime Steinhausser (ogizanagi) - Kévin Dunglas (dunglas) @@ -34,8 +34,8 @@ The Symfony Connect username in parenthesis allows to get more information - Tobias Nyholm (tobias) - Jérôme Tamarelle (gromnan) - Samuel ROZE (sroze) - - Pascal Borreli (pborreli) - Antoine Lamirault (alamirault) + - Pascal Borreli (pborreli) - Romain Neutron - HypeMC (hypemc) - Joseph Bielawski (stloyd) @@ -51,14 +51,14 @@ The Symfony Connect username in parenthesis allows to get more information - Igor Wiedler - Jan Schädlich (jschaedl) - Mathieu Lechat (mat_the_cat) - - Matthias Pigulla (mpdude) - Gabriel Ostrolucký (gadelat) + - Matthias Pigulla (mpdude) - Jonathan Wage (jwage) - Valentin Udaltsov (vudaltsov) + - Vincent Langlet (deviling) - Alexandre Salomé (alexandresalome) - Grégoire Paris (greg0ire) - William DURAND - - Vincent Langlet (deviling) - ornicar - Dany Maillard (maidmaid) - Eriksen Costa @@ -69,6 +69,7 @@ The Symfony Connect username in parenthesis allows to get more information - Francis Besset (francisbesset) - Titouan Galopin (tgalopin) - Pierre du Plessis (pierredup) + - Simon André (simonandre) - David Maicher (dmaicher) - Bulat Shakirzyanov (avalanche123) - Iltar van der Berg @@ -77,18 +78,17 @@ The Symfony Connect username in parenthesis allows to get more information - Saša Stamenković (umpirsky) - Allison Guilhem (a_guilhem) - Mathieu Piot (mpiot) - - Simon André (simonandre) - Mathieu Santostefano (welcomattic) - Alexander Schranz (alexander-schranz) - Vasilij Duško (staff) + - Tomasz Kowalczyk (thunderer) + - Mathias Arlaud (mtarld) - Sarah Khalil (saro0h) - Laurent VOULLEMIER (lvo) - Konstantin Kudryashov (everzet) - - Tomasz Kowalczyk (thunderer) - Guilhem N (guilhemn) - Bilal Amarni (bamarni) - Eriksen Costa - - Mathias Arlaud (mtarld) - Florin Patan (florinpatan) - Vladimir Reznichenko (kalessil) - Peter Rehm (rpet) @@ -96,8 +96,8 @@ The Symfony Connect username in parenthesis allows to get more information - Henrik Bjørnskov (henrikbjorn) - David Buchmann (dbu) - Andrej Hudec (pulzarraider) - - Jáchym Toušek (enumag) - Ruud Kamphuis (ruudk) + - Jáchym Toušek (enumag) - Christian Raue - Eric Clemmons (ericclemmons) - Denis (yethee) @@ -130,6 +130,7 @@ The Symfony Connect username in parenthesis allows to get more information - Vasilij Dusko | CREATION - Jordan Alliot (jalliot) - Phil E. Taylor (philetaylor) + - Joel Wurtz (brouznouf) - John Wards (johnwards) - Théo FIDRY - Antoine Hérault (herzult) @@ -137,7 +138,6 @@ The Symfony Connect username in parenthesis allows to get more information - Yanick Witschi (toflar) - Jeroen Spee (jeroens) - Arnaud Le Blanc (arnaud-lb) - - Joel Wurtz (brouznouf) - Sebastiaan Stok (sstok) - Maxime STEINHAUSSER - Rokas Mikalkėnas (rokasm) @@ -198,6 +198,7 @@ The Symfony Connect username in parenthesis allows to get more information - Daniel Gomes (danielcsgomes) - Hidenori Goto (hidenorigoto) - Niels Keurentjes (curry684) + - Dāvis Zālītis (k0d3r1s) - Arnaud Kleinpeter (nanocom) - Guilherme Blanco (guilhermeblanco) - Saif Eddin Gmati (azjezz) @@ -211,7 +212,6 @@ The Symfony Connect username in parenthesis allows to get more information - Pablo Godel (pgodel) - Florent Mata (fmata) - Alessandro Chitolina (alekitto) - - Dāvis Zālītis (k0d3r1s) - Rafael Dohms (rdohms) - Roman Martinuk (a2a4) - Thomas Landauer (thomas-landauer) @@ -262,6 +262,7 @@ The Symfony Connect username in parenthesis allows to get more information - Tyson Andre - GDIBass - Samuel NELA (snela) + - Florent Morselli (spomky_) - Vincent AUBERT (vincent) - Michael Voříšek - zairig imad (zairigimad) @@ -269,6 +270,7 @@ The Symfony Connect username in parenthesis allows to get more information - Sébastien Alfaiate (seb33300) - James Halsall (jaitsu) - Christian Scheb + - Bob van de Vijver (bobvandevijver) - Guillaume (guill) - Mikael Pajunen - Warnar Boekkooi (boekkooi) @@ -294,10 +296,10 @@ The Symfony Connect username in parenthesis allows to get more information - Chi-teck - Andre Rømcke (andrerom) - Baptiste Leduc (korbeil) + - Karoly Gossler (connorhu) - Timo Bakx (timobakx) - soyuka - Ruben Gonzalez (rubenrua) - - Bob van de Vijver (bobvandevijver) - Benjamin Dulau (dbenjamin) - Markus Fasselt (digilist) - Denis Brumann (dbrumann) @@ -308,6 +310,7 @@ The Symfony Connect username in parenthesis allows to get more information - Andreas Hucks (meandmymonkey) - Noel Guilbert (noel) - Bastien Jaillot (bastnic) + - Soner Sayakci - Stadly - Stepan Anchugov (kix) - bronze1man @@ -323,7 +326,6 @@ The Symfony Connect username in parenthesis allows to get more information - Pierre Minnieur (pminnieur) - Dominique Bongiraud - Hugo Monteiro (monteiro) - - Karoly Gossler (connorhu) - Bram Leeda (bram123) - Dmitrii Poddubnyi (karser) - Julien Pauli @@ -334,6 +336,7 @@ The Symfony Connect username in parenthesis allows to get more information - Leszek Prabucki (l3l0) - Giorgio Premi - Thomas Lallement (raziel057) + - Yassine Guedidi (yguedidi) - François Zaninotto (fzaninotto) - Dustin Whittle (dustinwhittle) - Timothée Barray (tyx) @@ -348,7 +351,6 @@ The Symfony Connect username in parenthesis allows to get more information - Michele Orselli (orso) - Sven Paulus (subsven) - Maxime Veber (nek-) - - Soner Sayakci - Valentine Boineau (valentineboineau) - Rui Marinho (ruimarinho) - Patrick Landolt (scube) @@ -367,7 +369,6 @@ The Symfony Connect username in parenthesis allows to get more information - Mantis Development - Marko Kaznovac (kaznovac) - Hidde Wieringa (hiddewie) - - Florent Morselli (spomky_) - dFayet - Rob Frawley 2nd (robfrawley) - Renan (renanbr) @@ -377,7 +378,6 @@ The Symfony Connect username in parenthesis allows to get more information - Daniel Tschinder - Christian Schmidt - Alexander Kotynia (olden) - - Yassine Guedidi (yguedidi) - Elnur Abdurrakhimov (elnur) - Manuel Reinhard (sprain) - BoShurik @@ -418,6 +418,7 @@ The Symfony Connect username in parenthesis allows to get more information - Marvin Petker - GordonsLondon - Ray + - Asis Pattisahusiwa - Philipp Cordes (corphi) - Chekote - Thomas Adam @@ -477,6 +478,7 @@ The Symfony Connect username in parenthesis allows to get more information - Thomas Bisignani (toma) - Florian Klein (docteurklein) - Damien Alexandre (damienalexandre) + - javaDeveloperKid - Manuel Kießling (manuelkiessling) - Alexey Kopytko (sanmai) - Warxcell (warxcell) @@ -487,7 +489,6 @@ The Symfony Connect username in parenthesis allows to get more information - Bertrand Zuchuat (garfield-fr) - Marc Morera (mmoreram) - Quynh Xuan Nguyen (seriquynh) - - Asis Pattisahusiwa - Gabor Toth (tgabi333) - realmfoo - Fabien S (bafs) @@ -518,6 +519,7 @@ The Symfony Connect username in parenthesis allows to get more information - Thierry T (lepiaf) - Lorenz Schori - Lukáš Holeczy (holicz) + - Jonathan H. Wage - Jeremy Livingston (jeremylivingston) - ivan - SUMIDA, Ippei (ippey_s) @@ -550,6 +552,7 @@ The Symfony Connect username in parenthesis allows to get more information - Artur Eshenbrener - Harm van Tilborg (hvt) - Thomas Perez (scullwm) + - Gwendolen Lynch - Cédric Anne - smoench - Felix Labrecque @@ -588,7 +591,6 @@ The Symfony Connect username in parenthesis allows to get more information - Kirill chEbba Chebunin - Pol Dellaiera (drupol) - Alex (aik099) - - javaDeveloperKid - Fabien Villepinte - SiD (plbsid) - Greg Thornton (xdissent) @@ -668,9 +670,9 @@ The Symfony Connect username in parenthesis allows to get more information - Dmitriy Mamontov (mamontovdmitriy) - Jan Schumann - Matheo Daninos (mathdns) + - Neil Peyssard (nepey) - Niklas Fiekas - Mark Challoner (markchalloner) - - Jonathan H. Wage - Markus Bachmann (baachi) - Matthieu Lempereur (mryamous) - Gunnstein Lye (glye) @@ -710,7 +712,6 @@ The Symfony Connect username in parenthesis allows to get more information - DerManoMann - Jérôme Tanghe (deuchnord) - Mathias STRASSER (roukmoute) - - Gwendolen Lynch - simon chrzanowski (simonch) - Kamil Kokot (pamil) - Seb Koelen @@ -905,6 +906,7 @@ The Symfony Connect username in parenthesis allows to get more information - Ramunas Pabreza (doobas) - Yuriy Vilks (igrizzli) - Terje Bråten + - Andrey Lebedev (alebedev) - Sebastian Krebs - Piotr Stankowski - Pierre-Emmanuel Tanguy (petanguy) @@ -970,7 +972,6 @@ The Symfony Connect username in parenthesis allows to get more information - Christophe Villeger (seragan) - Krystian Marcisz (simivar) - Julien Fredon - - Neil Peyssard (nepey) - Xavier Leune (xleune) - Hany el-Kerdany - Wang Jingyu @@ -1065,6 +1066,7 @@ The Symfony Connect username in parenthesis allows to get more information - Robin Lehrmann - Szijarto Tamas - Thomas P + - Stephan Vock (glaubinix) - Jaroslav Kuba - Benjamin Zikarsky (bzikarsky) - Kristijan Kanalaš (kristijan_kanalas_infostud) @@ -1149,6 +1151,7 @@ The Symfony Connect username in parenthesis allows to get more information - Ворожцов Максим (myks92) - Dalibor Karlović - Randy Geraads + - Jay Klehr - Andreas Leathley (iquito) - Vladimir Luchaninov (luchaninov) - Sebastian Grodzicki (sgrodzicki) @@ -1225,6 +1228,7 @@ The Symfony Connect username in parenthesis allows to get more information - Felds Liscia (felds) - Jérémy DECOOL (jdecool) - Sergey Panteleev + - Alexander Grimalovsky (flying) - Andrew Hilobok (hilobok) - Noah Heck (myesain) - Christian Soronellas (theunic) @@ -1421,6 +1425,7 @@ The Symfony Connect username in parenthesis allows to get more information - Michael Roterman (wtfzdotnet) - Philipp Keck - Pavol Tuka + - Shyim - Arno Geurts - Adán Lobato (adanlobato) - Ian Jenkins (jenkoian) @@ -1482,6 +1487,7 @@ The Symfony Connect username in parenthesis allows to get more information - MrMicky - Stewart Malik - Renan Taranto (renan-taranto) + - Ninos Ego - Stefan Graupner (efrane) - Gemorroj (gemorroj) - Adrien Chinour @@ -1492,6 +1498,7 @@ The Symfony Connect username in parenthesis allows to get more information - Uladzimir Tsykun - iamvar - Amaury Leroux de Lens (amo__) + - Rene de Lima Barbosa (renedelima) - Christian Jul Jensen - Alexandre GESLIN - The Whole Life to Learn @@ -1675,6 +1682,7 @@ The Symfony Connect username in parenthesis allows to get more information - Goran Juric - Laurent G. (laurentg) - Jean-Baptiste Nahan + - Thomas Decaux - Nicolas Macherey - Asil Barkin Elik (asilelik) - Bhujagendra Ishaya @@ -1740,7 +1748,6 @@ The Symfony Connect username in parenthesis allows to get more information - Denis Kop - Fabrice Locher - Kamil Szalewski (szal1k) - - Andrey Lebedev (alebedev) - Jean-Guilhem Rouel (jean-gui) - Yoann MOROCUTTI - Ivan Yivoff @@ -1769,6 +1776,7 @@ The Symfony Connect username in parenthesis allows to get more information - Hans Mackowiak - Hugo Fonseca (fonsecas72) - Marc Duboc (icemad) + - uncaught - Martynas Narbutas - Timothée BARRAY - Nilmar Sanchez Muguercia @@ -1875,6 +1883,7 @@ The Symfony Connect username in parenthesis allows to get more information - Clément - Gustavo Adrian - Jorrit Schippers (jorrit) + - Yann (yann_eugone) - Matthias Neid - Yannick - Kuzia @@ -1908,7 +1917,6 @@ The Symfony Connect username in parenthesis allows to get more information - Jason Schilling (chapterjason) - David de Boer (ddeboer) - Eno Mullaraj (emullaraj) - - Stephan Vock (glaubinix) - Guillem Fondin (guillemfondin) - Nathan PAGE (nathix) - Ryan Rogers @@ -2005,7 +2013,6 @@ The Symfony Connect username in parenthesis allows to get more information - Stefano A. (stefano93) - PierreRebeilleau - AlbinoDrought - - Jay Klehr - Sergey Yuferev - Monet Emilien - voodooism @@ -2166,7 +2173,6 @@ The Symfony Connect username in parenthesis allows to get more information - ShiraNai7 - Cedrick Oka - Antal Áron (antalaron) - - Alexander Grimalovsky (flying) - Guillaume Sainthillier (guillaume-sainthillier) - Ivan Pepelko (pepelko) - Vašek Purchart (vasek-purchart) @@ -2273,6 +2279,7 @@ The Symfony Connect username in parenthesis allows to get more information - roog - parinz1234 - Romain Geissler + - Martin Auswöger - Adrien Moiruad - Viktoriia Zolotova - Tomaz Ahlin @@ -2351,6 +2358,7 @@ The Symfony Connect username in parenthesis allows to get more information - Wouter Diesveld - Romain - Matěj Humpál + - Kasper Hansen - Amine Matmati - Kristen Gilden - caalholm @@ -2501,6 +2509,7 @@ The Symfony Connect username in parenthesis allows to get more information - Tiago Garcia (tiagojsag) - Artiom - Jakub Simon + - Eviljeks - robin.de.croock - Brandon Antonio Lorenzo - Bouke Haarsma @@ -2722,6 +2731,7 @@ The Symfony Connect username in parenthesis allows to get more information - Thomas Rothe - Edwin - Troy Crawford + - Kirill Roskolii - Jeroen van den Nieuwenhuisen - nietonfir - Andriy @@ -2933,6 +2943,7 @@ The Symfony Connect username in parenthesis allows to get more information - Joel Marcey - zolikonta - Daniel Bartoníček + - Michael Hüneburg - David Christmann - root - pf @@ -3314,6 +3325,7 @@ The Symfony Connect username in parenthesis allows to get more information - cmfcmf - sarah-eit - Michal Forbak + - CarolienBEER - Drew Butler - Alexey Berezuev - pawel-lewtak @@ -3330,7 +3342,6 @@ The Symfony Connect username in parenthesis allows to get more information - Anatol Belski - Javier - Alexis BOYER - - Shyim - bch36 - Kaipi Yann - wiseguy1394 @@ -3413,6 +3424,7 @@ The Symfony Connect username in parenthesis allows to get more information - Alex Nostadt - Michael Squires - Egor Gorbachev + - Julian Krzefski - Derek Stephen McLean - Norman Soetbeer - zorn @@ -3550,6 +3562,7 @@ The Symfony Connect username in parenthesis allows to get more information - Arkadiusz Kondas (itcraftsmanpl) - j0k (j0k) - joris de wit (jdewit) + - JG (jege) - Jérémy CROMBEZ (jeremy) - Jose Manuel Gonzalez (jgonzalez) - Joachim Krempel (jkrempel) diff --git a/README.md b/README.md index b343ef693bd8b..3eb56ada73cfd 100644 --- a/README.md +++ b/README.md @@ -17,26 +17,21 @@ Installation Sponsor ------- -Symfony 7.0 is [backed][27] by -- [Shopware][28] -- [Sulu][29] -- [Les-Tilleuls.coop][30] +Symfony 7.1 is [backed][27] by +- [Rector][29] +- [JoliCode][30] -**Shopware** is an open headless commerce platform powered by Symfony and Vue.js -that is used by thousands of shops and supported by a huge, worldwide community -of developers, agencies and merchants. +**Rector** helps successful and growing companies to get the most of the code +they already have. Including upgrading to the latest Symfony LTS. They deliver +automated refactoring, reduce maintenance costs, speed up feature delivery, and +transform legacy code into a strategic asset. They can handle the dirty work, +so you can focus on the features. -**Sulu** is the CMS for Symfony developers. It provides pre-built content-management -features while giving developers the freedom to build, deploy, and maintain custom -solutions using full-stack Symfony. Sulu is ideal for creating complex websites, -integrating external tools, and building custom-built solutions. +**JoliCode** is a team of passionate developers and open-source lovers, with a +strong expertise in PHP & Symfony technologies. They can help you build your +projects using state-of-the-art practices. -**Les-Tilleuls.coop** is a team of 70+ Symfony experts who can help you design, -develop and fix your projects. We provide a wide range of professional services -including development, consulting, coaching, training and audits. We also are -highly skilled in JS, Go and DevOps. We are a worker cooperative! - -Help Symfony by [sponsoring][31] its development! +Help Symfony by [sponsoring][28] its development! Documentation ------------- @@ -99,7 +94,6 @@ and supported by [Symfony contributors][19]. [25]: https://symfony.com/doc/current/contributing/code_of_conduct/care_team.html [26]: https://symfony.com/book [27]: https://symfony.com/backers -[28]: https://www.shopware.com -[29]: https://sulu.io -[30]: https://les-tilleuls.coop/ -[31]: https://symfony.com/sponsor +[28]: https://symfony.com/sponsor +[29]: https://getrector.com +[30]: https://jolicode.com diff --git a/UPGRADE-7.1.md b/UPGRADE-7.1.md new file mode 100644 index 0000000000000..51b5e994b3861 --- /dev/null +++ b/UPGRADE-7.1.md @@ -0,0 +1,122 @@ +UPGRADE FROM 7.0 to 7.1 +======================= + +Symfony 7.1 is a minor release. According to the Symfony release process, there should be no significant +backward compatibility breaks. Minor backward compatibility breaks are prefixed in this document with +`[BC BREAK]`, make sure your code is compatible with these entries before upgrading. +Read more about this in the [Symfony documentation](https://symfony.com/doc/7.1/setup/upgrade_minor.html). + +If you're upgrading from a version below 7.0, follow the [7.0 upgrade guide](UPGRADE-7.0.md) first. + +Table of Contents +----------------- + +Bundles + + * [FrameworkBundle](#FrameworkBundle) + * [SecurityBundle](#SecurityBundle) + * [TwigBundle](#TwigBundle) + +Bridges + + * [DoctrineBridge](#DoctrineBridge) + +Components + + * [AssetMapper](#AssetMapper) + * [Cache](#Cache) + * [DependencyInjection](#DependencyInjection) + * [ExpressionLanguage](#ExpressionLanguage) + * [Form](#Form) + * [Intl](#Intl) + * [HttpClient](#HttpClient) + * [PropertyInfo](#PropertyInfo) + * [Translation](#Translation) + * [Workflow](#Workflow) + +AssetMapper +----------- + + * Deprecate `ImportMapConfigReader::splitPackageNameAndFilePath()`, use `ImportMapEntry::splitPackageNameAndFilePath()` instead + +Cache +----- + + * Deprecate `CouchbaseBucketAdapter`, use `CouchbaseCollectionAdapter` with Couchbase 3 instead + +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 + +DoctrineBridge +-------------- + + * Deprecated `DoctrineExtractor::getTypes()`, use `DoctrineExtractor::getType()` instead + +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 + +Form +---- + + * Deprecate not configuring the `default_protocol` option of the `UrlType`, it will default to `null` in 8.0 (the current default is `'http'`) + +FrameworkBundle +--------------- + + * 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 + +HttpClient +---------- + + * Deprecate the `setLogger()` methods of the `NoPrivateNetworkHttpClient`, `TraceableHttpClient` and `ScopingHttpClient` classes, configure the logger of the wrapped clients directly instead + +Intl +---- + + * [BC BREAK] Extracted `EmojiTransliterator` to a separate `symfony/emoji` component, the new FQCN is `Symfony\Component\Emoji\EmojiTransliterator`. + You must install the `symfony/emoji` component if you're using the old `EmojiTransliterator` class in the Intl component. + +Mailer +------ + + * Postmark's "406 - Inactive recipient" API error code now results in a `PostmarkDeliveryEvent` instead of throwing a `HttpTransportException` + +HttpKernel +---------- + + * Deprecate `Extension::addAnnotatedClassesToCompile()` and related code infrastructure + +SecurityBundle +-------------- + + * Mark class `ExpressionCacheWarmer` as `final` + +Translation +----------- + + * Mark class `DataCollectorTranslator` as `final` + +TwigBundle +---------- + + * Mark class `TemplateCacheWarmer` as `final` + +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` + +Workflow +-------- + + * Add method `getEnabledTransition()` to `WorkflowInterface` + * Add `$nbToken` argument to `Marking::mark()` and `Marking::unmark()` diff --git a/composer.json b/composer.json index 008375cfbe7ec..f43a4e0f55fdb 100644 --- a/composer.json +++ b/composer.json @@ -47,7 +47,7 @@ "psr/http-message": "^1.0|^2.0", "psr/link": "^1.1|^2.0", "psr/log": "^1|^2|^3", - "symfony/contracts": "^2.5|^3.0", + "symfony/contracts": "^3.5", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-intl-grapheme": "~1.0", "symfony/polyfill-intl-icu": "~1.0", @@ -71,6 +71,7 @@ "symfony/doctrine-bridge": "self.version", "symfony/dom-crawler": "self.version", "symfony/dotenv": "self.version", + "symfony/emoji": "self.version", "symfony/error-handler": "self.version", "symfony/event-dispatcher": "self.version", "symfony/expression-language": "self.version", @@ -109,6 +110,7 @@ "symfony/translation": "self.version", "symfony/twig-bridge": "self.version", "symfony/twig-bundle": "self.version", + "symfony/type-info": "self.version", "symfony/uid": "self.version", "symfony/validator": "self.version", "symfony/var-dumper": "self.version", @@ -156,8 +158,7 @@ "twig/cssinliner-extra": "^2.12|^3", "twig/inky-extra": "^2.12|^3", "twig/markdown-extra": "^2.12|^3", - "web-token/jwt-checker": "^3.1", - "web-token/jwt-signature-algorithm-ecdsa": "^3.1" + "web-token/jwt-library": "^3.3.2" }, "conflict": { "ext-psr": "<1.1|>=2", @@ -207,7 +208,7 @@ "url": "src/Symfony/Contracts", "options": { "versions": { - "symfony/contracts": "3.4.x-dev" + "symfony/contracts": "3.5.x-dev" } } }, diff --git a/link b/link index 29f9600d6b94e..78f746e831130 100755 --- a/link +++ b/link @@ -49,7 +49,7 @@ $directories = array_merge(...array_values(array_map(function ($part) { $directories[] = __DIR__.'/src/Symfony/Contracts'; foreach ($directories as $dir) { if ($filesystem->exists($composer = "$dir/composer.json")) { - $sfPackages[json_decode(file_get_contents($composer))->name] = $dir; + $sfPackages[json_decode($filesystem->readFile($composer), flags: JSON_THROW_ON_ERROR)->name] = $dir; } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 5f5207576f4f6..594b5fe37de12 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -25,7 +25,7 @@ - + @@ -75,13 +75,14 @@ Cache\IntegrationTests Symfony\Bridge\Doctrine\Middleware\Debug - Symfony\Component\Cache - Symfony\Component\Cache\Tests\Fixtures - Symfony\Component\Cache\Tests\Traits - Symfony\Component\Cache\Traits - Symfony\Component\Console - Symfony\Component\HttpFoundation - Symfony\Component\Uid + Symfony\Bridge\Doctrine\Middleware\IdleConnection + Symfony\Component\Cache + Symfony\Component\Cache\Tests\Fixtures + Symfony\Component\Cache\Tests\Traits + Symfony\Component\Cache\Traits + Symfony\Component\Console + Symfony\Component\HttpFoundation + Symfony\Component\Uid diff --git a/psalm.xml b/psalm.xml index a21be22fe248f..f5f9c5b4c4e88 100644 --- a/psalm.xml +++ b/psalm.xml @@ -17,7 +17,8 @@ - + + diff --git a/src/Symfony/Bridge/Doctrine/ArgumentResolver/EntityValueResolver.php b/src/Symfony/Bridge/Doctrine/ArgumentResolver/EntityValueResolver.php index bdf975b32befd..d2ee05b42ce1d 100644 --- a/src/Symfony/Bridge/Doctrine/ArgumentResolver/EntityValueResolver.php +++ b/src/Symfony/Bridge/Doctrine/ArgumentResolver/EntityValueResolver.php @@ -60,9 +60,9 @@ public function resolve(Request $request, ArgumentMetadata $argument): array $message = sprintf(' The expression "%s" returned null.', $options->expr); } // find by identifier? - } elseif (false === $object = $this->find($manager, $request, $options, $argument->getName())) { + } elseif (false === $object = $this->find($manager, $request, $options, $argument)) { // find by criteria - if (!$criteria = $this->getCriteria($request, $options, $manager)) { + if (!$criteria = $this->getCriteria($request, $options, $manager, $argument)) { return []; } try { @@ -73,7 +73,7 @@ public function resolve(Request $request, ArgumentMetadata $argument): array } if (null === $object && !$argument->isNullable()) { - throw new NotFoundHttpException(sprintf('"%s" object not found by "%s".', $options->class, self::class).$message); + throw new NotFoundHttpException($options->message ?? (sprintf('"%s" object not found by "%s".', $options->class, self::class).$message)); } return [$object]; @@ -94,13 +94,13 @@ private function getManager(?string $name, string $class): ?ObjectManager return $manager->getMetadataFactory()->isTransient($class) ? null : $manager; } - private function find(ObjectManager $manager, Request $request, MapEntity $options, string $name): false|object|null + private function find(ObjectManager $manager, Request $request, MapEntity $options, ArgumentMetadata $argument): false|object|null { if ($options->mapping || $options->exclude) { return false; } - $id = $this->getIdentifier($request, $options, $name); + $id = $this->getIdentifier($request, $options, $argument); if (false === $id || null === $id) { return $id; } @@ -119,14 +119,14 @@ private function find(ObjectManager $manager, Request $request, MapEntity $optio } } - private function getIdentifier(Request $request, MapEntity $options, string $name): mixed + private function getIdentifier(Request $request, MapEntity $options, ArgumentMetadata $argument): mixed { if (\is_array($options->id)) { $id = []; foreach ($options->id as $field) { // Convert "%s_uuid" to "foobar_uuid" if (str_contains($field, '%s')) { - $field = sprintf($field, $name); + $field = sprintf($field, $argument->getName()); } $id[$field] = $request->attributes->get($field); @@ -135,28 +135,54 @@ private function getIdentifier(Request $request, MapEntity $options, string $nam return $id; } - if (null !== $options->id) { - $name = $options->id; + if ($options->id) { + return $request->attributes->get($options->id) ?? ($options->stripNull ? false : null); } + $name = $argument->getName(); + if ($request->attributes->has($name)) { - return $request->attributes->get($name) ?? ($options->stripNull ? false : null); + if (\is_array($id = $request->attributes->get($name))) { + return false; + } + + foreach ($request->attributes->get('_route_mapping') ?? [] as $parameter => $attribute) { + if ($name === $attribute) { + $options->mapping = [$name => $parameter]; + + return false; + } + } + + 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 (!$options->id && $request->attributes->has('id')) { return $request->attributes->get('id') ?? ($options->stripNull ? false : null); } return false; } - private function getCriteria(Request $request, MapEntity $options, ObjectManager $manager): array + private function getCriteria(Request $request, MapEntity $options, ObjectManager $manager, ArgumentMetadata $argument): array { - if (null === $mapping = $options->mapping) { + if (!($mapping = $options->mapping) && \is_array($criteria = $request->attributes->get($argument->getName()))) { + foreach ($options->exclude as $exclude) { + unset($criteria[$exclude]); + } + + if ($options->stripNull) { + $criteria = array_filter($criteria, static fn ($value) => null !== $value); + } + + return $criteria; + } elseif (null === $mapping) { + trigger_deprecation('symfony/doctrine-bridge', '7.1', 'Relying on auto-mapping for Doctrine entities is deprecated for argument $%s of "%s": declare the identifier using either the #[MapEntity] attribute or mapped route parameters.', $argument->getName(), method_exists($argument, 'getControllerName') ? $argument->getControllerName() : 'n/a'); $mapping = $request->attributes->keys(); } - if ($mapping && \is_array($mapping) && array_is_list($mapping)) { + if ($mapping && array_is_list($mapping)) { $mapping = array_combine($mapping, $mapping); } @@ -168,17 +194,11 @@ private function getCriteria(Request $request, MapEntity $options, ObjectManager return []; } - // if a specific id has been defined in the options and there is no corresponding attribute - // return false in order to avoid a fallback to the id which might be of another object - if (\is_string($options->id) && null === $request->attributes->get($options->id)) { - return []; - } - $criteria = []; - $metadata = $manager->getClassMetadata($options->class); + $metadata = null === $options->mapping ? $manager->getClassMetadata($options->class) : false; foreach ($mapping as $attribute => $field) { - if (!$metadata->hasField($field) && (!$metadata->hasAssociation($field) || !$metadata->isSingleValuedAssociation($field))) { + if ($metadata && !$metadata->hasField($field) && (!$metadata->hasAssociation($field) || !$metadata->isSingleValuedAssociation($field))) { continue; } @@ -192,7 +212,7 @@ private function getCriteria(Request $request, MapEntity $options, ObjectManager return $criteria; } - private function findViaExpression(ObjectManager $manager, Request $request, MapEntity $options): ?object + private function findViaExpression(ObjectManager $manager, Request $request, MapEntity $options): object|iterable|null { if (!$this->expressionLanguage) { throw new \LogicException(sprintf('You cannot use the "%s" if the ExpressionLanguage component is not available. Try running "composer require symfony/expression-language".', __CLASS__)); diff --git a/src/Symfony/Bridge/Doctrine/Attribute/MapEntity.php b/src/Symfony/Bridge/Doctrine/Attribute/MapEntity.php index 529bf05dc7767..73d73d58b23bb 100644 --- a/src/Symfony/Bridge/Doctrine/Attribute/MapEntity.php +++ b/src/Symfony/Bridge/Doctrine/Attribute/MapEntity.php @@ -20,6 +20,19 @@ #[\Attribute(\Attribute::TARGET_PARAMETER)] class MapEntity extends ValueResolver { + /** + * @param class-string|null $class The entity class + * @param string|null $objectManager Specify the object manager used to retrieve the entity + * @param string|null $expr An expression to fetch the entity using the {@see https://symfony.com/doc/current/components/expression_language.html ExpressionLanguage} syntax. + * Any request attribute are available as a variable, and your entity repository in the 'repository' variable. + * @param array|null $mapping Configures the properties and values to use with the findOneBy() method + * The key is the route placeholder name and the value is the Doctrine property name + * @param string[]|null $exclude Configures the properties that should be used in the findOneBy() method by excluding + * one or more properties so that not all are used + * @param bool|null $stripNull Whether to prevent null values from being used as parameters in the query (defaults to false) + * @param string[]|string|null $id If an id option is configured and matches a route parameter, then the resolver will find by the primary key + * @param bool|null $evictCache If true, forces Doctrine to always fetch the entity from the database instead of cache (defaults to false) + */ public function __construct( public ?string $class = null, public ?string $objectManager = null, @@ -31,8 +44,10 @@ public function __construct( public ?bool $evictCache = null, bool $disabled = false, string $resolver = EntityValueResolver::class, + public ?string $message = null, ) { parent::__construct($resolver, $disabled); + $this->selfValidate(); } public function withDefaults(self $defaults, ?string $class): static @@ -46,7 +61,24 @@ public function withDefaults(self $defaults, ?string $class): static $clone->stripNull ??= $defaults->stripNull ?? false; $clone->id ??= $defaults->id; $clone->evictCache ??= $defaults->evictCache ?? false; + $clone->message ??= $defaults->message; + + $clone->selfValidate(); return $clone; } + + private function selfValidate(): void + { + if (!$this->id) { + return; + } + if ($this->mapping) { + throw new \LogicException('The "id" and "mapping" options cannot be used together on #[MapEntity] attributes.'); + } + if ($this->exclude) { + throw new \LogicException('The "id" and "exclude" options cannot be used together on #[MapEntity] attributes.'); + } + $this->mapping = []; + } } diff --git a/src/Symfony/Bridge/Doctrine/CHANGELOG.md b/src/Symfony/Bridge/Doctrine/CHANGELOG.md index 754e6938da402..f4ca310235228 100644 --- a/src/Symfony/Bridge/Doctrine/CHANGELOG.md +++ b/src/Symfony/Bridge/Doctrine/CHANGELOG.md @@ -1,6 +1,15 @@ 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 + * Deprecate auto-mapping of entities in favor of mapped route parameters + 7.0 --- @@ -17,7 +26,7 @@ CHANGELOG 6.4 --- - * [BC BREAK] Add argument `$buildDir` to `ProxyCacheWarmer::warmUp()` + * [BC BREAK] Add argument `$buildDir` to `ProxyCacheWarmer::warmUp()` * [BC BREAK] Add return type-hints to `EntityFactory` * Deprecate `DbalLogger`, use a middleware instead * Deprecate not constructing `DoctrineDataCollector` with an instance of `DebugDataHolder` diff --git a/src/Symfony/Bridge/Doctrine/CacheWarmer/ProxyCacheWarmer.php b/src/Symfony/Bridge/Doctrine/CacheWarmer/ProxyCacheWarmer.php index abe688b013f1a..ddf222e2940d2 100644 --- a/src/Symfony/Bridge/Doctrine/CacheWarmer/ProxyCacheWarmer.php +++ b/src/Symfony/Bridge/Doctrine/CacheWarmer/ProxyCacheWarmer.php @@ -21,6 +21,8 @@ * since this information is necessary to build the proxies in the first place. * * @author Benjamin Eberlei + * + * @final since Symfony 7.1 */ class ProxyCacheWarmer implements CacheWarmerInterface { diff --git a/src/Symfony/Bridge/Doctrine/DataCollector/DoctrineDataCollector.php b/src/Symfony/Bridge/Doctrine/DataCollector/DoctrineDataCollector.php index 8cee1248b030e..ef0a369db9f95 100644 --- a/src/Symfony/Bridge/Doctrine/DataCollector/DoctrineDataCollector.php +++ b/src/Symfony/Bridge/Doctrine/DataCollector/DoctrineDataCollector.php @@ -163,8 +163,7 @@ private function sanitizeQuery(string $connectionName, array $query): array $query['types'][$j] = $type->getBindingType(); try { $param = $type->convertToDatabaseValue($param, $this->registry->getConnection($connectionName)->getDatabasePlatform()); - } catch (\TypeError $e) { - } catch (ConversionException $e) { + } catch (\TypeError|ConversionException) { } } } diff --git a/src/Symfony/Bridge/Doctrine/IdGenerator/UlidGenerator.php b/src/Symfony/Bridge/Doctrine/IdGenerator/UlidGenerator.php index ab539486b4dcf..4c227eee951e2 100644 --- a/src/Symfony/Bridge/Doctrine/IdGenerator/UlidGenerator.php +++ b/src/Symfony/Bridge/Doctrine/IdGenerator/UlidGenerator.php @@ -20,7 +20,7 @@ final class UlidGenerator extends AbstractIdGenerator { public function __construct( - private readonly ?UlidFactory $factory = null + private readonly ?UlidFactory $factory = null, ) { } diff --git a/src/Symfony/Bridge/Doctrine/Middleware/IdleConnection/Driver.php b/src/Symfony/Bridge/Doctrine/Middleware/IdleConnection/Driver.php new file mode 100644 index 0000000000000..566002cf8487c --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Middleware/IdleConnection/Driver.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Middleware\IdleConnection; + +use Doctrine\DBAL\Driver as DriverInterface; +use Doctrine\DBAL\Driver\Connection as ConnectionInterface; +use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware; + +final class Driver extends AbstractDriverMiddleware +{ + public function __construct( + DriverInterface $driver, + private \ArrayObject $connectionExpiries, + private readonly int $ttl, + private readonly string $connectionName, + ) { + parent::__construct($driver); + } + + public function connect(array $params): ConnectionInterface + { + $timestamp = time(); + $connection = parent::connect($params); + $this->connectionExpiries[$this->connectionName] = $timestamp + $this->ttl; + + return $connection; + } +} diff --git a/src/Symfony/Bridge/Doctrine/Middleware/IdleConnection/Listener.php b/src/Symfony/Bridge/Doctrine/Middleware/IdleConnection/Listener.php new file mode 100644 index 0000000000000..11f7053c5f702 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Middleware/IdleConnection/Listener.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Middleware\IdleConnection; + +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpKernel\Event\RequestEvent; +use Symfony\Component\HttpKernel\KernelEvents; + +final class Listener implements EventSubscriberInterface +{ + /** + * @param \ArrayObject $connectionExpiries + */ + public function __construct( + private readonly \ArrayObject $connectionExpiries, + private ContainerInterface $container, + ) { + } + + public function onKernelRequest(RequestEvent $event): void + { + $timestamp = time(); + + foreach ($this->connectionExpiries as $name => $expiry) { + if ($timestamp >= $expiry) { + // unset before so that we won't retry in case of any failure + $this->connectionExpiries->offsetUnset($name); + + try { + $connection = $this->container->get("doctrine.dbal.{$name}_connection"); + $connection->close(); + } catch (\Exception) { + // ignore exceptions to remain fail-safe + } + } + } + } + + public static function getSubscribedEvents(): array + { + return [ + KernelEvents::REQUEST => ['onKernelRequest', 192], // before session listeners since they could use the DB + ]; + } +} diff --git a/src/Symfony/Bridge/Doctrine/PropertyInfo/DoctrineExtractor.php b/src/Symfony/Bridge/Doctrine/PropertyInfo/DoctrineExtractor.php index 14d691f485a3b..7af2d5059abf1 100644 --- a/src/Symfony/Bridge/Doctrine/PropertyInfo/DoctrineExtractor.php +++ b/src/Symfony/Bridge/Doctrine/PropertyInfo/DoctrineExtractor.php @@ -24,7 +24,9 @@ use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface; use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; -use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\PropertyInfo\Type as LegacyType; +use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\TypeIdentifier; /** * Extracts data using Doctrine ORM and ODM metadata. @@ -55,8 +57,110 @@ public function getProperties(string $class, array $context = []): ?array return $properties; } + public function getType(string $class, string $property, array $context = []): ?Type + { + if (null === $metadata = $this->getMetadata($class)) { + return null; + } + + if ($metadata->hasAssociation($property)) { + $class = $metadata->getAssociationTargetClass($property); + + if ($metadata->isSingleValuedAssociation($property)) { + if ($metadata instanceof ClassMetadata) { + $associationMapping = $metadata->getAssociationMapping($property); + $nullable = $this->isAssociationNullable($associationMapping); + } else { + $nullable = false; + } + + return $nullable ? Type::nullable(Type::object($class)) : Type::object($class); + } + + $collectionKeyType = TypeIdentifier::INT; + + if ($metadata instanceof ClassMetadata) { + $associationMapping = $metadata->getAssociationMapping($property); + + if (self::getMappingValue($associationMapping, 'indexBy')) { + $subMetadata = $this->entityManager->getClassMetadata(self::getMappingValue($associationMapping, 'targetEntity')); + + // Check if indexBy value is a property + $fieldName = self::getMappingValue($associationMapping, 'indexBy'); + if (null === ($typeOfField = $subMetadata->getTypeOfField($fieldName))) { + $fieldName = $subMetadata->getFieldForColumn(self::getMappingValue($associationMapping, 'indexBy')); + // Not a property, maybe a column name? + if (null === ($typeOfField = $subMetadata->getTypeOfField($fieldName))) { + // Maybe the column name is the association join column? + $associationMapping = $subMetadata->getAssociationMapping($fieldName); + + $indexProperty = $subMetadata->getSingleAssociationReferencedJoinColumnName($fieldName); + $subMetadata = $this->entityManager->getClassMetadata(self::getMappingValue($associationMapping, 'targetEntity')); + + // Not a property, maybe a column name? + if (null === ($typeOfField = $subMetadata->getTypeOfField($indexProperty))) { + $fieldName = $subMetadata->getFieldForColumn($indexProperty); + $typeOfField = $subMetadata->getTypeOfField($fieldName); + } + } + } + + if (!$collectionKeyType = $this->getTypeIdentifier($typeOfField)) { + return null; + } + } + } + + return Type::collection(Type::object(Collection::class), Type::object($class), Type::builtin($collectionKeyType)); + } + + if ($metadata instanceof ClassMetadata && isset($metadata->embeddedClasses[$property])) { + return Type::object(self::getMappingValue($metadata->embeddedClasses[$property], 'class')); + } + + if (!$metadata->hasField($property)) { + return null; + } + + $typeOfField = $metadata->getTypeOfField($property); + + if (!$typeIdentifier = $this->getTypeIdentifier($typeOfField)) { + return null; + } + + $nullable = $metadata instanceof ClassMetadata && $metadata->isNullable($property); + $enumType = null; + + if (null !== $enumClass = self::getMappingValue($metadata->getFieldMapping($property), 'enumType') ?? null) { + $enumType = $nullable ? Type::nullable(Type::enum($enumClass)) : Type::enum($enumClass); + } + + $builtinType = $nullable ? Type::nullable(Type::builtin($typeIdentifier)) : Type::builtin($typeIdentifier); + + return match ($typeIdentifier) { + TypeIdentifier::OBJECT => match ($typeOfField) { + Types::DATE_MUTABLE, Types::DATETIME_MUTABLE, Types::DATETIMETZ_MUTABLE, 'vardatetime', Types::TIME_MUTABLE => $nullable ? Type::nullable(Type::object(\DateTime::class)) : Type::object(\DateTime::class), + Types::DATE_IMMUTABLE, Types::DATETIME_IMMUTABLE, Types::DATETIMETZ_IMMUTABLE, Types::TIME_IMMUTABLE => $nullable ? Type::nullable(Type::object(\DateTimeImmutable::class)) : Type::object(\DateTimeImmutable::class), + Types::DATEINTERVAL => $nullable ? Type::nullable(Type::object(\DateInterval::class)) : Type::object(\DateInterval::class), + default => $builtinType, + }, + TypeIdentifier::ARRAY => match ($typeOfField) { + 'array', 'json_array' => $enumType ? null : ($nullable ? Type::nullable(Type::array()) : Type::array()), + Types::SIMPLE_ARRAY => $nullable ? Type::nullable(Type::list($enumType ?? Type::string())) : Type::list($enumType ?? Type::string()), + default => $builtinType, + }, + TypeIdentifier::INT, TypeIdentifier::STRING => $enumType ? $enumType : $builtinType, + default => $builtinType, + }; + } + + /** + * @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; } @@ -73,10 +177,10 @@ public function getTypes(string $class, string $property, array $context = []): $nullable = false; } - return [new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, $class)]; + return [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, $nullable, $class)]; } - $collectionKeyType = Type::BUILTIN_TYPE_INT; + $collectionKeyType = LegacyType::BUILTIN_TYPE_INT; if ($metadata instanceof ClassMetadata) { $associationMapping = $metadata->getAssociationMapping($property); @@ -104,61 +208,61 @@ public function getTypes(string $class, string $property, array $context = []): } } - if (!$collectionKeyType = $this->getPhpType($typeOfField)) { + if (!$collectionKeyType = $this->getTypeIdentifierLegacy($typeOfField)) { return null; } } } - return [new Type( - Type::BUILTIN_TYPE_OBJECT, + return [new LegacyType( + LegacyType::BUILTIN_TYPE_OBJECT, false, Collection::class, true, - new Type($collectionKeyType), - new Type(Type::BUILTIN_TYPE_OBJECT, false, $class) + new LegacyType($collectionKeyType), + new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, $class) )]; } if ($metadata instanceof ClassMetadata && isset($metadata->embeddedClasses[$property])) { - return [new Type(Type::BUILTIN_TYPE_OBJECT, false, self::getMappingValue($metadata->embeddedClasses[$property], 'class'))]; + return [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, self::getMappingValue($metadata->embeddedClasses[$property], 'class'))]; } if ($metadata->hasField($property)) { $typeOfField = $metadata->getTypeOfField($property); - if (!$builtinType = $this->getPhpType($typeOfField)) { + if (!$builtinType = $this->getTypeIdentifierLegacy($typeOfField)) { return null; } $nullable = $metadata instanceof ClassMetadata && $metadata->isNullable($property); $enumType = null; if (null !== $enumClass = self::getMappingValue($metadata->getFieldMapping($property), 'enumType') ?? null) { - $enumType = new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, $enumClass); + $enumType = new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, $nullable, $enumClass); } switch ($builtinType) { - case Type::BUILTIN_TYPE_OBJECT: + case LegacyType::BUILTIN_TYPE_OBJECT: switch ($typeOfField) { case Types::DATE_MUTABLE: case Types::DATETIME_MUTABLE: case Types::DATETIMETZ_MUTABLE: case 'vardatetime': case Types::TIME_MUTABLE: - return [new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, 'DateTime')]; + return [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, $nullable, 'DateTime')]; case Types::DATE_IMMUTABLE: case Types::DATETIME_IMMUTABLE: case Types::DATETIMETZ_IMMUTABLE: case Types::TIME_IMMUTABLE: - return [new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, 'DateTimeImmutable')]; + return [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, $nullable, 'DateTimeImmutable')]; case Types::DATEINTERVAL: - return [new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, 'DateInterval')]; + return [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, $nullable, 'DateInterval')]; } break; - case Type::BUILTIN_TYPE_ARRAY: + case LegacyType::BUILTIN_TYPE_ARRAY: switch ($typeOfField) { case 'array': // DBAL < 4 case 'json_array': // DBAL < 3 @@ -167,21 +271,21 @@ public function getTypes(string $class, string $property, array $context = []): return null; } - return [new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true)]; + return [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, $nullable, null, true)]; case Types::SIMPLE_ARRAY: - return [new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true, new Type(Type::BUILTIN_TYPE_INT), $enumType ?? new Type(Type::BUILTIN_TYPE_STRING))]; + return [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, $nullable, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), $enumType ?? new LegacyType(LegacyType::BUILTIN_TYPE_STRING))]; } break; - case Type::BUILTIN_TYPE_INT: - case Type::BUILTIN_TYPE_STRING: + case LegacyType::BUILTIN_TYPE_INT: + case LegacyType::BUILTIN_TYPE_STRING: if ($enumType) { return [$enumType]; } break; } - return [new Type($builtinType, $nullable)]; + return [new LegacyType($builtinType, $nullable)]; } return null; @@ -244,20 +348,52 @@ private function isAssociationNullable(array|AssociationMapping $associationMapp /** * Gets the corresponding built-in PHP type. */ - private function getPhpType(string $doctrineType): ?string + private function getTypeIdentifier(string $doctrineType): ?TypeIdentifier + { + return match ($doctrineType) { + Types::SMALLINT, + Types::INTEGER => TypeIdentifier::INT, + Types::FLOAT => TypeIdentifier::FLOAT, + Types::BIGINT, + Types::STRING, + Types::TEXT, + Types::GUID, + Types::DECIMAL => TypeIdentifier::STRING, + Types::BOOLEAN => TypeIdentifier::BOOL, + Types::BLOB, + Types::BINARY => TypeIdentifier::RESOURCE, + 'object', // DBAL < 4 + Types::DATE_MUTABLE, + Types::DATETIME_MUTABLE, + Types::DATETIMETZ_MUTABLE, + 'vardatetime', + Types::TIME_MUTABLE, + Types::DATE_IMMUTABLE, + Types::DATETIME_IMMUTABLE, + Types::DATETIMETZ_IMMUTABLE, + Types::TIME_IMMUTABLE, + Types::DATEINTERVAL => TypeIdentifier::OBJECT, + 'array', // DBAL < 4 + 'json_array', // DBAL < 3 + Types::SIMPLE_ARRAY => TypeIdentifier::ARRAY, + default => null, + }; + } + + private function getTypeIdentifierLegacy(string $doctrineType): ?string { return match ($doctrineType) { Types::SMALLINT, - Types::INTEGER => Type::BUILTIN_TYPE_INT, - Types::FLOAT => Type::BUILTIN_TYPE_FLOAT, + Types::INTEGER => LegacyType::BUILTIN_TYPE_INT, + Types::FLOAT => LegacyType::BUILTIN_TYPE_FLOAT, Types::BIGINT, Types::STRING, Types::TEXT, Types::GUID, - Types::DECIMAL => Type::BUILTIN_TYPE_STRING, - Types::BOOLEAN => Type::BUILTIN_TYPE_BOOL, + Types::DECIMAL => LegacyType::BUILTIN_TYPE_STRING, + Types::BOOLEAN => LegacyType::BUILTIN_TYPE_BOOL, Types::BLOB, - Types::BINARY => Type::BUILTIN_TYPE_RESOURCE, + Types::BINARY => LegacyType::BUILTIN_TYPE_RESOURCE, 'object', // DBAL < 4 Types::DATE_MUTABLE, Types::DATETIME_MUTABLE, @@ -268,10 +404,10 @@ private function getPhpType(string $doctrineType): ?string Types::DATETIME_IMMUTABLE, Types::DATETIMETZ_IMMUTABLE, Types::TIME_IMMUTABLE, - Types::DATEINTERVAL => Type::BUILTIN_TYPE_OBJECT, + Types::DATEINTERVAL => LegacyType::BUILTIN_TYPE_OBJECT, 'array', // DBAL < 4 'json_array', // DBAL < 3 - Types::SIMPLE_ARRAY => Type::BUILTIN_TYPE_ARRAY, + Types::SIMPLE_ARRAY => LegacyType::BUILTIN_TYPE_ARRAY, default => null, }; } diff --git a/src/Symfony/Bridge/Doctrine/Tests/ArgumentResolver/EntityValueResolverTest.php b/src/Symfony/Bridge/Doctrine/Tests/ArgumentResolver/EntityValueResolverTest.php index 749e9b7792144..f9f0b9a71da64 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/ArgumentResolver/EntityValueResolverTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/ArgumentResolver/EntityValueResolverTest.php @@ -63,6 +63,9 @@ public function testResolveWithoutManager() $this->assertSame([], $resolver->resolve($request, $argument)); } + /** + * @group legacy + */ public function testResolveWithNoIdAndDataOptional() { $manager = $this->getMockBuilder(ObjectManager::class)->getMock(); @@ -83,18 +86,10 @@ public function testResolveWithStripNulls() $request = new Request(); $request->attributes->set('arg', null); - $argument = $this->createArgument('stdClass', new MapEntity(stripNull: true), 'arg', true); - - $metadata = $this->getMockBuilder(ClassMetadata::class)->getMock(); - $metadata->expects($this->once()) - ->method('hasField') - ->with('arg') - ->willReturn(true); + $argument = $this->createArgument('stdClass', new MapEntity(mapping: ['arg'], stripNull: true), 'arg', true); - $manager->expects($this->once()) - ->method('getClassMetadata') - ->with('stdClass') - ->willReturn($metadata); + $manager->expects($this->never()) + ->method('getClassMetadata'); $manager->expects($this->never()) ->method('getRepository'); @@ -139,7 +134,7 @@ public function testResolveWithNullId() $request = new Request(); $request->attributes->set('id', null); - $argument = $this->createArgument(isNullable: true); + $argument = $this->createArgument(isNullable: true, entity: new MapEntity(id: 'id')); $this->assertSame([null], $resolver->resolve($request, $argument)); } @@ -153,7 +148,7 @@ public function testResolveWithConversionFailedException() $request = new Request(); $request->attributes->set('id', 'test'); - $argument = $this->createArgument('stdClass', new MapEntity(id: 'id')); + $argument = $this->createArgument('stdClass', new MapEntity(id: 'id', message: 'Test')); $repository = $this->getMockBuilder(ObjectRepository::class)->getMock(); $repository->expects($this->once()) @@ -167,6 +162,7 @@ public function testResolveWithConversionFailedException() ->willReturn($repository); $this->expectException(NotFoundHttpException::class); + $this->expectExceptionMessage('Test'); $resolver->resolve($request, $argument); } @@ -194,6 +190,9 @@ public static function idsProvider(): iterable yield ['foo']; } + /** + * @group legacy + */ public function testResolveGuessOptional() { $manager = $this->getMockBuilder(ObjectManager::class)->getMock(); @@ -231,16 +230,8 @@ public function testResolveWithMappingAndExclude() new MapEntity(mapping: ['foo' => 'Foo'], exclude: ['bar']) ); - $metadata = $this->getMockBuilder(ClassMetadata::class)->getMock(); - $metadata->expects($this->once()) - ->method('hasField') - ->with('Foo') - ->willReturn(true); - - $manager->expects($this->once()) - ->method('getClassMetadata') - ->with('stdClass') - ->willReturn($metadata); + $manager->expects($this->never()) + ->method('getClassMetadata'); $repository = $this->getMockBuilder(ObjectRepository::class)->getMock(); $repository->expects($this->once()) @@ -256,6 +247,42 @@ public function testResolveWithMappingAndExclude() $this->assertSame([$object], $resolver->resolve($request, $argument)); } + public function testResolveWithRouteMapping() + { + $manager = $this->getMockBuilder(ObjectManager::class)->getMock(); + $registry = $this->createRegistry($manager); + $resolver = new EntityValueResolver($registry); + + $request = new Request(); + $request->attributes->set('conference', 'vienna-2024'); + $request->attributes->set('article', ['title' => 'foo']); + $request->attributes->set('_route_mapping', ['slug' => 'conference']); + + $argument1 = $this->createArgument('Conference', new MapEntity('Conference'), 'conference'); + $argument2 = $this->createArgument('Article', new MapEntity('Article'), 'article'); + + $manager->expects($this->never()) + ->method('getClassMetadata'); + + $conference = new \stdClass(); + $article = new \stdClass(); + + $repository = $this->getMockBuilder(ObjectRepository::class)->getMock(); + $repository->expects($this->any()) + ->method('findOneBy') + ->willReturnCallback(static fn ($v) => match ($v) { + ['slug' => 'vienna-2024'] => $conference, + ['title' => 'foo'] => $article, + }); + + $manager->expects($this->any()) + ->method('getRepository') + ->willReturn($repository); + + $this->assertSame([$conference], $resolver->resolve($request, $argument1)); + $this->assertSame([$article], $resolver->resolve($request, $argument2)); + } + public function testExceptionWithExpressionIfNoLanguageAvailable() { $manager = $this->getMockBuilder(ObjectManager::class)->getMock(); @@ -297,6 +324,7 @@ public function testExpressionFailureReturns404() $manager->expects($this->once()) ->method('getRepository') + ->with(\stdClass::class) ->willReturn($repository); $language->expects($this->once()) @@ -328,6 +356,7 @@ public function testExpressionMapsToArgument() $manager->expects($this->once()) ->method('getRepository') + ->with(\stdClass::class) ->willReturn($repository); $language->expects($this->once()) @@ -342,6 +371,48 @@ public function testExpressionMapsToArgument() $this->assertSame([$object], $resolver->resolve($request, $argument)); } + public function testExpressionMapsToIterableArgument() + { + $manager = $this->createMock(ObjectManager::class); + $registry = $this->createRegistry($manager); + $language = $this->createMock(ExpressionLanguage::class); + $resolver = new EntityValueResolver($registry, $language); + + $request = new Request(); + $request->attributes->set('id', 5); + $request->query->set('sort', 'ASC'); + $request->query->set('limit', 10); + $argument = $this->createArgument( + 'iterable', + new MapEntity( + class: \stdClass::class, + expr: $expr = 'repository.findBy({"author": id}, {"createdAt": request.query.get("sort", "DESC")}, request.query.getInt("limit", 10))', + ), + 'arg1', + ); + + $repository = $this->createMock(ObjectRepository::class); + // find should not be attempted on this repository as a fallback + $repository->expects($this->never()) + ->method('find'); + + $manager->expects($this->once()) + ->method('getRepository') + ->with(\stdClass::class) + ->willReturn($repository); + + $language->expects($this->once()) + ->method('evaluate') + ->with($expr, [ + 'repository' => $repository, + 'request' => $request, + 'id' => 5, + ]) + ->willReturn($objects = [new \stdClass(), new \stdClass()]); + + $this->assertSame([$objects], $resolver->resolve($request, $argument)); + } + public function testExpressionSyntaxErrorThrowsException() { $manager = $this->getMockBuilder(ObjectManager::class)->getMock(); @@ -363,6 +434,7 @@ public function testExpressionSyntaxErrorThrowsException() $manager->expects($this->once()) ->method('getRepository') + ->with(\stdClass::class) ->willReturn($repository); $language->expects($this->once()) diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/CreateDoubleNameEntity.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/CreateDoubleNameEntity.php new file mode 100644 index 0000000000000..421b67c5c1d77 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/CreateDoubleNameEntity.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\Fixtures; + +class CreateDoubleNameEntity +{ + public $primaryName; + public $secondaryName; + + public function __construct($primaryName, $secondaryName) + { + $this->primaryName = $primaryName; + $this->secondaryName = $secondaryName; + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Dto.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Dto.php new file mode 100644 index 0000000000000..1a9444324496b --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Dto.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\Fixtures; + +class Dto +{ + public string $foo; +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/HireAnEmployee.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/HireAnEmployee.php new file mode 100644 index 0000000000000..4ef9d610077a8 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/HireAnEmployee.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\Fixtures; + +class HireAnEmployee +{ + public $name; + + public function __construct($name) + { + $this->name = $name; + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/UpdateCompositeIntIdEntity.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/UpdateCompositeIntIdEntity.php new file mode 100644 index 0000000000000..3c134e084bea7 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/UpdateCompositeIntIdEntity.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\Fixtures; + +class UpdateCompositeIntIdEntity +{ + public $id1; + public $id2; + public $name; + + public function __construct($id1, $id2, $name) + { + $this->id1 = $id1; + $this->id2 = $id2; + $this->name = $name; + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/UpdateCompositeObjectNoToStringIdEntity.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/UpdateCompositeObjectNoToStringIdEntity.php new file mode 100644 index 0000000000000..4b18c54044aee --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/UpdateCompositeObjectNoToStringIdEntity.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\Fixtures; + +class UpdateCompositeObjectNoToStringIdEntity +{ + /** + * @var SingleIntIdNoToStringEntity + */ + protected $object1; + + /** + * @var SingleIntIdNoToStringEntity + */ + protected $object2; + + public $name; + + public function __construct(SingleIntIdNoToStringEntity $object1, SingleIntIdNoToStringEntity $object2, $name) + { + $this->object1 = $object1; + $this->object2 = $object2; + $this->name = $name; + } + + public function getObject1(): SingleIntIdNoToStringEntity + { + return $this->object1; + } + + public function getObject2(): SingleIntIdNoToStringEntity + { + return $this->object2; + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/UpdateEmployeeProfile.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/UpdateEmployeeProfile.php new file mode 100644 index 0000000000000..92c1d56a90e8d --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/UpdateEmployeeProfile.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\Fixtures; + +class UpdateEmployeeProfile +{ + public $id; + public $name; + + public function __construct($id, $name) + { + $this->id = $id; + $this->name = $name; + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/ORMQueryBuilderLoaderTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/ORMQueryBuilderLoaderTest.php index b8a0668e1d9f6..017b327b8a6eb 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/ORMQueryBuilderLoaderTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/ORMQueryBuilderLoaderTest.php @@ -12,7 +12,6 @@ namespace Symfony\Bridge\Doctrine\Tests\Form\ChoiceList; use Doctrine\DBAL\ArrayParameterType; -use Doctrine\DBAL\Result; use Doctrine\DBAL\Types\GuidType; use Doctrine\DBAL\Types\Type; use Doctrine\ORM\AbstractQuery; diff --git a/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrineOpenTransactionLoggerMiddlewareTest.php b/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrineOpenTransactionLoggerMiddlewareTest.php index 73b247ecc7f87..8d49caa38fa7e 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrineOpenTransactionLoggerMiddlewareTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrineOpenTransactionLoggerMiddlewareTest.php @@ -53,7 +53,7 @@ public function testMiddlewareWrapsInTransactionAndFlushes() { $this->connection->expects($this->exactly(1)) ->method('isTransactionActive') - ->will($this->onConsecutiveCalls(true, true, false)) + ->willReturn(true, true, false) ; $this->middleware->handle(new Envelope(new \stdClass()), $this->getStackMock()); diff --git a/src/Symfony/Bridge/Doctrine/Tests/Middleware/IdleConnection/DriverTest.php b/src/Symfony/Bridge/Doctrine/Tests/Middleware/IdleConnection/DriverTest.php new file mode 100644 index 0000000000000..010e1879a8ab4 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Middleware/IdleConnection/DriverTest.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\Middleware\IdleConnection; + +use Doctrine\DBAL\Driver as DriverInterface; +use Doctrine\DBAL\Driver\Connection as ConnectionInterface; +use PHPUnit\Framework\TestCase; +use Symfony\Bridge\Doctrine\Middleware\IdleConnection\Driver; + +class DriverTest extends TestCase +{ + /** + * @group time-sensitive + */ + public function testConnect() + { + $driverMock = $this->createMock(DriverInterface::class); + $connectionMock = $this->createMock(ConnectionInterface::class); + + $driverMock->expects($this->once()) + ->method('connect') + ->willReturn($connectionMock); + + $connectionExpiries = new \ArrayObject(); + + $driver = new Driver($driverMock, $connectionExpiries, 60, 'default'); + $connection = $driver->connect([]); + + $this->assertSame($connectionMock, $connection); + $this->assertArrayHasKey('default', $connectionExpiries); + $this->assertSame(time() + 60, $connectionExpiries['default']); + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Middleware/IdleConnection/ListenerTest.php b/src/Symfony/Bridge/Doctrine/Tests/Middleware/IdleConnection/ListenerTest.php new file mode 100644 index 0000000000000..099ab48777133 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Middleware/IdleConnection/ListenerTest.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Middleware\IdleConnection; + +use Doctrine\DBAL\Connection as ConnectionInterface; +use PHPUnit\Framework\TestCase; +use Symfony\Bridge\Doctrine\Middleware\IdleConnection\Listener; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpKernel\Event\RequestEvent; + +class ListenerTest extends TestCase +{ + public function testOnKernelRequest() + { + $containerMock = $this->createMock(ContainerInterface::class); + $connectionExpiries = new \ArrayObject(['connectionone' => time() - 30, 'connectiontwo' => time() + 40]); + + $connectionOneMock = $this->getMockBuilder(ConnectionInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $containerMock->expects($this->exactly(1)) + ->method('get') + ->with('doctrine.dbal.connectionone_connection') + ->willReturn($connectionOneMock); + + $listener = new Listener($connectionExpiries, $containerMock); + + $listener->onKernelRequest($this->createMock(RequestEvent::class)); + + $this->assertArrayNotHasKey('connectionone', (array) $connectionExpiries); + $this->assertArrayHasKey('connectiontwo', (array) $connectionExpiries); + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php index 4589f01488d56..35919529be459 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php @@ -29,7 +29,8 @@ use Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineWithEmbedded; use Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\EnumInt; use Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\EnumString; -use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\PropertyInfo\Type as LegacyType; +use Symfony\Component\TypeInfo\Type; /** * @author Kévin Dunglas @@ -106,17 +107,22 @@ public function testTestGetPropertiesWithEmbedded() } /** - * @dataProvider typesProvider + * @group legacy + * + * @dataProvider legacyTypesProvider */ - public function testExtract(string $property, ?array $type = null) + public function testExtractLegacy(string $property, ?array $type = null) { $this->assertEquals($type, $this->createExtractor()->getTypes(DoctrineDummy::class, $property, [])); } - public function testExtractWithEmbedded() + /** + * @group legacy + */ + public function testExtractWithEmbeddedLegacy() { - $expectedTypes = [new Type( - Type::BUILTIN_TYPE_OBJECT, + $expectedTypes = [new LegacyType( + LegacyType::BUILTIN_TYPE_OBJECT, false, DoctrineEmbeddable::class )]; @@ -130,97 +136,103 @@ public function testExtractWithEmbedded() $this->assertEquals($expectedTypes, $actualTypes); } - public function testExtractEnum() + /** + * @group legacy + */ + public function testExtractEnumLegacy() { - $this->assertEquals([new Type(Type::BUILTIN_TYPE_OBJECT, false, EnumString::class)], $this->createExtractor()->getTypes(DoctrineEnum::class, 'enumString', [])); - $this->assertEquals([new Type(Type::BUILTIN_TYPE_OBJECT, false, EnumInt::class)], $this->createExtractor()->getTypes(DoctrineEnum::class, 'enumInt', [])); + $this->assertEquals([new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, EnumString::class)], $this->createExtractor()->getTypes(DoctrineEnum::class, 'enumString', [])); + $this->assertEquals([new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, EnumInt::class)], $this->createExtractor()->getTypes(DoctrineEnum::class, 'enumInt', [])); $this->assertNull($this->createExtractor()->getTypes(DoctrineEnum::class, 'enumStringArray', [])); - $this->assertEquals([new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, EnumInt::class))], $this->createExtractor()->getTypes(DoctrineEnum::class, 'enumIntArray', [])); + $this->assertEquals([new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, EnumInt::class))], $this->createExtractor()->getTypes(DoctrineEnum::class, 'enumIntArray', [])); $this->assertNull($this->createExtractor()->getTypes(DoctrineEnum::class, 'enumCustom', [])); } - public static function typesProvider(): array + /** + * @group legacy + */ + public static function legacyTypesProvider(): array { return [ - ['id', [new Type(Type::BUILTIN_TYPE_INT)]], - ['guid', [new Type(Type::BUILTIN_TYPE_STRING)]], - ['bigint', [new Type(Type::BUILTIN_TYPE_STRING)]], - ['time', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTime')]], - ['timeImmutable', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable')]], - ['dateInterval', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateInterval')]], - ['float', [new Type(Type::BUILTIN_TYPE_FLOAT)]], - ['decimal', [new Type(Type::BUILTIN_TYPE_STRING)]], - ['bool', [new Type(Type::BUILTIN_TYPE_BOOL)]], - ['binary', [new Type(Type::BUILTIN_TYPE_RESOURCE)]], - ['jsonArray', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true)]], - ['foo', [new Type(Type::BUILTIN_TYPE_OBJECT, true, 'Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineRelation')]], - ['bar', [new Type( - Type::BUILTIN_TYPE_OBJECT, + ['id', [new LegacyType(LegacyType::BUILTIN_TYPE_INT)]], + ['guid', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING)]], + ['bigint', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING)]], + ['time', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'DateTime')]], + ['timeImmutable', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable')]], + ['dateInterval', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'DateInterval')]], + ['float', [new LegacyType(LegacyType::BUILTIN_TYPE_FLOAT)]], + ['decimal', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING)]], + ['bool', [new LegacyType(LegacyType::BUILTIN_TYPE_BOOL)]], + ['binary', [new LegacyType(LegacyType::BUILTIN_TYPE_RESOURCE)]], + ['jsonArray', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true)]], + ['foo', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, true, 'Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineRelation')]], + ['bar', [new LegacyType( + LegacyType::BUILTIN_TYPE_OBJECT, false, 'Doctrine\Common\Collections\Collection', true, - new Type(Type::BUILTIN_TYPE_INT), - new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineRelation') + new LegacyType(LegacyType::BUILTIN_TYPE_INT), + new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineRelation') )]], - ['indexedRguid', [new Type( - Type::BUILTIN_TYPE_OBJECT, + ['indexedRguid', [new LegacyType( + LegacyType::BUILTIN_TYPE_OBJECT, false, 'Doctrine\Common\Collections\Collection', true, - new Type(Type::BUILTIN_TYPE_STRING), - new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineRelation') + new LegacyType(LegacyType::BUILTIN_TYPE_STRING), + new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineRelation') )]], - ['indexedBar', [new Type( - Type::BUILTIN_TYPE_OBJECT, + ['indexedBar', [new LegacyType( + LegacyType::BUILTIN_TYPE_OBJECT, false, 'Doctrine\Common\Collections\Collection', true, - new Type(Type::BUILTIN_TYPE_STRING), - new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineRelation') + new LegacyType(LegacyType::BUILTIN_TYPE_STRING), + new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineRelation') )]], - ['indexedFoo', [new Type( - Type::BUILTIN_TYPE_OBJECT, + ['indexedFoo', [new LegacyType( + LegacyType::BUILTIN_TYPE_OBJECT, false, 'Doctrine\Common\Collections\Collection', true, - new Type(Type::BUILTIN_TYPE_STRING), - new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineRelation') + new LegacyType(LegacyType::BUILTIN_TYPE_STRING), + new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineRelation') )]], - ['indexedBaz', [new Type( - Type::BUILTIN_TYPE_OBJECT, + ['indexedBaz', [new LegacyType( + LegacyType::BUILTIN_TYPE_OBJECT, false, Collection::class, true, - new Type(Type::BUILTIN_TYPE_INT), - new Type(Type::BUILTIN_TYPE_OBJECT, false, DoctrineRelation::class) + new LegacyType(LegacyType::BUILTIN_TYPE_INT), + new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, DoctrineRelation::class) )]], - ['simpleArray', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING))]], + ['simpleArray', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_STRING))]], ['customFoo', null], ['notMapped', null], - ['indexedByDt', [new Type( - Type::BUILTIN_TYPE_OBJECT, + ['indexedByDt', [new LegacyType( + LegacyType::BUILTIN_TYPE_OBJECT, false, Collection::class, true, - new Type(Type::BUILTIN_TYPE_OBJECT), - new Type(Type::BUILTIN_TYPE_OBJECT, false, DoctrineRelation::class) + new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT), + new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, DoctrineRelation::class) )]], ['indexedByCustomType', null], - ['indexedBuz', [new Type( - Type::BUILTIN_TYPE_OBJECT, + ['indexedBuz', [new LegacyType( + LegacyType::BUILTIN_TYPE_OBJECT, false, Collection::class, true, - new Type(Type::BUILTIN_TYPE_STRING), - new Type(Type::BUILTIN_TYPE_OBJECT, false, DoctrineRelation::class) + new LegacyType(LegacyType::BUILTIN_TYPE_STRING), + new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, DoctrineRelation::class) )]], - ['dummyGeneratedValueList', [new Type( - Type::BUILTIN_TYPE_OBJECT, + ['dummyGeneratedValueList', [new LegacyType( + LegacyType::BUILTIN_TYPE_OBJECT, false, 'Doctrine\Common\Collections\Collection', true, - new Type(Type::BUILTIN_TYPE_INT), - new Type(Type::BUILTIN_TYPE_OBJECT, false, DoctrineRelation::class) + new LegacyType(LegacyType::BUILTIN_TYPE_INT), + new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, DoctrineRelation::class) )]], ['json', null], ]; @@ -231,7 +243,10 @@ public function testGetPropertiesCatchException() $this->assertNull($this->createExtractor()->getProperties('Not\Exist')); } - public function testGetTypesCatchException() + /** + * @group legacy + */ + public function testGetTypesCatchExceptionLegacy() { $this->assertNull($this->createExtractor()->getTypes('Not\Exist', 'baz')); } @@ -244,4 +259,66 @@ public function testGeneratedValueNotWritable() $this->assertNull($extractor->isWritable(DoctrineGeneratedValue::class, 'foo')); $this->assertNull($extractor->isReadable(DoctrineGeneratedValue::class, 'foo')); } + + public function testExtractWithEmbedded() + { + $this->assertEquals( + Type::object(DoctrineEmbeddable::class), + $this->createExtractor()->getType(DoctrineWithEmbedded::class, 'embedded'), + ); + } + + public function testExtractEnum() + { + $this->assertEquals(Type::enum(EnumString::class), $this->createExtractor()->getType(DoctrineEnum::class, 'enumString')); + $this->assertEquals(Type::enum(EnumInt::class), $this->createExtractor()->getType(DoctrineEnum::class, 'enumInt')); + $this->assertNull($this->createExtractor()->getType(DoctrineEnum::class, 'enumStringArray')); + $this->assertEquals(Type::list(Type::enum(EnumInt::class)), $this->createExtractor()->getType(DoctrineEnum::class, 'enumIntArray')); + $this->assertNull($this->createExtractor()->getType(DoctrineEnum::class, 'enumCustom')); + } + + /** + * @dataProvider typeProvider + */ + public function testExtract(string $property, ?Type $type) + { + $this->assertEquals($type, $this->createExtractor()->getType(DoctrineDummy::class, $property, [])); + } + + /** + * @return iterable + */ + public static function typeProvider(): iterable + { + yield ['id', Type::int()]; + yield ['guid', Type::string()]; + yield ['bigint', Type::string()]; + yield ['time', Type::object(\DateTime::class)]; + yield ['timeImmutable', Type::object(\DateTimeImmutable::class)]; + yield ['dateInterval', Type::object(\DateInterval::class)]; + yield ['float', Type::float()]; + yield ['decimal', Type::string()]; + yield ['bool', Type::bool()]; + yield ['binary', Type::resource()]; + yield ['jsonArray', Type::array()]; + yield ['foo', Type::nullable(Type::object(DoctrineRelation::class))]; + yield ['bar', Type::collection(Type::object(Collection::class), Type::object(DoctrineRelation::class), Type::int())]; + yield ['indexedRguid', Type::collection(Type::object(Collection::class), Type::object(DoctrineRelation::class), Type::string())]; + yield ['indexedBar', Type::collection(Type::object(Collection::class), Type::object(DoctrineRelation::class), Type::string())]; + yield ['indexedFoo', Type::collection(Type::object(Collection::class), Type::object(DoctrineRelation::class), Type::string())]; + yield ['indexedBaz', Type::collection(Type::object(Collection::class), Type::object(DoctrineRelation::class), Type::int())]; + yield ['simpleArray', Type::list(Type::string())]; + yield ['customFoo', null]; + yield ['notMapped', null]; + yield ['indexedByDt', Type::collection(Type::object(Collection::class), Type::object(DoctrineRelation::class), Type::object())]; + yield ['indexedByCustomType', null]; + yield ['indexedBuz', Type::collection(Type::object(Collection::class), Type::object(DoctrineRelation::class), Type::string())]; + yield ['dummyGeneratedValueList', Type::collection(Type::object(Collection::class), Type::object(DoctrineRelation::class), Type::int())]; + yield ['json', null]; + } + + public function testGetTypeCatchException() + { + $this->assertNull($this->createExtractor()->getType('Not\Exist', 'baz')); + } } diff --git a/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php b/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php index 5e29439368517..abe31d474dfab 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php @@ -25,9 +25,12 @@ use Symfony\Bridge\Doctrine\Tests\Fixtures\AssociationEntity2; use Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeIntIdEntity; use Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeObjectNoToStringIdEntity; +use Symfony\Bridge\Doctrine\Tests\Fixtures\CreateDoubleNameEntity; use Symfony\Bridge\Doctrine\Tests\Fixtures\DoubleNameEntity; use Symfony\Bridge\Doctrine\Tests\Fixtures\DoubleNullableNameEntity; +use Symfony\Bridge\Doctrine\Tests\Fixtures\Dto; use Symfony\Bridge\Doctrine\Tests\Fixtures\Employee; +use Symfony\Bridge\Doctrine\Tests\Fixtures\HireAnEmployee; use Symfony\Bridge\Doctrine\Tests\Fixtures\Person; use Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdEntity; use Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdNoToStringEntity; @@ -35,6 +38,9 @@ use Symfony\Bridge\Doctrine\Tests\Fixtures\SingleStringIdEntity; use Symfony\Bridge\Doctrine\Tests\Fixtures\Type\StringWrapper; use Symfony\Bridge\Doctrine\Tests\Fixtures\Type\StringWrapperType; +use Symfony\Bridge\Doctrine\Tests\Fixtures\UpdateCompositeIntIdEntity; +use Symfony\Bridge\Doctrine\Tests\Fixtures\UpdateCompositeObjectNoToStringIdEntity; +use Symfony\Bridge\Doctrine\Tests\Fixtures\UpdateEmployeeProfile; use Symfony\Bridge\Doctrine\Tests\TestRepositoryFactory; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntityValidator; @@ -292,7 +298,7 @@ public function testAllConfiguredFieldsAreCheckedOfBeingMappedByDoctrineWithIgno { $entity1 = new SingleIntIdEntity(1, null); - $this->expectException(\Symfony\Component\Validator\Exception\ConstraintDefinitionException::class); + $this->expectException(ConstraintDefinitionException::class); $this->validator->validate($entity1, $constraint); } @@ -943,4 +949,243 @@ public function rewind(): void }], ]; } + + public function testValidateDTOUniqueness() + { + $constraint = new UniqueEntity([ + 'message' => 'myMessage', + 'fields' => ['name'], + 'em' => self::EM_NAME, + 'entityClass' => Person::class, + ]); + + $entity = new Person(1, 'Foo'); + $dto = new HireAnEmployee('Foo'); + + $this->validator->validate($entity, $constraint); + + $this->assertNoViolation(); + + $this->em->persist($entity); + $this->em->flush(); + + $this->validator->validate($entity, $constraint); + + $this->assertNoViolation(); + + $this->validator->validate($dto, $constraint); + + $this->buildViolation('myMessage') + ->atPath('property.path.name') + ->setInvalidValue('Foo') + ->setCode(UniqueEntity::NOT_UNIQUE_ERROR) + ->setCause([$entity]) + ->setParameters(['{{ value }}' => '"Foo"']) + ->assertRaised(); + } + + public function testValidateMappingOfFieldNames() + { + $constraint = new UniqueEntity([ + 'message' => 'myMessage', + 'fields' => ['primaryName' => 'name', 'secondaryName' => 'name2'], + 'em' => self::EM_NAME, + 'entityClass' => DoubleNameEntity::class, + ]); + + $entity = new DoubleNameEntity(1, 'Foo', 'Bar'); + $dto = new CreateDoubleNameEntity('Foo', 'Bar'); + + $this->em->persist($entity); + $this->em->flush(); + + $this->validator->validate($dto, $constraint); + + $this->buildViolation('myMessage') + ->atPath('property.path.name') + ->setParameter('{{ value }}', '"Foo"') + ->setInvalidValue('Foo') + ->setCause([$entity]) + ->setCode(UniqueEntity::NOT_UNIQUE_ERROR) + ->assertRaised(); + } + + public function testInvalidateDTOFieldName() + { + $this->expectException(ConstraintDefinitionException::class); + $this->expectExceptionMessage('The field "primaryName" is not a property of class "Symfony\Bridge\Doctrine\Tests\Fixtures\HireAnEmployee".'); + $constraint = new UniqueEntity([ + 'message' => 'myMessage', + 'fields' => ['primaryName' => 'name'], + 'em' => self::EM_NAME, + 'entityClass' => SingleStringIdEntity::class, + ]); + + $dto = new HireAnEmployee('Foo'); + $this->validator->validate($dto, $constraint); + } + + public function testInvalidateEntityFieldName() + { + $this->expectException(ConstraintDefinitionException::class); + $this->expectExceptionMessage('The field "name2" is not mapped by Doctrine, so it cannot be validated for uniqueness.'); + $constraint = new UniqueEntity([ + 'message' => 'myMessage', + 'fields' => ['name2'], + 'em' => self::EM_NAME, + 'entityClass' => SingleStringIdEntity::class, + ]); + + $dto = new HireAnEmployee('Foo'); + $this->validator->validate($dto, $constraint); + } + + public function testValidateDTOUniquenessWhenUpdatingEntity() + { + $constraint = new UniqueEntity([ + 'message' => 'myMessage', + 'fields' => ['name'], + 'em' => self::EM_NAME, + 'entityClass' => Person::class, + 'identifierFieldNames' => ['id'], + ]); + + $entity1 = new Person(1, 'Foo'); + $entity2 = new Person(2, 'Bar'); + + $this->em->persist($entity1); + $this->em->persist($entity2); + $this->em->flush(); + + $dto = new UpdateEmployeeProfile(2, 'Foo'); + + $this->validator->validate($dto, $constraint); + + $this->buildViolation('myMessage') + ->atPath('property.path.name') + ->setInvalidValue('Foo') + ->setCode(UniqueEntity::NOT_UNIQUE_ERROR) + ->setCause([$entity1]) + ->setParameters(['{{ value }}' => '"Foo"']) + ->assertRaised(); + } + + public function testValidateDTOUniquenessWhenUpdatingEntityWithTheSameValue() + { + $constraint = new UniqueEntity([ + 'message' => 'myMessage', + 'fields' => ['name'], + 'em' => self::EM_NAME, + 'entityClass' => CompositeIntIdEntity::class, + 'identifierFieldNames' => ['id1', 'id2'], + ]); + + $entity = new CompositeIntIdEntity(1, 2, 'Foo'); + + $this->em->persist($entity); + $this->em->flush(); + + $dto = new UpdateCompositeIntIdEntity(1, 2, 'Foo'); + + $this->validator->validate($dto, $constraint); + + $this->assertNoViolation(); + } + + public function testValidateIdentifierMappingOfFieldNames() + { + $constraint = new UniqueEntity([ + 'message' => 'myMessage', + 'fields' => ['object1' => 'objectOne', 'object2' => 'objectTwo'], + 'em' => self::EM_NAME, + 'entityClass' => CompositeObjectNoToStringIdEntity::class, + 'identifierFieldNames' => ['object1' => 'objectOne', 'object2' => 'objectTwo'], + ]); + + $objectOne = new SingleIntIdNoToStringEntity(1, 'foo'); + $objectTwo = new SingleIntIdNoToStringEntity(2, 'bar'); + + $this->em->persist($objectOne); + $this->em->persist($objectTwo); + $this->em->flush(); + + $entity = new CompositeObjectNoToStringIdEntity($objectOne, $objectTwo); + + $this->em->persist($entity); + $this->em->flush(); + + $dto = new UpdateCompositeObjectNoToStringIdEntity($objectOne, $objectTwo, 'Foo'); + + $this->validator->validate($dto, $constraint); + + $this->assertNoViolation(); + } + + public function testInvalidateMissingIdentifierFieldName() + { + $this->expectException(ConstraintDefinitionException::class); + $this->expectExceptionMessage('The "Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeObjectNoToStringIdEntity" entity identifier field names should be "objectOne, objectTwo", not "objectTwo".'); + $constraint = new UniqueEntity([ + 'message' => 'myMessage', + 'fields' => ['object1' => 'objectOne', 'object2' => 'objectTwo'], + 'em' => self::EM_NAME, + 'entityClass' => CompositeObjectNoToStringIdEntity::class, + 'identifierFieldNames' => ['object2' => 'objectTwo'], + ]); + + $objectOne = new SingleIntIdNoToStringEntity(1, 'foo'); + $objectTwo = new SingleIntIdNoToStringEntity(2, 'bar'); + + $this->em->persist($objectOne); + $this->em->persist($objectTwo); + $this->em->flush(); + + $entity = new CompositeObjectNoToStringIdEntity($objectOne, $objectTwo); + + $this->em->persist($entity); + $this->em->flush(); + + $dto = new UpdateCompositeObjectNoToStringIdEntity($objectOne, $objectTwo, 'Foo'); + $this->validator->validate($dto, $constraint); + } + + public function testUninitializedValueThrowException() + { + $this->expectExceptionMessage('Typed property Symfony\Bridge\Doctrine\Tests\Fixtures\Dto::$foo must not be accessed before initialization'); + $constraint = new UniqueEntity([ + 'message' => 'myMessage', + 'fields' => ['foo' => 'name'], + 'em' => self::EM_NAME, + 'entityClass' => DoubleNameEntity::class, + ]); + + $entity = new DoubleNameEntity(1, 'Foo', 'Bar'); + $dto = new Dto(); + + $this->em->persist($entity); + $this->em->flush(); + + $this->validator->validate($dto, $constraint); + } + + public function testEntityManagerNullObjectWhenDTO() + { + $this->expectException(ConstraintDefinitionException::class); + $this->expectExceptionMessage('Unable to find the object manager associated with an entity of class "Symfony\Bridge\Doctrine\Tests\Fixtures\Person"'); + $constraint = new UniqueEntity([ + 'message' => 'myMessage', + 'fields' => ['name'], + 'entityClass' => Person::class, + // no "em" option set + ]); + + $this->em = null; + $this->registry = $this->createRegistryMock($this->em); + $this->validator = $this->createValidator(); + $this->validator->initialize($this->context); + + $dto = new HireAnEmployee('Foo'); + + $this->validator->validate($dto, $constraint); + } } diff --git a/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntity.php b/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntity.php index 2a565b39af644..5786d4c224fce 100644 --- a/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntity.php +++ b/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntity.php @@ -35,10 +35,16 @@ class UniqueEntity extends Constraint public array|string $fields = []; public ?string $errorPath = null; public bool|array|string $ignoreNull = true; + public array $identifierFieldNames = []; /** - * @param array|string $fields The combination of fields that must contain unique values or a set of options - * @param bool|array|string $ignoreNull The combination of fields that ignore null values + * @param array|string $fields The combination of fields that must contain unique values or a set of options + * @param bool|string[]|string $ignoreNull The combination of fields that ignore null values + * @param string|null $em The entity manager used to query for uniqueness instead of the manager of this class + * @param string|null $entityClass The entity class to enforce uniqueness on instead of the current class + * @param string|null $repositoryMethod The repository method to check uniqueness instead of findBy. The method will receive as its argument + * a fieldName => value associative array according to the fields option configuration + * @param string|null $errorPath Bind the constraint violation to this field instead of the first one in the fields option configuration */ public function __construct( array|string $fields, @@ -49,9 +55,10 @@ public function __construct( ?string $repositoryMethod = null, ?string $errorPath = null, bool|string|array|null $ignoreNull = null, + ?array $identifierFieldNames = null, ?array $groups = null, $payload = null, - array $options = [] + array $options = [], ) { if (\is_array($fields) && \is_string(key($fields))) { $options = array_merge($fields, $options); @@ -68,6 +75,7 @@ public function __construct( $this->repositoryMethod = $repositoryMethod ?? $this->repositoryMethod; $this->errorPath = $errorPath ?? $this->errorPath; $this->ignoreNull = $ignoreNull ?? $this->ignoreNull; + $this->identifierFieldNames = $identifierFieldNames ?? $this->identifierFieldNames; } public function getRequiredOptions(): array diff --git a/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php b/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php index 4c3216187cb41..5591170140d01 100644 --- a/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php +++ b/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php @@ -11,8 +11,10 @@ namespace Symfony\Bridge\Doctrine\Validator\Constraints; +use Doctrine\ORM\Mapping\MappingException as ORMMappingException; use Doctrine\Persistence\ManagerRegistry; use Doctrine\Persistence\Mapping\ClassMetadata; +use Doctrine\Persistence\Mapping\MappingException as PersistenceMappingException; use Doctrine\Persistence\ObjectManager; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; @@ -33,12 +35,10 @@ public function __construct( } /** - * @param object $entity - * * @throws UnexpectedTypeException * @throws ConstraintDefinitionException */ - public function validate(mixed $entity, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof UniqueEntity) { throw new UnexpectedTypeException($constraint, UniqueEntity::class); @@ -58,14 +58,16 @@ public function validate(mixed $entity, Constraint $constraint): void throw new ConstraintDefinitionException('At least one field has to be specified.'); } - if (null === $entity) { + if (null === $value) { return; } - if (!\is_object($entity)) { - throw new UnexpectedValueException($entity, 'object'); + if (!\is_object($value)) { + throw new UnexpectedValueException($value, 'object'); } + $entityClass = $constraint->entityClass ?? $value::class; + if ($constraint->em) { $em = $this->registry->getManager($constraint->em); @@ -73,25 +75,28 @@ public function validate(mixed $entity, Constraint $constraint): void throw new ConstraintDefinitionException(sprintf('Object manager "%s" does not exist.', $constraint->em)); } } else { - $em = $this->registry->getManagerForClass($entity::class); + $em = $this->registry->getManagerForClass($entityClass); if (!$em) { - throw new ConstraintDefinitionException(sprintf('Unable to find the object manager associated with an entity of class "%s".', get_debug_type($entity))); + throw new ConstraintDefinitionException(sprintf('Unable to find the object manager associated with an entity of class "%s".', $entityClass)); } } - $class = $em->getClassMetadata($entity::class); + try { + $em->getRepository($value::class); + $isValueEntity = true; + } catch (ORMMappingException|PersistenceMappingException) { + $isValueEntity = false; + } + + $class = $em->getClassMetadata($entityClass); $criteria = []; $hasIgnorableNullValue = false; - foreach ($fields as $fieldName) { - if (!$class->hasField($fieldName) && !$class->hasAssociation($fieldName)) { - throw new ConstraintDefinitionException(sprintf('The field "%s" is not mapped by Doctrine, so it cannot be validated for uniqueness.', $fieldName)); - } - - $fieldValue = $class->reflFields[$fieldName]->getValue($entity); + $fieldValues = $this->getFieldValues($value, $class, $fields, $isValueEntity); + foreach ($fieldValues as $fieldName => $fieldValue) { if (null === $fieldValue && $this->ignoreNullForField($constraint, $fieldName)) { $hasIgnorableNullValue = true; @@ -116,7 +121,7 @@ public function validate(mixed $entity, Constraint $constraint): void // skip validation if there are no criteria (this can happen when the // "ignoreNull" option is enabled and fields to be checked are null - if (empty($criteria)) { + if (!$criteria) { return; } @@ -128,11 +133,12 @@ public function validate(mixed $entity, Constraint $constraint): void $repository = $em->getRepository($constraint->entityClass); $supportedClass = $repository->getClassName(); - if (!$entity instanceof $supportedClass) { + if ($isValueEntity && !$value instanceof $supportedClass) { + $class = $em->getClassMetadata($value::class); throw new ConstraintDefinitionException(sprintf('The "%s" entity repository does not support the "%s" entity. The entity should be an instance of or extend "%s".', $constraint->entityClass, $class->getName(), $supportedClass)); } } else { - $repository = $em->getRepository($entity::class); + $repository = $em->getRepository($value::class); } $arguments = [$criteria]; @@ -173,12 +179,36 @@ public function validate(mixed $entity, Constraint $constraint): void * which is the same as the entity being validated, the criteria is * unique. */ - if (!$result || (1 === \count($result) && current($result) === $entity)) { + if (!$result || (1 === \count($result) && current($result) === $value)) { return; } - $errorPath = $constraint->errorPath ?? $fields[0]; - $invalidValue = $criteria[$errorPath] ?? $criteria[$fields[0]]; + /* If a single entity matched the query criteria, which is the same as + * the entity being updated by validated object, the criteria is unique. + */ + if (!$isValueEntity && !empty($constraint->identifierFieldNames) && 1 === \count($result)) { + $fieldValues = $this->getFieldValues($value, $class, $constraint->identifierFieldNames); + if (array_values($class->getIdentifierFieldNames()) != array_values($constraint->identifierFieldNames)) { + throw new ConstraintDefinitionException(sprintf('The "%s" entity identifier field names should be "%s", not "%s".', $entityClass, implode(', ', $class->getIdentifierFieldNames()), implode(', ', $constraint->identifierFieldNames))); + } + + $entityMatched = true; + + foreach ($constraint->identifierFieldNames as $identifierFieldName) { + $propertyValue = $this->getPropertyValue($entityClass, $identifierFieldName, current($result)); + if ($fieldValues[$identifierFieldName] !== $propertyValue) { + $entityMatched = false; + break; + } + } + + if ($entityMatched) { + return; + } + } + + $errorPath = $constraint->errorPath ?? current($fields); + $invalidValue = $criteria[$errorPath] ?? $criteria[current($fields)]; $this->context->buildViolation($constraint->message) ->atPath($errorPath) @@ -209,11 +239,11 @@ private function formatWithIdentifiers(ObjectManager $em, ClassMetadata $class, } if ($class->getName() !== $idClass = $value::class) { - // non unique value might be a composite PK that consists of other entity objects + // non-unique value might be a composite PK that consists of other entity objects if ($em->getMetadataFactory()->hasMetadataFor($idClass)) { $identifiers = $em->getClassMetadata($idClass)->getIdentifierValues($value); } else { - // this case might happen if the non unique column has a custom doctrine type and its value is an object + // this case might happen if the non-unique column has a custom doctrine type and its value is an object // in which case we cannot get any identifiers for it $identifiers = []; } @@ -237,4 +267,36 @@ private function formatWithIdentifiers(ObjectManager $em, ClassMetadata $class, return sprintf('object("%s") identified by (%s)', $idClass, implode(', ', $identifiers)); } + + private function getFieldValues(mixed $object, ClassMetadata $class, array $fields, bool $isValueEntity = false): array + { + if (!$isValueEntity) { + $reflectionObject = new \ReflectionObject($object); + } + + $fieldValues = []; + $objectClass = $object::class; + + foreach ($fields as $objectFieldName => $entityFieldName) { + if (!$class->hasField($entityFieldName) && !$class->hasAssociation($entityFieldName)) { + throw new ConstraintDefinitionException(sprintf('The field "%s" is not mapped by Doctrine, so it cannot be validated for uniqueness.', $entityFieldName)); + } + + $fieldName = \is_int($objectFieldName) ? $entityFieldName : $objectFieldName; + if (!$isValueEntity && !$reflectionObject->hasProperty($fieldName)) { + throw new ConstraintDefinitionException(sprintf('The field "%s" is not a property of class "%s".', $fieldName, $objectClass)); + } + + $fieldValues[$entityFieldName] = $this->getPropertyValue($objectClass, $fieldName, $object); + } + + return $fieldValues; + } + + private function getPropertyValue(string $class, string $name, mixed $object): mixed + { + $property = new \ReflectionProperty($class, $name); + + return $property->getValue($object); + } } diff --git a/src/Symfony/Bridge/Doctrine/Validator/DoctrineLoader.php b/src/Symfony/Bridge/Doctrine/Validator/DoctrineLoader.php index 15916dc596166..93413c4f8e6d8 100644 --- a/src/Symfony/Bridge/Doctrine/Validator/DoctrineLoader.php +++ b/src/Symfony/Bridge/Doctrine/Validator/DoctrineLoader.php @@ -17,7 +17,6 @@ use Doctrine\ORM\Mapping\MappingException as OrmMappingException; use Doctrine\Persistence\Mapping\MappingException; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; -use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\Mapping\AutoMappingStrategy; diff --git a/src/Symfony/Bridge/Doctrine/composer.json b/src/Symfony/Bridge/Doctrine/composer.json index be35a0a335994..00cc394d114be 100644 --- a/src/Symfony/Bridge/Doctrine/composer.json +++ b/src/Symfony/Bridge/Doctrine/composer.json @@ -19,6 +19,7 @@ "php": ">=8.2", "doctrine/event-manager": "^2", "doctrine/persistence": "^3.1", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^2.5|^3" @@ -38,6 +39,7 @@ "symfony/security-core": "^6.4|^7.0", "symfony/stopwatch": "^6.4|^7.0", "symfony/translation": "^6.4|^7.0", + "symfony/type-info": "^7.1", "symfony/uid": "^6.4|^7.0", "symfony/validator": "^6.4|^7.0", "symfony/var-dumper": "^6.4|^7.0", diff --git a/src/Symfony/Bridge/Doctrine/phpunit.xml.dist b/src/Symfony/Bridge/Doctrine/phpunit.xml.dist index f99086654ecab..0b1a67afd1249 100644 --- a/src/Symfony/Bridge/Doctrine/phpunit.xml.dist +++ b/src/Symfony/Bridge/Doctrine/phpunit.xml.dist @@ -33,7 +33,12 @@ - Symfony\Bridge\Doctrine\Middleware\Debug + + + Symfony\Bridge\Doctrine\Middleware\Debug + Symfony\Bridge\Doctrine\Middleware\IdleConnection + + diff --git a/src/Symfony/Bridge/Monolog/Formatter/ConsoleFormatter.php b/src/Symfony/Bridge/Monolog/Formatter/ConsoleFormatter.php index b8f797e34d1ed..bc08363b6b414 100644 --- a/src/Symfony/Bridge/Monolog/Formatter/ConsoleFormatter.php +++ b/src/Symfony/Bridge/Monolog/Formatter/ConsoleFormatter.php @@ -100,14 +100,14 @@ public function format(LogRecord $record): mixed { $record = $this->replacePlaceHolder($record); - if (!$this->options['ignore_empty_context_and_extra'] || !empty($record->context)) { + if (!$this->options['ignore_empty_context_and_extra'] || $record->context) { $context = $record->context; $context = ($this->options['multiline'] ? "\n" : ' ').$this->dumpData($context); } else { $context = ''; } - if (!$this->options['ignore_empty_context_and_extra'] || !empty($record->extra)) { + if (!$this->options['ignore_empty_context_and_extra'] || $record->extra) { $extra = $record->extra; $extra = ($this->options['multiline'] ? "\n" : ' ').$this->dumpData($extra); } else { diff --git a/src/Symfony/Bridge/Monolog/Handler/FingersCrossed/NotFoundActivationStrategy.php b/src/Symfony/Bridge/Monolog/Handler/FingersCrossed/NotFoundActivationStrategy.php index b2b78a8d56f28..8955d6f15de60 100644 --- a/src/Symfony/Bridge/Monolog/Handler/FingersCrossed/NotFoundActivationStrategy.php +++ b/src/Symfony/Bridge/Monolog/Handler/FingersCrossed/NotFoundActivationStrategy.php @@ -30,7 +30,7 @@ final class NotFoundActivationStrategy implements ActivationStrategyInterface public function __construct( private RequestStack $requestStack, array $excludedUrls, - private ActivationStrategyInterface $inner + private ActivationStrategyInterface $inner, ) { $this->exclude = '{('.implode('|', $excludedUrls).')}i'; } diff --git a/src/Symfony/Bridge/Monolog/Tests/Handler/ConsoleHandlerTest.php b/src/Symfony/Bridge/Monolog/Tests/Handler/ConsoleHandlerTest.php index ef70fb33261f6..6ff0e05f63e77 100644 --- a/src/Symfony/Bridge/Monolog/Tests/Handler/ConsoleHandlerTest.php +++ b/src/Symfony/Bridge/Monolog/Tests/Handler/ConsoleHandlerTest.php @@ -111,10 +111,7 @@ public function testVerbosityChanged() $output ->expects($this->exactly(2)) ->method('getVerbosity') - ->willReturnOnConsecutiveCalls( - OutputInterface::VERBOSITY_QUIET, - OutputInterface::VERBOSITY_DEBUG - ) + ->willReturn(OutputInterface::VERBOSITY_QUIET, OutputInterface::VERBOSITY_DEBUG) ; $handler = new ConsoleHandler($output); $this->assertFalse($handler->isHandling(RecordFactory::create(Level::Notice)), diff --git a/src/Symfony/Bridge/Monolog/Tests/Handler/MailerHandlerTest.php b/src/Symfony/Bridge/Monolog/Tests/Handler/MailerHandlerTest.php index 02b3cd43aaf33..2540cd5ba8e77 100644 --- a/src/Symfony/Bridge/Monolog/Tests/Handler/MailerHandlerTest.php +++ b/src/Symfony/Bridge/Monolog/Tests/Handler/MailerHandlerTest.php @@ -14,7 +14,6 @@ use Monolog\Formatter\HtmlFormatter; use Monolog\Formatter\LineFormatter; use Monolog\Level; -use Monolog\Logger; use Monolog\LogRecord; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; diff --git a/src/Symfony/Bridge/Monolog/Tests/RecordFactory.php b/src/Symfony/Bridge/Monolog/Tests/RecordFactory.php index b9bd1f6675d55..268a10bade1d3 100644 --- a/src/Symfony/Bridge/Monolog/Tests/RecordFactory.php +++ b/src/Symfony/Bridge/Monolog/Tests/RecordFactory.php @@ -12,8 +12,8 @@ namespace Symfony\Bridge\Monolog\Tests; use Monolog\Level; -use Monolog\LogRecord; use Monolog\Logger; +use Monolog\LogRecord; class RecordFactory { diff --git a/src/Symfony/Bridge/PhpUnit/Tests/CoverageListenerTest.php b/src/Symfony/Bridge/PhpUnit/Tests/CoverageListenerTest.php index 19408df6d2dfe..22f3565fab44c 100644 --- a/src/Symfony/Bridge/PhpUnit/Tests/CoverageListenerTest.php +++ b/src/Symfony/Bridge/PhpUnit/Tests/CoverageListenerTest.php @@ -17,29 +17,17 @@ class CoverageListenerTest extends TestCase { public function test() { - if ('\\' === \DIRECTORY_SEPARATOR) { - $this->markTestSkipped('This test cannot be run on Windows.'); - } - - exec('type phpdbg 2> /dev/null', $output, $returnCode); - - if (0 === $returnCode) { - $php = 'phpdbg -qrr'; - } else { - exec('php --ri xdebug -d zend_extension=xdebug.so 2> /dev/null', $output, $returnCode); - if (0 !== $returnCode) { - $this->markTestSkipped('Xdebug is required to run this test.'); - } - $php = 'php -d zend_extension=xdebug.so'; - } - $dir = __DIR__.'/../Tests/Fixtures/coverage'; $phpunit = $_SERVER['argv'][0]; + $php = $this->findCoverageDriver(); + + $output = ''; exec("$php $phpunit -c $dir/phpunit-without-listener.xml.dist $dir/tests/ --coverage-text --colors=never 2> /dev/null", $output); $output = implode("\n", $output); $this->assertMatchesRegularExpression('/FooCov\n\s*Methods:\s+100.00%[^\n]+Lines:\s+100.00%/', $output); + $output = ''; exec("$php $phpunit -c $dir/phpunit-with-listener.xml.dist $dir/tests/ --coverage-text --colors=never 2> /dev/null", $output); $output = implode("\n", $output); @@ -54,4 +42,28 @@ public function test() $this->assertStringNotContainsString("CoversDefaultClassTest::test\nCould not find the tested class.", $output); $this->assertStringNotContainsString("CoversNothingTest::test\nCould not find the tested class.", $output); } + + private function findCoverageDriver(): string + { + if ('\\' === \DIRECTORY_SEPARATOR) { + $this->markTestSkipped('This test cannot be run on Windows.'); + } + + exec('php --ri xdebug -d zend_extension=xdebug 2> /dev/null', $output, $returnCode); + if (0 === $returnCode) { + return 'php -d zend_extension=xdebug'; + } + + exec('php --ri pcov -d zend_extension=pcov 2> /dev/null', $output, $returnCode); + if (0 === $returnCode) { + return 'php -d zend_extension=pcov'; + } + + exec('type phpdbg 2> /dev/null', $output, $returnCode); + if (0 === $returnCode) { + return 'phpdbg -qrr'; + } + + $this->markTestSkipped('Xdebug or pvoc is required to run this test.'); + } } diff --git a/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/phpunit-with-listener.xml.dist b/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/phpunit-with-listener.xml.dist index 797407e19e5b7..1dbca04bec6ee 100644 --- a/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/phpunit-with-listener.xml.dist +++ b/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/phpunit-with-listener.xml.dist @@ -1,30 +1,26 @@ - - + + + src + + tests - - - - src - - - - + true diff --git a/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/phpunit-without-listener.xml.dist b/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/phpunit-without-listener.xml.dist index 4af525d043371..40680ab215174 100644 --- a/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/phpunit-without-listener.xml.dist +++ b/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/phpunit-without-listener.xml.dist @@ -1,23 +1,20 @@ - - + + + src + + tests - - - - src - - diff --git a/src/Symfony/Bridge/PsrHttpMessage/Factory/PsrHttpFactory.php b/src/Symfony/Bridge/PsrHttpMessage/Factory/PsrHttpFactory.php index 78a36ded1d709..a09ed2c5aa86b 100644 --- a/src/Symfony/Bridge/PsrHttpMessage/Factory/PsrHttpFactory.php +++ b/src/Symfony/Bridge/PsrHttpMessage/Factory/PsrHttpFactory.php @@ -178,7 +178,7 @@ public function createResponse(Response $symfonyResponse): ResponseInterface $headers = $symfonyResponse->headers->all(); $cookies = $symfonyResponse->headers->getCookies(); - if (!empty($cookies)) { + if ($cookies) { $headers['Set-Cookie'] = []; foreach ($cookies as $cookie) { diff --git a/src/Symfony/Bridge/PsrHttpMessage/Tests/Fixtures/Uri.php b/src/Symfony/Bridge/PsrHttpMessage/Tests/Fixtures/Uri.php index ded92bfc52b8d..66431492b2b35 100644 --- a/src/Symfony/Bridge/PsrHttpMessage/Tests/Fixtures/Uri.php +++ b/src/Symfony/Bridge/PsrHttpMessage/Tests/Fixtures/Uri.php @@ -47,13 +47,13 @@ public function getScheme(): string public function getAuthority(): string { - if (empty($this->host)) { + if (!$this->host) { return ''; } $authority = $this->host; - if (!empty($this->userInfo)) { + if ($this->userInfo) { $authority = $this->userInfo.'@'.$authority; } diff --git a/src/Symfony/Bridge/Twig/Attribute/Template.php b/src/Symfony/Bridge/Twig/Attribute/Template.php index f094f42a4a6e2..e265e23951f6a 100644 --- a/src/Symfony/Bridge/Twig/Attribute/Template.php +++ b/src/Symfony/Bridge/Twig/Attribute/Template.php @@ -11,23 +11,20 @@ namespace Symfony\Bridge\Twig\Attribute; +/** + * Define the template to render in the controller. + */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_FUNCTION)] class Template { + /** + * @param string $template The name of the template to render + * @param string[]|null $vars The controller method arguments to pass to the template + * @param bool $stream Enables streaming the template + */ public function __construct( - /** - * The name of the template to render. - */ public string $template, - - /** - * The controller method arguments to pass to the template. - */ public ?array $vars = null, - - /** - * Enables streaming the template. - */ public bool $stream = false, ) { } diff --git a/src/Symfony/Bridge/Twig/CHANGELOG.md b/src/Symfony/Bridge/Twig/CHANGELOG.md index cc2d334538c34..df8f28f01a6f0 100644 --- a/src/Symfony/Bridge/Twig/CHANGELOG.md +++ b/src/Symfony/Bridge/Twig/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.1 +--- + + * Add `emojify` Twig filter + 7.0 --- diff --git a/src/Symfony/Bridge/Twig/Command/DebugCommand.php b/src/Symfony/Bridge/Twig/Command/DebugCommand.php index e8c873495af8f..21ede17aabea5 100644 --- a/src/Symfony/Bridge/Twig/Command/DebugCommand.php +++ b/src/Symfony/Bridge/Twig/Command/DebugCommand.php @@ -36,27 +36,19 @@ #[AsCommand(name: 'debug:twig', description: 'Show a list of twig functions, filters, globals and tests')] class DebugCommand extends Command { - private Environment $twig; - private ?string $projectDir; - private array $bundlesMetadata; - private ?string $twigDefaultPath; - /** * @var FilesystemLoader[] */ private array $filesystemLoaders; - private ?FileLinkFormatter $fileLinkFormatter; - - public function __construct(Environment $twig, ?string $projectDir = null, array $bundlesMetadata = [], ?string $twigDefaultPath = null, ?FileLinkFormatter $fileLinkFormatter = null) - { + public function __construct( + private Environment $twig, + private ?string $projectDir = null, + private array $bundlesMetadata = [], + private ?string $twigDefaultPath = null, + private ?FileLinkFormatter $fileLinkFormatter = null, + ) { parent::__construct(); - - $this->twig = $twig; - $this->projectDir = $projectDir; - $this->bundlesMetadata = $bundlesMetadata; - $this->twigDefaultPath = $twigDefaultPath; - $this->fileLinkFormatter = $fileLinkFormatter; } protected function configure(): void @@ -587,11 +579,7 @@ private function getFilesystemLoaders(): array private function getFileLink(string $absolutePath): string { - if (null === $this->fileLinkFormatter) { - return ''; - } - - return (string) $this->fileLinkFormatter->format($absolutePath, 1); + return (string) $this->fileLinkFormatter?->format($absolutePath, 1); } private function getAvailableFormatOptions(): array diff --git a/src/Symfony/Bridge/Twig/Command/LintCommand.php b/src/Symfony/Bridge/Twig/Command/LintCommand.php index d570d32bbc043..14c00ba112659 100644 --- a/src/Symfony/Bridge/Twig/Command/LintCommand.php +++ b/src/Symfony/Bridge/Twig/Command/LintCommand.php @@ -39,6 +39,7 @@ #[AsCommand(name: 'lint:twig', description: 'Lint a Twig template and outputs encountered errors')] class LintCommand extends Command { + private array $excludes; private string $format; public function __construct( @@ -54,6 +55,7 @@ protected function configure(): void ->addOption('format', null, InputOption::VALUE_REQUIRED, sprintf('The output format ("%s")', implode('", "', $this->getAvailableFormatOptions()))) ->addOption('show-deprecations', null, InputOption::VALUE_NONE, 'Show deprecations as errors') ->addArgument('filename', InputArgument::IS_ARRAY, 'A file, a directory or "-" for reading from STDIN') + ->addOption('excludes', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Excluded directories', []) ->setHelp(<<<'EOF' The %command.name% command lints a template and outputs to STDOUT the first encountered syntax error. @@ -81,6 +83,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $io = new SymfonyStyle($input, $output); $filenames = $input->getArgument('filename'); $showDeprecations = $input->getOption('show-deprecations'); + $this->excludes = $input->getOption('excludes'); $this->format = $input->getOption('format') ?? (GithubActionReporter::isGithubActionEnvironment() ? 'github' : 'txt'); if (['-'] === $filenames) { @@ -145,7 +148,7 @@ protected function findFiles(string $filename): iterable if (is_file($filename)) { return [$filename]; } elseif (is_dir($filename)) { - return Finder::create()->files()->in($filename)->name($this->namePatterns); + return Finder::create()->files()->in($filename)->name($this->namePatterns)->exclude($this->excludes); } throw new RuntimeException(sprintf('File or directory "%s" is not readable.', $filename)); diff --git a/src/Symfony/Bridge/Twig/DataCollector/TwigDataCollector.php b/src/Symfony/Bridge/Twig/DataCollector/TwigDataCollector.php index a5786d2f82f6c..f63d85a615a2f 100644 --- a/src/Symfony/Bridge/Twig/DataCollector/TwigDataCollector.php +++ b/src/Symfony/Bridge/Twig/DataCollector/TwigDataCollector.php @@ -28,14 +28,12 @@ */ class TwigDataCollector extends DataCollector implements LateDataCollectorInterface { - private Profile $profile; - private ?Environment $twig; private array $computed; - public function __construct(Profile $profile, ?Environment $twig = null) - { - $this->profile = $profile; - $this->twig = $twig; + public function __construct( + private Profile $profile, + private ?Environment $twig = null, + ) { } public function collect(Request $request, Response $response, ?\Throwable $exception = null): void diff --git a/src/Symfony/Bridge/Twig/ErrorRenderer/TwigErrorRenderer.php b/src/Symfony/Bridge/Twig/ErrorRenderer/TwigErrorRenderer.php index 50d8b44d2a742..0ea9b9aad47fc 100644 --- a/src/Symfony/Bridge/Twig/ErrorRenderer/TwigErrorRenderer.php +++ b/src/Symfony/Bridge/Twig/ErrorRenderer/TwigErrorRenderer.php @@ -25,16 +25,17 @@ */ class TwigErrorRenderer implements ErrorRendererInterface { - private Environment $twig; private HtmlErrorRenderer $fallbackErrorRenderer; private \Closure|bool $debug; /** * @param bool|callable $debug The debugging mode as a boolean or a callable that should return it */ - public function __construct(Environment $twig, ?HtmlErrorRenderer $fallbackErrorRenderer = null, bool|callable $debug = false) - { - $this->twig = $twig; + public function __construct( + private Environment $twig, + ?HtmlErrorRenderer $fallbackErrorRenderer = null, + bool|callable $debug = false, + ) { $this->fallbackErrorRenderer = $fallbackErrorRenderer ?? new HtmlErrorRenderer(); $this->debug = \is_bool($debug) ? $debug : $debug(...); } diff --git a/src/Symfony/Bridge/Twig/Extension/AssetExtension.php b/src/Symfony/Bridge/Twig/Extension/AssetExtension.php index 7a7aba0d69148..ce9fee7251d8a 100644 --- a/src/Symfony/Bridge/Twig/Extension/AssetExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/AssetExtension.php @@ -22,11 +22,9 @@ */ final class AssetExtension extends AbstractExtension { - private Packages $packages; - - public function __construct(Packages $packages) - { - $this->packages = $packages; + public function __construct( + private Packages $packages, + ) { } public function getFunctions(): array diff --git a/src/Symfony/Bridge/Twig/Extension/CsrfRuntime.php b/src/Symfony/Bridge/Twig/Extension/CsrfRuntime.php index 216d9c92f10ed..29267116eee97 100644 --- a/src/Symfony/Bridge/Twig/Extension/CsrfRuntime.php +++ b/src/Symfony/Bridge/Twig/Extension/CsrfRuntime.php @@ -19,11 +19,9 @@ */ final class CsrfRuntime { - private CsrfTokenManagerInterface $csrfTokenManager; - - public function __construct(CsrfTokenManagerInterface $csrfTokenManager) - { - $this->csrfTokenManager = $csrfTokenManager; + public function __construct( + private CsrfTokenManagerInterface $csrfTokenManager, + ) { } public function getCsrfToken(string $tokenId): string diff --git a/src/Symfony/Bridge/Twig/Extension/DumpExtension.php b/src/Symfony/Bridge/Twig/Extension/DumpExtension.php index 1bf2beeed5d1c..a9006165ad096 100644 --- a/src/Symfony/Bridge/Twig/Extension/DumpExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/DumpExtension.php @@ -26,13 +26,10 @@ */ final class DumpExtension extends AbstractExtension { - private ClonerInterface $cloner; - private ?HtmlDumper $dumper; - - public function __construct(ClonerInterface $cloner, ?HtmlDumper $dumper = null) - { - $this->cloner = $cloner; - $this->dumper = $dumper; + public function __construct( + private ClonerInterface $cloner, + private ?HtmlDumper $dumper = null, + ) { } public function getFunctions(): array diff --git a/src/Symfony/Bridge/Twig/Extension/EmojiExtension.php b/src/Symfony/Bridge/Twig/Extension/EmojiExtension.php new file mode 100644 index 0000000000000..b98798dac014a --- /dev/null +++ b/src/Symfony/Bridge/Twig/Extension/EmojiExtension.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Twig\Extension; + +use Symfony\Component\Emoji\EmojiTransliterator; +use Twig\Extension\AbstractExtension; +use Twig\TwigFilter; + +/** + * @author Grégoire Pineau + */ +final class EmojiExtension extends AbstractExtension +{ + private static array $transliterators = []; + + public function __construct( + private readonly string $defaultCatalog = 'text', + ) { + if (!class_exists(EmojiTransliterator::class)) { + throw new \LogicException('You cannot use the "emojify" filter as the "Emoji" component is not installed. Try running "composer require symfony/emoji".'); + } + } + + public function getFilters(): array + { + return [ + new TwigFilter('emojify', $this->emojify(...)), + ]; + } + + /** + * Converts emoji short code (:wave:) to real emoji (👋) + */ + public function emojify(string $string, ?string $catalog = null): string + { + $catalog ??= $this->defaultCatalog; + + try { + $tr = self::$transliterators[$catalog] ??= EmojiTransliterator::create($catalog, EmojiTransliterator::REVERSE); + } catch (\IntlException $e) { + throw new \LogicException(sprintf('The emoji catalog "%s" is not available.', $catalog), previous: $e); + } + + return (string) $tr->transliterate($string); + } +} diff --git a/src/Symfony/Bridge/Twig/Extension/FormExtension.php b/src/Symfony/Bridge/Twig/Extension/FormExtension.php index 673f8199f9a2a..014154149fb2c 100644 --- a/src/Symfony/Bridge/Twig/Extension/FormExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/FormExtension.php @@ -33,11 +33,9 @@ */ final class FormExtension extends AbstractExtension { - private ?TranslatorInterface $translator; - - public function __construct(?TranslatorInterface $translator = null) - { - $this->translator = $translator; + public function __construct( + private ?TranslatorInterface $translator = null, + ) { } public function getTokenParsers(): array diff --git a/src/Symfony/Bridge/Twig/Extension/HttpFoundationExtension.php b/src/Symfony/Bridge/Twig/Extension/HttpFoundationExtension.php index 938d3ddabf256..e06f1b3976c4d 100644 --- a/src/Symfony/Bridge/Twig/Extension/HttpFoundationExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/HttpFoundationExtension.php @@ -23,11 +23,9 @@ */ final class HttpFoundationExtension extends AbstractExtension { - private UrlHelper $urlHelper; - - public function __construct(UrlHelper $urlHelper) - { - $this->urlHelper = $urlHelper; + public function __construct( + private UrlHelper $urlHelper, + ) { } public function getFunctions(): array diff --git a/src/Symfony/Bridge/Twig/Extension/HttpKernelRuntime.php b/src/Symfony/Bridge/Twig/Extension/HttpKernelRuntime.php index 5456de33d2b6a..0aefed8f94899 100644 --- a/src/Symfony/Bridge/Twig/Extension/HttpKernelRuntime.php +++ b/src/Symfony/Bridge/Twig/Extension/HttpKernelRuntime.php @@ -22,13 +22,10 @@ */ final class HttpKernelRuntime { - private FragmentHandler $handler; - private ?FragmentUriGeneratorInterface $fragmentUriGenerator; - - public function __construct(FragmentHandler $handler, ?FragmentUriGeneratorInterface $fragmentUriGenerator = null) - { - $this->handler = $handler; - $this->fragmentUriGenerator = $fragmentUriGenerator; + public function __construct( + private FragmentHandler $handler, + private ?FragmentUriGeneratorInterface $fragmentUriGenerator = null, + ) { } /** diff --git a/src/Symfony/Bridge/Twig/Extension/ImportMapRuntime.php b/src/Symfony/Bridge/Twig/Extension/ImportMapRuntime.php index 97632a2bc420c..902e0a42a9b19 100644 --- a/src/Symfony/Bridge/Twig/Extension/ImportMapRuntime.php +++ b/src/Symfony/Bridge/Twig/Extension/ImportMapRuntime.php @@ -18,8 +18,9 @@ */ class ImportMapRuntime { - public function __construct(private readonly ImportMapRenderer $importMapRenderer) - { + public function __construct( + private readonly ImportMapRenderer $importMapRenderer, + ) { } public function importmap(string|array $entryPoint = 'app', array $attributes = []): string diff --git a/src/Symfony/Bridge/Twig/Extension/LogoutUrlExtension.php b/src/Symfony/Bridge/Twig/Extension/LogoutUrlExtension.php index a576a6dd6b152..15089d3c1dc03 100644 --- a/src/Symfony/Bridge/Twig/Extension/LogoutUrlExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/LogoutUrlExtension.php @@ -22,11 +22,9 @@ */ final class LogoutUrlExtension extends AbstractExtension { - private LogoutUrlGenerator $generator; - - public function __construct(LogoutUrlGenerator $generator) - { - $this->generator = $generator; + public function __construct( + private LogoutUrlGenerator $generator, + ) { } public function getFunctions(): array diff --git a/src/Symfony/Bridge/Twig/Extension/ProfilerExtension.php b/src/Symfony/Bridge/Twig/Extension/ProfilerExtension.php index ab56f22a1efd6..2dbc4ec42aaaf 100644 --- a/src/Symfony/Bridge/Twig/Extension/ProfilerExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/ProfilerExtension.php @@ -21,18 +21,17 @@ */ final class ProfilerExtension extends BaseProfilerExtension { - private ?Stopwatch $stopwatch; - /** * @var \SplObjectStorage */ private \SplObjectStorage $events; - public function __construct(Profile $profile, ?Stopwatch $stopwatch = null) - { + public function __construct( + Profile $profile, + private ?Stopwatch $stopwatch = null, + ) { parent::__construct($profile); - $this->stopwatch = $stopwatch; $this->events = new \SplObjectStorage(); } diff --git a/src/Symfony/Bridge/Twig/Extension/RoutingExtension.php b/src/Symfony/Bridge/Twig/Extension/RoutingExtension.php index 5827640d5bd0d..eace52329e669 100644 --- a/src/Symfony/Bridge/Twig/Extension/RoutingExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/RoutingExtension.php @@ -25,11 +25,9 @@ */ final class RoutingExtension extends AbstractExtension { - private UrlGeneratorInterface $generator; - - public function __construct(UrlGeneratorInterface $generator) - { - $this->generator = $generator; + public function __construct( + private UrlGeneratorInterface $generator, + ) { } public function getFunctions(): array diff --git a/src/Symfony/Bridge/Twig/Extension/SecurityExtension.php b/src/Symfony/Bridge/Twig/Extension/SecurityExtension.php index c94912e35f683..863df15606735 100644 --- a/src/Symfony/Bridge/Twig/Extension/SecurityExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/SecurityExtension.php @@ -25,13 +25,10 @@ */ final class SecurityExtension extends AbstractExtension { - private ?AuthorizationCheckerInterface $securityChecker; - private ?ImpersonateUrlGenerator $impersonateUrlGenerator; - - public function __construct(?AuthorizationCheckerInterface $securityChecker = null, ?ImpersonateUrlGenerator $impersonateUrlGenerator = null) - { - $this->securityChecker = $securityChecker; - $this->impersonateUrlGenerator = $impersonateUrlGenerator; + public function __construct( + private ?AuthorizationCheckerInterface $securityChecker = null, + private ?ImpersonateUrlGenerator $impersonateUrlGenerator = null, + ) { } public function isGranted(mixed $role, mixed $object = null, ?string $field = null): bool diff --git a/src/Symfony/Bridge/Twig/Extension/SerializerRuntime.php b/src/Symfony/Bridge/Twig/Extension/SerializerRuntime.php index b48be3aae0163..227157335c6ee 100644 --- a/src/Symfony/Bridge/Twig/Extension/SerializerRuntime.php +++ b/src/Symfony/Bridge/Twig/Extension/SerializerRuntime.php @@ -19,11 +19,9 @@ */ final class SerializerRuntime implements RuntimeExtensionInterface { - private SerializerInterface $serializer; - - public function __construct(SerializerInterface $serializer) - { - $this->serializer = $serializer; + public function __construct( + private SerializerInterface $serializer, + ) { } public function serialize(mixed $data, string $format = 'json', array $context = []): string diff --git a/src/Symfony/Bridge/Twig/Extension/StopwatchExtension.php b/src/Symfony/Bridge/Twig/Extension/StopwatchExtension.php index 49df52cff7e58..ba56d1275baa5 100644 --- a/src/Symfony/Bridge/Twig/Extension/StopwatchExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/StopwatchExtension.php @@ -23,13 +23,10 @@ */ final class StopwatchExtension extends AbstractExtension { - private ?Stopwatch $stopwatch; - private bool $enabled; - - public function __construct(?Stopwatch $stopwatch = null, bool $enabled = true) - { - $this->stopwatch = $stopwatch; - $this->enabled = $enabled; + public function __construct( + private ?Stopwatch $stopwatch = null, + private bool $enabled = true, + ) { } public function getStopwatch(): Stopwatch diff --git a/src/Symfony/Bridge/Twig/Extension/TranslationExtension.php b/src/Symfony/Bridge/Twig/Extension/TranslationExtension.php index ba5758f3f1bfc..bf8b81bd61d90 100644 --- a/src/Symfony/Bridge/Twig/Extension/TranslationExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/TranslationExtension.php @@ -34,13 +34,10 @@ class_exists(TranslatorTrait::class); */ final class TranslationExtension extends AbstractExtension { - private ?TranslatorInterface $translator; - private ?TranslationNodeVisitor $translationNodeVisitor; - - public function __construct(?TranslatorInterface $translator = null, ?TranslationNodeVisitor $translationNodeVisitor = null) - { - $this->translator = $translator; - $this->translationNodeVisitor = $translationNodeVisitor; + public function __construct( + private ?TranslatorInterface $translator = null, + private ?TranslationNodeVisitor $translationNodeVisitor = null, + ) { } public function getTranslator(): TranslatorInterface diff --git a/src/Symfony/Bridge/Twig/Extension/WebLinkExtension.php b/src/Symfony/Bridge/Twig/Extension/WebLinkExtension.php index 11eca517c5d69..9eeb305aee36c 100644 --- a/src/Symfony/Bridge/Twig/Extension/WebLinkExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/WebLinkExtension.php @@ -24,11 +24,9 @@ */ final class WebLinkExtension extends AbstractExtension { - private RequestStack $requestStack; - - public function __construct(RequestStack $requestStack) - { - $this->requestStack = $requestStack; + public function __construct( + private RequestStack $requestStack, + ) { } public function getFunctions(): array diff --git a/src/Symfony/Bridge/Twig/Extension/WorkflowExtension.php b/src/Symfony/Bridge/Twig/Extension/WorkflowExtension.php index b50130ccbc5a9..0fcc9b3fd51f5 100644 --- a/src/Symfony/Bridge/Twig/Extension/WorkflowExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/WorkflowExtension.php @@ -25,11 +25,9 @@ */ final class WorkflowExtension extends AbstractExtension { - private Registry $workflowRegistry; - - public function __construct(Registry $workflowRegistry) - { - $this->workflowRegistry = $workflowRegistry; + public function __construct( + private Registry $workflowRegistry, + ) { } public function getFunctions(): array diff --git a/src/Symfony/Bridge/Twig/Form/TwigRendererEngine.php b/src/Symfony/Bridge/Twig/Form/TwigRendererEngine.php index d07e6e1c9dc9f..ff5568e021581 100644 --- a/src/Symfony/Bridge/Twig/Form/TwigRendererEngine.php +++ b/src/Symfony/Bridge/Twig/Form/TwigRendererEngine.php @@ -21,13 +21,13 @@ */ class TwigRendererEngine extends AbstractRendererEngine { - private Environment $environment; private Template $template; - public function __construct(array $defaultThemes, Environment $environment) - { + public function __construct( + array $defaultThemes, + private Environment $environment, + ) { parent::__construct($defaultThemes); - $this->environment = $environment; } public function renderBlock(FormView $view, mixed $resource, string $blockName, array $variables = []): string diff --git a/src/Symfony/Bridge/Twig/Mime/BodyRenderer.php b/src/Symfony/Bridge/Twig/Mime/BodyRenderer.php index d5b6d14c139a0..25d87353fd550 100644 --- a/src/Symfony/Bridge/Twig/Mime/BodyRenderer.php +++ b/src/Symfony/Bridge/Twig/Mime/BodyRenderer.php @@ -26,17 +26,15 @@ */ final class BodyRenderer implements BodyRendererInterface { - private Environment $twig; - private array $context; private HtmlToTextConverterInterface $converter; - private ?LocaleSwitcher $localeSwitcher = null; - public function __construct(Environment $twig, array $context = [], ?HtmlToTextConverterInterface $converter = null, ?LocaleSwitcher $localeSwitcher = null) - { - $this->twig = $twig; - $this->context = $context; + public function __construct( + private Environment $twig, + private array $context = [], + ?HtmlToTextConverterInterface $converter = null, + private ?LocaleSwitcher $localeSwitcher = null, + ) { $this->converter = $converter ?: (interface_exists(HtmlConverterInterface::class) ? new LeagueHtmlToMarkdownConverter() : new DefaultHtmlToTextConverter()); - $this->localeSwitcher = $localeSwitcher; } public function render(Message $message): void diff --git a/src/Symfony/Bridge/Twig/Mime/WrappedTemplatedEmail.php b/src/Symfony/Bridge/Twig/Mime/WrappedTemplatedEmail.php index e72335a5ececd..a327e94b3321e 100644 --- a/src/Symfony/Bridge/Twig/Mime/WrappedTemplatedEmail.php +++ b/src/Symfony/Bridge/Twig/Mime/WrappedTemplatedEmail.php @@ -23,13 +23,10 @@ */ final class WrappedTemplatedEmail { - private Environment $twig; - private TemplatedEmail $message; - - public function __construct(Environment $twig, TemplatedEmail $message) - { - $this->twig = $twig; - $this->message = $message; + public function __construct( + private Environment $twig, + private TemplatedEmail $message, + ) { } public function toName(): string diff --git a/src/Symfony/Bridge/Twig/Node/DumpNode.php b/src/Symfony/Bridge/Twig/Node/DumpNode.php index 9b736da23c44e..5c3ac6ca1b4f1 100644 --- a/src/Symfony/Bridge/Twig/Node/DumpNode.php +++ b/src/Symfony/Bridge/Twig/Node/DumpNode.php @@ -21,10 +21,12 @@ #[YieldReady] final class DumpNode extends Node { - private string $varPrefix; - - public function __construct(string $varPrefix, ?Node $values, int $lineno, ?string $tag = null) - { + public function __construct( + private string $varPrefix, + ?Node $values, + int $lineno, + ?string $tag = null, + ) { $nodes = []; if (null !== $values) { $nodes['values'] = $values; diff --git a/src/Symfony/Bridge/Twig/NodeVisitor/Scope.php b/src/Symfony/Bridge/Twig/NodeVisitor/Scope.php index 66904b09b5303..4914506fd15ee 100644 --- a/src/Symfony/Bridge/Twig/NodeVisitor/Scope.php +++ b/src/Symfony/Bridge/Twig/NodeVisitor/Scope.php @@ -16,13 +16,12 @@ */ class Scope { - private ?self $parent; private array $data = []; private bool $left = false; - public function __construct(?self $parent = null) - { - $this->parent = $parent; + public function __construct( + private ?self $parent = null, + ) { } /** diff --git a/src/Symfony/Bridge/Twig/NodeVisitor/TranslationDefaultDomainNodeVisitor.php b/src/Symfony/Bridge/Twig/NodeVisitor/TranslationDefaultDomainNodeVisitor.php index 3449751b1ff92..b071677d4ac59 100644 --- a/src/Symfony/Bridge/Twig/NodeVisitor/TranslationDefaultDomainNodeVisitor.php +++ b/src/Symfony/Bridge/Twig/NodeVisitor/TranslationDefaultDomainNodeVisitor.php @@ -114,6 +114,6 @@ private function isNamedArguments(Node $arguments): bool private function getVarName(): string { - return sprintf('__internal_%s', hash('sha256', uniqid(mt_rand(), true), false)); + return sprintf('__internal_%s', hash('xxh128', uniqid(mt_rand(), true))); } } diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig index 02628b5a14446..1e421d5f9f5a9 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig @@ -68,7 +68,11 @@ {% set render_preferred_choices = true %} {{- block('choice_widget_options') -}} {%- if choices|length > 0 and separator is not none -%} - + {%- if separator_html is not defined or separator_html is same as(false) -%} + + {% else %} + {{ separator|raw }} + {% endif %} {%- endif -%} {%- endif -%} {%- set options = choices -%} diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/foundation_5_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/foundation_5_layout.html.twig index 78dbe0d86bac5..23e463e6822f0 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/foundation_5_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/foundation_5_layout.html.twig @@ -163,7 +163,11 @@ {% set render_preferred_choices = true %} {{- block('choice_widget_options') -}} {% if choices|length > 0 and separator is not none -%} - + {%- if separator_html is not defined or separator_html is same as(false) -%} + + {% else %} + {{ separator|raw }} + {% endif %} {%- endif %} {%- endif -%} {% set options = choices -%} diff --git a/src/Symfony/Bridge/Twig/Tests/AppVariableTest.php b/src/Symfony/Bridge/Twig/Tests/AppVariableTest.php index beed252e96573..0367f7704b684 100644 --- a/src/Symfony/Bridge/Twig/Tests/AppVariableTest.php +++ b/src/Symfony/Bridge/Twig/Tests/AppVariableTest.php @@ -291,12 +291,15 @@ public function testGetCurrentRouteParametersWithRequestStackNotSet() $this->appVariable->getCurrent_route_parameters(); } - protected function setRequestStack($request) + protected function setRequestStack(?Request $request) { - $requestStackMock = $this->createMock(RequestStack::class); - $requestStackMock->method('getCurrentRequest')->willReturn($request); + $requestStack = new RequestStack(); - $this->appVariable->setRequestStack($requestStackMock); + if (null !== $request) { + $requestStack->push($request); + } + + $this->appVariable->setRequestStack($requestStack); } protected function setTokenStorage($user) diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/EmojiExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/EmojiExtensionTest.php new file mode 100644 index 0000000000000..492929a341e7d --- /dev/null +++ b/src/Symfony/Bridge/Twig/Tests/Extension/EmojiExtensionTest.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Twig\Tests\Extension; + +use PHPUnit\Framework\TestCase; +use Symfony\Bridge\Twig\Extension\EmojiExtension; + +/** + * @requires extension intl + */ +class EmojiExtensionTest extends TestCase +{ + /** + * @testWith ["🅰️", ":a:"] + * ["🅰️", ":a:", "slack"] + * ["🅰", ":a:", "github"] + */ + public function testEmojify(string $expected, string $string, ?string $catalog = null) + { + $extension = new EmojiExtension(); + $this->assertSame($expected, $extension->emojify($string, $catalog)); + } +} diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php index 5bce112d19d0c..c214bcd8b9b07 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php @@ -48,8 +48,7 @@ public function testRenderFragment() public function testUnknownFragmentRenderer() { - $context = $this->createMock(RequestStack::class); - $renderer = new FragmentHandler($context); + $renderer = new FragmentHandler(new RequestStack()); $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('The "inline" renderer does not exist.'); @@ -90,9 +89,9 @@ protected function getFragmentHandler($return) $strategy->expects($this->once())->method('getName')->willReturn('inline'); $strategy->expects($this->once())->method('render')->will($return); - $context = $this->createMock(RequestStack::class); + $context = new RequestStack(); - $context->expects($this->any())->method('getCurrentRequest')->willReturn(Request::create('/')); + $context->push(Request::create('/')); return new FragmentHandler($context, [$strategy], false); } diff --git a/src/Symfony/Bridge/Twig/TokenParser/StopwatchTokenParser.php b/src/Symfony/Bridge/Twig/TokenParser/StopwatchTokenParser.php index b332485d02577..810e7c27232cc 100644 --- a/src/Symfony/Bridge/Twig/TokenParser/StopwatchTokenParser.php +++ b/src/Symfony/Bridge/Twig/TokenParser/StopwatchTokenParser.php @@ -24,11 +24,9 @@ */ final class StopwatchTokenParser extends AbstractTokenParser { - private bool $stopwatchIsAvailable; - - public function __construct(bool $stopwatchIsAvailable) - { - $this->stopwatchIsAvailable = $stopwatchIsAvailable; + public function __construct( + private bool $stopwatchIsAvailable, + ) { } public function parse(Token $token): Node diff --git a/src/Symfony/Bridge/Twig/Translation/TwigExtractor.php b/src/Symfony/Bridge/Twig/Translation/TwigExtractor.php index 8a911ea03cfe9..a4b4bbe50ddab 100644 --- a/src/Symfony/Bridge/Twig/Translation/TwigExtractor.php +++ b/src/Symfony/Bridge/Twig/Translation/TwigExtractor.php @@ -38,11 +38,9 @@ class TwigExtractor extends AbstractFileExtractor implements ExtractorInterface */ private string $prefix = ''; - private Environment $twig; - - public function __construct(Environment $twig) - { - $this->twig = $twig; + public function __construct( + private Environment $twig, + ) { } public function extract($resource, MessageCatalogue $catalogue): void diff --git a/src/Symfony/Bridge/Twig/UndefinedCallableHandler.php b/src/Symfony/Bridge/Twig/UndefinedCallableHandler.php index ede634e196fcf..c9f502f67bd4a 100644 --- a/src/Symfony/Bridge/Twig/UndefinedCallableHandler.php +++ b/src/Symfony/Bridge/Twig/UndefinedCallableHandler.php @@ -25,6 +25,7 @@ class UndefinedCallableHandler private const FILTER_COMPONENTS = [ 'humanize' => 'form', 'form_encode_currency' => 'form', + 'serialize' => 'serializer', 'trans' => 'translation', 'sanitize_html' => 'html-sanitizer', 'yaml_encode' => 'yaml', @@ -36,6 +37,7 @@ class UndefinedCallableHandler 'asset_version' => 'asset', 'importmap' => 'asset-mapper', 'dump' => 'debug-bundle', + 'emojify' => 'emoji', 'encore_entry_link_tags' => 'webpack-encore-bundle', 'encore_entry_script_tags' => 'webpack-encore-bundle', 'expression' => 'expression-language', @@ -59,6 +61,11 @@ class UndefinedCallableHandler 'logout_url' => 'security-http', 'logout_path' => 'security-http', 'is_granted' => 'security-core', + 'impersonation_path' => 'security-http', + 'impersonation_url' => 'security-http', + 'impersonation_exit_path' => 'security-http', + 'impersonation_exit_url' => 'security-http', + 't' => 'translation', 'link' => 'web-link', 'preload' => 'web-link', 'dns_prefetch' => 'web-link', diff --git a/src/Symfony/Bridge/Twig/composer.json b/src/Symfony/Bridge/Twig/composer.json index 71fb6a2f8af23..90eb69ed0c645 100644 --- a/src/Symfony/Bridge/Twig/composer.json +++ b/src/Symfony/Bridge/Twig/composer.json @@ -27,6 +27,7 @@ "symfony/asset": "^6.4|^7.0", "symfony/asset-mapper": "^6.4|^7.0", "symfony/dependency-injection": "^6.4|^7.0", + "symfony/emoji": "^7.1", "symfony/finder": "^6.4|^7.0", "symfony/form": "^6.4|^7.0", "symfony/html-sanitizer": "^6.4|^7.0", diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index ed3b53d14ac42..4b0475167c04b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -1,6 +1,22 @@ CHANGELOG ========= +7.1 +--- + + * Add `CheckAliasValidityPass` to `lint:container` command + * Add `private_ranges` as a shortcut for private IP address ranges to the `trusted_proxies` option + * Mark classes `ConfigBuilderCacheWarmer`, `Router`, `SerializerCacheWarmer`, `TranslationsCacheWarmer`, `Translator` and `ValidatorCacheWarmer` as `final` + * Move the Router `cache_dir` to `kernel.build_dir` + * Deprecate the `router.cache_dir` config option + * Add `rate_limiter` tags to rate limiter services + * Add `secrets:reveal` command + * Add `rate_limiter` option to `http_client.default_options` and `http_client.scoped_clients` + * Attach the workflow's configuration to the `workflow` tag + * Add the `allowed_recipients` option for mailer to allow some users to receive + emails even if `recipients` is defined. + * Reset env vars when resetting the container + 7.0 --- diff --git a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/AbstractPhpFileCacheWarmer.php b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/AbstractPhpFileCacheWarmer.php index 3d7c99e4faa6c..d809888be13ff 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/AbstractPhpFileCacheWarmer.php +++ b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/AbstractPhpFileCacheWarmer.php @@ -19,14 +19,12 @@ abstract class AbstractPhpFileCacheWarmer implements CacheWarmerInterface { - private string $phpArrayFile; - /** * @param string $phpArrayFile The PHP file where metadata are cached */ - public function __construct(string $phpArrayFile) - { - $this->phpArrayFile = $phpArrayFile; + public function __construct( + private string $phpArrayFile, + ) { } public function isOptional(): bool diff --git a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/ConfigBuilderCacheWarmer.php b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/ConfigBuilderCacheWarmer.php index 6693ddd3e0ada..8b692c9c71448 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/ConfigBuilderCacheWarmer.php +++ b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/ConfigBuilderCacheWarmer.php @@ -29,6 +29,8 @@ * Generate all config builders. * * @author Tobias Nyholm + * + * @final since Symfony 7.1 */ class ConfigBuilderCacheWarmer implements CacheWarmerInterface { diff --git a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/RouterCacheWarmer.php b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/RouterCacheWarmer.php index c2b9478a331a2..eed548046b88b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/RouterCacheWarmer.php +++ b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/RouterCacheWarmer.php @@ -36,6 +36,10 @@ public function __construct(ContainerInterface $container) public function warmUp(string $cacheDir, ?string $buildDir = null): array { + if (!$buildDir) { + return []; + } + $router = $this->container->get('router'); if ($router instanceof WarmableInterface) { diff --git a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/SerializerCacheWarmer.php b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/SerializerCacheWarmer.php index fcd67af1f82e9..46da4daaab4d1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/SerializerCacheWarmer.php +++ b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/SerializerCacheWarmer.php @@ -23,19 +23,20 @@ * Warms up XML and YAML serializer metadata. * * @author Titouan Galopin + * + * @final since Symfony 7.1 */ class SerializerCacheWarmer extends AbstractPhpFileCacheWarmer { - private array $loaders; - /** * @param LoaderInterface[] $loaders The serializer metadata loaders * @param string $phpArrayFile The PHP file where metadata are cached */ - public function __construct(array $loaders, string $phpArrayFile) - { + public function __construct( + private array $loaders, + string $phpArrayFile, + ) { parent::__construct($phpArrayFile); - $this->loaders = $loaders; } protected function doWarmUp(string $cacheDir, ArrayAdapter $arrayAdapter, ?string $buildDir = null): bool diff --git a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/TranslationsCacheWarmer.php b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/TranslationsCacheWarmer.php index b1c0fc6d7f58b..19b2725c93d4c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/TranslationsCacheWarmer.php +++ b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/TranslationsCacheWarmer.php @@ -21,6 +21,8 @@ * Generates the catalogues for translations. * * @author Xavier Leune + * + * @final since Symfony 7.1 */ class TranslationsCacheWarmer implements CacheWarmerInterface, ServiceSubscriberInterface { diff --git a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/ValidatorCacheWarmer.php b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/ValidatorCacheWarmer.php index bf1e5035fb04b..6ecaa4bd14d01 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/ValidatorCacheWarmer.php +++ b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/ValidatorCacheWarmer.php @@ -24,18 +24,19 @@ * Warms up XML and YAML validator metadata. * * @author Titouan Galopin + * + * @final since Symfony 7.1 */ class ValidatorCacheWarmer extends AbstractPhpFileCacheWarmer { - private ValidatorBuilder $validatorBuilder; - /** * @param string $phpArrayFile The PHP file where metadata are cached */ - public function __construct(ValidatorBuilder $validatorBuilder, string $phpArrayFile) - { + public function __construct( + private ValidatorBuilder $validatorBuilder, + string $phpArrayFile, + ) { parent::__construct($phpArrayFile); - $this->validatorBuilder = $validatorBuilder; } protected function doWarmUp(string $cacheDir, ArrayAdapter $arrayAdapter, ?string $buildDir = null): bool diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/AssetsInstallCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/AssetsInstallCommand.php index 264955d7951eb..32b38de9af025 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/AssetsInstallCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/AssetsInstallCommand.php @@ -41,15 +41,11 @@ class AssetsInstallCommand extends Command public const METHOD_ABSOLUTE_SYMLINK = 'absolute symlink'; public const METHOD_RELATIVE_SYMLINK = 'relative symlink'; - private Filesystem $filesystem; - private string $projectDir; - - public function __construct(Filesystem $filesystem, string $projectDir) - { + public function __construct( + private Filesystem $filesystem, + private string $projectDir, + ) { parent::__construct(); - - $this->filesystem = $filesystem; - $this->projectDir = $projectDir; } protected function configure(): void @@ -264,7 +260,7 @@ private function getPublicDirectory(ContainerInterface $container): string return $defaultPublicDir; } - $composerConfig = json_decode(file_get_contents($composerFilePath), true); + $composerConfig = json_decode($this->filesystem->readFile($composerFilePath), true, flags: \JSON_THROW_ON_ERROR); return $composerConfig['extra']['public-dir'] ?? $defaultPublicDir; } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php index cdfc7f34f3730..55813664b7eee 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php @@ -37,14 +37,14 @@ #[AsCommand(name: 'cache:clear', description: 'Clear the cache')] class CacheClearCommand extends Command { - private CacheClearerInterface $cacheClearer; private Filesystem $filesystem; - public function __construct(CacheClearerInterface $cacheClearer, ?Filesystem $filesystem = null) - { + public function __construct( + private CacheClearerInterface $cacheClearer, + ?Filesystem $filesystem = null, + ) { parent::__construct(); - $this->cacheClearer = $cacheClearer; $this->filesystem = $filesystem ?? new Filesystem(); } @@ -232,7 +232,7 @@ private function warmup(string $warmupDir, string $realBuildDir): void $search = [$warmupDir, str_replace('\\', '\\\\', $warmupDir)]; $replace = str_replace('\\', '/', $realBuildDir); foreach (Finder::create()->files()->in($warmupDir) as $file) { - $content = str_replace($search, $replace, file_get_contents($file), $count); + $content = str_replace($search, $replace, $this->filesystem->readFile($file), $count); if ($count) { file_put_contents($file, $content); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolClearCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolClearCommand.php index 8b8b9cd35e51e..d5320e7a9e328 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolClearCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolClearCommand.php @@ -32,18 +32,14 @@ #[AsCommand(name: 'cache:pool:clear', description: 'Clear cache pools')] final class CachePoolClearCommand extends Command { - private Psr6CacheClearer $poolClearer; - private ?array $poolNames; - /** * @param string[]|null $poolNames */ - public function __construct(Psr6CacheClearer $poolClearer, ?array $poolNames = null) - { + public function __construct( + private Psr6CacheClearer $poolClearer, + private ?array $poolNames = null, + ) { parent::__construct(); - - $this->poolClearer = $poolClearer; - $this->poolNames = $poolNames; } protected function configure(): void diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolDeleteCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolDeleteCommand.php index dfa307bc0b73c..e634e00f03bd0 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolDeleteCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolDeleteCommand.php @@ -29,18 +29,14 @@ #[AsCommand(name: 'cache:pool:delete', description: 'Delete an item from a cache pool')] final class CachePoolDeleteCommand extends Command { - private Psr6CacheClearer $poolClearer; - private ?array $poolNames; - /** * @param string[]|null $poolNames */ - public function __construct(Psr6CacheClearer $poolClearer, ?array $poolNames = null) - { + public function __construct( + private Psr6CacheClearer $poolClearer, + private ?array $poolNames = null, + ) { parent::__construct(); - - $this->poolClearer = $poolClearer; - $this->poolNames = $poolNames; } protected function configure(): void diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolInvalidateTagsCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolInvalidateTagsCommand.php index 9e6ef9330e24a..f879a6d0df9eb 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolInvalidateTagsCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolInvalidateTagsCommand.php @@ -30,14 +30,13 @@ #[AsCommand(name: 'cache:pool:invalidate-tags', description: 'Invalidate cache tags for all or a specific pool')] final class CachePoolInvalidateTagsCommand extends Command { - private ServiceProviderInterface $pools; private array $poolNames; - public function __construct(ServiceProviderInterface $pools) - { + public function __construct( + private ServiceProviderInterface $pools, + ) { parent::__construct(); - $this->pools = $pools; $this->poolNames = array_keys($pools->getProvidedServices()); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolListCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolListCommand.php index 2659ad8fe05c2..6b8e71eb0469e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolListCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolListCommand.php @@ -25,16 +25,13 @@ #[AsCommand(name: 'cache:pool:list', description: 'List available cache pools')] final class CachePoolListCommand extends Command { - private array $poolNames; - /** * @param string[] $poolNames */ - public function __construct(array $poolNames) - { + public function __construct( + private array $poolNames, + ) { parent::__construct(); - - $this->poolNames = $poolNames; } protected function configure(): void diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolPruneCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolPruneCommand.php index fc0dc6d795e0d..fba1033f9199b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolPruneCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolPruneCommand.php @@ -26,16 +26,13 @@ #[AsCommand(name: 'cache:pool:prune', description: 'Prune cache pools')] final class CachePoolPruneCommand extends Command { - private iterable $pools; - /** * @param iterable $pools */ - public function __construct(iterable $pools) - { + public function __construct( + private iterable $pools, + ) { parent::__construct(); - - $this->pools = $pools; } protected function configure(): void diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/CacheWarmupCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/CacheWarmupCommand.php index 6f1073de4ea75..a4b32a56167f4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/CacheWarmupCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/CacheWarmupCommand.php @@ -31,13 +31,10 @@ #[AsCommand(name: 'cache:warmup', description: 'Warm up an empty cache')] class CacheWarmupCommand extends Command { - private CacheWarmerAggregate $cacheWarmer; - - public function __construct(CacheWarmerAggregate $cacheWarmer) - { + public function __construct( + private CacheWarmerAggregate $cacheWarmer, + ) { parent::__construct(); - - $this->cacheWarmer = $cacheWarmer; } protected function configure(): void diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php index b63ebe431787e..cd6e0657ccac9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php @@ -19,6 +19,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\DependencyInjection\Compiler\CheckAliasValidityPass; use Symfony\Component\DependencyInjection\Compiler\CheckTypeDeclarationsPass; use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\Compiler\ResolveFactoryClassPass; @@ -107,6 +108,7 @@ private function getContainerBuilder(): ContainerBuilder $container->setParameter('container.build_hash', 'lint_container'); $container->setParameter('container.build_id', 'lint_container'); + $container->addCompilerPass(new CheckAliasValidityPass(), PassConfig::TYPE_BEFORE_REMOVING, -100); $container->addCompilerPass(new CheckTypeDeclarationsPass(true), PassConfig::TYPE_AFTER_REMOVING, -100); return $this->container = $container; diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/DebugAutowiringCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/DebugAutowiringCommand.php index f6efd8bef8ce1..77011b185e8e0 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/DebugAutowiringCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/DebugAutowiringCommand.php @@ -33,11 +33,10 @@ #[AsCommand(name: 'debug:autowiring', description: 'List classes/interfaces you can use for autowiring')] class DebugAutowiringCommand extends ContainerDebugCommand { - private ?FileLinkFormatter $fileLinkFormatter; - - public function __construct(?string $name = null, ?FileLinkFormatter $fileLinkFormatter = null) - { - $this->fileLinkFormatter = $fileLinkFormatter; + public function __construct( + ?string $name = null, + private ?FileLinkFormatter $fileLinkFormatter = null, + ) { parent::__construct($name); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/EventDispatcherDebugCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/EventDispatcherDebugCommand.php index 1a74e86824548..52816e7de69d1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/EventDispatcherDebugCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/EventDispatcherDebugCommand.php @@ -37,13 +37,10 @@ class EventDispatcherDebugCommand extends Command { private const DEFAULT_DISPATCHER = 'event_dispatcher'; - private ContainerInterface $dispatchers; - - public function __construct(ContainerInterface $dispatchers) - { + public function __construct( + private ContainerInterface $dispatchers, + ) { parent::__construct(); - - $this->dispatchers = $dispatchers; } protected function configure(): void diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/RouterDebugCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/RouterDebugCommand.php index 9318b46be50d7..54df494318028 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/RouterDebugCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/RouterDebugCommand.php @@ -39,15 +39,11 @@ class RouterDebugCommand extends Command { use BuildDebugContainerTrait; - private RouterInterface $router; - private ?FileLinkFormatter $fileLinkFormatter; - - public function __construct(RouterInterface $router, ?FileLinkFormatter $fileLinkFormatter = null) - { + public function __construct( + private RouterInterface $router, + private ?FileLinkFormatter $fileLinkFormatter = null, + ) { parent::__construct(); - - $this->router = $router; - $this->fileLinkFormatter = $fileLinkFormatter; } protected function configure(): void diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/RouterMatchCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/RouterMatchCommand.php index 7efd1f3ed3708..475b403ca5f54 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/RouterMatchCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/RouterMatchCommand.php @@ -33,18 +33,14 @@ #[AsCommand(name: 'router:match', description: 'Help debug routes by simulating a path info match')] class RouterMatchCommand extends Command { - private RouterInterface $router; - private iterable $expressionLanguageProviders; - /** * @param iterable $expressionLanguageProviders */ - public function __construct(RouterInterface $router, iterable $expressionLanguageProviders = []) - { + public function __construct( + private RouterInterface $router, + private iterable $expressionLanguageProviders = [], + ) { parent::__construct(); - - $this->router = $router; - $this->expressionLanguageProviders = $expressionLanguageProviders; } protected function configure(): void diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsDecryptToLocalCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsDecryptToLocalCommand.php index ac711e3dbd850..f76e1d05a5be9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsDecryptToLocalCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsDecryptToLocalCommand.php @@ -28,14 +28,10 @@ #[AsCommand(name: 'secrets:decrypt-to-local', description: 'Decrypt all secrets and stores them in the local vault')] final class SecretsDecryptToLocalCommand extends Command { - private AbstractVault $vault; - private ?AbstractVault $localVault; - - public function __construct(AbstractVault $vault, ?AbstractVault $localVault = null) - { - $this->vault = $vault; - $this->localVault = $localVault; - + public function __construct( + private AbstractVault $vault, + private ?AbstractVault $localVault = null, + ) { parent::__construct(); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsEncryptFromLocalCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsEncryptFromLocalCommand.php index 46e0baffc9242..9740098e5b80c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsEncryptFromLocalCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsEncryptFromLocalCommand.php @@ -27,14 +27,10 @@ #[AsCommand(name: 'secrets:encrypt-from-local', description: 'Encrypt all local secrets to the vault')] final class SecretsEncryptFromLocalCommand extends Command { - private AbstractVault $vault; - private ?AbstractVault $localVault; - - public function __construct(AbstractVault $vault, ?AbstractVault $localVault = null) - { - $this->vault = $vault; - $this->localVault = $localVault; - + public function __construct( + private AbstractVault $vault, + private ?AbstractVault $localVault = null, + ) { parent::__construct(); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsGenerateKeysCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsGenerateKeysCommand.php index 989eff9fd2977..66a752eac7e47 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsGenerateKeysCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsGenerateKeysCommand.php @@ -30,14 +30,10 @@ #[AsCommand(name: 'secrets:generate-keys', description: 'Generate new encryption keys')] final class SecretsGenerateKeysCommand extends Command { - private AbstractVault $vault; - private ?AbstractVault $localVault; - - public function __construct(AbstractVault $vault, ?AbstractVault $localVault = null) - { - $this->vault = $vault; - $this->localVault = $localVault; - + public function __construct( + private AbstractVault $vault, + private ?AbstractVault $localVault = null, + ) { parent::__construct(); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsListCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsListCommand.php index 9a24f4a90fbb6..cdfc51c39b0e6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsListCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsListCommand.php @@ -31,14 +31,10 @@ #[AsCommand(name: 'secrets:list', description: 'List all secrets')] final class SecretsListCommand extends Command { - private AbstractVault $vault; - private ?AbstractVault $localVault; - - public function __construct(AbstractVault $vault, ?AbstractVault $localVault = null) - { - $this->vault = $vault; - $this->localVault = $localVault; - + public function __construct( + private AbstractVault $vault, + private ?AbstractVault $localVault = null, + ) { parent::__construct(); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsRemoveCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsRemoveCommand.php index 1789f2981b11b..11660b00d778a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsRemoveCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsRemoveCommand.php @@ -32,14 +32,10 @@ #[AsCommand(name: 'secrets:remove', description: 'Remove a secret from the vault')] final class SecretsRemoveCommand extends Command { - private AbstractVault $vault; - private ?AbstractVault $localVault; - - public function __construct(AbstractVault $vault, ?AbstractVault $localVault = null) - { - $this->vault = $vault; - $this->localVault = $localVault; - + public function __construct( + private AbstractVault $vault, + private ?AbstractVault $localVault = null, + ) { parent::__construct(); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsRevealCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsRevealCommand.php new file mode 100644 index 0000000000000..bcbdea11f079c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsRevealCommand.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Command; + +use Symfony\Bundle\FrameworkBundle\Secrets\AbstractVault; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +/** + * @internal + */ +#[AsCommand(name: 'secrets:reveal', description: 'Reveal the value of a secret')] +final class SecretsRevealCommand extends Command +{ + public function __construct( + private readonly AbstractVault $vault, + private readonly ?AbstractVault $localVault = null, + ) { + parent::__construct(); + } + + protected function configure(): void + { + $this + ->addArgument('name', InputArgument::REQUIRED, 'The name of the secret to reveal', null, fn () => array_keys($this->vault->list())) + ->setHelp(<<<'EOF' +The %command.name% command reveals a stored secret. + + %command.full_name% +EOF + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output); + + $secrets = $this->vault->list(true); + $localSecrets = $this->localVault?->list(true); + + $name = (string) $input->getArgument('name'); + + if (null !== $localSecrets && \array_key_exists($name, $localSecrets)) { + $io->writeln($localSecrets[$name]); + } else { + if (!\array_key_exists($name, $secrets)) { + $io->error(sprintf('The secret "%s" does not exist.', $name)); + + return self::INVALID; + } + + $io->writeln($secrets[$name]); + } + + return self::SUCCESS; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsSetCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsSetCommand.php index 2d2b8c5cb6b42..49a20af76c5d7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsSetCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsSetCommand.php @@ -33,14 +33,10 @@ #[AsCommand(name: 'secrets:set', description: 'Set a secret in the vault')] final class SecretsSetCommand extends Command { - private AbstractVault $vault; - private ?AbstractVault $localVault; - - public function __construct(AbstractVault $vault, ?AbstractVault $localVault = null) - { - $this->vault = $vault; - $this->localVault = $localVault; - + public function __construct( + private AbstractVault $vault, + private ?AbstractVault $localVault = null, + ) { parent::__construct(); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php index ecb0ad8d7080f..2438d3feb3413 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php @@ -50,27 +50,17 @@ class TranslationDebugCommand extends Command public const MESSAGE_UNUSED = 1; public const MESSAGE_EQUALS_FALLBACK = 2; - private TranslatorInterface $translator; - private TranslationReaderInterface $reader; - private ExtractorInterface $extractor; - private ?string $defaultTransPath; - private ?string $defaultViewsPath; - private array $transPaths; - private array $codePaths; - private array $enabledLocales; - - public function __construct(TranslatorInterface $translator, TranslationReaderInterface $reader, ExtractorInterface $extractor, ?string $defaultTransPath = null, ?string $defaultViewsPath = null, array $transPaths = [], array $codePaths = [], array $enabledLocales = []) - { + public function __construct( + private TranslatorInterface $translator, + private TranslationReaderInterface $reader, + private ExtractorInterface $extractor, + private ?string $defaultTransPath = null, + private ?string $defaultViewsPath = null, + private array $transPaths = [], + private array $codePaths = [], + private array $enabledLocales = [], + ) { parent::__construct(); - - $this->translator = $translator; - $this->reader = $reader; - $this->extractor = $extractor; - $this->defaultTransPath = $defaultTransPath; - $this->defaultViewsPath = $defaultViewsPath; - $this->transPaths = $transPaths; - $this->codePaths = $codePaths; - $this->enabledLocales = $enabledLocales; } protected function configure(): void @@ -223,8 +213,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int } } - if (!\in_array(self::MESSAGE_UNUSED, $states) && $input->getOption('only-unused') - || !\in_array(self::MESSAGE_MISSING, $states) && $input->getOption('only-missing') + if (!\in_array(self::MESSAGE_UNUSED, $states, true) && $input->getOption('only-unused') + || !\in_array(self::MESSAGE_MISSING, $states, true) && $input->getOption('only-missing') ) { continue; } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php index fc95e0217908a..1a883f81edc88 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php @@ -50,29 +50,18 @@ class TranslationUpdateCommand extends Command 'xlf20' => ['xlf', '2.0'], ]; - private TranslationWriterInterface $writer; - private TranslationReaderInterface $reader; - private ExtractorInterface $extractor; - private string $defaultLocale; - private ?string $defaultTransPath; - private ?string $defaultViewsPath; - private array $transPaths; - private array $codePaths; - private array $enabledLocales; - - public function __construct(TranslationWriterInterface $writer, TranslationReaderInterface $reader, ExtractorInterface $extractor, string $defaultLocale, ?string $defaultTransPath = null, ?string $defaultViewsPath = null, array $transPaths = [], array $codePaths = [], array $enabledLocales = []) - { + public function __construct( + private TranslationWriterInterface $writer, + private TranslationReaderInterface $reader, + private ExtractorInterface $extractor, + private string $defaultLocale, + private ?string $defaultTransPath = null, + private ?string $defaultViewsPath = null, + private array $transPaths = [], + private array $codePaths = [], + private array $enabledLocales = [], + ) { parent::__construct(); - - $this->writer = $writer; - $this->reader = $reader; - $this->extractor = $extractor; - $this->defaultLocale = $defaultLocale; - $this->defaultTransPath = $defaultTransPath; - $this->defaultViewsPath = $defaultViewsPath; - $this->transPaths = $transPaths; - $this->codePaths = $codePaths; - $this->enabledLocales = $enabledLocales; } protected function configure(): void diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Application.php b/src/Symfony/Bundle/FrameworkBundle/Console/Application.php index 14a907e2e9fb7..1c41849e794db 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Application.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Application.php @@ -30,14 +30,12 @@ */ class Application extends BaseApplication { - private KernelInterface $kernel; private bool $commandsRegistered = false; private array $registrationErrors = []; - public function __construct(KernelInterface $kernel) - { - $this->kernel = $kernel; - + public function __construct( + private KernelInterface $kernel, + ) { parent::__construct('Symfony', Kernel::VERSION); $inputDefinition = $this->getDefinition(); diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/JsonDescriptor.php b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/JsonDescriptor.php index 3f5085ef2a7b4..88cf4162c6c83 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/JsonDescriptor.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/JsonDescriptor.php @@ -323,7 +323,7 @@ private function getContainerAliasData(Alias $alias): array private function getEventDispatcherListenersData(EventDispatcherInterface $eventDispatcher, array $options): array { $data = []; - $event = \array_key_exists('event', $options) ? $options['event'] : null; + $event = $options['event'] ?? null; if (null !== $event) { foreach ($eventDispatcher->getListeners($event) as $listener) { @@ -393,7 +393,7 @@ private function getCallableData(mixed $callable): array $data['type'] = 'closure'; $r = new \ReflectionFunction($callable); - if (str_contains($r->name, '{closure')) { + if ($r->isAnonymous()) { return $data; } $data['name'] = $r->name; diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/MarkdownDescriptor.php b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/MarkdownDescriptor.php index 7537e3d4228e7..7965990bdf207 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/MarkdownDescriptor.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/MarkdownDescriptor.php @@ -403,7 +403,7 @@ protected function describeCallable(mixed $callable, array $options = []): void $string .= "\n- Type: `closure`"; $r = new \ReflectionFunction($callable); - if (str_contains($r->name, '{closure')) { + if ($r->isAnonymous()) { $this->write($string."\n"); return; diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/TextDescriptor.php b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/TextDescriptor.php index 60834250b3370..d728128ce9106 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/TextDescriptor.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/TextDescriptor.php @@ -38,11 +38,9 @@ */ class TextDescriptor extends Descriptor { - private ?FileLinkFormatter $fileLinkFormatter; - - public function __construct(?FileLinkFormatter $fileLinkFormatter = null) - { - $this->fileLinkFormatter = $fileLinkFormatter; + public function __construct( + private ?FileLinkFormatter $fileLinkFormatter = null, + ) { } protected function describeRouteCollection(RouteCollection $routes, array $options = []): void @@ -649,7 +647,7 @@ private function formatCallable(mixed $callable): string if ($callable instanceof \Closure) { $r = new \ReflectionFunction($callable); - if (str_contains($r->name, '{closure')) { + if ($r->isAnonymous()) { return 'Closure()'; } if ($class = $r->getClosureCalledClass()) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/XmlDescriptor.php b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/XmlDescriptor.php index 05848b1b24f6c..c52b196674364 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/XmlDescriptor.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/XmlDescriptor.php @@ -501,7 +501,7 @@ private function getContainerParameterDocument(mixed $parameter, ?array $depreca private function getEventDispatcherListenersDocument(EventDispatcherInterface $eventDispatcher, array $options): \DOMDocument { - $event = \array_key_exists('event', $options) ? $options['event'] : null; + $event = $options['event'] ?? null; $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->appendChild($eventDispatcherXML = $dom->createElement('event-dispatcher')); @@ -581,7 +581,7 @@ private function getCallableDocument(mixed $callable): \DOMDocument $callableXML->setAttribute('type', 'closure'); $r = new \ReflectionFunction($callable); - if (str_contains($r->name, '{closure')) { + if ($r->isAnonymous()) { return $dom; } $callableXML->setAttribute('name', $r->name); diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/RedirectController.php b/src/Symfony/Bundle/FrameworkBundle/Controller/RedirectController.php index fbb52ead7507d..1001fad632cf9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/RedirectController.php +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/RedirectController.php @@ -27,15 +27,11 @@ */ class RedirectController { - private ?UrlGeneratorInterface $router; - private ?int $httpPort; - private ?int $httpsPort; - - public function __construct(?UrlGeneratorInterface $router = null, ?int $httpPort = null, ?int $httpsPort = null) - { - $this->router = $router; - $this->httpPort = $httpPort; - $this->httpsPort = $httpsPort; + public function __construct( + private ?UrlGeneratorInterface $router = null, + private ?int $httpPort = null, + private ?int $httpsPort = null, + ) { } /** diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/TemplateController.php b/src/Symfony/Bundle/FrameworkBundle/Controller/TemplateController.php index 97631572c9c62..bcbcc382d7f64 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/TemplateController.php +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/TemplateController.php @@ -23,11 +23,9 @@ */ class TemplateController { - private ?Environment $twig; - - public function __construct(?Environment $twig = null) - { - $this->twig = $twig; + public function __construct( + private ?Environment $twig = null, + ) { } /** diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php index 1d21c6b663688..a2a141afb42ac 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php @@ -82,6 +82,7 @@ class UnusedTagsPass implements CompilerPassInterface 'routing.route_loader', 'scheduler.schedule_provider', 'scheduler.task', + 'security.access_token_handler.oidc.signature_algorithm', 'security.authenticator.login_linker', 'security.expression_language_provider', 'security.remember_me_handler', @@ -110,7 +111,7 @@ public function process(ContainerBuilder $container): void foreach ($container->findUnusedTags() as $tag) { // skip known tags - if (\in_array($tag, self::KNOWN_TAGS)) { + if (\in_array($tag, self::KNOWN_TAGS, true)) { continue; } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 6a006845f415d..92c20d139da6e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -29,6 +29,7 @@ use Symfony\Component\HtmlSanitizer\HtmlSanitizerInterface; use Symfony\Component\HttpClient\HttpClient; use Symfony\Component\HttpFoundation\Cookie; +use Symfony\Component\HttpFoundation\IpUtils; use Symfony\Component\Lock\Lock; use Symfony\Component\Lock\Store\SemaphoreStore; use Symfony\Component\Mailer\Mailer; @@ -43,6 +44,7 @@ use Symfony\Component\Serializer\Encoder\JsonDecode; use Symfony\Component\Serializer\Serializer; use Symfony\Component\Translation\Translator; +use Symfony\Component\TypeInfo\Type; use Symfony\Component\Uid\Factory\UuidFactory; use Symfony\Component\Validator\Validation; use Symfony\Component\Webhook\Controller\WebhookController; @@ -54,14 +56,12 @@ */ class Configuration implements ConfigurationInterface { - private bool $debug; - /** * @param bool $debug Whether debugging is enabled or not */ - public function __construct(bool $debug) - { - $this->debug = $debug; + public function __construct( + private bool $debug, + ) { } /** @@ -111,7 +111,12 @@ public function getConfigTreeBuilder(): TreeBuilder ->beforeNormalization()->ifString()->then(fn ($v) => [$v])->end() ->prototype('scalar')->end() ->end() - ->scalarNode('trusted_proxies')->end() + ->scalarNode('trusted_proxies') + ->beforeNormalization() + ->ifTrue(fn ($v) => 'private_ranges' === $v) + ->then(fn ($v) => implode(',', IpUtils::PRIVATE_SUBNETS)) + ->end() + ->end() ->arrayNode('trusted_headers') ->fixXmlConfig('trusted_header') ->performNoDeepMerging() @@ -158,6 +163,7 @@ public function getConfigTreeBuilder(): TreeBuilder $this->addAnnotationsSection($rootNode); $this->addSerializerSection($rootNode, $enableIfStandalone); $this->addPropertyAccessSection($rootNode, $willBeAvailable); + $this->addTypeInfoSection($rootNode, $enableIfStandalone); $this->addPropertyInfoSection($rootNode, $enableIfStandalone); $this->addCacheSection($rootNode, $willBeAvailable); $this->addPhpErrorsSection($rootNode); @@ -435,7 +441,7 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void if (!\is_string($value)) { return true; } - if (class_exists(WorkflowEvents::class) && !\in_array($value, WorkflowEvents::ALIASES)) { + if (class_exists(WorkflowEvents::class) && !\in_array($value, WorkflowEvents::ALIASES, true)) { return true; } } @@ -478,8 +484,6 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void return array_values($places); }) ->end() - ->isRequired() - ->requiresAtLeastOneElement() ->prototype('array') ->children() ->scalarNode('name') @@ -613,7 +617,10 @@ private function addRouterSection(ArrayNodeDefinition $rootNode): void ->children() ->scalarNode('resource')->isRequired()->end() ->scalarNode('type')->end() - ->scalarNode('cache_dir')->defaultValue('%kernel.cache_dir%')->end() + ->scalarNode('cache_dir') + ->defaultValue('%kernel.build_dir%') + ->setDeprecated('symfony/framework-bundle', '7.1', 'Setting the "%path%.%node%" configuration option is deprecated. It will be removed in version 8.0.') + ->end() ->scalarNode('default_uri') ->info('The default URI used to generate URLs in a non-HTTP context') ->defaultNull() @@ -1111,7 +1118,6 @@ private function addSerializerSection(ArrayNodeDefinition $rootNode, callable $e ->end() ->arrayNode('default_context') ->normalizeKeys(false) - ->useAttributeAsKey('name') ->beforeNormalization() ->ifTrue(fn () => $this->debug && class_exists(JsonParser::class)) ->then(fn (array $v) => $v + [JsonDecode::DETAILED_ERROR_MESSAGES => true]) @@ -1157,6 +1163,18 @@ private function addPropertyInfoSection(ArrayNodeDefinition $rootNode, callable ; } + private function addTypeInfoSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone): void + { + $rootNode + ->children() + ->arrayNode('type_info') + ->info('Type info configuration') + ->{$enableIfStandalone('symfony/type-info', Type::class)}() + ->end() + ->end() + ; + } + private function addCacheSection(ArrayNodeDefinition $rootNode, callable $willBeAvailable): void { $rootNode @@ -1587,6 +1605,7 @@ function ($a) { ->integerNode('delay')->defaultValue(1000)->min(0)->info('Time in ms to delay (or the initial value when multiplier is used)')->end() ->floatNode('multiplier')->defaultValue(2)->min(1)->info('If greater than 1, delay will grow exponentially for each retry: this delay = (delay * (multiple ^ retries))')->end() ->integerNode('max_delay')->defaultValue(0)->min(0)->info('Max time in ms that a retry should ever be delayed (0 = infinite)')->end() + ->floatNode('jitter')->defaultValue(0.1)->min(0)->max(1)->info('Randomness to apply to the delay (between 0 and 1)')->end() ->end() ->end() ->scalarNode('rate_limiter') @@ -1710,17 +1729,32 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode, callable $e ->fixXmlConfig('scoped_client') ->beforeNormalization() ->always(function ($config) { - if (empty($config['scoped_clients']) || !\is_array($config['default_options']['retry_failed'] ?? null)) { + if (empty($config['scoped_clients'])) { + return $config; + } + + $hasDefaultRateLimiter = isset($config['default_options']['rate_limiter']); + $hasDefaultRetryFailed = \is_array($config['default_options']['retry_failed'] ?? null); + + if (!$hasDefaultRateLimiter && !$hasDefaultRetryFailed) { return $config; } foreach ($config['scoped_clients'] as &$scopedConfig) { - if (!isset($scopedConfig['retry_failed']) || true === $scopedConfig['retry_failed']) { - $scopedConfig['retry_failed'] = $config['default_options']['retry_failed']; - continue; + if ($hasDefaultRateLimiter) { + if (!isset($scopedConfig['rate_limiter']) || true === $scopedConfig['rate_limiter']) { + $scopedConfig['rate_limiter'] = $config['default_options']['rate_limiter']; + } elseif (false === $scopedConfig['rate_limiter']) { + $scopedConfig['rate_limiter'] = null; + } } - if (\is_array($scopedConfig['retry_failed'])) { - $scopedConfig['retry_failed'] += $config['default_options']['retry_failed']; + + if ($hasDefaultRetryFailed) { + if (!isset($scopedConfig['retry_failed']) || true === $scopedConfig['retry_failed']) { + $scopedConfig['retry_failed'] = $config['default_options']['retry_failed']; + } elseif (\is_array($scopedConfig['retry_failed'])) { + $scopedConfig['retry_failed'] += $config['default_options']['retry_failed']; + } } } @@ -1825,6 +1859,10 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode, callable $e ->normalizeKeys(false) ->variablePrototype()->end() ->end() + ->scalarNode('rate_limiter') + ->defaultNull() + ->info('Rate limiter name to use for throttling requests') + ->end() ->append($this->createHttpClientRetrySection()) ->end() ->end() @@ -1973,6 +2011,10 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode, callable $e ->normalizeKeys(false) ->variablePrototype()->end() ->end() + ->scalarNode('rate_limiter') + ->defaultNull() + ->info('Rate limiter name to use for throttling requests') + ->end() ->append($this->createHttpClientRetrySection()) ->end() ->end() @@ -2075,12 +2117,23 @@ private function addMailerSection(ArrayNodeDefinition $rootNode, callable $enabl ->arrayNode('envelope') ->info('Mailer Envelope configuration') ->fixXmlConfig('recipient') + ->fixXmlConfig('allowed_recipient') ->children() ->scalarNode('sender')->end() ->arrayNode('recipients') ->performNoDeepMerging() ->beforeNormalization() - ->ifArray() + ->ifArray() + ->then(fn ($v) => array_filter(array_values($v))) + ->end() + ->prototype('scalar')->end() + ->end() + ->arrayNode('allowed_recipients') + ->info('A list of regular expressions that allow recipients when "recipients" option is defined.') + ->example(['.*@example\.com']) + ->performNoDeepMerging() + ->beforeNormalization() + ->ifArray() ->then(fn ($v) => array_filter(array_values($v))) ->end() ->prototype('scalar')->end() diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 0f44e66a707f3..8786d04bd8da7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -53,6 +53,7 @@ use Symfony\Component\Console\Debug\CliRequest; use Symfony\Component\Console\Messenger\RunCommandMessageHandler; use Symfony\Component\DependencyInjection\Alias; +use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -70,6 +71,7 @@ use Symfony\Component\EventDispatcher\Attribute\AsEventListener; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; +use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Finder\Finder; use Symfony\Component\Finder\Glob; use Symfony\Component\Form\Extension\HtmlSanitizer\Type\TextTypeHtmlSanitizerExtension; @@ -85,6 +87,7 @@ use Symfony\Component\HttpClient\Retry\GenericRetryStrategy; use Symfony\Component\HttpClient\RetryableHttpClient; use Symfony\Component\HttpClient\ScopingHttpClient; +use Symfony\Component\HttpClient\ThrottlingHttpClient; use Symfony\Component\HttpClient\UriTemplateHttpClient; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Attribute\AsController; @@ -167,6 +170,8 @@ use Symfony\Component\Translation\LocaleSwitcher; use Symfony\Component\Translation\PseudoLocalizationTranslator; use Symfony\Component\Translation\Translator; +use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\TypeResolver\StringTypeResolver; use Symfony\Component\Uid\Factory\UuidFactory; use Symfony\Component\Uid\UuidV4; use Symfony\Component\Validator\Constraints\ExpressionLanguageProvider; @@ -345,6 +350,7 @@ public function load(array $configs, ContainerBuilder $container): void } if ($this->readConfigEnabled('http_client', $container, $config['http_client'])) { + $this->readConfigEnabled('rate_limiter', $container, $config['rate_limiter']); // makes sure that isInitializedConfigEnabled() will work $this->registerHttpClientConfiguration($config['http_client'], $container, $loader); } @@ -388,6 +394,10 @@ public function load(array $configs, ContainerBuilder $container): void $container->removeDefinition('console.command.serializer_debug'); } + if ($this->readConfigEnabled('type_info', $container, $config['type_info'])) { + $this->registerTypeInfoConfiguration($container, $loader); + } + if ($propertyInfoEnabled) { $this->registerPropertyInfoConfiguration($container, $loader); } @@ -689,9 +699,9 @@ public function load(array $configs, ContainerBuilder $container): void $taskAttributeClass, static function (ChildDefinition $definition, AsPeriodicTask|AsCronTask $attribute, \ReflectionClass|\ReflectionMethod $reflector): void { $tagAttributes = get_object_vars($attribute) + [ - 'trigger' => match ($attribute::class) { - AsPeriodicTask::class => 'every', - AsCronTask::class => 'cron', + 'trigger' => match (true) { + $attribute instanceof AsPeriodicTask => 'every', + $attribute instanceof AsCronTask => 'cron', }, ]; if ($reflector instanceof \ReflectionMethod) { @@ -1020,7 +1030,7 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $ $workflowDefinition->replaceArgument(3, $name); $workflowDefinition->replaceArgument(4, $workflow['events_to_dispatch']); - $workflowDefinition->addTag('workflow', ['name' => $name]); + $workflowDefinition->addTag('workflow', ['name' => $name, 'metadata' => $workflow['metadata']]); if ('workflow' === $type) { $workflowDefinition->addTag('workflow.workflow', ['name' => $name]); } elseif ('state_machine' === $type) { @@ -1094,29 +1104,6 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $ $container->setParameter('workflow.has_guard_listeners', true); } } - - $listenerAttributes = [ - Workflow\Attribute\AsAnnounceListener::class, - Workflow\Attribute\AsCompletedListener::class, - Workflow\Attribute\AsEnterListener::class, - Workflow\Attribute\AsEnteredListener::class, - Workflow\Attribute\AsGuardListener::class, - Workflow\Attribute\AsLeaveListener::class, - Workflow\Attribute\AsTransitionListener::class, - ]; - - foreach ($listenerAttributes as $attribute) { - $container->registerAttributeForAutoconfiguration($attribute, static function (ChildDefinition $definition, AsEventListener $attribute, \ReflectionClass|\ReflectionMethod $reflector) { - $tagAttributes = get_object_vars($attribute); - if ($reflector instanceof \ReflectionMethod) { - if (isset($tagAttributes['method'])) { - throw new LogicException(sprintf('"%s" attribute cannot declare a method on "%s::%s()".', $attribute::class, $reflector->class, $reflector->name)); - } - $tagAttributes['method'] = $reflector->getName(); - } - $definition->addTag('kernel.event_listener', $tagAttributes); - }); - } } private function registerDebugConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader): void @@ -1773,6 +1760,7 @@ private function registerSecretsConfiguration(array $config, ContainerBuilder $c if (!$this->readConfigEnabled('secrets', $container, $config)) { $container->removeDefinition('console.command.secrets_set'); $container->removeDefinition('console.command.secrets_list'); + $container->removeDefinition('console.command.secrets_reveal'); $container->removeDefinition('console.command.secrets_remove'); $container->removeDefinition('console.command.secrets_generate_key'); $container->removeDefinition('console.command.secrets_decrypt_to_local'); @@ -1912,18 +1900,19 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder $container->setParameter('serializer.default_context', $defaultContext); } + $arguments = $container->getDefinition('serializer.normalizer.object')->getArguments(); + $context = []; + if (isset($config['circular_reference_handler']) && $config['circular_reference_handler']) { - $arguments = $container->getDefinition('serializer.normalizer.object')->getArguments(); - $context = ($arguments[6] ?? $defaultContext) + ['circular_reference_handler' => new Reference($config['circular_reference_handler'])]; + $context += ($arguments[6] ?? $defaultContext) + ['circular_reference_handler' => new Reference($config['circular_reference_handler'])]; $container->getDefinition('serializer.normalizer.object')->setArgument(5, null); - $container->getDefinition('serializer.normalizer.object')->setArgument(6, $context); } if ($config['max_depth_handler'] ?? false) { - $arguments = $container->getDefinition('serializer.normalizer.object')->getArguments(); - $context = ($arguments[6] ?? $defaultContext) + ['max_depth_handler' => new Reference($config['max_depth_handler'])]; - $container->getDefinition('serializer.normalizer.object')->setArgument(6, $context); + $context += ($arguments[6] ?? $defaultContext) + ['max_depth_handler' => new Reference($config['max_depth_handler'])]; } + + $container->getDefinition('serializer.normalizer.object')->setArgument(6, $context); } private function registerPropertyInfoConfiguration(ContainerBuilder $container, PhpFileLoader $loader): void @@ -1953,6 +1942,25 @@ private function registerPropertyInfoConfiguration(ContainerBuilder $container, } } + private function registerTypeInfoConfiguration(ContainerBuilder $container, PhpFileLoader $loader): void + { + if (!class_exists(Type::class)) { + throw new LogicException('TypeInfo support cannot be enabled as the TypeInfo component is not installed. Try running "composer require symfony/type-info".'); + } + + $loader->load('type_info.php'); + + if (ContainerBuilder::willBeAvailable('phpstan/phpdoc-parser', PhpDocParser::class, ['symfony/framework-bundle', 'symfony/type-info'])) { + $container->register('type_info.resolver.string', StringTypeResolver::class); + + /** @var ServiceLocatorArgument $resolversLocator */ + $resolversLocator = $container->getDefinition('type_info.resolver')->getArgument(0); + $resolversLocator->setValues($resolversLocator->getValues() + [ + 'string' => new Reference('type_info.resolver.string'), + ]); + } + } + private function registerLockConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader): void { $loader->load('lock.php'); @@ -2178,10 +2186,9 @@ private function registerMessengerConfiguration(array $config, ContainerBuilder ->setFactory([new Reference('messenger.transport_factory'), 'createTransport']) ->setArguments([$transport['dsn'], $transport['options'] + ['transport_name' => $name], new Reference($serializerId)]) ->addTag('messenger.receiver', [ - 'alias' => $name, - 'is_failure_transport' => \in_array($name, $failureTransports), - ] - ) + 'alias' => $name, + 'is_failure_transport' => \in_array($name, $failureTransports, true), + ]) ; $container->setDefinition($transportId = 'messenger.transport.'.$name, $transportDefinition); $senderAliases[$name] = $transportId; @@ -2195,7 +2202,8 @@ private function registerMessengerConfiguration(array $config, ContainerBuilder ->replaceArgument(0, $transport['retry_strategy']['max_retries']) ->replaceArgument(1, $transport['retry_strategy']['delay']) ->replaceArgument(2, $transport['retry_strategy']['multiplier']) - ->replaceArgument(3, $transport['retry_strategy']['max_delay']); + ->replaceArgument(3, $transport['retry_strategy']['max_delay']) + ->replaceArgument(4, $transport['retry_strategy']['jitter']); $container->setDefinition($retryServiceId, $retryDefinition); $transportRetryReferences[$name] = new Reference($retryServiceId); @@ -2407,6 +2415,8 @@ private function registerHttpClientConfiguration(array $config, ContainerBuilder $loader->load('http_client.php'); $options = $config['default_options'] ?? []; + $rateLimiter = $options['rate_limiter'] ?? null; + unset($options['rate_limiter']); $retryOptions = $options['retry_failed'] ?? ['enabled' => false]; unset($options['retry_failed']); $defaultUriTemplateVars = $options['vars'] ?? []; @@ -2428,6 +2438,10 @@ private function registerHttpClientConfiguration(array $config, ContainerBuilder $container->removeAlias(HttpClient::class); } + if (null !== $rateLimiter) { + $this->registerThrottlingHttpClient($rateLimiter, 'http_client', $container); + } + if ($this->readConfigEnabled('http_client.retry_failed', $container, $retryOptions)) { $this->registerRetryableHttpClient($retryOptions, 'http_client', $container); } @@ -2449,6 +2463,8 @@ private function registerHttpClientConfiguration(array $config, ContainerBuilder $scope = $scopeConfig['scope'] ?? null; unset($scopeConfig['scope']); + $rateLimiter = $scopeConfig['rate_limiter'] ?? null; + unset($scopeConfig['rate_limiter']); $retryOptions = $scopeConfig['retry_failed'] ?? ['enabled' => false]; unset($scopeConfig['retry_failed']); @@ -2468,6 +2484,10 @@ private function registerHttpClientConfiguration(array $config, ContainerBuilder ; } + if (null !== $rateLimiter) { + $this->registerThrottlingHttpClient($rateLimiter, $name, $container); + } + if ($this->readConfigEnabled('http_client.scoped_clients.'.$name.'.retry_failed', $container, $retryOptions)) { $this->registerRetryableHttpClient($retryOptions, $name, $container); } @@ -2505,6 +2525,25 @@ private function registerHttpClientConfiguration(array $config, ContainerBuilder } } + private function registerThrottlingHttpClient(string $rateLimiter, string $name, ContainerBuilder $container): void + { + if (!class_exists(ThrottlingHttpClient::class)) { + throw new LogicException('Rate limiter support cannot be enabled as version 7.1+ of the HttpClient component is required.'); + } + + if (!$this->isInitializedConfigEnabled('rate_limiter')) { + throw new LogicException('Rate limiter cannot be used within HttpClient as the RateLimiter component is not enabled.'); + } + + $container->register($name.'.throttling.limiter', LimiterInterface::class) + ->setFactory([new Reference('limiter.'.$rateLimiter), 'create']); + + $container + ->register($name.'.throttling', ThrottlingHttpClient::class) + ->setDecoratedService($name, null, 15) // higher priority than RetryableHttpClient (10) + ->setArguments([new Reference($name.'.throttling.inner'), new Reference($name.'.throttling.limiter')]); + } + private function registerRetryableHttpClient(array $options, string $name, ContainerBuilder $container): void { if (null !== $options['retry_strategy']) { @@ -2561,6 +2600,7 @@ private function registerMailerConfiguration(array $config, ContainerBuilder $co } $classToServices = [ + MailerBridge\Azure\Transport\AzureTransportFactory::class => 'mailer.transport_factory.azure', MailerBridge\Brevo\Transport\BrevoTransportFactory::class => 'mailer.transport_factory.brevo', MailerBridge\Google\Transport\GmailTransportFactory::class => 'mailer.transport_factory.gmail', MailerBridge\Infobip\Transport\InfobipTransportFactory::class => 'mailer.transport_factory.infobip', @@ -2570,6 +2610,7 @@ private function registerMailerConfiguration(array $config, ContainerBuilder $co MailerBridge\MailPace\Transport\MailPaceTransportFactory::class => 'mailer.transport_factory.mailpace', MailerBridge\Mailchimp\Transport\MandrillTransportFactory::class => 'mailer.transport_factory.mailchimp', MailerBridge\Postmark\Transport\PostmarkTransportFactory::class => 'mailer.transport_factory.postmark', + MailerBridge\Resend\Transport\ResendTransportFactory::class => 'mailer.transport_factory.resend', MailerBridge\Scaleway\Transport\ScalewayTransportFactory::class => 'mailer.transport_factory.scaleway', MailerBridge\Sendgrid\Transport\SendgridTransportFactory::class => 'mailer.transport_factory.sendgrid', MailerBridge\Amazon\Transport\SesTransportFactory::class => 'mailer.transport_factory.amazon', @@ -2586,9 +2627,11 @@ private function registerMailerConfiguration(array $config, ContainerBuilder $co if ($webhookEnabled) { $webhookRequestParsers = [ MailerBridge\Brevo\Webhook\BrevoRequestParser::class => 'mailer.webhook.request_parser.brevo', + MailerBridge\MailerSend\Webhook\MailerSendRequestParser::class => 'mailer.webhook.request_parser.mailersend', MailerBridge\Mailgun\Webhook\MailgunRequestParser::class => 'mailer.webhook.request_parser.mailgun', MailerBridge\Mailjet\Webhook\MailjetRequestParser::class => 'mailer.webhook.request_parser.mailjet', MailerBridge\Postmark\Webhook\PostmarkRequestParser::class => 'mailer.webhook.request_parser.postmark', + MailerBridge\Resend\Webhook\ResendRequestParser::class => 'mailer.webhook.request_parser.resend', MailerBridge\Sendgrid\Webhook\SendgridRequestParser::class => 'mailer.webhook.request_parser.sendgrid', ]; @@ -2604,6 +2647,7 @@ private function registerMailerConfiguration(array $config, ContainerBuilder $co $envelopeListener = $container->getDefinition('mailer.envelope_listener'); $envelopeListener->setArgument(0, $config['envelope']['sender'] ?? null); $envelopeListener->setArgument(1, $config['envelope']['recipients'] ?? null); + $envelopeListener->setArgument(2, $config['envelope']['allowed_recipients'] ?? []); if ($config['headers']) { $headers = new Definition(Headers::class); @@ -2696,6 +2740,7 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $ NotifierBridge\AllMySms\AllMySmsTransportFactory::class => 'notifier.transport_factory.all-my-sms', NotifierBridge\AmazonSns\AmazonSnsTransportFactory::class => 'notifier.transport_factory.amazon-sns', NotifierBridge\Bandwidth\BandwidthTransportFactory::class => 'notifier.transport_factory.bandwidth', + NotifierBridge\Bluesky\BlueskyTransportFactory::class => 'notifier.transport_factory.bluesky', NotifierBridge\Brevo\BrevoTransportFactory::class => 'notifier.transport_factory.brevo', NotifierBridge\Chatwork\ChatworkTransportFactory::class => 'notifier.transport_factory.chatwork', NotifierBridge\Clickatell\ClickatellTransportFactory::class => 'notifier.transport_factory.clickatell', @@ -2721,6 +2766,7 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $ NotifierBridge\LightSms\LightSmsTransportFactory::class => 'notifier.transport_factory.light-sms', NotifierBridge\LineNotify\LineNotifyTransportFactory::class => 'notifier.transport_factory.line-notify', NotifierBridge\LinkedIn\LinkedInTransportFactory::class => 'notifier.transport_factory.linked-in', + NotifierBridge\Lox24\Lox24TransportFactory::class => 'notifier.transport_factory.lox24', NotifierBridge\Mailjet\MailjetTransportFactory::class => 'notifier.transport_factory.mailjet', NotifierBridge\Mastodon\MastodonTransportFactory::class => 'notifier.transport_factory.mastodon', NotifierBridge\Mattermost\MattermostTransportFactory::class => 'notifier.transport_factory.mattermost', @@ -2738,19 +2784,24 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $ NotifierBridge\PagerDuty\PagerDutyTransportFactory::class => 'notifier.transport_factory.pager-duty', NotifierBridge\Plivo\PlivoTransportFactory::class => 'notifier.transport_factory.plivo', NotifierBridge\Pushover\PushoverTransportFactory::class => 'notifier.transport_factory.pushover', + NotifierBridge\Pushy\PushyTransportFactory::class => 'notifier.transport_factory.pushy', NotifierBridge\Redlink\RedlinkTransportFactory::class => 'notifier.transport_factory.redlink', NotifierBridge\RingCentral\RingCentralTransportFactory::class => 'notifier.transport_factory.ring-central', NotifierBridge\RocketChat\RocketChatTransportFactory::class => 'notifier.transport_factory.rocket-chat', NotifierBridge\Sendberry\SendberryTransportFactory::class => 'notifier.transport_factory.sendberry', NotifierBridge\SimpleTextin\SimpleTextinTransportFactory::class => 'notifier.transport_factory.simple-textin', + NotifierBridge\Sevenio\SevenIoTransportFactory::class => 'notifier.transport_factory.sevenio', NotifierBridge\Sinch\SinchTransportFactory::class => 'notifier.transport_factory.sinch', NotifierBridge\Slack\SlackTransportFactory::class => 'notifier.transport_factory.slack', NotifierBridge\Sms77\Sms77TransportFactory::class => 'notifier.transport_factory.sms77', NotifierBridge\Smsapi\SmsapiTransportFactory::class => 'notifier.transport_factory.smsapi', NotifierBridge\SmsBiuras\SmsBiurasTransportFactory::class => 'notifier.transport_factory.sms-biuras', + NotifierBridge\Smsbox\SmsboxTransportFactory::class => 'notifier.transport_factory.smsbox', NotifierBridge\Smsc\SmscTransportFactory::class => 'notifier.transport_factory.smsc', NotifierBridge\SmsFactor\SmsFactorTransportFactory::class => 'notifier.transport_factory.sms-factor', NotifierBridge\Smsmode\SmsmodeTransportFactory::class => 'notifier.transport_factory.smsmode', + NotifierBridge\SmsSluzba\SmsSluzbaTransportFactory::class => 'notifier.transport_factory.sms-sluzba', + NotifierBridge\Smsense\SmsenseTransportFactory::class => 'notifier.transport_factory.smsense', NotifierBridge\SpotHit\SpotHitTransportFactory::class => 'notifier.transport_factory.spot-hit', NotifierBridge\Telegram\TelegramTransportFactory::class => 'notifier.transport_factory.telegram', NotifierBridge\Telnyx\TelnyxTransportFactory::class => 'notifier.transport_factory.telnyx', @@ -2758,6 +2809,7 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $ NotifierBridge\TurboSms\TurboSmsTransportFactory::class => 'notifier.transport_factory.turbo-sms', NotifierBridge\Twilio\TwilioTransportFactory::class => 'notifier.transport_factory.twilio', NotifierBridge\Twitter\TwitterTransportFactory::class => 'notifier.transport_factory.twitter', + NotifierBridge\Unifonic\UnifonicTransportFactory::class => 'notifier.transport_factory.unifonic', NotifierBridge\Vonage\VonageTransportFactory::class => 'notifier.transport_factory.vonage', NotifierBridge\Yunpian\YunpianTransportFactory::class => 'notifier.transport_factory.yunpian', NotifierBridge\Zendesk\ZendeskTransportFactory::class => 'notifier.transport_factory.zendesk', @@ -2864,7 +2916,8 @@ private function registerRateLimiterConfiguration(array $config, ContainerBuilde // default configuration (when used by other DI extensions) $limiterConfig += ['lock_factory' => 'lock.factory', 'cache_pool' => 'cache.rate_limiter']; - $limiter = $container->setDefinition($limiterId = 'limiter.'.$name, new ChildDefinition('limiter')); + $limiter = $container->setDefinition($limiterId = 'limiter.'.$name, new ChildDefinition('limiter')) + ->addTag('rate_limiter', ['name' => $name]); if (null !== $limiterConfig['lock_factory']) { if (!interface_exists(LockInterface::class)) { @@ -3068,7 +3121,7 @@ private function getPublicDirectory(ContainerBuilder $container): string } $container->addResource(new FileResource($composerFilePath)); - $composerConfig = json_decode(file_get_contents($composerFilePath), true); + $composerConfig = json_decode((new Filesystem())->readFile($composerFilePath), true, flags: \JSON_THROW_ON_ERROR); return isset($composerConfig['extra']['public-dir']) ? $projectDir.'/'.$composerConfig['extra']['public-dir'] : $defaultPublicDir; } diff --git a/src/Symfony/Bundle/FrameworkBundle/EventListener/ConsoleProfilerListener.php b/src/Symfony/Bundle/FrameworkBundle/EventListener/ConsoleProfilerListener.php index c2a71d0a7cf50..7bf23f04c59e4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/EventListener/ConsoleProfilerListener.php +++ b/src/Symfony/Bundle/FrameworkBundle/EventListener/ConsoleProfilerListener.php @@ -77,7 +77,7 @@ public function initialize(ConsoleCommandEvent $event): void return; } - $request->attributes->set('_stopwatch_token', substr(hash('sha256', uniqid(mt_rand(), true)), 0, 6)); + $request->attributes->set('_stopwatch_token', substr(hash('xxh128', uniqid(mt_rand(), true)), 0, 6)); $this->stopwatch->openSection(); } diff --git a/src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php b/src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php index 38eee7361392c..f163708ccb263 100644 --- a/src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php +++ b/src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php @@ -28,19 +28,19 @@ class HttpCache extends BaseHttpCache { protected ?string $cacheDir = null; - protected KernelInterface $kernel; private ?StoreInterface $store = null; - private ?SurrogateInterface $surrogate; private array $options; /** * @param $cache The cache directory (default used if null) or the storage instance */ - public function __construct(KernelInterface $kernel, string|StoreInterface|null $cache = null, ?SurrogateInterface $surrogate = null, ?array $options = null) - { - $this->kernel = $kernel; - $this->surrogate = $surrogate; + public function __construct( + protected KernelInterface $kernel, + string|StoreInterface|null $cache = null, + private ?SurrogateInterface $surrogate = null, + ?array $options = null, + ) { $this->options = $options ?? []; if ($cache instanceof StoreInterface) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Kernel/MicroKernelTrait.php b/src/Symfony/Bundle/FrameworkBundle/Kernel/MicroKernelTrait.php index f8c7460135112..b3f49c0596e12 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Kernel/MicroKernelTrait.php +++ b/src/Symfony/Bundle/FrameworkBundle/Kernel/MicroKernelTrait.php @@ -214,7 +214,7 @@ public function loadRoutes(LoaderInterface $loader): RouteCollection if (\is_array($controller) && [0, 1] === array_keys($controller) && $this === $controller[0]) { $route->setDefault('_controller', ['kernel', $controller[1]]); - } elseif ($controller instanceof \Closure && $this === ($r = new \ReflectionFunction($controller))->getClosureThis() && !str_contains($r->name, '{closure')) { + } elseif ($controller instanceof \Closure && $this === ($r = new \ReflectionFunction($controller))->getClosureThis() && !$r->isAnonymous()) { $route->setDefault('_controller', ['kernel', $r->name]); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php index 334d20426c68c..b4f7dfcf3ea5e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php @@ -33,6 +33,7 @@ use Symfony\Bundle\FrameworkBundle\Command\SecretsGenerateKeysCommand; use Symfony\Bundle\FrameworkBundle\Command\SecretsListCommand; use Symfony\Bundle\FrameworkBundle\Command\SecretsRemoveCommand; +use Symfony\Bundle\FrameworkBundle\Command\SecretsRevealCommand; use Symfony\Bundle\FrameworkBundle\Command\SecretsSetCommand; use Symfony\Bundle\FrameworkBundle\Command\TranslationDebugCommand; use Symfony\Bundle\FrameworkBundle\Command\TranslationUpdateCommand; @@ -355,6 +356,13 @@ ]) ->tag('console.command') + ->set('console.command.secrets_reveal', SecretsRevealCommand::class) + ->args([ + service('secrets.vault'), + service('secrets.local_vault')->ignoreOnInvalid(), + ]) + ->tag('console.command') + ->set('console.command.secrets_decrypt_to_local', SecretsDecryptToLocalCommand::class) ->args([ service('secrets.vault'), diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.php index 06c9632d80003..5434b4c56e6b2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.php @@ -12,6 +12,7 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; use Symfony\Component\Mailer\Bridge\Amazon\Transport\SesTransportFactory; +use Symfony\Component\Mailer\Bridge\Azure\Transport\AzureTransportFactory; use Symfony\Component\Mailer\Bridge\Brevo\Transport\BrevoTransportFactory; use Symfony\Component\Mailer\Bridge\Google\Transport\GmailTransportFactory; use Symfony\Component\Mailer\Bridge\Infobip\Transport\InfobipTransportFactory; @@ -21,6 +22,7 @@ use Symfony\Component\Mailer\Bridge\Mailjet\Transport\MailjetTransportFactory; use Symfony\Component\Mailer\Bridge\MailPace\Transport\MailPaceTransportFactory; use Symfony\Component\Mailer\Bridge\Postmark\Transport\PostmarkTransportFactory; +use Symfony\Component\Mailer\Bridge\Resend\Transport\ResendTransportFactory; use Symfony\Component\Mailer\Bridge\Scaleway\Transport\ScalewayTransportFactory; use Symfony\Component\Mailer\Bridge\Sendgrid\Transport\SendgridTransportFactory; use Symfony\Component\Mailer\Transport\AbstractTransportFactory; @@ -38,69 +40,33 @@ service('http_client')->ignoreOnInvalid(), service('logger')->ignoreOnInvalid(), ]) - ->tag('monolog.logger', ['channel' => 'mailer']) - - ->set('mailer.transport_factory.amazon', SesTransportFactory::class) - ->parent('mailer.transport_factory.abstract') - ->tag('mailer.transport_factory') - - ->set('mailer.transport_factory.brevo', BrevoTransportFactory::class) - ->parent('mailer.transport_factory.abstract') - ->tag('mailer.transport_factory') - - ->set('mailer.transport_factory.gmail', GmailTransportFactory::class) - ->parent('mailer.transport_factory.abstract') - ->tag('mailer.transport_factory') - - ->set('mailer.transport_factory.infobip', InfobipTransportFactory::class) - ->parent('mailer.transport_factory.abstract') - ->tag('mailer.transport_factory') - - ->set('mailer.transport_factory.mailersend', MailerSendTransportFactory::class) - ->parent('mailer.transport_factory.abstract') - ->tag('mailer.transport_factory') - - ->set('mailer.transport_factory.mailchimp', MandrillTransportFactory::class) - ->parent('mailer.transport_factory.abstract') - ->tag('mailer.transport_factory') - - ->set('mailer.transport_factory.mailjet', MailjetTransportFactory::class) - ->parent('mailer.transport_factory.abstract') - ->tag('mailer.transport_factory') - - ->set('mailer.transport_factory.mailgun', MailgunTransportFactory::class) - ->parent('mailer.transport_factory.abstract') - ->tag('mailer.transport_factory') - - ->set('mailer.transport_factory.mailpace', MailPaceTransportFactory::class) - ->parent('mailer.transport_factory.abstract') - ->tag('mailer.transport_factory') - - ->set('mailer.transport_factory.postmark', PostmarkTransportFactory::class) - ->parent('mailer.transport_factory.abstract') - ->tag('mailer.transport_factory') - - ->set('mailer.transport_factory.sendgrid', SendgridTransportFactory::class) - ->parent('mailer.transport_factory.abstract') - ->tag('mailer.transport_factory') - - ->set('mailer.transport_factory.null', NullTransportFactory::class) - ->parent('mailer.transport_factory.abstract') - ->tag('mailer.transport_factory') - - ->set('mailer.transport_factory.scaleway', ScalewayTransportFactory::class) - ->parent('mailer.transport_factory.abstract') - ->tag('mailer.transport_factory') - - ->set('mailer.transport_factory.sendmail', SendmailTransportFactory::class) - ->parent('mailer.transport_factory.abstract') - ->tag('mailer.transport_factory') - - ->set('mailer.transport_factory.smtp', EsmtpTransportFactory::class) - ->parent('mailer.transport_factory.abstract') - ->tag('mailer.transport_factory', ['priority' => -100]) - - ->set('mailer.transport_factory.native', NativeTransportFactory::class) - ->parent('mailer.transport_factory.abstract') - ->tag('mailer.transport_factory'); + ->tag('monolog.logger', ['channel' => 'mailer']); + + $factories = [ + 'amazon' => SesTransportFactory::class, + 'azure' => AzureTransportFactory::class, + 'brevo' => BrevoTransportFactory::class, + 'gmail' => GmailTransportFactory::class, + 'infobip' => InfobipTransportFactory::class, + 'mailchimp' => MandrillTransportFactory::class, + 'mailersend' => MailerSendTransportFactory::class, + 'mailgun' => MailgunTransportFactory::class, + 'mailjet' => MailjetTransportFactory::class, + 'mailpace' => MailPaceTransportFactory::class, + 'native' => NativeTransportFactory::class, + 'null' => NullTransportFactory::class, + 'postmark' => PostmarkTransportFactory::class, + 'resend' => ResendTransportFactory::class, + 'scaleway' => ScalewayTransportFactory::class, + 'sendgrid' => SendgridTransportFactory::class, + 'sendmail' => SendmailTransportFactory::class, + 'smtp' => EsmtpTransportFactory::class, + ]; + + foreach ($factories as $name => $class) { + $container->services() + ->set('mailer.transport_factory.'.$name, $class) + ->parent('mailer.transport_factory.abstract') + ->tag('mailer.transport_factory'); + } }; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_webhook.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_webhook.php index bb487b36c0f21..f9d2b9686ff03 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_webhook.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_webhook.php @@ -13,12 +13,16 @@ use Symfony\Component\Mailer\Bridge\Brevo\RemoteEvent\BrevoPayloadConverter; use Symfony\Component\Mailer\Bridge\Brevo\Webhook\BrevoRequestParser; +use Symfony\Component\Mailer\Bridge\MailerSend\RemoteEvent\MailerSendPayloadConverter; +use Symfony\Component\Mailer\Bridge\MailerSend\Webhook\MailerSendRequestParser; use Symfony\Component\Mailer\Bridge\Mailgun\RemoteEvent\MailgunPayloadConverter; use Symfony\Component\Mailer\Bridge\Mailgun\Webhook\MailgunRequestParser; use Symfony\Component\Mailer\Bridge\Mailjet\RemoteEvent\MailjetPayloadConverter; use Symfony\Component\Mailer\Bridge\Mailjet\Webhook\MailjetRequestParser; use Symfony\Component\Mailer\Bridge\Postmark\RemoteEvent\PostmarkPayloadConverter; use Symfony\Component\Mailer\Bridge\Postmark\Webhook\PostmarkRequestParser; +use Symfony\Component\Mailer\Bridge\Resend\RemoteEvent\ResendPayloadConverter; +use Symfony\Component\Mailer\Bridge\Resend\Webhook\ResendRequestParser; use Symfony\Component\Mailer\Bridge\Sendgrid\RemoteEvent\SendgridPayloadConverter; use Symfony\Component\Mailer\Bridge\Sendgrid\Webhook\SendgridRequestParser; @@ -29,6 +33,11 @@ ->args([service('mailer.payload_converter.brevo')]) ->alias(BrevoRequestParser::class, 'mailer.webhook.request_parser.brevo') + ->set('mailer.payload_converter.mailersend', MailerSendPayloadConverter::class) + ->set('mailer.webhook.request_parser.mailersend', MailerSendRequestParser::class) + ->args([service('mailer.payload_converter.mailersend')]) + ->alias(MailerSendRequestParser::class, 'mailer.webhook.request_parser.mailersend') + ->set('mailer.payload_converter.mailgun', MailgunPayloadConverter::class) ->set('mailer.webhook.request_parser.mailgun', MailgunRequestParser::class) ->args([service('mailer.payload_converter.mailgun')]) @@ -44,6 +53,11 @@ ->args([service('mailer.payload_converter.postmark')]) ->alias(PostmarkRequestParser::class, 'mailer.webhook.request_parser.postmark') + ->set('mailer.payload_converter.resend', ResendPayloadConverter::class) + ->set('mailer.webhook.request_parser.resend', ResendRequestParser::class) + ->args([service('mailer.payload_converter.resend')]) + ->alias(ResendRequestParser::class, 'mailer.webhook.request_parser.resend') + ->set('mailer.payload_converter.sendgrid', SendgridPayloadConverter::class) ->set('mailer.webhook.request_parser.sendgrid', SendgridRequestParser::class) ->args([service('mailer.payload_converter.sendgrid')]) diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.php index 556affd070c6f..df247609653f3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.php @@ -135,6 +135,9 @@ ->tag('messenger.transport_factory') ->set('messenger.transport.in_memory.factory', InMemoryTransportFactory::class) + ->args([ + service('clock')->nullOnInvalid(), + ]) ->tag('messenger.transport_factory') ->tag('kernel.reset', ['method' => 'reset']) @@ -160,6 +163,7 @@ abstract_arg('delay ms'), abstract_arg('multiplier'), abstract_arg('max delay ms'), + abstract_arg('jitter'), ]) // rate limiter diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php index 3feb1c080c623..df9be94ed5e32 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php @@ -20,148 +20,108 @@ ->set('notifier.transport_factory.abstract', AbstractTransportFactory::class) ->abstract() - ->args([service('event_dispatcher'), service('http_client')->ignoreOnInvalid()]) - - ->set('notifier.transport_factory.brevo', Bridge\Brevo\BrevoTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.slack', Bridge\Slack\SlackTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('chatter.transport_factory') - - ->set('notifier.transport_factory.linked-in', Bridge\LinkedIn\LinkedInTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('chatter.transport_factory') - - ->set('notifier.transport_factory.telegram', Bridge\Telegram\TelegramTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('chatter.transport_factory') - - ->set('notifier.transport_factory.mattermost', Bridge\Mattermost\MattermostTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('chatter.transport_factory') - - ->set('notifier.transport_factory.vonage', Bridge\Vonage\VonageTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.rocket-chat', Bridge\RocketChat\RocketChatTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('chatter.transport_factory') - - ->set('notifier.transport_factory.google-chat', Bridge\GoogleChat\GoogleChatTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('chatter.transport_factory') - - ->set('notifier.transport_factory.twilio', Bridge\Twilio\TwilioTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.twitter', Bridge\Twitter\TwitterTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('chatter.transport_factory') - - ->set('notifier.transport_factory.all-my-sms', Bridge\AllMySms\AllMySmsTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.firebase', Bridge\Firebase\FirebaseTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('chatter.transport_factory') - - ->set('notifier.transport_factory.forty-six-elks', Bridge\FortySixElks\FortySixElksTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.free-mobile', Bridge\FreeMobile\FreeMobileTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.spot-hit', Bridge\SpotHit\SpotHitTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.fake-chat', Bridge\FakeChat\FakeChatTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('chatter.transport_factory') - - ->set('notifier.transport_factory.fake-sms', Bridge\FakeSms\FakeSmsTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.ovh-cloud', Bridge\OvhCloud\OvhCloudTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.sinch', Bridge\Sinch\SinchTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.zulip', Bridge\Zulip\ZulipTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('chatter.transport_factory') - - ->set('notifier.transport_factory.infobip', Bridge\Infobip\InfobipTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.isendpro', Bridge\Isendpro\IsendproTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.mobyt', Bridge\Mobyt\MobytTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.smsapi', Bridge\Smsapi\SmsapiTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.esendex', Bridge\Esendex\EsendexTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.sendberry', Bridge\Sendberry\SendberryTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.iqsms', Bridge\Iqsms\IqsmsTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.octopush', Bridge\Octopush\OctopushTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.discord', Bridge\Discord\DiscordTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('chatter.transport_factory') - - ->set('notifier.transport_factory.microsoft-teams', Bridge\MicrosoftTeams\MicrosoftTeamsTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('chatter.transport_factory') - - ->set('notifier.transport_factory.gateway-api', Bridge\GatewayApi\GatewayApiTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.mercure', Bridge\Mercure\MercureTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('chatter.transport_factory') - - ->set('notifier.transport_factory.gitter', Bridge\Gitter\GitterTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('chatter.transport_factory') - - ->set('notifier.transport_factory.clickatell', Bridge\Clickatell\ClickatellTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.contact-everyone', Bridge\ContactEveryone\ContactEveryoneTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') + ->args([ + service('event_dispatcher'), + service('http_client')->ignoreOnInvalid(), + ]); + + $chatterFactories = [ + 'bluesky' => Bridge\Bluesky\BlueskyTransportFactory::class, + 'brevo' => Bridge\Brevo\BrevoTransportFactory::class, + 'chatwork' => Bridge\Chatwork\ChatworkTransportFactory::class, + 'discord' => Bridge\Discord\DiscordTransportFactory::class, + 'fake-chat' => Bridge\FakeChat\FakeChatTransportFactory::class, + 'firebase' => Bridge\Firebase\FirebaseTransportFactory::class, + 'gitter' => Bridge\Gitter\GitterTransportFactory::class, + 'google-chat' => Bridge\GoogleChat\GoogleChatTransportFactory::class, + 'line-notify' => Bridge\LineNotify\LineNotifyTransportFactory::class, + 'linked-in' => Bridge\LinkedIn\LinkedInTransportFactory::class, + 'mastodon' => Bridge\Mastodon\MastodonTransportFactory::class, + 'mattermost' => Bridge\Mattermost\MattermostTransportFactory::class, + 'mercure' => Bridge\Mercure\MercureTransportFactory::class, + 'microsoft-teams' => Bridge\MicrosoftTeams\MicrosoftTeamsTransportFactory::class, + 'pager-duty' => Bridge\PagerDuty\PagerDutyTransportFactory::class, + 'rocket-chat' => Bridge\RocketChat\RocketChatTransportFactory::class, + 'slack' => Bridge\Slack\SlackTransportFactory::class, + 'telegram' => Bridge\Telegram\TelegramTransportFactory::class, + 'twitter' => Bridge\Twitter\TwitterTransportFactory::class, + 'zendesk' => Bridge\Zendesk\ZendeskTransportFactory::class, + 'zulip' => Bridge\Zulip\ZulipTransportFactory::class, + ]; + + foreach ($chatterFactories as $name => $class) { + $container->services() + ->set('notifier.transport_factory.'.$name, $class) + ->parent('notifier.transport_factory.abstract') + ->tag('chatter.transport_factory'); + } + + $texterFactories = [ + 'all-my-sms' => Bridge\AllMySms\AllMySmsTransportFactory::class, + 'bandwidth' => Bridge\Bandwidth\BandwidthTransportFactory::class, + 'click-send' => Bridge\ClickSend\ClickSendTransportFactory::class, + 'clickatell' => Bridge\Clickatell\ClickatellTransportFactory::class, + 'contact-everyone' => Bridge\ContactEveryone\ContactEveryoneTransportFactory::class, + 'engagespot' => Bridge\Engagespot\EngagespotTransportFactory::class, + 'esendex' => Bridge\Esendex\EsendexTransportFactory::class, + 'expo' => Bridge\Expo\ExpoTransportFactory::class, + 'fake-sms' => Bridge\FakeSms\FakeSmsTransportFactory::class, + 'forty-six-elks' => Bridge\FortySixElks\FortySixElksTransportFactory::class, + 'free-mobile' => Bridge\FreeMobile\FreeMobileTransportFactory::class, + 'gateway-api' => Bridge\GatewayApi\GatewayApiTransportFactory::class, + 'go-ip' => Bridge\GoIp\GoIpTransportFactory::class, + 'infobip' => Bridge\Infobip\InfobipTransportFactory::class, + 'iqsms' => Bridge\Iqsms\IqsmsTransportFactory::class, + 'isendpro' => Bridge\Isendpro\IsendproTransportFactory::class, + 'kaz-info-teh' => Bridge\KazInfoTeh\KazInfoTehTransportFactory::class, + 'light-sms' => Bridge\LightSms\LightSmsTransportFactory::class, + 'lox24' => Bridge\Lox24\Lox24TransportFactory::class, + 'mailjet' => Bridge\Mailjet\MailjetTransportFactory::class, + 'message-bird' => Bridge\MessageBird\MessageBirdTransportFactory::class, + 'message-media' => Bridge\MessageMedia\MessageMediaTransportFactory::class, + 'mobyt' => Bridge\Mobyt\MobytTransportFactory::class, + 'novu' => Bridge\Novu\NovuTransportFactory::class, + 'ntfy' => Bridge\Ntfy\NtfyTransportFactory::class, + 'octopush' => Bridge\Octopush\OctopushTransportFactory::class, + 'one-signal' => Bridge\OneSignal\OneSignalTransportFactory::class, + 'orange-sms' => Bridge\OrangeSms\OrangeSmsTransportFactory::class, + 'ovh-cloud' => Bridge\OvhCloud\OvhCloudTransportFactory::class, + 'plivo' => Bridge\Plivo\PlivoTransportFactory::class, + 'pushover' => Bridge\Pushover\PushoverTransportFactory::class, + 'pushy' => Bridge\Pushy\PushyTransportFactory::class, + 'redlink' => Bridge\Redlink\RedlinkTransportFactory::class, + 'ring-central' => Bridge\RingCentral\RingCentralTransportFactory::class, + 'sendberry' => Bridge\Sendberry\SendberryTransportFactory::class, + 'sevenio' => Bridge\Sevenio\SevenIoTransportFactory::class, + 'simple-textin' => Bridge\SimpleTextin\SimpleTextinTransportFactory::class, + 'sinch' => Bridge\Sinch\SinchTransportFactory::class, + 'sms-biuras' => Bridge\SmsBiuras\SmsBiurasTransportFactory::class, + 'sms-factor' => Bridge\SmsFactor\SmsFactorTransportFactory::class, + 'sms-sluzba' => Bridge\SmsSluzba\SmsSluzbaTransportFactory::class, + 'sms77' => Bridge\Sms77\Sms77TransportFactory::class, + 'smsapi' => Bridge\Smsapi\SmsapiTransportFactory::class, + 'smsbox' => Bridge\Smsbox\SmsboxTransportFactory::class, + 'smsc' => Bridge\Smsc\SmscTransportFactory::class, + 'smsense' => Bridge\Smsense\SmsenseTransportFactory::class, + 'smsmode' => Bridge\Smsmode\SmsmodeTransportFactory::class, + 'spot-hit' => Bridge\SpotHit\SpotHitTransportFactory::class, + 'telnyx' => Bridge\Telnyx\TelnyxTransportFactory::class, + 'termii' => Bridge\Termii\TermiiTransportFactory::class, + 'turbo-sms' => Bridge\TurboSms\TurboSmsTransportFactory::class, + 'twilio' => Bridge\Twilio\TwilioTransportFactory::class, + 'unifonic' => Bridge\Unifonic\UnifonicTransportFactory::class, + 'vonage' => Bridge\Vonage\VonageTransportFactory::class, + 'yunpian' => Bridge\Yunpian\YunpianTransportFactory::class, + ]; + + foreach ($texterFactories as $name => $class) { + $container->services() + ->set('notifier.transport_factory.'.$name, $class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory'); + } + $container->services() ->set('notifier.transport_factory.amazon-sns', Bridge\AmazonSns\AmazonSnsTransportFactory::class) ->parent('notifier.transport_factory.abstract') ->tag('texter.transport_factory') @@ -171,136 +131,5 @@ ->parent('notifier.transport_factory.abstract') ->tag('chatter.transport_factory') ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.light-sms', Bridge\LightSms\LightSmsTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.sms-biuras', Bridge\SmsBiuras\SmsBiurasTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.smsc', Bridge\Smsc\SmscTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.sms-factor', Bridge\SmsFactor\SmsFactorTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.message-bird', Bridge\MessageBird\MessageBirdTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.message-media', Bridge\MessageMedia\MessageMediaTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.telnyx', Bridge\Telnyx\TelnyxTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.mailjet', Bridge\Mailjet\MailjetTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.yunpian', Bridge\Yunpian\YunpianTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.turbo-sms', Bridge\TurboSms\TurboSmsTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.sms77', Bridge\Sms77\Sms77TransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.one-signal', Bridge\OneSignal\OneSignalTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.orange-sms', Bridge\OrangeSms\OrangeSmsTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.expo', Bridge\Expo\ExpoTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.kaz-info-teh', Bridge\KazInfoTeh\KazInfoTehTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.engagespot', Bridge\Engagespot\EngagespotTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.zendesk', Bridge\Zendesk\ZendeskTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('chatter.transport_factory') - - ->set('notifier.transport_factory.chatwork', Bridge\Chatwork\ChatworkTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('chatter.transport_factory') - - ->set('notifier.transport_factory.termii', Bridge\Termii\TermiiTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.ring-central', Bridge\RingCentral\RingCentralTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.plivo', Bridge\Plivo\PlivoTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.bandwidth', Bridge\Bandwidth\BandwidthTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.line-notify', Bridge\LineNotify\LineNotifyTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('chatter.transport_factory') - - ->set('notifier.transport_factory.mastodon', Bridge\Mastodon\MastodonTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('chatter.transport_factory') - - ->set('notifier.transport_factory.pager-duty', Bridge\PagerDuty\PagerDutyTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('chatter.transport_factory') - - ->set('notifier.transport_factory.pushover', Bridge\Pushover\PushoverTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.simple-textin', Bridge\SimpleTextin\SimpleTextinTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.click-send', Bridge\ClickSend\ClickSendTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.smsmode', Bridge\Smsmode\SmsmodeTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.novu', Bridge\Novu\NovuTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.ntfy', Bridge\Ntfy\NtfyTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - - ->set('notifier.transport_factory.redlink', Bridge\Redlink\RedlinkTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') - ->set('notifier.transport_factory.go-ip', Bridge\GoIp\GoIpTransportFactory::class) - ->parent('notifier.transport_factory.abstract') - ->tag('texter.transport_factory') ; }; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd index 2f70a3481de11..d8d23168d1887 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd @@ -27,6 +27,7 @@ + @@ -327,6 +328,10 @@ + + + + @@ -609,6 +614,7 @@ + @@ -660,6 +666,7 @@ + @@ -690,6 +697,7 @@ + @@ -756,6 +764,7 @@ + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/secrets.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/secrets.php index a21d282702e13..8192f2f065c6f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/secrets.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/secrets.php @@ -13,6 +13,7 @@ use Symfony\Bundle\FrameworkBundle\Secrets\DotenvVault; use Symfony\Bundle\FrameworkBundle\Secrets\SodiumVault; +use Symfony\Component\DependencyInjection\StaticEnvVarLoader; return static function (ContainerConfigurator $container) { $container->services() @@ -21,6 +22,9 @@ abstract_arg('Secret dir, set in FrameworkExtension'), service('secrets.decryption_key')->ignoreOnInvalid(), ]) + + ->set('secrets.env_var_loader', StaticEnvVarLoader::class) + ->args([service('secrets.vault')]) ->tag('container.env_var_loader') ->set('secrets.decryption_key') diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php index bd3f863aa54b0..1135d37525340 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php @@ -128,6 +128,8 @@ service('property_info')->ignoreOnInvalid(), service('serializer.mapping.class_discriminator_resolver')->ignoreOnInvalid(), null, + null, + service('property_info')->ignoreOnInvalid(), ]) ->tag('serializer.normalizer', ['priority' => -1000]) @@ -139,7 +141,6 @@ service('serializer.mapping.class_discriminator_resolver')->ignoreOnInvalid(), null, [], - service('property_info')->ignoreOnInvalid(), ]) ->set('serializer.denormalizer.array', ArrayDenormalizer::class) diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/type_info.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/type_info.php new file mode 100644 index 0000000000000..71e3646a1e041 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/type_info.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory; +use Symfony\Component\TypeInfo\TypeResolver\ReflectionParameterTypeResolver; +use Symfony\Component\TypeInfo\TypeResolver\ReflectionPropertyTypeResolver; +use Symfony\Component\TypeInfo\TypeResolver\ReflectionReturnTypeResolver; +use Symfony\Component\TypeInfo\TypeResolver\ReflectionTypeResolver; +use Symfony\Component\TypeInfo\TypeResolver\TypeResolver; +use Symfony\Component\TypeInfo\TypeResolver\TypeResolverInterface; + +return static function (ContainerConfigurator $container) { + $container->services() + // type context + ->set('type_info.type_context_factory', TypeContextFactory::class) + ->args([service('type_info.resolver.string')->nullOnInvalid()]) + + // type resolvers + ->set('type_info.resolver', TypeResolver::class) + ->args([service_locator([ + \ReflectionType::class => service('type_info.resolver.reflection_type'), + \ReflectionParameter::class => service('type_info.resolver.reflection_parameter'), + \ReflectionProperty::class => service('type_info.resolver.reflection_property'), + \ReflectionFunctionAbstract::class => service('type_info.resolver.reflection_return'), + ])]) + ->alias(TypeResolverInterface::class, 'type_info.resolver') + + ->set('type_info.resolver.reflection_type', ReflectionTypeResolver::class) + ->args([service('type_info.type_context_factory')]) + + ->set('type_info.resolver.reflection_parameter', ReflectionParameterTypeResolver::class) + ->args([service('type_info.resolver.reflection_type'), service('type_info.type_context_factory')]) + + ->set('type_info.resolver.reflection_property', ReflectionPropertyTypeResolver::class) + ->args([service('type_info.resolver.reflection_type'), service('type_info.type_context_factory')]) + + ->set('type_info.resolver.reflection_return', ReflectionReturnTypeResolver::class) + ->args([service('type_info.resolver.reflection_type'), service('type_info.type_context_factory')]) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Routing/Attribute/AsRoutingConditionService.php b/src/Symfony/Bundle/FrameworkBundle/Routing/Attribute/AsRoutingConditionService.php index 13f8ff26a2ebd..5e481d73aa626 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Routing/Attribute/AsRoutingConditionService.php +++ b/src/Symfony/Bundle/FrameworkBundle/Routing/Attribute/AsRoutingConditionService.php @@ -41,6 +41,10 @@ #[\Attribute(\Attribute::TARGET_CLASS)] class AsRoutingConditionService extends AutoconfigureTag { + /** + * @param string|null $alias The alias of the service to use it in routing condition expressions + * @param int $priority Defines a priority that allows the routing condition service to override a service with the same alias + */ public function __construct( ?string $alias = null, int $priority = 0, diff --git a/src/Symfony/Bundle/FrameworkBundle/Routing/DelegatingLoader.php b/src/Symfony/Bundle/FrameworkBundle/Routing/DelegatingLoader.php index 3239d1094bba5..42d4617739cd4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Routing/DelegatingLoader.php +++ b/src/Symfony/Bundle/FrameworkBundle/Routing/DelegatingLoader.php @@ -29,14 +29,12 @@ class DelegatingLoader extends BaseDelegatingLoader { private bool $loading = false; - private array $defaultOptions; - private array $defaultRequirements; - - public function __construct(LoaderResolverInterface $resolver, array $defaultOptions = [], array $defaultRequirements = []) - { - $this->defaultOptions = $defaultOptions; - $this->defaultRequirements = $defaultRequirements; + public function __construct( + LoaderResolverInterface $resolver, + private array $defaultOptions = [], + private array $defaultRequirements = [], + ) { parent::__construct($resolver); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Routing/Router.php b/src/Symfony/Bundle/FrameworkBundle/Routing/Router.php index 53d8b06d04fc4..4dfb71e747487 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Routing/Router.php +++ b/src/Symfony/Bundle/FrameworkBundle/Routing/Router.php @@ -30,6 +30,8 @@ * This Router creates the Loader only when the cache is empty. * * @author Fabien Potencier + * + * @final since Symfony 7.1 */ class Router extends BaseRouter implements WarmableInterface, ServiceSubscriberInterface { @@ -82,10 +84,14 @@ public function getRouteCollection(): RouteCollection public function warmUp(string $cacheDir, ?string $buildDir = null): array { + if (!$buildDir) { + return []; + } + $currentDir = $this->getOption('cache_dir'); - // force cache generation - $this->setOption('cache_dir', $cacheDir); + // force cache generation in build_dir + $this->setOption('cache_dir', $buildDir); $this->getMatcher(); $this->getGenerator(); diff --git a/src/Symfony/Bundle/FrameworkBundle/Secrets/DotenvVault.php b/src/Symfony/Bundle/FrameworkBundle/Secrets/DotenvVault.php index 994b31d18be59..7bdd74c373583 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Secrets/DotenvVault.php +++ b/src/Symfony/Bundle/FrameworkBundle/Secrets/DotenvVault.php @@ -16,10 +16,9 @@ */ class DotenvVault extends AbstractVault { - private string $dotenvFile; - - public function __construct(string $dotenvFile) - { + public function __construct( + private string $dotenvFile, + ) { $this->dotenvFile = strtr($dotenvFile, '/', \DIRECTORY_SEPARATOR); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/BrowserKitAssertionsTrait.php b/src/Symfony/Bundle/FrameworkBundle/Test/BrowserKitAssertionsTrait.php index 125aa45a74c01..7eea648cb6acd 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Test/BrowserKitAssertionsTrait.php +++ b/src/Symfony/Bundle/FrameworkBundle/Test/BrowserKitAssertionsTrait.php @@ -28,14 +28,14 @@ */ trait BrowserKitAssertionsTrait { - public static function assertResponseIsSuccessful(string $message = ''): void + public static function assertResponseIsSuccessful(string $message = '', bool $verbose = true): void { - self::assertThatForResponse(new ResponseConstraint\ResponseIsSuccessful(), $message); + self::assertThatForResponse(new ResponseConstraint\ResponseIsSuccessful($verbose), $message); } - public static function assertResponseStatusCodeSame(int $expectedCode, string $message = ''): void + public static function assertResponseStatusCodeSame(int $expectedCode, string $message = '', bool $verbose = true): void { - self::assertThatForResponse(new ResponseConstraint\ResponseStatusCodeSame($expectedCode), $message); + self::assertThatForResponse(new ResponseConstraint\ResponseStatusCodeSame($expectedCode, $verbose), $message); } public static function assertResponseFormatSame(?string $expectedFormat, string $message = ''): void @@ -43,9 +43,9 @@ public static function assertResponseFormatSame(?string $expectedFormat, string self::assertThatForResponse(new ResponseConstraint\ResponseFormatSame(self::getRequest(), $expectedFormat), $message); } - public static function assertResponseRedirects(?string $expectedLocation = null, ?int $expectedCode = null, string $message = ''): void + public static function assertResponseRedirects(?string $expectedLocation = null, ?int $expectedCode = null, string $message = '', bool $verbose = true): void { - $constraint = new ResponseConstraint\ResponseIsRedirected(); + $constraint = new ResponseConstraint\ResponseIsRedirected($verbose); if ($expectedLocation) { if (class_exists(ResponseConstraint\ResponseHeaderLocationSame::class)) { $locationConstraint = new ResponseConstraint\ResponseHeaderLocationSame(self::getRequest(), $expectedLocation); @@ -100,9 +100,9 @@ public static function assertResponseCookieValueSame(string $name, string $expec ), $message); } - public static function assertResponseIsUnprocessable(string $message = ''): void + public static function assertResponseIsUnprocessable(string $message = '', bool $verbose = true): void { - self::assertThatForResponse(new ResponseConstraint\ResponseIsUnprocessable(), $message); + self::assertThatForResponse(new ResponseConstraint\ResponseIsUnprocessable($verbose), $message); } public static function assertBrowserHasCookie(string $name, string $path = '/', ?string $domain = null, string $message = ''): void diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/DomCrawlerAssertionsTrait.php b/src/Symfony/Bundle/FrameworkBundle/Test/DomCrawlerAssertionsTrait.php index a167094614097..024c78f75845e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Test/DomCrawlerAssertionsTrait.php +++ b/src/Symfony/Bundle/FrameworkBundle/Test/DomCrawlerAssertionsTrait.php @@ -26,12 +26,12 @@ trait DomCrawlerAssertionsTrait { public static function assertSelectorExists(string $selector, string $message = ''): void { - self::assertThat(self::getCrawler(), new DomCrawlerConstraint\CrawlerSelectorExists($selector), $message); + self::assertThat(self::getCrawler(), new CrawlerSelectorExists($selector), $message); } public static function assertSelectorNotExists(string $selector, string $message = ''): void { - self::assertThat(self::getCrawler(), new LogicalNot(new DomCrawlerConstraint\CrawlerSelectorExists($selector)), $message); + self::assertThat(self::getCrawler(), new LogicalNot(new CrawlerSelectorExists($selector)), $message); } public static function assertSelectorCount(int $expectedCount, string $selector, string $message = ''): void @@ -42,7 +42,7 @@ public static function assertSelectorCount(int $expectedCount, string $selector, public static function assertSelectorTextContains(string $selector, string $text, string $message = ''): void { self::assertThat(self::getCrawler(), LogicalAnd::fromConstraints( - new DomCrawlerConstraint\CrawlerSelectorExists($selector), + new CrawlerSelectorExists($selector), new DomCrawlerConstraint\CrawlerSelectorTextContains($selector, $text) ), $message); } @@ -50,7 +50,7 @@ public static function assertSelectorTextContains(string $selector, string $text public static function assertAnySelectorTextContains(string $selector, string $text, string $message = ''): void { self::assertThat(self::getCrawler(), LogicalAnd::fromConstraints( - new DomCrawlerConstraint\CrawlerSelectorExists($selector), + new CrawlerSelectorExists($selector), new DomCrawlerConstraint\CrawlerAnySelectorTextContains($selector, $text) ), $message); } @@ -58,7 +58,7 @@ public static function assertAnySelectorTextContains(string $selector, string $t public static function assertSelectorTextSame(string $selector, string $text, string $message = ''): void { self::assertThat(self::getCrawler(), LogicalAnd::fromConstraints( - new DomCrawlerConstraint\CrawlerSelectorExists($selector), + new CrawlerSelectorExists($selector), new DomCrawlerConstraint\CrawlerSelectorTextSame($selector, $text) ), $message); } @@ -66,7 +66,7 @@ public static function assertSelectorTextSame(string $selector, string $text, st public static function assertAnySelectorTextSame(string $selector, string $text, string $message = ''): void { self::assertThat(self::getCrawler(), LogicalAnd::fromConstraints( - new DomCrawlerConstraint\CrawlerSelectorExists($selector), + new CrawlerSelectorExists($selector), new DomCrawlerConstraint\CrawlerAnySelectorTextSame($selector, $text) ), $message); } @@ -74,7 +74,7 @@ public static function assertAnySelectorTextSame(string $selector, string $text, public static function assertSelectorTextNotContains(string $selector, string $text, string $message = ''): void { self::assertThat(self::getCrawler(), LogicalAnd::fromConstraints( - new DomCrawlerConstraint\CrawlerSelectorExists($selector), + new CrawlerSelectorExists($selector), new LogicalNot(new DomCrawlerConstraint\CrawlerSelectorTextContains($selector, $text)) ), $message); } @@ -82,7 +82,7 @@ public static function assertSelectorTextNotContains(string $selector, string $t public static function assertAnySelectorTextNotContains(string $selector, string $text, string $message = ''): void { self::assertThat(self::getCrawler(), LogicalAnd::fromConstraints( - new DomCrawlerConstraint\CrawlerSelectorExists($selector), + new CrawlerSelectorExists($selector), new LogicalNot(new DomCrawlerConstraint\CrawlerAnySelectorTextContains($selector, $text)) ), $message); } @@ -100,7 +100,7 @@ public static function assertPageTitleContains(string $expectedTitle, string $me public static function assertInputValueSame(string $fieldName, string $expectedValue, string $message = ''): void { self::assertThat(self::getCrawler(), LogicalAnd::fromConstraints( - new DomCrawlerConstraint\CrawlerSelectorExists("input[name=\"$fieldName\"]"), + new CrawlerSelectorExists("input[name=\"$fieldName\"]"), new DomCrawlerConstraint\CrawlerSelectorAttributeValueSame("input[name=\"$fieldName\"]", 'value', $expectedValue) ), $message); } @@ -108,7 +108,7 @@ public static function assertInputValueSame(string $fieldName, string $expectedV public static function assertInputValueNotSame(string $fieldName, string $expectedValue, string $message = ''): void { self::assertThat(self::getCrawler(), LogicalAnd::fromConstraints( - new DomCrawlerConstraint\CrawlerSelectorExists("input[name=\"$fieldName\"]"), + new CrawlerSelectorExists("input[name=\"$fieldName\"]"), new LogicalNot(new DomCrawlerConstraint\CrawlerSelectorAttributeValueSame("input[name=\"$fieldName\"]", 'value', $expectedValue)) ), $message); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/TestBrowserToken.php b/src/Symfony/Bundle/FrameworkBundle/Test/TestBrowserToken.php index 25d71d084a25b..e186b2c4424f5 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Test/TestBrowserToken.php +++ b/src/Symfony/Bundle/FrameworkBundle/Test/TestBrowserToken.php @@ -21,17 +21,16 @@ */ class TestBrowserToken extends AbstractToken { - private string $firewallName; - - public function __construct(array $roles = [], ?UserInterface $user = null, string $firewallName = 'main') - { + public function __construct( + array $roles = [], + ?UserInterface $user = null, + private string $firewallName = 'main', + ) { parent::__construct($roles); if (null !== $user) { $this->setUser($user); } - - $this->firewallName = $firewallName; } public function getFirewallName(): string diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/RouterCacheWarmerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/RouterCacheWarmerTest.php index 727b566e1ddb3..615010a47be53 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/RouterCacheWarmerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/RouterCacheWarmerTest.php @@ -12,43 +12,68 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\CacheWarmer; use PHPUnit\Framework\TestCase; -use Psr\Container\ContainerInterface; use Symfony\Bundle\FrameworkBundle\CacheWarmer\RouterCacheWarmer; +use Symfony\Component\DependencyInjection\Container; use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface; use Symfony\Component\Routing\RouterInterface; class RouterCacheWarmerTest extends TestCase { - public function testWarmUpWithWarmebleInterface() + public function testWarmUpWithWarmableInterfaceWithBuildDir() { - $containerMock = $this->getMockBuilder(ContainerInterface::class)->onlyMethods(['get', 'has'])->getMock(); + $container = new Container(); - $routerMock = $this->getMockBuilder(testRouterInterfaceWithWarmebleInterface::class)->onlyMethods(['match', 'generate', 'getContext', 'setContext', 'getRouteCollection', 'warmUp'])->getMock(); - $containerMock->expects($this->any())->method('get')->with('router')->willReturn($routerMock); - $routerCacheWarmer = new RouterCacheWarmer($containerMock); + $routerMock = $this->getMockBuilder(testRouterInterfaceWithWarmableInterface::class)->onlyMethods(['match', 'generate', 'getContext', 'setContext', 'getRouteCollection', 'warmUp'])->getMock(); + $container->set('router', $routerMock); + $routerCacheWarmer = new RouterCacheWarmer($container); - $routerCacheWarmer->warmUp('/tmp'); - $routerMock->expects($this->any())->method('warmUp')->with('/tmp')->willReturn([]); + $routerCacheWarmer->warmUp('/tmp/cache', '/tmp/build'); + $routerMock->expects($this->any())->method('warmUp')->with('/tmp/cache', '/tmp/build')->willReturn([]); $this->addToAssertionCount(1); } - public function testWarmUpWithoutWarmebleInterface() + public function testWarmUpWithoutWarmableInterfaceWithBuildDir() { - $containerMock = $this->getMockBuilder(ContainerInterface::class)->onlyMethods(['get', 'has'])->getMock(); + $container = new Container(); - $routerMock = $this->getMockBuilder(testRouterInterfaceWithoutWarmebleInterface::class)->onlyMethods(['match', 'generate', 'getContext', 'setContext', 'getRouteCollection'])->getMock(); - $containerMock->expects($this->any())->method('get')->with('router')->willReturn($routerMock); - $routerCacheWarmer = new RouterCacheWarmer($containerMock); + $routerMock = $this->getMockBuilder(testRouterInterfaceWithoutWarmableInterface::class)->onlyMethods(['match', 'generate', 'getContext', 'setContext', 'getRouteCollection'])->getMock(); + $container->set('router', $routerMock); + $routerCacheWarmer = new RouterCacheWarmer($container); $this->expectException(\LogicException::class); $this->expectExceptionMessage('cannot be warmed up because it does not implement "Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface"'); - $routerCacheWarmer->warmUp('/tmp'); + $routerCacheWarmer->warmUp('/tmp/cache', '/tmp/build'); + } + + public function testWarmUpWithWarmableInterfaceWithoutBuildDir() + { + $container = new Container(); + + $routerMock = $this->getMockBuilder(testRouterInterfaceWithWarmableInterface::class)->onlyMethods(['match', 'generate', 'getContext', 'setContext', 'getRouteCollection', 'warmUp'])->getMock(); + $container->set('router', $routerMock); + $routerCacheWarmer = new RouterCacheWarmer($container); + + $preload = $routerCacheWarmer->warmUp('/tmp'); + $routerMock->expects($this->never())->method('warmUp'); + self::assertSame([], $preload); + $this->addToAssertionCount(1); + } + + public function testWarmUpWithoutWarmableInterfaceWithoutBuildDir() + { + $container = new Container(); + + $routerMock = $this->getMockBuilder(testRouterInterfaceWithoutWarmableInterface::class)->onlyMethods(['match', 'generate', 'getContext', 'setContext', 'getRouteCollection'])->getMock(); + $container->set('router', $routerMock); + $routerCacheWarmer = new RouterCacheWarmer($container); + $preload = $routerCacheWarmer->warmUp('/tmp'); + self::assertSame([], $preload); } } -interface testRouterInterfaceWithWarmebleInterface extends RouterInterface, WarmableInterface +interface testRouterInterfaceWithWarmableInterface extends RouterInterface, WarmableInterface { } -interface testRouterInterfaceWithoutWarmebleInterface extends RouterInterface +interface testRouterInterfaceWithoutWarmableInterface extends RouterInterface { } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CacheClearCommand/CacheClearCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CacheClearCommand/CacheClearCommandTest.php index 78b13905ebf31..b950b5fd96c1c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CacheClearCommand/CacheClearCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CacheClearCommand/CacheClearCommandTest.php @@ -75,7 +75,7 @@ function () use ($file) { $kernelRef = new \ReflectionObject($this->kernel); $kernelFile = $kernelRef->getFileName(); /** @var ResourceInterface[] $meta */ - $meta = unserialize(file_get_contents($containerMetaFile)); + $meta = unserialize($this->fs->readFile($containerMetaFile)); $found = false; foreach ($meta as $resource) { if ((string) $resource === $kernelFile) { @@ -93,7 +93,7 @@ function () use ($file) { ); $this->assertMatchesRegularExpression( sprintf('/\'kernel.container_class\'\s*=>\s*\'%s\'/', $containerClass), - file_get_contents($containerFile), + $this->fs->readFile($containerFile), 'kernel.container_class is properly set on the dumped container' ); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CachePoolClearCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CachePoolClearCommandTest.php index fb73588319cda..3a927f217874d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CachePoolClearCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CachePoolClearCommandTest.php @@ -17,7 +17,7 @@ use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\Tests\TestCase; use Symfony\Component\Console\Tester\CommandCompletionTester; -use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\Container; use Symfony\Component\HttpKernel\CacheClearer\Psr6CacheClearer; use Symfony\Component\HttpKernel\KernelInterface; @@ -54,13 +54,11 @@ public static function provideCompletionSuggestions(): iterable private function getKernel(): MockObject&KernelInterface { - $container = $this->createMock(ContainerInterface::class); - $kernel = $this->createMock(KernelInterface::class); $kernel ->expects($this->any()) ->method('getContainer') - ->willReturn($container); + ->willReturn(new Container()); $kernel ->expects($this->once()) diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CachePoolDeleteCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CachePoolDeleteCommandTest.php index caa7eb550f543..3db39e12173e6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CachePoolDeleteCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CachePoolDeleteCommandTest.php @@ -18,7 +18,7 @@ use Symfony\Bundle\FrameworkBundle\Tests\TestCase; use Symfony\Component\Console\Tester\CommandCompletionTester; use Symfony\Component\Console\Tester\CommandTester; -use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\Container; use Symfony\Component\HttpKernel\CacheClearer\Psr6CacheClearer; use Symfony\Component\HttpKernel\KernelInterface; @@ -108,13 +108,11 @@ public static function provideCompletionSuggestions(): iterable private function getKernel(): MockObject&KernelInterface { - $container = $this->createMock(ContainerInterface::class); - $kernel = $this->createMock(KernelInterface::class); $kernel ->expects($this->any()) ->method('getContainer') - ->willReturn($container); + ->willReturn(new Container()); $kernel ->expects($this->once()) diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CachePruneCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CachePruneCommandTest.php index 54467f1efe879..a2d0ad7fef8f6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CachePruneCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CachePruneCommandTest.php @@ -18,7 +18,7 @@ use Symfony\Component\Cache\PruneableInterface; use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\DependencyInjection\Argument\RewindableGenerator; -use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\Container; use Symfony\Component\HttpKernel\KernelInterface; class CachePruneCommandTest extends TestCase @@ -50,13 +50,11 @@ private function getEmptyRewindableGenerator(): RewindableGenerator private function getKernel(): MockObject&KernelInterface { - $container = $this->createMock(ContainerInterface::class); - $kernel = $this->createMock(KernelInterface::class); $kernel ->expects($this->any()) ->method('getContainer') - ->willReturn($container); + ->willReturn(new Container()); $kernel ->expects($this->once()) diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/RouterMatchCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/RouterMatchCommandTest.php index 7dab41991b1b1..b6b6771f928ab 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/RouterMatchCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/RouterMatchCommandTest.php @@ -16,7 +16,7 @@ use Symfony\Bundle\FrameworkBundle\Command\RouterMatchCommand; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Component\Console\Tester\CommandTester; -use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\Container; use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Routing\RequestContext; use Symfony\Component\Routing\Route; @@ -72,24 +72,11 @@ private function getRouter() private function getKernel() { - $container = $this->createMock(ContainerInterface::class); - $container - ->expects($this->atLeastOnce()) - ->method('has') - ->willReturnCallback(fn ($id) => 'console.command_loader' !== $id) - ; - $container - ->expects($this->any()) - ->method('get') - ->with('router') - ->willReturn($this->getRouter()) - ; - $kernel = $this->createMock(KernelInterface::class); $kernel ->expects($this->any()) ->method('getContainer') - ->willReturn($container) + ->willReturn(new Container()) ; $kernel ->expects($this->once()) diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/SecretsRevealCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/SecretsRevealCommandTest.php new file mode 100644 index 0000000000000..94643db2c92c5 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/SecretsRevealCommandTest.php @@ -0,0 +1,86 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Command; + +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\FrameworkBundle\Command\SecretsRevealCommand; +use Symfony\Bundle\FrameworkBundle\Secrets\AbstractVault; +use Symfony\Bundle\FrameworkBundle\Secrets\DotenvVault; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Tester\CommandTester; + +class SecretsRevealCommandTest extends TestCase +{ + public function testExecute() + { + $vault = $this->createMock(AbstractVault::class); + $vault->method('list')->willReturn(['secretKey' => 'secretValue']); + + $command = new SecretsRevealCommand($vault); + + $tester = new CommandTester($command); + $this->assertSame(Command::SUCCESS, $tester->execute(['name' => 'secretKey'])); + + $this->assertEquals('secretValue', trim($tester->getDisplay(true))); + } + + public function testInvalidName() + { + $vault = $this->createMock(AbstractVault::class); + $vault->method('list')->willReturn(['secretKey' => 'secretValue']); + + $command = new SecretsRevealCommand($vault); + + $tester = new CommandTester($command); + $this->assertSame(Command::INVALID, $tester->execute(['name' => 'undefinedKey'])); + + $this->assertStringContainsString('The secret "undefinedKey" does not exist.', trim($tester->getDisplay(true))); + } + + /** + * @backupGlobals enabled + */ + public function testLocalVaultOverride() + { + $vault = $this->createMock(AbstractVault::class); + $vault->method('list')->willReturn(['secretKey' => 'secretValue']); + + $_ENV = ['secretKey' => 'newSecretValue']; + $localVault = new DotenvVault('/not/a/path'); + + $command = new SecretsRevealCommand($vault, $localVault); + + $tester = new CommandTester($command); + $this->assertSame(Command::SUCCESS, $tester->execute(['name' => 'secretKey'])); + + $this->assertEquals('newSecretValue', trim($tester->getDisplay(true))); + } + + /** + * @backupGlobals enabled + */ + public function testOnlyLocalVaultContainsName() + { + $vault = $this->createMock(AbstractVault::class); + $vault->method('list')->willReturn(['otherKey' => 'secretValue']); + + $_ENV = ['secretKey' => 'secretValue']; + $localVault = new DotenvVault('/not/a/path'); + + $command = new SecretsRevealCommand($vault, $localVault); + + $tester = new CommandTester($command); + $this->assertSame(Command::SUCCESS, $tester->execute(['name' => 'secretKey'])); + + $this->assertEquals('secretValue', trim($tester->getDisplay(true))); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php index 4411d59ba7ea9..9afb5a2fd85f6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php @@ -22,11 +22,11 @@ use Symfony\Component\Console\Output\NullOutput; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Tester\ApplicationTester; +use Symfony\Component\DependencyInjection\Container; use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\HttpKernel\Bundle\BundleInterface; use Symfony\Component\HttpKernel\KernelInterface; @@ -241,12 +241,10 @@ private function createEventForSuggestingPackages(string $command, array $altern private function getKernel(array $bundles, $useDispatcher = false) { - $container = $this->createMock(ContainerInterface::class); - - $requestStack = $this->createMock(RequestStack::class); - $requestStack->expects($this->any()) - ->method('push') - ; + $container = new Container(new ParameterBag([ + 'console.command.ids' => [], + 'console.lazy_command.ids' => [], + ])); if ($useDispatcher) { $dispatcher = $this->createMock(EventDispatcherInterface::class); @@ -255,45 +253,9 @@ private function getKernel(array $bundles, $useDispatcher = false) ->method('dispatch') ; - $container->expects($this->atLeastOnce()) - ->method('get') - ->willReturnMap([ - ['.virtual_request_stack', 2, $requestStack], - ['event_dispatcher', 1, $dispatcher], - ]) - ; + $container->set('event_dispatcher', $dispatcher); } - $container - ->expects($this->exactly(2)) - ->method('hasParameter') - ->willReturnCallback(function (...$args) { - static $series = [ - ['console.command.ids'], - ['console.lazy_command.ids'], - ]; - - $this->assertSame(array_shift($series), $args); - - return true; - }) - ; - - $container - ->expects($this->exactly(2)) - ->method('getParameter') - ->willReturnCallback(function (...$args) { - static $series = [ - ['console.lazy_command.ids'], - ['console.command.ids'], - ]; - - $this->assertSame(array_shift($series), $args); - - return []; - }) - ; - $kernel = $this->createMock(KernelInterface::class); $kernel->expects($this->once())->method('boot'); $kernel diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerResolverTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerResolverTest.php index 3571ac13adbae..7c7398fd32331 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerResolverTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerResolverTest.php @@ -16,7 +16,6 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\ControllerResolver; use Symfony\Component\DependencyInjection\Container; -use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Tests\Controller\ContainerControllerResolverTest; @@ -107,7 +106,7 @@ class_exists(AbstractControllerTest::class); protected function createControllerResolver(?LoggerInterface $logger = null, ?Psr11ContainerInterface $container = null) { if (!$container) { - $container = $this->createMockContainer(); + $container = new Container(); } return new ControllerResolver($container, $logger); @@ -117,11 +116,6 @@ protected function createMockParser() { return $this->createMock(ControllerNameParser::class); } - - protected function createMockContainer() - { - return $this->createMock(ContainerInterface::class); - } } class DummyController extends AbstractController diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php index aed072dfba492..b32d8681b43b3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -28,6 +28,7 @@ use Symfony\Component\RateLimiter\Policy\TokenBucketLimiter; use Symfony\Component\Scheduler\Messenger\SchedulerTransportFactory; use Symfony\Component\Serializer\Encoder\JsonDecode; +use Symfony\Component\TypeInfo\Type; use Symfony\Component\Uid\Factory\UuidFactory; class ConfigurationTest extends TestCase @@ -566,6 +567,46 @@ public function testEnabledLockNeedsResources() ]); } + public function testScopedHttpClientsInheritRateLimiterAndRetryFailedConfiguration() + { + $processor = new Processor(); + $configuration = new Configuration(true); + + $config = $processor->processConfiguration($configuration, [[ + 'http_client' => [ + 'default_options' => ['rate_limiter' => 'default_limiter', 'retry_failed' => ['max_retries' => 77]], + 'scoped_clients' => [ + 'foo' => ['base_uri' => 'http://example.com'], + 'bar' => ['base_uri' => 'http://example.com', 'rate_limiter' => true, 'retry_failed' => true], + 'baz' => ['base_uri' => 'http://example.com', 'rate_limiter' => false, 'retry_failed' => false], + 'qux' => ['base_uri' => 'http://example.com', 'rate_limiter' => 'foo_limiter', 'retry_failed' => ['max_retries' => 88, 'delay' => 999]], + ], + ], + ]]); + + $scopedClients = $config['http_client']['scoped_clients']; + + $this->assertSame('default_limiter', $scopedClients['foo']['rate_limiter']); + $this->assertTrue($scopedClients['foo']['retry_failed']['enabled']); + $this->assertSame(77, $scopedClients['foo']['retry_failed']['max_retries']); + $this->assertSame(1000, $scopedClients['foo']['retry_failed']['delay']); + + $this->assertSame('default_limiter', $scopedClients['bar']['rate_limiter']); + $this->assertTrue($scopedClients['bar']['retry_failed']['enabled']); + $this->assertSame(77, $scopedClients['bar']['retry_failed']['max_retries']); + $this->assertSame(1000, $scopedClients['bar']['retry_failed']['delay']); + + $this->assertNull($scopedClients['baz']['rate_limiter']); + $this->assertFalse($scopedClients['baz']['retry_failed']['enabled']); + $this->assertSame(3, $scopedClients['baz']['retry_failed']['max_retries']); + $this->assertSame(1000, $scopedClients['baz']['retry_failed']['delay']); + + $this->assertSame('foo_limiter', $scopedClients['qux']['rate_limiter']); + $this->assertTrue($scopedClients['qux']['retry_failed']['enabled']); + $this->assertSame(88, $scopedClients['qux']['retry_failed']['max_retries']); + $this->assertSame(999, $scopedClients['qux']['retry_failed']['delay']); + } + protected static function getBundleDefaultConfig() { return [ @@ -660,6 +701,9 @@ protected static function getBundleDefaultConfig() 'throw_exception_on_invalid_index' => false, 'throw_exception_on_invalid_property_path' => true, ], + 'type_info' => [ + 'enabled' => !class_exists(FullStack::class) && class_exists(Type::class), + ], 'property_info' => [ 'enabled' => !class_exists(FullStack::class), ], @@ -670,7 +714,7 @@ protected static function getBundleDefaultConfig() 'https_port' => 443, 'strict_requirements' => true, 'utf8' => true, - 'cache_dir' => '%kernel.cache_dir%', + 'cache_dir' => '%kernel.build_dir%', ], 'session' => [ 'enabled' => false, diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/full.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/full.php index b5d8061e4d0af..4fbf72a9f6eea 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/full.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/full.php @@ -70,6 +70,7 @@ 'default_context' => ['enable_max_depth' => true], ], 'property_info' => true, + 'type_info' => true, 'ide' => 'file%%link%%format', 'request' => [ 'formats' => [ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_rate_limiter.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_rate_limiter.php new file mode 100644 index 0000000000000..c8256d91348d6 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_rate_limiter.php @@ -0,0 +1,27 @@ +loadFromExtension('framework', [ + 'annotations' => false, + 'http_method_override' => false, + 'handle_all_throwables' => true, + 'php_errors' => ['log' => true], + 'rate_limiter' => [ + 'foo_limiter' => [ + 'lock_factory' => null, + 'policy' => 'token_bucket', + 'limit' => 10, + 'rate' => ['interval' => '5 seconds', 'amount' => 10], + ], + ], + 'http_client' => [ + 'default_options' => [ + 'rate_limiter' => 'default_limiter', + ], + 'scoped_clients' => [ + 'foo' => [ + 'base_uri' => 'http://example.com', + 'rate_limiter' => 'foo_limiter', + ], + ], + ], +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer_with_dsn.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer_with_dsn.php index 68387298270a3..3357bf354182f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer_with_dsn.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer_with_dsn.php @@ -13,6 +13,7 @@ 'envelope' => [ 'sender' => 'sender@example.org', 'recipients' => ['redirected@example.org'], + 'allowed_recipients' => ['foobar@example\.org'], ], 'headers' => [ 'from' => 'from@example.org', diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer_with_transports.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer_with_transports.php index 361fe731ccb0e..e51fd056b5912 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer_with_transports.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer_with_transports.php @@ -16,6 +16,7 @@ 'envelope' => [ 'sender' => 'sender@example.org', 'recipients' => ['redirected@example.org', 'redirected1@example.org'], + 'allowed_recipients' => ['foobar@example\.org', '.*@example\.com'], ], 'headers' => [ 'from' => 'from@example.org', diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/trusted_proxies_private_ranges.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/trusted_proxies_private_ranges.php new file mode 100644 index 0000000000000..e3a5dc4a5cace --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/trusted_proxies_private_ranges.php @@ -0,0 +1,5 @@ +loadFromExtension('framework', [ + 'trusted_proxies' => 'private_ranges', +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/type_info.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/type_info.php new file mode 100644 index 0000000000000..0e7dcbae0e1da --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/type_info.php @@ -0,0 +1,11 @@ +loadFromExtension('framework', [ + 'annotations' => false, + 'http_method_override' => false, + 'handle_all_throwables' => true, + 'php_errors' => ['log' => true], + 'type_info' => [ + 'enabled' => true, + ], +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/full.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/full.xml index 92e4405a003fd..fd5d52e1c5de5 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/full.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/full.xml @@ -40,5 +40,6 @@ + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_rate_limiter.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_rate_limiter.xml new file mode 100644 index 0000000000000..8c9dbcdad40a5 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_rate_limiter.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_dsn.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_dsn.xml index 3436cf417caf7..d48b7423afb02 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_dsn.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_dsn.xml @@ -13,6 +13,7 @@ sender@example.org redirected@example.org + foobar@example\.org from@example.org bcc1@example.org diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_transports.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_transports.xml index 1cd8523b680f4..9bfd18d9160b2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_transports.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_transports.xml @@ -16,6 +16,8 @@ sender@example.org redirected@example.org redirected1@example.org + foobar@example\.org + .*@example\.com from@example.org bcc1@example.org diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/trusted_proxies_private_ranges.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/trusted_proxies_private_ranges.xml new file mode 100644 index 0000000000000..700f8495980a6 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/trusted_proxies_private_ranges.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/type_info.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/type_info.xml new file mode 100644 index 0000000000000..0fe4d525d1d5c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/type_info.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/full.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/full.yml index 883e9d6c20ebb..96001f1d2dc88 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/full.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/full.yml @@ -59,6 +59,7 @@ framework: max_depth_handler: my.max.depth.handler default_context: enable_max_depth: true + type_info: ~ property_info: ~ ide: file%%link%%format request: diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_rate_limiter.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_rate_limiter.yml new file mode 100644 index 0000000000000..6376192b76182 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_rate_limiter.yml @@ -0,0 +1,19 @@ +framework: + annotations: false + http_method_override: false + handle_all_throwables: true + php_errors: + log: true + rate_limiter: + foo_limiter: + lock_factory: null + policy: token_bucket + limit: 10 + rate: { interval: '5 seconds', amount: 10 } + http_client: + default_options: + rate_limiter: default_limiter + scoped_clients: + foo: + base_uri: http://example.com + rate_limiter: foo_limiter diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer_with_dsn.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer_with_dsn.yml index e826d6bdcff97..ea703bdad8d1d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer_with_dsn.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer_with_dsn.yml @@ -10,6 +10,8 @@ framework: sender: sender@example.org recipients: - redirected@example.org + allowed_recipients: + - foobar@example\.org headers: from: from@example.org bcc: [bcc1@example.org, bcc2@example.org] diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer_with_transports.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer_with_transports.yml index 59a5f14fd3159..ae10f6aee8896 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer_with_transports.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer_with_transports.yml @@ -13,6 +13,9 @@ framework: recipients: - redirected@example.org - redirected1@example.org + allowed_recipients: + - foobar@example\.org + - .*@example\.com headers: from: from@example.org bcc: [bcc1@example.org, bcc2@example.org] diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/trusted_proxies_private_ranges.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/trusted_proxies_private_ranges.yml new file mode 100644 index 0000000000000..b98bb2f781c1f --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/trusted_proxies_private_ranges.yml @@ -0,0 +1,2 @@ +framework: + trusted_proxies: private_ranges diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/type_info.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/type_info.yml new file mode 100644 index 0000000000000..4d6b405b28821 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/type_info.yml @@ -0,0 +1,8 @@ +framework: + annotations: false + http_method_override: false + handle_all_throwables: true + php_errors: + log: true + type_info: + enabled: true diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php index 845056799d27a..cb97f2a471c3f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php @@ -52,6 +52,8 @@ use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\RetryableHttpClient; use Symfony\Component\HttpClient\ScopingHttpClient; +use Symfony\Component\HttpClient\ThrottlingHttpClient; +use Symfony\Component\HttpFoundation\IpUtils; use Symfony\Component\HttpKernel\DependencyInjection\LoggerPass; use Symfony\Component\HttpKernel\Fragment\FragmentUriGeneratorInterface; use Symfony\Component\Messenger\Bridge\AmazonSqs\Transport\AmazonSqsTransportFactory; @@ -83,7 +85,6 @@ use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Webhook\Client\RequestParser; use Symfony\Component\Webhook\Controller\WebhookController; -use Symfony\Component\Workflow; use Symfony\Component\Workflow\Exception\InvalidDefinitionException; use Symfony\Component\Workflow\Metadata\InMemoryMetadataStore; use Symfony\Component\Workflow\WorkflowEvents; @@ -301,7 +302,15 @@ public function testWorkflows() $this->assertArrayHasKey('index_4', $args); $this->assertNull($args['index_4'], 'Workflows has eventsToDispatch=null'); - $this->assertSame(['workflow' => [['name' => 'article']], 'workflow.workflow' => [['name' => 'article']]], $container->getDefinition('workflow.article')->getTags()); + $tags = $container->getDefinition('workflow.article')->getTags(); + $this->assertArrayHasKey('workflow', $tags); + $this->assertArrayHasKey('workflow.workflow', $tags); + $this->assertSame([['name' => 'article']], $tags['workflow.workflow']); + $this->assertSame('article', $tags['workflow'][0]['name'] ?? null); + $this->assertSame([ + 'title' => 'article workflow', + 'description' => 'workflow for articles', + ], $tags['workflow'][0]['metadata'] ?? null); $this->assertTrue($container->hasDefinition('workflow.article.definition'), 'Workflow definition is registered as a service'); @@ -332,7 +341,14 @@ public function testWorkflows() $this->assertSame('state_machine.abstract', $container->getDefinition('state_machine.pull_request')->getParent()); $this->assertTrue($container->hasDefinition('state_machine.pull_request.definition'), 'State machine definition is registered as a service'); - $this->assertSame(['workflow' => [['name' => 'pull_request']], 'workflow.state_machine' => [['name' => 'pull_request']]], $container->getDefinition('state_machine.pull_request')->getTags()); + $tags = $container->getDefinition('state_machine.pull_request')->getTags(); + $this->assertArrayHasKey('workflow', $tags); + $this->assertArrayHasKey('workflow.state_machine', $tags); + $this->assertSame([['name' => 'pull_request']], $tags['workflow.state_machine']); + $this->assertSame('pull_request', $tags['workflow'][0]['name'] ?? null); + $this->assertSame([ + 'title' => 'workflow title', + ], $tags['workflow'][0]['metadata'] ?? null); $stateMachineDefinition = $container->getDefinition('state_machine.pull_request.definition'); @@ -1630,6 +1646,12 @@ public function testSerializerServiceIsNotRegisteredWhenDisabled() $this->assertFalse($container->hasDefinition('serializer')); } + public function testTypeInfoEnabled() + { + $container = $this->createContainerFromFile('type_info'); + $this->assertTrue($container->has('type_info.resolver')); + } + public function testPropertyInfoEnabled() { $container = $this->createContainerFromFile('property_info'); @@ -1944,9 +1966,6 @@ public function testHttpClientOverrideDefaultOptions() public function testHttpClientRetry() { - if (!class_exists(RetryableHttpClient::class)) { - $this->expectException(LogicException::class); - } $container = $this->createContainerFromFile('http_client_retry'); $this->assertSame([429, 500 => ['GET', 'HEAD']], $container->getDefinition('http_client.retry_strategy')->getArgument(0)); @@ -2004,12 +2023,42 @@ public function testHttpClientFullDefaultOptions() $this->assertSame(['foo' => ['bar' => 'baz']], $defaultOptions['extra']); } + public function testHttpClientRateLimiter() + { + if (!class_exists(ThrottlingHttpClient::class)) { + $this->expectException(LogicException::class); + } + + $container = $this->createContainerFromFile('http_client_rate_limiter'); + + $this->assertTrue($container->hasDefinition('http_client.throttling')); + $definition = $container->getDefinition('http_client.throttling'); + $this->assertSame(ThrottlingHttpClient::class, $definition->getClass()); + $this->assertSame('http_client', $definition->getDecoratedService()[0]); + $this->assertCount(2, $arguments = $definition->getArguments()); + $this->assertInstanceOf(Reference::class, $arguments[0]); + $this->assertSame('http_client.throttling.inner', (string) $arguments[0]); + $this->assertInstanceOf(Reference::class, $arguments[1]); + $this->assertSame('http_client.throttling.limiter', (string) $arguments[1]); + + $this->assertTrue($container->hasDefinition('foo.throttling')); + $definition = $container->getDefinition('foo.throttling'); + $this->assertSame(ThrottlingHttpClient::class, $definition->getClass()); + $this->assertSame('foo', $definition->getDecoratedService()[0]); + $this->assertCount(2, $arguments = $definition->getArguments()); + $this->assertInstanceOf(Reference::class, $arguments[0]); + $this->assertSame('foo.throttling.inner', (string) $arguments[0]); + $this->assertInstanceOf(Reference::class, $arguments[1]); + $this->assertSame('foo.throttling.limiter', (string) $arguments[1]); + } + public static function provideMailer(): iterable { yield [ 'mailer_with_dsn', ['main' => 'smtp://example.com'], ['redirected@example.org'], + ['foobar@example\.org'], ]; yield [ 'mailer_with_transports', @@ -2018,13 +2067,14 @@ public static function provideMailer(): iterable 'transport2' => 'smtp://example2.com', ], ['redirected@example.org', 'redirected1@example.org'], + ['foobar@example\.org', '.*@example\.com'], ]; } /** * @dataProvider provideMailer */ - public function testMailer(string $configFile, array $expectedTransports, array $expectedRecipients) + public function testMailer(string $configFile, array $expectedTransports, array $expectedRecipients, array $expectedAllowedRecipients) { $container = $this->createContainerFromFile($configFile); @@ -2037,6 +2087,7 @@ public function testMailer(string $configFile, array $expectedTransports, array $l = $container->getDefinition('mailer.envelope_listener'); $this->assertSame('sender@example.org', $l->getArgument(0)); $this->assertSame($expectedRecipients, $l->getArgument(1)); + $this->assertSame($expectedAllowedRecipients, $l->getArgument(2)); $this->assertEquals(new Reference('messenger.default_bus', ContainerInterface::NULL_ON_INVALID_REFERENCE), $container->getDefinition('mailer.mailer')->getArgument(1)); $this->assertTrue($container->hasDefinition('mailer.message_listener')); @@ -2286,6 +2337,13 @@ public function testNotifierWithSpecificMessageBus() $this->assertEquals(new Reference('app.another_bus'), $container->getDefinition('notifier.channel.sms')->getArgument(1)); } + public function testTrustedProxiesWithPrivateRanges() + { + $container = $this->createContainerFromFile('trusted_proxies_private_ranges'); + + $this->assertSame(IpUtils::PRIVATE_SUBNETS, array_map('trim', explode(',', $container->getParameter('kernel.trusted_proxies')))); + } + public function testWebhook() { if (!class_exists(WebhookController::class)) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php index 53268ffd283d8..deac159b6f9b0 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php @@ -245,4 +245,24 @@ public function testRateLimiterLockFactory() $container->getDefinition('limiter.without_lock')->getArgument(2); } + + public function testRateLimiterIsTagged() + { + $container = $this->createContainerFromClosure(function (ContainerBuilder $container) { + $container->loadFromExtension('framework', [ + 'annotations' => false, + 'http_method_override' => false, + 'handle_all_throwables' => true, + 'php_errors' => ['log' => true], + 'lock' => true, + 'rate_limiter' => [ + 'first' => ['policy' => 'fixed_window', 'limit' => 10, 'interval' => '1 hour'], + 'second' => ['policy' => 'fixed_window', 'limit' => 10, 'interval' => '1 hour'], + ], + ]); + }); + + $this->assertSame('first', $container->getDefinition('limiter.first')->getTag('rate_limiter')[0]['name']); + $this->assertSame('second', $container->getDefinition('limiter.second')->getTag('rate_limiter')[0]['name']); + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/PropertyInfoTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/PropertyInfoTest.php index c61955d37bc20..18cd61b08519c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/PropertyInfoTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/PropertyInfoTest.php @@ -11,7 +11,8 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; -use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\PropertyInfo\Type as LegacyType; +use Symfony\Component\TypeInfo\Type; class PropertyInfoTest extends AbstractWebTestCase { @@ -19,7 +20,29 @@ public function testPhpDocPriority() { static::bootKernel(['test_case' => 'Serializer']); - $this->assertEquals([new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_INT))], static::getContainer()->get('property_info')->getTypes('Symfony\Bundle\FrameworkBundle\Tests\Functional\Dummy', 'codes')); + $propertyInfo = static::getContainer()->get('property_info'); + + if (!method_exists($propertyInfo, 'getType')) { + $this->markTestSkipped(); + } + + $this->assertEquals(Type::list(Type::int()), $propertyInfo->getType(Dummy::class, 'codes')); + } + + /** + * @group legacy + */ + public function testPhpDocPriorityLegacy() + { + static::bootKernel(['test_case' => 'Serializer']); + + $propertyInfo = static::getContainer()->get('property_info'); + + if (!method_exists($propertyInfo, 'getTypes')) { + $this->markTestSkipped(); + } + + $this->assertEquals([new LegacyType('array', false, null, true, new LegacyType('int'), new LegacyType('int'))], $propertyInfo->getTypes(Dummy::class, 'codes')); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/TypeInfoTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/TypeInfoTest.php new file mode 100644 index 0000000000000..6acdb9c814548 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/TypeInfoTest.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; + +use PHPStan\PhpDocParser\Parser\PhpDocParser; +use Symfony\Bundle\FrameworkBundle\Tests\Functional\app\TypeInfo\Dummy; +use Symfony\Component\TypeInfo\Type; + +class TypeInfoTest extends AbstractWebTestCase +{ + public function testComponent() + { + static::bootKernel(['test_case' => 'TypeInfo']); + + $this->assertEquals(Type::string(), static::getContainer()->get('type_info.resolver')->resolve(new \ReflectionProperty(Dummy::class, 'name'))); + + if (!class_exists(PhpDocParser::class)) { + $this->markTestSkipped('"phpstan/phpdoc-parser" dependency is required.'); + } + + $this->assertEquals(Type::int(), static::getContainer()->get('type_info.resolver')->resolve('int')); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/TypeInfo/Dummy.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/TypeInfo/Dummy.php new file mode 100644 index 0000000000000..0f517df5139d0 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/TypeInfo/Dummy.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Functional\app\TypeInfo; + +/** + * @author Mathias Arlaud + * @author Baptiste Leduc + */ +class Dummy +{ + public string $name; +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/TypeInfo/bundles.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/TypeInfo/bundles.php new file mode 100644 index 0000000000000..15ff182c6fed5 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/TypeInfo/bundles.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Bundle\FrameworkBundle\FrameworkBundle; +use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\TestBundle; + +return [ + new FrameworkBundle(), + new TestBundle(), +]; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/TypeInfo/config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/TypeInfo/config.yml new file mode 100644 index 0000000000000..35c7bb4c46c09 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/TypeInfo/config.yml @@ -0,0 +1,11 @@ +imports: + - { resource: ../config/default.yml } + +framework: + http_method_override: false + type_info: true + +services: + type_info.resolver.alias: + alias: type_info.resolver + public: true diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RouterTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RouterTest.php index 3e185b54c5553..11c0dc7e6e259 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RouterTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RouterTest.php @@ -23,6 +23,8 @@ use Symfony\Component\DependencyInjection\Container; use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException; use Symfony\Component\DependencyInjection\Exception\RuntimeException; +use Symfony\Component\DependencyInjection\ParameterBag\ContainerBag; +use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; use Symfony\Component\Routing\Loader\YamlFileLoader; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; @@ -406,7 +408,8 @@ public function testExceptionOnNonStringParameter() $routes->add('foo', new Route('/%object%')); $sc = $this->getPsr11ServiceContainer($routes); - $parameters = $this->getParameterBag(['object' => new \stdClass()]); + $parameters = new Container(); + $parameters->set('object', new \stdClass()); $router = new Router($sc, 'foo', [], null, $parameters); @@ -424,19 +427,15 @@ public function testExceptionOnNonStringParameterWithSfContainer() $sc = $this->getServiceContainer($routes); - $pc = $this->createMock(ContainerInterface::class); - $pc - ->expects($this->once()) - ->method('get') - ->willReturn(new \stdClass()) - ; + $pc = new Container(); + $pc->set('object', new \stdClass()); $router = new Router($sc, 'foo', [], null, $pc); $this->expectException(RuntimeException::class); $this->expectExceptionMessage('The container parameter "object", used in the route configuration value "/%object%", must be a string or numeric, but it is of type "stdClass".'); - $router->getRouteCollection()->get('foo'); + $router->getRouteCollection(); } /** @@ -483,7 +482,9 @@ public function testGetRouteCollectionAddsContainerParametersResource() $router = new Router($sc, 'foo', [], null, $parameters); - $router->getRouteCollection(); + $routeCollection = $router->getRouteCollection(); + + $this->assertEquals([new ContainerParametersResource(['locale' => 'en'])], $routeCollection->getResources()); } public function testGetRouteCollectionAddsContainerParametersResourceWithSfContainer() @@ -617,13 +618,8 @@ private function getServiceContainer(RouteCollection $routes): Container ->willReturn($routes) ; - $sc = $this->getMockBuilder(Container::class)->onlyMethods(['get'])->getMock(); - - $sc - ->expects($this->once()) - ->method('get') - ->willReturn($loader) - ; + $sc = new Container(); + $sc->set('routing.loader', $loader); return $sc; } @@ -638,26 +634,14 @@ private function getPsr11ServiceContainer(RouteCollection $routes): ContainerInt ->willReturn($routes) ; - $sc = $this->createMock(ContainerInterface::class); - - $sc - ->expects($this->once()) - ->method('get') - ->willReturn($loader) - ; + $container = new Container(); + $container->set('routing.loader', $loader); - return $sc; + return $container; } private function getParameterBag(array $params = []): ContainerInterface { - $bag = $this->createMock(ContainerInterface::class); - $bag - ->expects($this->any()) - ->method('get') - ->willReturnCallback(fn ($key) => $params[$key] ?? null) - ; - - return $bag; + return new ContainerBag(new Container(new ParameterBag($params))); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Secrets/SodiumVaultTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Secrets/SodiumVaultTest.php index 603d13504770f..96d5dcea132a5 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Secrets/SodiumVaultTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Secrets/SodiumVaultTest.php @@ -21,16 +21,18 @@ class SodiumVaultTest extends TestCase { private string $secretsDir; + private Filesystem $filesystem; protected function setUp(): void { + $this->filesystem = new Filesystem(); $this->secretsDir = sys_get_temp_dir().'/sf_secrets/test/'; - (new Filesystem())->remove($this->secretsDir); + $this->filesystem->remove($this->secretsDir); } protected function tearDown(): void { - (new Filesystem())->remove($this->secretsDir); + $this->filesystem->remove($this->secretsDir); } public function testGenerateKeys() @@ -41,8 +43,8 @@ public function testGenerateKeys() $this->assertFileExists($this->secretsDir.'/test.encrypt.public.php'); $this->assertFileExists($this->secretsDir.'/test.decrypt.private.php'); - $encKey = file_get_contents($this->secretsDir.'/test.encrypt.public.php'); - $decKey = file_get_contents($this->secretsDir.'/test.decrypt.private.php'); + $encKey = $this->filesystem->readFile($this->secretsDir.'/test.encrypt.public.php'); + $decKey = $this->filesystem->readFile($this->secretsDir.'/test.decrypt.private.php'); $this->assertFalse($vault->generateKeys()); $this->assertStringEqualsFile($this->secretsDir.'/test.encrypt.public.php', $encKey); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php index 2ef550ef0d8b8..e481a965e717d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php @@ -15,7 +15,7 @@ use Symfony\Bundle\FrameworkBundle\Translation\Translator; use Symfony\Component\Config\Resource\DirectoryResource; use Symfony\Component\Config\Resource\FileExistenceResource; -use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\Container; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Component\Translation\Formatter\MessageFormatter; @@ -100,16 +100,6 @@ public function testTransWithCaching() $this->assertEquals('foobarbax (sr@latin)', $translator->trans('foobarbax')); } - public function testTransWithCachingWithInvalidLocale() - { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Invalid "invalid locale" locale.'); - $loader = $this->createMock(LoaderInterface::class); - $translator = $this->getTranslator($loader, ['cache_dir' => $this->tmpDir], 'loader', TranslatorWithInvalidLocale::class); - - $translator->trans('foo'); - } - public function testLoadResourcesWithoutCaching() { $loader = new YamlFileLoader(); @@ -127,8 +117,7 @@ public function testLoadResourcesWithoutCaching() public function testGetDefaultLocale() { - $container = $this->createMock(\Psr\Container\ContainerInterface::class); - $translator = new Translator($container, new MessageFormatter(), 'en'); + $translator = new Translator(new Container(), new MessageFormatter(), 'en'); $this->assertSame('en', $translator->getLocale()); } @@ -137,9 +126,8 @@ public function testInvalidOptions() { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('The Translator does not support the following options: \'foo\''); - $container = $this->createMock(ContainerInterface::class); - new Translator($container, new MessageFormatter(), 'en', [], ['foo' => 'bar']); + new Translator(new Container(), new MessageFormatter(), 'en', [], ['foo' => 'bar']); } /** @dataProvider getDebugModeAndCacheDirCombinations */ @@ -272,7 +260,7 @@ protected function getLoader() $loader ->expects($this->exactly(7)) ->method('load') - ->willReturnOnConsecutiveCalls( + ->willReturn( $this->getCatalogue('fr', [ 'foo' => 'foo (FR)', ]), @@ -304,12 +292,9 @@ protected function getLoader() protected function getContainer($loader) { - $container = $this->createMock(ContainerInterface::class); - $container - ->expects($this->any()) - ->method('get') - ->willReturn($loader) - ; + $container = new Container(); + $container->set('loader', $loader); + $container->set('yml', $loader); return $container; } @@ -418,11 +403,3 @@ private function createTranslator($loader, $options, $translatorClass = Translat ); } } - -class TranslatorWithInvalidLocale extends Translator -{ - public function getLocale(): string - { - return 'invalid locale'; - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php b/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php index 2f25aa74cc979..870d69ae15ecf 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php +++ b/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php @@ -21,11 +21,11 @@ /** * @author Fabien Potencier + * + * @final since Symfony 7.1 */ class Translator extends BaseTranslator implements WarmableInterface { - protected ContainerInterface $container; - protected array $loaderIds; protected array $options = [ 'cache_dir' => null, 'debug' => false, @@ -57,11 +57,6 @@ class Translator extends BaseTranslator implements WarmableInterface */ private array $scannedDirectories; - /** - * @var string[] - */ - private array $enabledLocales; - /** * Constructor. * @@ -72,14 +67,18 @@ class Translator extends BaseTranslator implements WarmableInterface * * resource_files: List of translation resources available grouped by locale. * * cache_vary: An array of data that is serialized to generate the cached catalogue name. * + * @param string[] $enabledLocales + * * @throws InvalidArgumentException */ - public function __construct(ContainerInterface $container, MessageFormatterInterface $formatter, string $defaultLocale, array $loaderIds = [], array $options = [], array $enabledLocales = []) - { - $this->container = $container; - $this->loaderIds = $loaderIds; - $this->enabledLocales = $enabledLocales; - + public function __construct( + protected ContainerInterface $container, + MessageFormatterInterface $formatter, + string $defaultLocale, + protected array $loaderIds = [], + array $options = [], + private array $enabledLocales = [], + ) { // check option names if ($diff = array_diff(array_keys($options), array_keys($this->options))) { throw new InvalidArgumentException(sprintf('The Translator does not support the following options: \'%s\'.', implode('\', \'', $diff))); diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index ed45087d26ebf..af934f35df91f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -21,14 +21,14 @@ "ext-xml": "*", "symfony/cache": "^6.4|^7.0", "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", + "symfony/dependency-injection": "^7.1", "symfony/deprecation-contracts": "^2.5|^3", "symfony/error-handler": "^6.4|^7.0", "symfony/event-dispatcher": "^6.4|^7.0", "symfony/http-foundation": "^6.4|^7.0", "symfony/http-kernel": "^6.4|^7.0", "symfony/polyfill-mbstring": "~1.0", - "symfony/filesystem": "^6.4|^7.0", + "symfony/filesystem": "^7.1", "symfony/finder": "^6.4|^7.0", "symfony/routing": "^6.4|^7.0" }, @@ -64,6 +64,7 @@ "symfony/string": "^6.4|^7.0", "symfony/translation": "^6.4|^7.0", "symfony/twig-bundle": "^6.4|^7.0", + "symfony/type-info": "^7.1", "symfony/validator": "^6.4|^7.0", "symfony/workflow": "^6.4|^7.0", "symfony/yaml": "^6.4|^7.0", diff --git a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md index e5ff8a1fb7a49..abc0c49762e9f 100644 --- a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md @@ -1,6 +1,13 @@ CHANGELOG ========= +7.1 +--- + + * Mark class `ExpressionCacheWarmer` as `final` + * Support multiple signature algorithms for OIDC Token + * Support JWK or JWKSet for OIDC Token + 7.0 --- diff --git a/src/Symfony/Bundle/SecurityBundle/CacheWarmer/ExpressionCacheWarmer.php b/src/Symfony/Bundle/SecurityBundle/CacheWarmer/ExpressionCacheWarmer.php index 6da90a0141044..5b146871cbe07 100644 --- a/src/Symfony/Bundle/SecurityBundle/CacheWarmer/ExpressionCacheWarmer.php +++ b/src/Symfony/Bundle/SecurityBundle/CacheWarmer/ExpressionCacheWarmer.php @@ -15,6 +15,9 @@ use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface; use Symfony\Component\Security\Core\Authorization\ExpressionLanguage; +/** + * @final since Symfony 7.1 + */ class ExpressionCacheWarmer implements CacheWarmerInterface { private iterable $expressions; diff --git a/src/Symfony/Bundle/SecurityBundle/Command/DebugFirewallCommand.php b/src/Symfony/Bundle/SecurityBundle/Command/DebugFirewallCommand.php index 6afabfbec9b45..ffc3035a53eb5 100644 --- a/src/Symfony/Bundle/SecurityBundle/Command/DebugFirewallCommand.php +++ b/src/Symfony/Bundle/SecurityBundle/Command/DebugFirewallCommand.php @@ -238,7 +238,7 @@ private function formatCallable(mixed $callable): string if ($callable instanceof \Closure) { $r = new \ReflectionFunction($callable); - if (str_contains($r->name, '{closure')) { + if ($r->isAnonymous()) { return 'Closure()'; } if ($class = $r->getClosureCalledClass()) { diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/MakeFirewallsEventDispatcherTraceablePass.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/MakeFirewallsEventDispatcherTraceablePass.php index 85f2f2955a61a..d786317ec1f2f 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/MakeFirewallsEventDispatcherTraceablePass.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/MakeFirewallsEventDispatcherTraceablePass.php @@ -50,7 +50,9 @@ public function process(ContainerBuilder $container): void new Reference('debug.stopwatch'), new Reference('logger', ContainerInterface::NULL_ON_INVALID_REFERENCE), new Reference('request_stack', ContainerInterface::NULL_ON_INVALID_REFERENCE), - ]); + ]) + ->addTag('monolog.logger', ['channel' => 'event']) + ->addTag('kernel.reset', ['method' => 'reset']); } foreach (['kernel.event_subscriber', 'kernel.event_listener'] as $tagName) { diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterCsrfFeaturesPass.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterCsrfFeaturesPass.php index 20b79b07c49d2..1d2c0f835dda0 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterCsrfFeaturesPass.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterCsrfFeaturesPass.php @@ -13,10 +13,12 @@ use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\Security\Csrf\TokenStorage\ClearableTokenStorageInterface; use Symfony\Component\Security\Http\EventListener\CsrfProtectionListener; use Symfony\Component\Security\Http\EventListener\CsrfTokenClearingLogoutListener; +use Symfony\Component\Security\Http\EventListener\IsCsrfTokenValidAttributeListener; /** * @author Christian Flothmann @@ -34,6 +36,10 @@ public function process(ContainerBuilder $container): void private function registerCsrfProtectionListener(ContainerBuilder $container): void { + if (!$container->hasDefinition('cache.system')) { + $container->removeDefinition('cache.security_is_csrf_token_valid_attribute_expression_language'); + } + if (!$container->has('security.authenticator.manager') || !$container->has('security.csrf.token_manager')) { return; } @@ -41,6 +47,11 @@ private function registerCsrfProtectionListener(ContainerBuilder $container): vo $container->register('security.listener.csrf_protection', CsrfProtectionListener::class) ->addArgument(new Reference('security.csrf.token_manager')) ->addTag('kernel.event_subscriber'); + + $container->register('controller.is_csrf_token_valid_attribute_listener', IsCsrfTokenValidAttributeListener::class) + ->addArgument(new Reference('security.csrf.token_manager')) + ->addArgument(new Reference('security.is_csrf_token_valid_attribute_expression_language', ContainerInterface::NULL_ON_INVALID_REFERENCE)) + ->addTag('kernel.event_subscriber'); } protected function registerLogoutHandler(ContainerBuilder $container): void diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/AccessToken/CasTokenHandlerFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/AccessToken/CasTokenHandlerFactory.php new file mode 100644 index 0000000000000..a0c2ca047bc40 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/AccessToken/CasTokenHandlerFactory.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken; + +use Symfony\Component\Config\Definition\Builder\NodeBuilder; +use Symfony\Component\DependencyInjection\ChildDefinition; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\Security\Http\AccessToken\Cas\Cas2Handler; + +class CasTokenHandlerFactory implements TokenHandlerFactoryInterface +{ + public function create(ContainerBuilder $container, string $id, array|string $config): void + { + $container->setDefinition($id, new ChildDefinition('security.access_token_handler.cas')); + + $container + ->register('security.access_token_handler.cas', Cas2Handler::class) + ->setArguments([ + new Reference('request_stack'), + $config['validation_url'], + $config['prefix'], + $config['http_client'] ? new Reference($config['http_client']) : null, + ]); + } + + public function getKey(): string + { + return 'cas'; + } + + public function addConfiguration(NodeBuilder $node): void + { + $node + ->arrayNode($this->getKey()) + ->fixXmlConfig($this->getKey()) + ->children() + ->scalarNode('validation_url') + ->info('CAS server validation URL') + ->isRequired() + ->end() + ->scalarNode('prefix') + ->info('CAS prefix') + ->defaultValue('cas') + ->end() + ->scalarNode('http_client') + ->info('HTTP Client service') + ->defaultNull() + ->end() + ->end() + ->end(); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/AccessToken/OidcTokenHandlerFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/AccessToken/OidcTokenHandlerFactory.php index 7be00eaff35df..a1b418129f088 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/AccessToken/OidcTokenHandlerFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/AccessToken/OidcTokenHandlerFactory.php @@ -13,10 +13,10 @@ use Jose\Component\Core\Algorithm; use Symfony\Component\Config\Definition\Builder\NodeBuilder; +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Exception\LogicException; -use Symfony\Component\DependencyInjection\Reference; /** * Configures a token handler for decoding and validating an OIDC token. @@ -31,22 +31,15 @@ public function create(ContainerBuilder $container, string $id, array|string $co ->replaceArgument(4, $config['claim']) ); - if (!ContainerBuilder::willBeAvailable('web-token/jwt-core', Algorithm::class, ['symfony/security-bundle'])) { - throw new LogicException('You cannot use the "oidc" token handler since "web-token/jwt-core" is not installed. Try running "composer require web-token/jwt-core".'); + if (!ContainerBuilder::willBeAvailable('web-token/jwt-library', Algorithm::class, ['symfony/security-bundle'])) { + throw new LogicException('You cannot use the "oidc" token handler since "web-token/jwt-library" is not installed. Try running "composer require web-token/jwt-library".'); } - // @see Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SignatureAlgorithmFactory - // for supported algorithms - if (\in_array($config['algorithm'], ['ES256', 'ES384', 'ES512'], true)) { - $tokenHandlerDefinition->replaceArgument(0, new Reference('security.access_token_handler.oidc.signature.'.$config['algorithm'])); - } else { - $tokenHandlerDefinition->replaceArgument(0, (new ChildDefinition('security.access_token_handler.oidc.signature')) - ->replaceArgument(0, $config['algorithm']) - ); - } + $tokenHandlerDefinition->replaceArgument(0, (new ChildDefinition('security.access_token_handler.oidc.signature')) + ->replaceArgument(0, $config['algorithms'])); - $tokenHandlerDefinition->replaceArgument(1, (new ChildDefinition('security.access_token_handler.oidc.jwk')) - ->replaceArgument(0, $config['key']) + $tokenHandlerDefinition->replaceArgument(1, (new ChildDefinition('security.access_token_handler.oidc.jwkset')) + ->replaceArgument(0, $config['keyset']) ); } @@ -60,6 +53,37 @@ public function addConfiguration(NodeBuilder $node): void $node ->arrayNode($this->getKey()) ->fixXmlConfig($this->getKey()) + ->validate() + ->ifTrue(static fn ($v) => !isset($v['algorithm']) && !isset($v['algorithms'])) + ->thenInvalid('You must set either "algorithm" or "algorithms".') + ->end() + ->validate() + ->ifTrue(static fn ($v) => !isset($v['key']) && !isset($v['keyset'])) + ->thenInvalid('You must set either "key" or "keyset".') + ->end() + ->beforeNormalization() + ->ifTrue(static fn ($v) => isset($v['algorithm']) && \is_string($v['algorithm'])) + ->then(static function ($v) { + if (isset($v['algorithms'])) { + throw new InvalidConfigurationException('You cannot use both "algorithm" and "algorithms" at the same time.'); + } + $v['algorithms'] = [$v['algorithm']]; + unset($v['algorithm']); + + return $v; + }) + ->end() + ->beforeNormalization() + ->ifTrue(static fn ($v) => isset($v['key']) && \is_string($v['key'])) + ->then(static function ($v) { + if (isset($v['keyset'])) { + throw new InvalidConfigurationException('You cannot use both "key" and "keyset" at the same time.'); + } + $v['keyset'] = sprintf('{"keys":[%s]}', $v['key']); + + return $v; + }) + ->end() ->children() ->scalarNode('claim') ->info('Claim which contains the user identifier (e.g.: sub, email..).') @@ -72,14 +96,23 @@ public function addConfiguration(NodeBuilder $node): void ->arrayNode('issuers') ->info('Issuers allowed to generate the token, for validation purpose.') ->isRequired() - ->prototype('scalar')->end() + ->scalarPrototype()->end() ->end() - ->scalarNode('algorithm') + ->arrayNode('algorithm') ->info('Algorithm used to sign the token.') + ->setDeprecated('symfony/security-bundle', '7.1', 'The "%node%" option is deprecated and will be removed in 8.0. Use the "algorithms" option instead.') + ->end() + ->arrayNode('algorithms') + ->info('Algorithms used to sign the token.') ->isRequired() + ->scalarPrototype()->end() ->end() ->scalarNode('key') ->info('JSON-encoded JWK used to sign the token (must contain a "kty" key).') + ->setDeprecated('symfony/security-bundle', '7.1', 'The "%node%" option is deprecated and will be removed in 8.0. Use the "keyset" option instead.') + ->end() + ->scalarNode('keyset') + ->info('JSON-encoded JWKSet used to sign the token (must contain a list of valid keys).') ->isRequired() ->end() ->end() diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SignatureAlgorithmFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SignatureAlgorithmFactory.php index feb63c26350be..e69de29bb2d1d 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SignatureAlgorithmFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SignatureAlgorithmFactory.php @@ -1,43 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory; - -use Jose\Component\Core\Algorithm as AlgorithmInterface; -use Jose\Component\Signature\Algorithm; -use Symfony\Component\Security\Core\Exception\InvalidArgumentException; -use Symfony\Component\Security\Http\AccessToken\Oidc\OidcTokenHandler; - -/** - * Creates a signature algorithm for {@see OidcTokenHandler}. - * - * @internal - */ -final class SignatureAlgorithmFactory -{ - public static function create(string $algorithm): AlgorithmInterface - { - switch ($algorithm) { - case 'ES256': - case 'ES384': - case 'ES512': - if (!class_exists(Algorithm::class.'\\'.$algorithm)) { - throw new \LogicException(sprintf('You cannot use the "%s" signature algorithm since "web-token/jwt-signature-algorithm-ecdsa" is not installed. Try running "composer require web-token/jwt-signature-algorithm-ecdsa".', $algorithm)); - } - - $algorithm = Algorithm::class.'\\'.$algorithm; - - return new $algorithm(); - } - - throw new InvalidArgumentException(sprintf('Unsupported signature algorithm "%s". Only ES* algorithms are supported. If you want to use another algorithm, create your TokenHandler as a service.', $algorithm)); - } -} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index e5e37ed4e3401..383c7c41b3c9e 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -16,7 +16,6 @@ use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FirewallListenerFactoryInterface; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\StatelessAuthenticatorFactoryInterface; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\UserProviderFactoryInterface; -use Symfony\Bundle\SecurityBundle\SecurityBundle; use Symfony\Component\Config\Definition\ConfigurationInterface; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\Config\FileLocator; @@ -61,6 +60,7 @@ use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Http\Authenticator\Debug\TraceableAuthenticatorManagerListener; use Symfony\Component\Security\Http\Event\CheckPassportEvent; +use Symfony\Flex\Command\InstallRecipesCommand; /** * SecurityExtension. @@ -91,7 +91,9 @@ public function prepend(ContainerBuilder $container): void public function load(array $configs, ContainerBuilder $container): void { if (!array_filter($configs)) { - throw new InvalidConfigurationException(sprintf('Enabling bundle "%s" and not configuring it is not allowed.', SecurityBundle::class)); + $hint = class_exists(InstallRecipesCommand::class) ? 'Try running "composer symfony:recipes:install symfony/security-bundle".' : 'Please define your settings for the "security" config section.'; + + throw new InvalidConfigurationException('The SecurityBundle is enabled but is not configured. '.$hint); } $mainConfig = $this->getConfiguration($configs, $container); @@ -121,6 +123,7 @@ public function load(array $configs, ContainerBuilder $container): void $container->removeDefinition('security.expression_language'); $container->removeDefinition('security.access.expression_voter'); $container->removeDefinition('security.is_granted_attribute_expression_language'); + $container->removeDefinition('security.is_csrf_token_valid_attribute_expression_language'); } if (!class_exists(PasswordHasherExtension::class)) { @@ -752,7 +755,7 @@ private function createHasher(array $config): Reference|array $config['algorithm'] = 'native'; $config['native_algorithm'] = \PASSWORD_ARGON2I; } else { - throw new InvalidConfigurationException(sprintf('Algorithm "argon2i" is not available. Either use "%s" or upgrade to PHP 7.2+ instead.', \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13') ? 'argon2id", "auto' : 'auto')); + throw new InvalidConfigurationException(sprintf('Algorithm "argon2i" is not available; use "%s" instead.', \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13') ? 'argon2id" or "auto' : 'auto')); } return $this->createHasher($config); @@ -765,7 +768,7 @@ private function createHasher(array $config): Reference|array $config['algorithm'] = 'native'; $config['native_algorithm'] = \PASSWORD_ARGON2ID; } else { - throw new InvalidConfigurationException(sprintf('Algorithm "argon2id" is not available. Either use "%s", upgrade to PHP 7.3+ or use libsodium 1.0.15+ instead.', \defined('PASSWORD_ARGON2I') || $hasSodium ? 'argon2i", "auto' : 'auto')); + throw new InvalidConfigurationException(sprintf('Algorithm "argon2id" is not available; use "%s" or libsodium 1.0.15+ instead.', \defined('PASSWORD_ARGON2I') || $hasSodium ? 'argon2i", "auto' : 'auto')); } return $this->createHasher($config); diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.php b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.php index 8019058e9a55e..852ce968d16d3 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.php +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.php @@ -303,5 +303,12 @@ ->set('cache.security_is_granted_attribute_expression_language') ->parent('cache.system') ->tag('cache.pool') + + ->set('security.is_csrf_token_valid_attribute_expression_language', BaseExpressionLanguage::class) + ->args([service('cache.security_is_csrf_token_valid_attribute_expression_language')->nullOnInvalid()]) + + ->set('cache.security_is_csrf_token_valid_attribute_expression_language') + ->parent('cache.system') + ->tag('cache.pool') ; }; diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator_access_token.php b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator_access_token.php index 66716b23ad892..c0fced49ae9ca 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator_access_token.php +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator_access_token.php @@ -11,12 +11,19 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; -use Jose\Component\Core\Algorithm; +use Jose\Component\Core\AlgorithmManager; +use Jose\Component\Core\AlgorithmManagerFactory; use Jose\Component\Core\JWK; +use Jose\Component\Core\JWKSet; use Jose\Component\Signature\Algorithm\ES256; use Jose\Component\Signature\Algorithm\ES384; use Jose\Component\Signature\Algorithm\ES512; -use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SignatureAlgorithmFactory; +use Jose\Component\Signature\Algorithm\PS256; +use Jose\Component\Signature\Algorithm\PS384; +use Jose\Component\Signature\Algorithm\PS512; +use Jose\Component\Signature\Algorithm\RS256; +use Jose\Component\Signature\Algorithm\RS384; +use Jose\Component\Signature\Algorithm\RS512; use Symfony\Component\Security\Http\AccessToken\ChainAccessTokenExtractor; use Symfony\Component\Security\Http\AccessToken\FormEncodedBodyExtractor; use Symfony\Component\Security\Http\AccessToken\HeaderAccessTokenExtractor; @@ -77,28 +84,56 @@ ->set('security.access_token_handler.oidc.jwk', JWK::class) ->abstract() + ->deprecate('symfony/security-http', '7.1', 'The "%service_id%" service is deprecated. Please use "security.access_token_handler.oidc.jwkset" instead') ->factory([JWK::class, 'createFromJson']) ->args([ abstract_arg('signature key'), ]) - ->set('security.access_token_handler.oidc.signature', Algorithm::class) + ->set('security.access_token_handler.oidc.jwkset', JWKSet::class) ->abstract() - ->factory([SignatureAlgorithmFactory::class, 'create']) + ->factory([JWKSet::class, 'createFromJson']) ->args([ - abstract_arg('signature algorithm'), + abstract_arg('signature keyset'), + ]) + + ->set('security.access_token_handler.oidc.algorithm_manager_factory', AlgorithmManagerFactory::class) + ->args([ + tagged_iterator('security.access_token_handler.oidc.signature_algorithm'), + ]) + + ->set('security.access_token_handler.oidc.signature', AlgorithmManager::class) + ->abstract() + ->factory([service('security.access_token_handler.oidc.algorithm_manager_factory'), 'create']) + ->args([ + abstract_arg('signature algorithms'), ]) ->set('security.access_token_handler.oidc.signature.ES256', ES256::class) - ->parent('security.access_token_handler.oidc.signature') - ->args(['index_0' => 'ES256']) + ->tag('security.access_token_handler.oidc.signature_algorithm') ->set('security.access_token_handler.oidc.signature.ES384', ES384::class) - ->parent('security.access_token_handler.oidc.signature') - ->args(['index_0' => 'ES384']) + ->tag('security.access_token_handler.oidc.signature_algorithm') ->set('security.access_token_handler.oidc.signature.ES512', ES512::class) - ->parent('security.access_token_handler.oidc.signature') - ->args(['index_0' => 'ES512']) + ->tag('security.access_token_handler.oidc.signature_algorithm') + + ->set('security.access_token_handler.oidc.signature.RS256', RS256::class) + ->tag('security.access_token_handler.oidc.signature_algorithm') + + ->set('security.access_token_handler.oidc.signature.RS384', RS384::class) + ->tag('security.access_token_handler.oidc.signature_algorithm') + + ->set('security.access_token_handler.oidc.signature.RS512', RS512::class) + ->tag('security.access_token_handler.oidc.signature_algorithm') + + ->set('security.access_token_handler.oidc.signature.PS256', PS256::class) + ->tag('security.access_token_handler.oidc.signature_algorithm') + + ->set('security.access_token_handler.oidc.signature.PS384', PS384::class) + ->tag('security.access_token_handler.oidc.signature_algorithm') + + ->set('security.access_token_handler.oidc.signature.PS512', PS512::class) + ->tag('security.access_token_handler.oidc.signature_algorithm') ; }; diff --git a/src/Symfony/Bundle/SecurityBundle/Security/FirewallConfig.php b/src/Symfony/Bundle/SecurityBundle/Security/FirewallConfig.php index 6525a23e4b9c5..16edc6319a806 100644 --- a/src/Symfony/Bundle/SecurityBundle/Security/FirewallConfig.php +++ b/src/Symfony/Bundle/SecurityBundle/Security/FirewallConfig.php @@ -29,7 +29,7 @@ public function __construct( private readonly ?string $accessDeniedUrl = null, private readonly array $authenticators = [], private readonly ?array $switchUser = null, - private readonly ?array $logout = null + private readonly ?array $logout = null, ) { } diff --git a/src/Symfony/Bundle/SecurityBundle/Security/FirewallMap.php b/src/Symfony/Bundle/SecurityBundle/Security/FirewallMap.php index 6f1bdfcdd4892..fbb44caeded62 100644 --- a/src/Symfony/Bundle/SecurityBundle/Security/FirewallMap.php +++ b/src/Symfony/Bundle/SecurityBundle/Security/FirewallMap.php @@ -46,13 +46,7 @@ public function getListeners(Request $request): array public function getFirewallConfig(Request $request): ?FirewallConfig { - $context = $this->getFirewallContext($request); - - if (null === $context) { - return null; - } - - return $context->getConfig(); + return $this->getFirewallContext($request)?->getConfig(); } private function getFirewallContext(Request $request): ?FirewallContext diff --git a/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php b/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php index b2e81a7f4b92b..3247ff1276ffa 100644 --- a/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php +++ b/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php @@ -23,6 +23,7 @@ use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterTokenUsageTrackingPass; use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\ReplaceDecoratedRememberMeHandlerPass; use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\SortFirewallListenersPass; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken\CasTokenHandlerFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken\OidcTokenHandlerFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken\OidcUserInfoTokenHandlerFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken\ServiceTokenHandlerFactory; @@ -78,6 +79,7 @@ public function build(ContainerBuilder $container): void new ServiceTokenHandlerFactory(), new OidcUserInfoTokenHandlerFactory(), new OidcTokenHandlerFactory(), + new CasTokenHandlerFactory(), ])); $extension->addUserProviderFactory(new InMemoryFactory()); @@ -102,6 +104,6 @@ public function build(ContainerBuilder $container): void ))); // must be registered before DecoratorServicePass - $container->addCompilerPass(new MakeFirewallsEventDispatcherTraceablePass(), PassConfig::TYPE_OPTIMIZE, 10); + $container->addCompilerPass(new MakeFirewallsEventDispatcherTraceablePass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 10); } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTestCase.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTestCase.php index d9b7bedaf73bc..04fba9fe584d3 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTestCase.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTestCase.php @@ -129,7 +129,7 @@ public function testFirewalls() $configs[] = array_values($configDef->getArguments()); } - // the IDs of the services are case sensitive or insensitive depending on + // the IDs of the services are case-sensitive or insensitive depending on // the Symfony version. Transform them to lowercase to simplify tests. $configs[0][2] = strtolower($configs[0][2]); $configs[2][2] = strtolower($configs[2][2]); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/AccessTokenFactoryTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/AccessTokenFactoryTest.php index e1f55817eee68..65e54af3c6f4b 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/AccessTokenFactoryTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/AccessTokenFactoryTest.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\SecurityBundle\Tests\DependencyInjection\Security\Factory; use PHPUnit\Framework\TestCase; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken\CasTokenHandlerFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken\OidcTokenHandlerFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken\OidcUserInfoTokenHandlerFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken\ServiceTokenHandlerFactory; @@ -76,6 +77,186 @@ public function testIdTokenHandlerConfiguration() $this->assertTrue($container->hasDefinition('security.access_token_handler.firewall1')); } + public function testCasTokenHandlerConfiguration() + { + $container = new ContainerBuilder(); + $config = [ + 'token_handler' => ['cas' => ['validation_url' => 'https://www.example.com/cas/validate']], + ]; + + $factory = new AccessTokenFactory($this->createTokenHandlerFactories()); + $finalizedConfig = $this->processConfig($config, $factory); + + $factory->createAuthenticator($container, 'firewall1', $finalizedConfig, 'userprovider'); + + $this->assertTrue($container->hasDefinition('security.access_token_handler.cas')); + + $arguments = $container->getDefinition('security.access_token_handler.cas')->getArguments(); + $this->assertSame((string) $arguments[0], 'request_stack'); + $this->assertSame($arguments[1], 'https://www.example.com/cas/validate'); + $this->assertSame($arguments[2], 'cas'); + $this->assertNull($arguments[3]); + } + + public function testInvalidOidcTokenHandlerConfigurationKeyMissing() + { + $config = [ + 'token_handler' => [ + 'oidc' => [ + 'algorithm' => 'RS256', + 'issuers' => ['https://www.example.com'], + 'audience' => 'audience', + ], + ], + ]; + + $factory = new AccessTokenFactory($this->createTokenHandlerFactories()); + + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('The child config "keyset" under "access_token.token_handler.oidc" must be configured: JSON-encoded JWKSet used to sign the token (must contain a list of valid keys).'); + + $this->processConfig($config, $factory); + } + + public function testInvalidOidcTokenHandlerConfigurationDuplicatedKeyParameters() + { + $config = [ + 'token_handler' => [ + 'oidc' => [ + 'algorithm' => 'RS256', + 'issuers' => ['https://www.example.com'], + 'audience' => 'audience', + 'key' => 'key', + 'keyset' => 'keyset', + ], + ], + ]; + + $factory = new AccessTokenFactory($this->createTokenHandlerFactories()); + + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('You cannot use both "key" and "keyset" at the same time.'); + + $this->processConfig($config, $factory); + } + + public function testInvalidOidcTokenHandlerConfigurationDuplicatedAlgorithmParameters() + { + $config = [ + 'token_handler' => [ + 'oidc' => [ + 'algorithm' => 'RS256', + 'algorithms' => ['RS256'], + 'issuers' => ['https://www.example.com'], + 'audience' => 'audience', + 'keyset' => 'keyset', + ], + ], + ]; + + $factory = new AccessTokenFactory($this->createTokenHandlerFactories()); + + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('You cannot use both "algorithm" and "algorithms" at the same time.'); + + $this->processConfig($config, $factory); + } + + public function testInvalidOidcTokenHandlerConfigurationMissingAlgorithmParameters() + { + $config = [ + 'token_handler' => [ + 'oidc' => [ + 'issuers' => ['https://www.example.com'], + 'audience' => 'audience', + 'keyset' => 'keyset', + ], + ], + ]; + + $factory = new AccessTokenFactory($this->createTokenHandlerFactories()); + + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('The child config "algorithms" under "access_token.token_handler.oidc" must be configured: Algorithms used to sign the token.'); + + $this->processConfig($config, $factory); + } + + /** + * @group legacy + * + * @expectedDeprecation Since symfony/security-bundle 7.1: The "key" option is deprecated and will be removed in 8.0. Use the "keyset" option instead. + */ + public function testOidcTokenHandlerConfigurationWithSingleAlgorithm() + { + $container = new ContainerBuilder(); + $jwk = '{"kty":"EC","crv":"P-256","x":"0QEAsI1wGI-dmYatdUZoWSRWggLEpyzopuhwk-YUnA4","y":"KYl-qyZ26HobuYwlQh-r0iHX61thfP82qqEku7i0woo","d":"iA_TV2zvftni_9aFAQwFO_9aypfJFCSpcCyevDvz220"}'; + $config = [ + 'token_handler' => [ + 'oidc' => [ + 'algorithm' => 'RS256', + 'issuers' => ['https://www.example.com'], + 'audience' => 'audience', + 'key' => $jwk, + ], + ], + ]; + + $factory = new AccessTokenFactory($this->createTokenHandlerFactories()); + $finalizedConfig = $this->processConfig($config, $factory); + + $factory->createAuthenticator($container, 'firewall1', $finalizedConfig, 'userprovider'); + + $this->assertTrue($container->hasDefinition('security.authenticator.access_token.firewall1')); + $this->assertTrue($container->hasDefinition('security.access_token_handler.firewall1')); + + $expected = [ + 'index_0' => (new ChildDefinition('security.access_token_handler.oidc.signature')) + ->replaceArgument(0, ['RS256']), + 'index_1' => (new ChildDefinition('security.access_token_handler.oidc.jwkset')) + ->replaceArgument(0, sprintf('{"keys":[%s]}', $jwk)), + 'index_2' => 'audience', + 'index_3' => ['https://www.example.com'], + 'index_4' => 'sub', + ]; + $this->assertEquals($expected, $container->getDefinition('security.access_token_handler.firewall1')->getArguments()); + } + + public function testOidcTokenHandlerConfigurationWithMultipleAlgorithms() + { + $container = new ContainerBuilder(); + $jwkset = '{"keys":[{"kty":"EC","crv":"P-256","x":"FtgMtrsKDboRO-Zo0XC7tDJTATHVmwuf9GK409kkars","y":"rWDE0ERU2SfwGYCo1DWWdgFEbZ0MiAXLRBBOzBgs_jY","d":"4G7bRIiKih0qrFxc0dtvkHUll19tTyctoCR3eIbOrO0"},{"kty":"EC","crv":"P-256","x":"0QEAsI1wGI-dmYatdUZoWSRWggLEpyzopuhwk-YUnA4","y":"KYl-qyZ26HobuYwlQh-r0iHX61thfP82qqEku7i0woo","d":"iA_TV2zvftni_9aFAQwFO_9aypfJFCSpcCyevDvz220"}]}'; + $config = [ + 'token_handler' => [ + 'oidc' => [ + 'algorithms' => ['RS256', 'ES256'], + 'issuers' => ['https://www.example.com'], + 'audience' => 'audience', + 'keyset' => $jwkset, + ], + ], + ]; + + $factory = new AccessTokenFactory($this->createTokenHandlerFactories()); + $finalizedConfig = $this->processConfig($config, $factory); + + $factory->createAuthenticator($container, 'firewall1', $finalizedConfig, 'userprovider'); + + $this->assertTrue($container->hasDefinition('security.authenticator.access_token.firewall1')); + $this->assertTrue($container->hasDefinition('security.access_token_handler.firewall1')); + + $expected = [ + 'index_0' => (new ChildDefinition('security.access_token_handler.oidc.signature')) + ->replaceArgument(0, ['RS256', 'ES256']), + 'index_1' => (new ChildDefinition('security.access_token_handler.oidc.jwkset')) + ->replaceArgument(0, $jwkset), + 'index_2' => 'audience', + 'index_3' => ['https://www.example.com'], + 'index_4' => 'sub', + ]; + $this->assertEquals($expected, $container->getDefinition('security.access_token_handler.firewall1')->getArguments()); + } + public function testOidcUserInfoTokenHandlerConfigurationWithExistingClient() { $container = new ContainerBuilder(); @@ -218,6 +399,7 @@ private function createTokenHandlerFactories(): array new ServiceTokenHandlerFactory(), new OidcUserInfoTokenHandlerFactory(), new OidcTokenHandlerFactory(), + new CasTokenHandlerFactory(), ]; } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php index c62f9407bd1ee..23aa17b9adb57 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php @@ -869,11 +869,9 @@ public function testNothingDoneWithEmptyConfiguration() $container->loadFromExtension('security'); $this->expectException(InvalidConfigurationException::class); - $this->expectExceptionMessage('Enabling bundle "Symfony\Bundle\SecurityBundle\SecurityBundle" and not configuring it is not allowed.'); + $this->expectExceptionMessage('The SecurityBundle is enabled but is not configured. Please define your settings for the "security" config section.'); $container->compile(); - - $this->assertFalse($container->has('security.authorization_checker')); } public function testCustomHasherWithMigrateFrom() diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AccessTokenTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AccessTokenTest.php index 6cc2b1f0fb150..00c11bf40a211 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AccessTokenTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AccessTokenTest.php @@ -17,6 +17,8 @@ use Jose\Component\Signature\JWSBuilder; use Jose\Component\Signature\Serializer\CompactSerializer; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\HttpClient\Response\MockResponse; use Symfony\Component\HttpFoundation\Response; class AccessTokenTest extends AbstractWebTestCase @@ -383,4 +385,27 @@ public function testOidcSuccess() $this->assertSame(200, $response->getStatusCode()); $this->assertSame(['message' => 'Welcome @dunglas!'], json_decode($response->getContent(), true)); } + + public function testCasSuccess() + { + $casResponse = new MockResponse(<< + + dunglas + PGTIOU-84678-8a9d + + + BODY + ); + + $client = $this->createClient(['test_case' => 'AccessToken', 'root_config' => 'config_cas.yml']); + $client->getContainer()->set('Symfony\Contracts\HttpClient\HttpClientInterface', new MockHttpClient($casResponse)); + + $client->request('GET', '/foo?ticket=PGTIOU-84678-8a9d', [], [], []); + $response = $client->getResponse(); + + $this->assertInstanceOf(Response::class, $response); + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame(['message' => 'Welcome @dunglas!'], json_decode($response->getContent(), true)); + } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AccessToken/config_cas.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AccessToken/config_cas.yml new file mode 100644 index 0000000000000..2cd2abc566c05 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AccessToken/config_cas.yml @@ -0,0 +1,41 @@ +imports: + - { resource: ./../config/framework.yml } + +framework: + http_method_override: false + serializer: ~ + +security: + password_hashers: + Symfony\Component\Security\Core\User\InMemoryUser: plaintext + + providers: + in_memory: + memory: + users: + dunglas: { password: foo, roles: [ROLE_USER] } + + firewalls: + main: + pattern: ^/ + access_token: + token_handler: + cas: + validation_url: 'https://www.example.com/cas/serviceValidate' + http_client: 'Symfony\Contracts\HttpClient\HttpClientInterface' + token_extractors: + - security.access_token_extractor.cas + + access_control: + - { path: ^/foo, roles: ROLE_USER } + +services: + _defaults: + public: true + + security.access_token_extractor.cas: + class: Symfony\Component\Security\Http\AccessToken\QueryAccessTokenExtractor + arguments: + - 'ticket' + + Symfony\Contracts\HttpClient\HttpClientInterface: ~ diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AccessToken/config_oidc.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AccessToken/config_oidc.yml index dd770e4520e41..68f8a1f9dd47a 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AccessToken/config_oidc.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AccessToken/config_oidc.yml @@ -26,7 +26,7 @@ security: issuers: [ 'https://www.example.com' ] algorithm: 'ES256' # tip: use https://mkjwk.org/ to generate a JWK - key: '{"kty":"EC","d":"iA_TV2zvftni_9aFAQwFO_9aypfJFCSpcCyevDvz220","crv":"P-256","x":"0QEAsI1wGI-dmYatdUZoWSRWggLEpyzopuhwk-YUnA4","y":"KYl-qyZ26HobuYwlQh-r0iHX61thfP82qqEku7i0woo"}' + keyset: '{"keys":[{"kty":"EC","d":"iA_TV2zvftni_9aFAQwFO_9aypfJFCSpcCyevDvz220","crv":"P-256","x":"0QEAsI1wGI-dmYatdUZoWSRWggLEpyzopuhwk-YUnA4","y":"KYl-qyZ26HobuYwlQh-r0iHX61thfP82qqEku7i0woo"}]}' token_extractors: 'header' realm: 'My API' diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/LoginLink/FirewallAwareLoginLinkHandlerTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/LoginLink/FirewallAwareLoginLinkHandlerTest.php index 92703f41ec3c7..eff35a8304749 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/LoginLink/FirewallAwareLoginLinkHandlerTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/LoginLink/FirewallAwareLoginLinkHandlerTest.php @@ -12,10 +12,10 @@ namespace Symfony\Bundle\SecurityBundle\Tests\LoginLink; use PHPUnit\Framework\TestCase; -use Psr\Container\ContainerInterface; use Symfony\Bundle\SecurityBundle\LoginLink\FirewallAwareLoginLinkHandler; use Symfony\Bundle\SecurityBundle\Security\FirewallConfig; use Symfony\Bundle\SecurityBundle\Security\FirewallMap; +use Symfony\Component\DependencyInjection\Container; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\Security\Core\User\UserInterface; @@ -66,13 +66,11 @@ private function createFirewallMap(string $firewallName) private function createLocator(array $linkers) { - $locator = $this->createMock(ContainerInterface::class); - $locator->expects($this->any()) - ->method('has') - ->willReturnCallback(fn ($firewallName) => isset($linkers[$firewallName])); - $locator->expects($this->any()) - ->method('get') - ->willReturnCallback(fn ($firewallName) => $linkers[$firewallName]); + $locator = new Container(); + + foreach ($linkers as $class => $service) { + $locator->set($class, $service); + } return $locator; } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/SecurityTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/SecurityTest.php index 045dfc70a7c5e..4be14111682bb 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/SecurityTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/SecurityTest.php @@ -16,6 +16,7 @@ use Symfony\Bundle\SecurityBundle\Security; use Symfony\Bundle\SecurityBundle\Security\FirewallConfig; use Symfony\Bundle\SecurityBundle\Security\FirewallMap; +use Symfony\Component\DependencyInjection\Container; use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; @@ -127,26 +128,20 @@ public function testLogin() { $request = new Request(); $authenticator = $this->createMock(AuthenticatorInterface::class); - $requestStack = $this->createMock(RequestStack::class); + $requestStack = new RequestStack(); + $requestStack->push($request); $firewallMap = $this->createMock(FirewallMap::class); $firewall = new FirewallConfig('main', 'main'); $userAuthenticator = $this->createMock(UserAuthenticatorInterface::class); $user = $this->createMock(UserInterface::class); $userChecker = $this->createMock(UserCheckerInterface::class); - $container = $this->createMock(ContainerInterface::class); - $container - ->expects($this->atLeastOnce()) - ->method('get') - ->willReturnMap([ - ['request_stack', $requestStack], - ['security.firewall.map', $firewallMap], - ['security.authenticator.managers_locator', $this->createContainer('main', $userAuthenticator)], - ['security.user_checker', $userChecker], - ]) - ; + $container = new Container(); + $container->set('request_stack', $requestStack); + $container->set('security.firewall.map', $firewallMap); + $container->set('security.authenticator.managers_locator', $this->createContainer('main', $userAuthenticator)); + $container->set('security.user_checker', $userChecker); - $requestStack->expects($this->once())->method('getCurrentRequest')->willReturn($request); $firewallMap->expects($this->once())->method('getFirewallConfig')->willReturn($firewall); $userAuthenticator->expects($this->once())->method('authenticateUser')->with($user, $authenticator, $request); $userChecker->expects($this->once())->method('checkPreAuth')->with($user); @@ -173,26 +168,20 @@ public function testLoginReturnsAuthenticatorResponse() { $request = new Request(); $authenticator = $this->createMock(AuthenticatorInterface::class); - $requestStack = $this->createMock(RequestStack::class); + $requestStack = new RequestStack(); + $requestStack->push($request); $firewallMap = $this->createMock(FirewallMap::class); $firewall = new FirewallConfig('main', 'main'); $user = $this->createMock(UserInterface::class); $userChecker = $this->createMock(UserCheckerInterface::class); $userAuthenticator = $this->createMock(UserAuthenticatorInterface::class); - $container = $this->createMock(ContainerInterface::class); - $container - ->expects($this->atLeastOnce()) - ->method('get') - ->willReturnMap([ - ['request_stack', $requestStack], - ['security.firewall.map', $firewallMap], - ['security.authenticator.managers_locator', $this->createContainer('main', $userAuthenticator)], - ['security.user_checker', $userChecker], - ]) - ; + $container = new Container(); + $container->set('request_stack', $requestStack); + $container->set('security.firewall.map', $firewallMap); + $container->set('security.authenticator.managers_locator', $this->createContainer('main', $userAuthenticator)); + $container->set('security.user_checker', $userChecker); - $requestStack->expects($this->once())->method('getCurrentRequest')->willReturn($request); $firewallMap->expects($this->once())->method('getFirewallConfig')->willReturn($firewall); $userChecker->expects($this->once())->method('checkPreAuth')->with($user); $userAuthenticator->expects($this->once())->method('authenticateUser') @@ -223,25 +212,18 @@ public function testLoginReturnsAuthenticatorResponse() public function testLoginWithoutAuthenticatorThrows() { $request = new Request(); - $authenticator = $this->createMock(AuthenticatorInterface::class); - $requestStack = $this->createMock(RequestStack::class); + $requestStack = new RequestStack(); + $requestStack->push($request); $firewallMap = $this->createMock(FirewallMap::class); $firewall = new FirewallConfig('main', 'main'); $user = $this->createMock(UserInterface::class); $userChecker = $this->createMock(UserCheckerInterface::class); - $container = $this->createMock(ContainerInterface::class); - $container - ->expects($this->atLeastOnce()) - ->method('get') - ->willReturnMap([ - ['request_stack', $requestStack], - ['security.firewall.map', $firewallMap], - ['security.user_checker', $userChecker], - ]) - ; + $container = new Container(); + $container->set('request_stack', $requestStack); + $container->set('security.firewall.map', $firewallMap); + $container->set('security.user_checker', $userChecker); - $requestStack->expects($this->once())->method('getCurrentRequest')->willReturn($request); $firewallMap->expects($this->once())->method('getFirewallConfig')->willReturn($firewall); $security = new Security($container, ['main' => null]); @@ -257,14 +239,8 @@ public function testLoginWithoutRequestContext() $requestStack = new RequestStack(); $user = $this->createMock(UserInterface::class); - $container = $this->createMock(ContainerInterface::class); - $container - ->expects($this->atLeastOnce()) - ->method('get') - ->willReturnMap([ - ['request_stack', $requestStack], - ]) - ; + $container = new Container(); + $container->set('request_stack', $requestStack); $security = new Security($container, ['main' => null]); @@ -277,8 +253,8 @@ public function testLoginWithoutRequestContext() public function testLogout() { $request = new Request(); - $requestStack = $this->createMock(RequestStack::class); - $requestStack->expects($this->once())->method('getMainRequest')->willReturn($request); + $requestStack = new RequestStack(); + $requestStack->push($request); $token = $this->createMock(TokenInterface::class); $token->method('getUser')->willReturn(new InMemoryUser('foo', 'bar')); @@ -301,26 +277,14 @@ public function testLogout() ->willReturn($firewallConfig) ; - $eventDispatcherLocator = $this->createMock(ContainerInterface::class); - $eventDispatcherLocator - ->expects($this->atLeastOnce()) - ->method('get') - ->willReturnMap([ - ['my_firewall', $eventDispatcher], - ]) - ; + $eventDispatcherLocator = new Container(); + $eventDispatcherLocator->set('my_firewall', $eventDispatcher); - $container = $this->createMock(ContainerInterface::class); - $container - ->expects($this->atLeastOnce()) - ->method('get') - ->willReturnMap([ - ['request_stack', $requestStack], - ['security.token_storage', $tokenStorage], - ['security.firewall.map', $firewallMap], - ['security.firewall.event_dispatcher_locator', $eventDispatcherLocator], - ]) - ; + $container = new Container(); + $container->set('request_stack', $requestStack); + $container->set('security.token_storage', $tokenStorage); + $container->set('security.firewall.map', $firewallMap); + $container->set('security.firewall.event_dispatcher_locator', $eventDispatcherLocator); $security = new Security($container); $security->logout(false); } @@ -328,8 +292,8 @@ public function testLogout() public function testLogoutWithoutFirewall() { $request = new Request(); - $requestStack = $this->createMock(RequestStack::class); - $requestStack->expects($this->once())->method('getMainRequest')->willReturn($request); + $requestStack = new RequestStack(); + $requestStack->push($request); $token = $this->createMock(TokenInterface::class); $token->method('getUser')->willReturn(new InMemoryUser('foo', 'bar')); @@ -347,16 +311,10 @@ public function testLogoutWithoutFirewall() ->willReturn(null) ; - $container = $this->createMock(ContainerInterface::class); - $container - ->expects($this->atLeastOnce()) - ->method('get') - ->willReturnMap([ - ['request_stack', $requestStack], - ['security.token_storage', $tokenStorage], - ['security.firewall.map', $firewallMap], - ]) - ; + $container = new Container(); + $container->set('request_stack', $requestStack); + $container->set('security.token_storage', $tokenStorage); + $container->set('security.firewall.map', $firewallMap); $this->expectException(LogicException::class); $security = new Security($container); @@ -366,8 +324,8 @@ public function testLogoutWithoutFirewall() public function testLogoutWithResponse() { $request = new Request(); - $requestStack = $this->createMock(RequestStack::class); - $requestStack->expects($this->once())->method('getMainRequest')->willReturn($request); + $requestStack = new RequestStack(); + $requestStack->push($request); $token = $this->createMock(TokenInterface::class); $token->method('getUser')->willReturn(new InMemoryUser('foo', 'bar')); @@ -394,24 +352,14 @@ public function testLogoutWithResponse() $firewallConfig = new FirewallConfig('my_firewall', 'user_checker'); $firewallMap->expects($this->once())->method('getFirewallConfig')->willReturn($firewallConfig); - $eventDispatcherLocator = $this->createMock(ContainerInterface::class); - $eventDispatcherLocator - ->expects($this->atLeastOnce()) - ->method('get') - ->willReturnMap([['my_firewall', $eventDispatcher]]) - ; + $eventDispatcherLocator = new Container(); + $eventDispatcherLocator->set('my_firewall', $eventDispatcher); - $container = $this->createMock(ContainerInterface::class); - $container - ->expects($this->atLeastOnce()) - ->method('get') - ->willReturnMap([ - ['request_stack', $requestStack], - ['security.token_storage', $tokenStorage], - ['security.firewall.map', $firewallMap], - ['security.firewall.event_dispatcher_locator', $eventDispatcherLocator], - ]) - ; + $container = new Container(); + $container->set('request_stack', $requestStack); + $container->set('security.token_storage', $tokenStorage); + $container->set('security.firewall.map', $firewallMap); + $container->set('security.firewall.event_dispatcher_locator', $eventDispatcherLocator); $security = new Security($container); $response = $security->logout(false); @@ -422,8 +370,8 @@ public function testLogoutWithResponse() public function testLogoutWithValidCsrf() { $request = new Request(['_csrf_token' => 'dummytoken']); - $requestStack = $this->createMock(RequestStack::class); - $requestStack->expects($this->once())->method('getMainRequest')->willReturn($request); + $requestStack = new RequestStack(); + $requestStack->push($request); $token = $this->createMock(TokenInterface::class); $token->method('getUser')->willReturn(new InMemoryUser('foo', 'bar')); @@ -450,29 +398,18 @@ public function testLogoutWithValidCsrf() $firewallConfig = new FirewallConfig(name: 'my_firewall', userChecker: 'user_checker', logout: ['csrf_parameter' => '_csrf_token', 'csrf_token_id' => 'logout']); $firewallMap->expects($this->once())->method('getFirewallConfig')->willReturn($firewallConfig); - $eventDispatcherLocator = $this->createMock(ContainerInterface::class); - $eventDispatcherLocator - ->expects($this->atLeastOnce()) - ->method('get') - ->willReturnMap([['my_firewall', $eventDispatcher]]) - ; + $eventDispatcherLocator = new Container(); + $eventDispatcherLocator->set('my_firewall', $eventDispatcher); $csrfTokenManager = $this->createMock(CsrfTokenManagerInterface::class); $csrfTokenManager->expects($this->once())->method('isTokenValid')->with($this->equalTo(new CsrfToken('logout', 'dummytoken')))->willReturn(true); - $container = $this->createMock(ContainerInterface::class); - $container->expects($this->once())->method('has')->with('security.csrf.token_manager')->willReturn(true); - $container - ->expects($this->atLeastOnce()) - ->method('get') - ->willReturnMap([ - ['request_stack', $requestStack], - ['security.token_storage', $tokenStorage], - ['security.firewall.map', $firewallMap], - ['security.firewall.event_dispatcher_locator', $eventDispatcherLocator], - ['security.csrf.token_manager', $csrfTokenManager], - ]) - ; + $container = new Container(); + $container->set('request_stack', $requestStack); + $container->set('security.token_storage', $tokenStorage); + $container->set('security.firewall.map', $firewallMap); + $container->set('security.firewall.event_dispatcher_locator', $eventDispatcherLocator); + $container->set('security.csrf.token_manager', $csrfTokenManager); $security = new Security($container); $response = $security->logout(); @@ -484,14 +421,8 @@ public function testLogoutWithoutRequestContext() { $requestStack = new RequestStack(); - $container = $this->createMock(ContainerInterface::class); - $container - ->expects($this->atLeastOnce()) - ->method('get') - ->willReturnMap([ - ['request_stack', $requestStack], - ]) - ; + $container = new Container(); + $container->set('request_stack', $requestStack); $security = new Security($container, ['main' => null]); diff --git a/src/Symfony/Bundle/SecurityBundle/composer.json b/src/Symfony/Bundle/SecurityBundle/composer.json index cc48593fc663a..b335e73e07a9d 100644 --- a/src/Symfony/Bundle/SecurityBundle/composer.json +++ b/src/Symfony/Bundle/SecurityBundle/composer.json @@ -28,7 +28,7 @@ "symfony/password-hasher": "^6.4|^7.0", "symfony/security-core": "^6.4|^7.0", "symfony/security-csrf": "^6.4|^7.0", - "symfony/security-http": "^6.4|^7.0", + "symfony/security-http": "^7.1", "symfony/service-contracts": "^2.5|^3" }, "require-dev": { @@ -51,12 +51,7 @@ "symfony/validator": "^6.4|^7.0", "symfony/yaml": "^6.4|^7.0", "twig/twig": "^3.0.4", - "web-token/jwt-checker": "^3.1", - "web-token/jwt-signature-algorithm-hmac": "^3.1", - "web-token/jwt-signature-algorithm-ecdsa": "^3.1", - "web-token/jwt-signature-algorithm-rsa": "^3.1", - "web-token/jwt-signature-algorithm-eddsa": "^3.1", - "web-token/jwt-signature-algorithm-none": "^3.1" + "web-token/jwt-library": "^3.3.2" }, "conflict": { "symfony/browser-kit": "<6.4", diff --git a/src/Symfony/Bundle/TwigBundle/CHANGELOG.md b/src/Symfony/Bundle/TwigBundle/CHANGELOG.md index e1603edc06e03..91722564d0bd1 100644 --- a/src/Symfony/Bundle/TwigBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/TwigBundle/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.1 +--- + + * Mark class `TemplateCacheWarmer` as `final` + 7.0 --- diff --git a/src/Symfony/Bundle/TwigBundle/CacheWarmer/TemplateCacheWarmer.php b/src/Symfony/Bundle/TwigBundle/CacheWarmer/TemplateCacheWarmer.php index e56a9dd96f320..69b0b2cecbd83 100644 --- a/src/Symfony/Bundle/TwigBundle/CacheWarmer/TemplateCacheWarmer.php +++ b/src/Symfony/Bundle/TwigBundle/CacheWarmer/TemplateCacheWarmer.php @@ -21,6 +21,8 @@ * Generates the Twig cache for all templates. * * @author Fabien Potencier + * + * @final since Symfony 7.1 */ class TemplateCacheWarmer implements CacheWarmerInterface, ServiceSubscriberInterface { diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExtensionPass.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExtensionPass.php index 58aa921686204..b21e4f37ece2b 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExtensionPass.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExtensionPass.php @@ -15,6 +15,7 @@ use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\Emoji\EmojiTransliterator; use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Workflow\Workflow; @@ -31,6 +32,10 @@ public function process(ContainerBuilder $container): void $container->removeDefinition('twig.extension.assets'); } + if (!class_exists(\Transliterator::class) || !class_exists(EmojiTransliterator::class)) { + $container->removeDefinition('twig.extension.emoji'); + } + if (!class_exists(Expression::class)) { $container->removeDefinition('twig.extension.expression'); } @@ -125,6 +130,10 @@ public function process(ContainerBuilder $container): void $container->getDefinition('twig.extension.expression')->addTag('twig.extension'); } + if ($container->hasDefinition('twig.extension.emoji')) { + $container->getDefinition('twig.extension.emoji')->addTag('twig.extension'); + } + if (!class_exists(Workflow::class) || !$container->has('workflow.registry')) { $container->removeDefinition('workflow.twig_extension'); } else { diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php index dbe31be30d369..ca23a0dfe3661 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php @@ -64,7 +64,7 @@ private function addFormThemesSection(ArrayNodeDefinition $rootNode): void ->prototype('scalar')->defaultValue('form_div_layout.html.twig')->end() ->example(['@My/form.html.twig']) ->validate() - ->ifTrue(fn ($v) => !\in_array('form_div_layout.html.twig', $v)) + ->ifTrue(fn ($v) => !\in_array('form_div_layout.html.twig', $v, true)) ->then(fn ($v) => array_merge(['form_div_layout.html.twig'], $v)) ->end() ->end() @@ -129,7 +129,11 @@ private function addTwigOptions(ArrayNodeDefinition $rootNode): void ->children() ->scalarNode('autoescape_service')->defaultNull()->end() ->scalarNode('autoescape_service_method')->defaultNull()->end() - ->scalarNode('base_template_class')->example('Twig\Template')->cannotBeEmpty()->end() + ->scalarNode('base_template_class') + ->setDeprecated('symfony/twig-bundle', '7.1') + ->example('Twig\Template') + ->cannotBeEmpty() + ->end() ->scalarNode('cache')->defaultValue('%kernel.cache_dir%/twig')->end() ->scalarNode('charset')->defaultValue('%kernel.charset%')->end() ->booleanNode('debug')->defaultValue('%kernel.debug%')->end() diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configurator/EnvironmentConfigurator.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configurator/EnvironmentConfigurator.php index a57ad7c3d3fa0..64d76c00303f2 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configurator/EnvironmentConfigurator.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configurator/EnvironmentConfigurator.php @@ -15,9 +15,6 @@ use Twig\Environment; use Twig\Extension\CoreExtension; -// BC/FC with namespaced Twig -class_exists(Environment::class); - /** * Twig environment configurator. * diff --git a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.php b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.php index e1b47de245580..02631d28c39a4 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.php +++ b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.php @@ -17,6 +17,7 @@ use Symfony\Bridge\Twig\ErrorRenderer\TwigErrorRenderer; use Symfony\Bridge\Twig\EventListener\TemplateAttributeListener; use Symfony\Bridge\Twig\Extension\AssetExtension; +use Symfony\Bridge\Twig\Extension\EmojiExtension; use Symfony\Bridge\Twig\Extension\ExpressionExtension; use Symfony\Bridge\Twig\Extension\HtmlSanitizerExtension; use Symfony\Bridge\Twig\Extension\HttpFoundationExtension; @@ -115,6 +116,8 @@ ->set('twig.extension.expression', ExpressionExtension::class) + ->set('twig.extension.emoji', EmojiExtension::class) + ->set('twig.extension.htmlsanitizer', HtmlSanitizerExtension::class) ->args([tagged_locator('html_sanitizer', 'sanitizer')]) diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/full.php b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/full.php index 9e7b500795ec6..f87af5a1baba4 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/full.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/full.php @@ -11,7 +11,6 @@ 'bad' => ['key' => 'foo'], ], 'auto_reload' => true, - 'base_template_class' => 'stdClass', 'cache' => '/tmp', 'charset' => 'ISO-8859-1', 'debug' => true, diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/templateClass.php b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/templateClass.php new file mode 100644 index 0000000000000..bf995046314fa --- /dev/null +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/templateClass.php @@ -0,0 +1,5 @@ +loadFromExtension('twig', [ + 'base_template_class' => 'stdClass', +]); diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/extra.xml b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/extra.xml index 92767e411057f..f1cf8985329d0 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/extra.xml +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/extra.xml @@ -6,7 +6,7 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd http://symfony.com/schema/dic/twig https://symfony.com/schema/dic/twig/twig-1.0.xsd"> - + namespaced_path3 diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/full.xml b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/full.xml index 3f7d1de266ec5..528a466b0452c 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/full.xml +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/full.xml @@ -6,7 +6,7 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd http://symfony.com/schema/dic/twig https://symfony.com/schema/dic/twig/twig-1.0.xsd"> - + MyBundle::form.html.twig @@qux diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/templateClass.xml b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/templateClass.xml new file mode 100644 index 0000000000000..a735ed8da258e --- /dev/null +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/templateClass.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/yml/full.yml b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/yml/full.yml index d186724539927..6c249d378ff22 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/yml/full.yml +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/yml/full.yml @@ -7,7 +7,6 @@ twig: pi: 3.14 bad: {key: foo} auto_reload: true - base_template_class: stdClass cache: /tmp charset: ISO-8859-1 debug: true diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/yml/templateClass.yml b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/yml/templateClass.yml new file mode 100644 index 0000000000000..886a5ee60d9a5 --- /dev/null +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/yml/templateClass.yml @@ -0,0 +1,2 @@ +twig: + base_template_class: stdClass diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/TwigExtensionTest.php b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/TwigExtensionTest.php index 7a874e7bab8bc..bc7013c3cb70a 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/TwigExtensionTest.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/TwigExtensionTest.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\TwigBundle\Tests\DependencyInjection; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Bundle\TwigBundle\DependencyInjection\Compiler\RuntimeLoaderPass; use Symfony\Bundle\TwigBundle\DependencyInjection\TwigExtension; use Symfony\Bundle\TwigBundle\Tests\DependencyInjection\AcmeBundle\AcmeBundle; @@ -31,6 +32,8 @@ class TwigExtensionTest extends TestCase { + use ExpectDeprecationTrait; + public function testLoadEmptyConfiguration() { $container = $this->createContainer(); @@ -56,7 +59,7 @@ public function testLoadEmptyConfiguration() /** * @dataProvider getFormats */ - public function testLoadFullConfiguration($format) + public function testLoadFullConfiguration(string $format) { $container = $this->createContainer(); $container->registerExtension(new TwigExtension()); @@ -91,17 +94,36 @@ public function testLoadFullConfiguration($format) $options = $container->getDefinition('twig')->getArgument(1); $this->assertTrue($options['auto_reload'], '->load() sets the auto_reload option'); $this->assertSame('name', $options['autoescape'], '->load() sets the autoescape option'); - $this->assertEquals('stdClass', $options['base_template_class'], '->load() sets the base_template_class option'); + $this->assertArrayNotHasKey('base_template_class', $options, '->load() does not set the base_template_class if none is provided'); $this->assertEquals('/tmp', $options['cache'], '->load() sets the cache option'); $this->assertEquals('ISO-8859-1', $options['charset'], '->load() sets the charset option'); $this->assertTrue($options['debug'], '->load() sets the debug option'); $this->assertTrue($options['strict_variables'], '->load() sets the strict_variables option'); } + /** + * @group legacy + * + * @dataProvider getFormats + */ + public function testLoadCustomBaseTemplateClassConfiguration(string $format) + { + $container = $this->createContainer(); + $container->registerExtension(new TwigExtension()); + + $this->expectDeprecation('Since symfony/twig-bundle 7.1: The child node "base_template_class" at path "twig" is deprecated.'); + + $this->loadFromFile($container, 'templateClass', $format); + $this->compileContainer($container); + + $options = $container->getDefinition('twig')->getArgument(1); + $this->assertEquals('stdClass', $options['base_template_class'], '->load() sets the base_template_class option'); + } + /** * @dataProvider getFormats */ - public function testLoadCustomTemplateEscapingGuesserConfiguration($format) + public function testLoadCustomTemplateEscapingGuesserConfiguration(string $format) { $container = $this->createContainer(); $container->registerExtension(new TwigExtension()); @@ -115,7 +137,7 @@ public function testLoadCustomTemplateEscapingGuesserConfiguration($format) /** * @dataProvider getFormats */ - public function testLoadDefaultTemplateEscapingGuesserConfiguration($format) + public function testLoadDefaultTemplateEscapingGuesserConfiguration(string $format) { $container = $this->createContainer(); $container->registerExtension(new TwigExtension()); @@ -129,7 +151,7 @@ public function testLoadDefaultTemplateEscapingGuesserConfiguration($format) /** * @dataProvider getFormats */ - public function testLoadCustomDateFormats($fileFormat) + public function testLoadCustomDateFormats(string $fileFormat) { $container = $this->createContainer(); $container->registerExtension(new TwigExtension()); @@ -178,7 +200,7 @@ public function testGlobalsWithDifferentTypesAndValues() /** * @dataProvider getFormats */ - public function testTwigLoaderPaths($format) + public function testTwigLoaderPaths(string $format) { $container = $this->createContainer(); $container->registerExtension(new TwigExtension()); @@ -207,7 +229,7 @@ public function testTwigLoaderPaths($format) ], $paths); } - public static function getFormats() + public static function getFormats(): array { return [ ['php'], @@ -219,7 +241,7 @@ public static function getFormats() /** * @dataProvider stopwatchExtensionAvailabilityProvider */ - public function testStopwatchExtensionAvailability($debug, $stopwatchEnabled, $expected) + public function testStopwatchExtensionAvailability(bool $debug, bool $stopwatchEnabled, bool $expected) { $container = $this->createContainer(); $container->setParameter('kernel.debug', $debug); @@ -290,7 +312,7 @@ public function testCustomHtmlToTextConverterService(string $format) $this->assertEquals(new Reference('my_converter'), $bodyRenderer->getArgument('$converter')); } - private function createContainer() + private function createContainer(): ContainerBuilder { $container = new ContainerBuilder(new ParameterBag([ 'kernel.cache_dir' => __DIR__, @@ -311,7 +333,7 @@ private function createContainer() return $container; } - private function compileContainer(ContainerBuilder $container) + private function compileContainer(ContainerBuilder $container): void { $container->getCompilerPassConfig()->setOptimizationPasses([]); $container->getCompilerPassConfig()->setRemovingPasses([]); @@ -319,7 +341,7 @@ private function compileContainer(ContainerBuilder $container) $container->compile(); } - private function loadFromFile(ContainerBuilder $container, $file, $format) + private function loadFromFile(ContainerBuilder $container, string $file, string $format): void { $locator = new FileLocator(__DIR__.'/Fixtures/'.$format); diff --git a/src/Symfony/Bundle/WebProfilerBundle/.gitattributes b/src/Symfony/Bundle/WebProfilerBundle/.gitattributes index 14c3c35940427..9277fc7ed107c 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/.gitattributes +++ b/src/Symfony/Bundle/WebProfilerBundle/.gitattributes @@ -1,3 +1,4 @@ /Tests export-ignore /phpunit.xml.dist export-ignore +/Resources/views/Script/Mermaid/Makefile export-ignore /.git* export-ignore diff --git a/src/Symfony/Bundle/WebProfilerBundle/.gitignore b/src/Symfony/Bundle/WebProfilerBundle/.gitignore index c49a5d8df5c65..431f2b6529f61 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/.gitignore +++ b/src/Symfony/Bundle/WebProfilerBundle/.gitignore @@ -1,3 +1,4 @@ vendor/ composer.lock phpunit.xml +/Resources/views/Script/Mermaid/repo-* diff --git a/src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md b/src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md index c3a2d8c8aab6e..f1cb83280b9d8 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.1 +--- + + * Set `XDEBUG_IGNORE` query parameter when sending toolbar XHR + 6.4 --- diff --git a/src/Symfony/Bundle/WebProfilerBundle/Controller/ExceptionPanelController.php b/src/Symfony/Bundle/WebProfilerBundle/Controller/ExceptionPanelController.php index a0704bb532cf8..17c052daa8ed0 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Controller/ExceptionPanelController.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Controller/ExceptionPanelController.php @@ -25,13 +25,10 @@ */ class ExceptionPanelController { - private HtmlErrorRenderer $errorRenderer; - private ?Profiler $profiler; - - public function __construct(HtmlErrorRenderer $errorRenderer, ?Profiler $profiler = null) - { - $this->errorRenderer = $errorRenderer; - $this->profiler = $profiler; + public function __construct( + private HtmlErrorRenderer $errorRenderer, + private ?Profiler $profiler = null, + ) { } /** diff --git a/src/Symfony/Bundle/WebProfilerBundle/Controller/ProfilerController.php b/src/Symfony/Bundle/WebProfilerBundle/Controller/ProfilerController.php index 23895f70bb6ec..9ca5c1c042865 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Controller/ProfilerController.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Controller/ProfilerController.php @@ -34,21 +34,15 @@ class ProfilerController { private TemplateManager $templateManager; - private UrlGeneratorInterface $generator; - private ?Profiler $profiler; - private Environment $twig; - private array $templates; - private ?ContentSecurityPolicyHandler $cspHandler; - private ?string $baseDir; - - public function __construct(UrlGeneratorInterface $generator, ?Profiler $profiler, Environment $twig, array $templates, ?ContentSecurityPolicyHandler $cspHandler = null, ?string $baseDir = null) - { - $this->generator = $generator; - $this->profiler = $profiler; - $this->twig = $twig; - $this->templates = $templates; - $this->cspHandler = $cspHandler; - $this->baseDir = $baseDir; + + public function __construct( + private UrlGeneratorInterface $generator, + private ?Profiler $profiler, + private Environment $twig, + private array $templates, + private ?ContentSecurityPolicyHandler $cspHandler = null, + private ?string $baseDir = null, + ) { } /** @@ -195,7 +189,6 @@ public function searchBarAction(Request $request): Response 'end' => $request->query->get('end', $session?->get('_profiler_search_end')), 'limit' => $request->query->get('limit', $session?->get('_profiler_search_limit')), 'request' => $request, - 'render_hidden_by_default' => false, 'profile_type' => $request->query->get('type', $session?->get('_profiler_search_type', 'request')), ]), 200, @@ -275,7 +268,7 @@ public function searchAction(Request $request): Response $session->set('_profiler_search_type', $profileType); } - if (!empty($token)) { + if ($token) { return new RedirectResponse($this->generator->generate('_profiler', ['token' => $token]), 302, ['Content-Type' => 'text/html']); } diff --git a/src/Symfony/Bundle/WebProfilerBundle/Controller/RouterController.php b/src/Symfony/Bundle/WebProfilerBundle/Controller/RouterController.php index f9f7686dcb249..4a3f5306095c1 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Controller/RouterController.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Controller/RouterController.php @@ -30,23 +30,19 @@ */ class RouterController { - private ?Profiler $profiler; - private Environment $twig; - private ?UrlMatcherInterface $matcher; - private ?RouteCollection $routes; - /** - * @var ExpressionFunctionProviderInterface[] + * @param ExpressionFunctionProviderInterface[] $expressionLanguageProviders */ - private iterable $expressionLanguageProviders; - - public function __construct(?Profiler $profiler, Environment $twig, ?UrlMatcherInterface $matcher = null, ?RouteCollection $routes = null, iterable $expressionLanguageProviders = []) - { - $this->profiler = $profiler; - $this->twig = $twig; - $this->matcher = $matcher; - $this->routes = (null === $routes && $matcher instanceof RouterInterface) ? $matcher->getRouteCollection() : $routes; - $this->expressionLanguageProviders = $expressionLanguageProviders; + public function __construct( + private ?Profiler $profiler, + private Environment $twig, + private ?UrlMatcherInterface $matcher = null, + private ?RouteCollection $routes = null, + private iterable $expressionLanguageProviders = [], + ) { + if ($this->matcher instanceof RouterInterface) { + $this->routes ??= $this->matcher->getRouteCollection(); + } } /** diff --git a/src/Symfony/Bundle/WebProfilerBundle/Csp/ContentSecurityPolicyHandler.php b/src/Symfony/Bundle/WebProfilerBundle/Csp/ContentSecurityPolicyHandler.php index f7d8f5f1590b7..3ac92abadb250 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Csp/ContentSecurityPolicyHandler.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Csp/ContentSecurityPolicyHandler.php @@ -23,12 +23,11 @@ */ class ContentSecurityPolicyHandler { - private NonceGenerator $nonceGenerator; private bool $cspDisabled = false; - public function __construct(NonceGenerator $nonceGenerator) - { - $this->nonceGenerator = $nonceGenerator; + public function __construct( + private NonceGenerator $nonceGenerator, + ) { } /** @@ -124,10 +123,10 @@ private function updateCspHeaders(Response $response, array $nonces = []): array $headers = $this->getCspHeaders($response); $types = [ - 'script-src' => 'csp_script_nonce', - 'script-src-elem' => 'csp_script_nonce', - 'style-src' => 'csp_style_nonce', - 'style-src-elem' => 'csp_style_nonce', + 'script-src' => 'csp_script_nonce', + 'script-src-elem' => 'csp_script_nonce', + 'style-src' => 'csp_style_nonce', + 'style-src-elem' => 'csp_style_nonce', ]; foreach ($headers as $header => $directives) { diff --git a/src/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.php b/src/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.php index c2b350ff05d68..f3e818ba78399 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.php +++ b/src/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.php @@ -40,23 +40,15 @@ class WebDebugToolbarListener implements EventSubscriberInterface public const DISABLED = 1; public const ENABLED = 2; - private Environment $twig; - private ?UrlGeneratorInterface $urlGenerator; - private bool $interceptRedirects; - private int $mode; - private string $excludedAjaxPaths; - private ?ContentSecurityPolicyHandler $cspHandler; - private ?DumpDataCollector $dumpDataCollector; - - public function __construct(Environment $twig, bool $interceptRedirects = false, int $mode = self::ENABLED, ?UrlGeneratorInterface $urlGenerator = null, string $excludedAjaxPaths = '^/bundles|^/_wdt', ?ContentSecurityPolicyHandler $cspHandler = null, ?DumpDataCollector $dumpDataCollector = null) - { - $this->twig = $twig; - $this->urlGenerator = $urlGenerator; - $this->interceptRedirects = $interceptRedirects; - $this->mode = $mode; - $this->excludedAjaxPaths = $excludedAjaxPaths; - $this->cspHandler = $cspHandler; - $this->dumpDataCollector = $dumpDataCollector; + public function __construct( + private Environment $twig, + private bool $interceptRedirects = false, + private int $mode = self::ENABLED, + private ?UrlGeneratorInterface $urlGenerator = null, + private string $excludedAjaxPaths = '^/bundles|^/_wdt', + private ?ContentSecurityPolicyHandler $cspHandler = null, + private ?DumpDataCollector $dumpDataCollector = null, + ) { } public function isEnabled(): bool diff --git a/src/Symfony/Bundle/WebProfilerBundle/Profiler/CodeExtension.php b/src/Symfony/Bundle/WebProfilerBundle/Profiler/CodeExtension.php index 7fb51772d6d3d..c74744c4f13d2 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Profiler/CodeExtension.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Profiler/CodeExtension.php @@ -26,14 +26,14 @@ final class CodeExtension extends AbstractExtension { private string|FileLinkFormatter|array|false $fileLinkFormat; - private string $charset; - private string $projectDir; - public function __construct(string|FileLinkFormatter $fileLinkFormat, string $projectDir, string $charset) - { + public function __construct( + string|FileLinkFormatter $fileLinkFormat, + private string $projectDir, + private string $charset, + ) { $this->fileLinkFormat = $fileLinkFormat ?: \ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format'); $this->projectDir = str_replace('\\', '/', $projectDir).'/'; - $this->charset = $charset; } public function getFilters(): array diff --git a/src/Symfony/Bundle/WebProfilerBundle/Profiler/TemplateManager.php b/src/Symfony/Bundle/WebProfilerBundle/Profiler/TemplateManager.php index c75158c97388f..2b3f8c2f2c509 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Profiler/TemplateManager.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Profiler/TemplateManager.php @@ -24,15 +24,11 @@ */ class TemplateManager { - protected Environment $twig; - protected array $templates; - protected Profiler $profiler; - - public function __construct(Profiler $profiler, Environment $twig, array $templates) - { - $this->profiler = $profiler; - $this->twig = $twig; - $this->templates = $templates; + public function __construct( + protected Profiler $profiler, + protected Environment $twig, + protected array $templates, + ) { } /** diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/workflow.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/workflow.html.twig index 377b74f609f21..385e6c13999e3 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/workflow.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/workflow.html.twig @@ -3,7 +3,15 @@ {% block stylesheets %} {{ parent() }}