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() }}