diff --git a/.github/patch-types.php b/.github/patch-types.php index 2b633c9d99557..abcfb79109953 100644 --- a/.github/patch-types.php +++ b/.github/patch-types.php @@ -30,11 +30,15 @@ case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Prototype/BadClasses/MissingParent.php'): case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/'): case false !== strpos($file, '/src/Symfony/Component/ErrorHandler/Tests/Fixtures/'): - case false !== strpos($file, '/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php'): - case false !== strpos($file, '/src/Symfony/Component/PropertyInfo/Tests/Fixtures/ParentDummy.php'): + case false !== strpos($file, '/src/Symfony/Component/Form/Tests/Fixtures/Answer.php'): + case false !== strpos($file, '/src/Symfony/Component/Form/Tests/Fixtures/Number.php'): + case false !== strpos($file, '/src/Symfony/Component/Form/Tests/Fixtures/Suit.php'): + case false !== strpos($file, '/src/Symfony/Component/PropertyInfo/Tests/Fixtures/'): case false !== strpos($file, '/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php81Dummy.php'): case false !== strpos($file, '/src/Symfony/Component/Runtime/Internal/ComposerPlugin.php'): + case false !== strpos($file, '/src/Symfony/Component/Serializer/Tests/Fixtures/'): case false !== strpos($file, '/src/Symfony/Component/Serializer/Tests/Normalizer/Features/ObjectOuter.php'): + case false !== strpos($file, '/src/Symfony/Component/Validator/Tests/Fixtures/NestedAttribute/Entity.php'): case false !== strpos($file, '/src/Symfony/Component/VarDumper/Tests/Fixtures/NotLoadableClass.php'): case false !== strpos($file, '/src/Symfony/Component/VarDumper/Tests/Fixtures/ReflectionIntersectionTypeFixture.php'): continue 2; diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 19e3da080c93b..0820ae8d918b3 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -122,7 +122,7 @@ jobs: uses: shivammathur/setup-php@v2 with: coverage: "none" - extensions: "json,couchbase,memcached,mongodb,redis,rdkafka,xsl,ldap" + extensions: "json,couchbase,memcached,mongodb-1.10.0,redis,rdkafka,xsl,ldap" ini-values: date.timezone=Europe/Paris,memory_limit=-1,default_socket_timeout=10,session.gc_probability=0,apc.enable_cli=1,zend.assertions=1 php-version: "${{ matrix.php }}" tools: pecl @@ -146,7 +146,7 @@ jobs: echo COMPOSER_ROOT_VERSION=$COMPOSER_ROOT_VERSION >> $GITHUB_ENV echo "::group::composer update" - composer require --dev --no-update mongodb/mongodb:@stable + composer require --dev --no-update mongodb/mongodb:"1.9.1@dev|^1.9.1@stable" composer update --no-progress --ansi echo "::endgroup::" diff --git a/.github/workflows/psalm.yml b/.github/workflows/psalm.yml index c5c9bfecf4d09..3eeb7201c486d 100644 --- a/.github/workflows/psalm.yml +++ b/.github/workflows/psalm.yml @@ -17,7 +17,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: '8.0' - extensions: "json,memcached,mongodb,redis,xsl,ldap,dom" + extensions: "json,couchbase,memcached,mongodb,redis,xsl,ldap,dom" ini-values: "memory_limit=-1" coverage: none diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 1eefe6f5583c6..5d7c706647ee4 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -138,9 +138,11 @@ jobs: if: "${{ matrix.php == '8.1' && ! matrix.mode }}" run: | sed -i 's/"\*\*\/Tests\/"//' composer.json + git add . composer install -q --optimize-autoloader - SYMFONY_PATCH_TYPE_DECLARATIONS=force=1 php .github/patch-types.php - SYMFONY_PATCH_TYPE_DECLARATIONS=force=1 php .github/patch-types.php # ensure the script is idempotent + SYMFONY_PATCH_TYPE_DECLARATIONS='force=1&php=7.2' php .github/patch-types.php + SYMFONY_PATCH_TYPE_DECLARATIONS='force=1&php=7.2' php .github/patch-types.php # ensure the script is idempotent + git diff --exit-code echo PHPUNIT="$PHPUNIT,legacy" >> $GITHUB_ENV - name: Run tests diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index cfe2bb4456738..79b6547957e3b 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -41,6 +41,8 @@ ->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 + ->notPath('Symfony/Component/Messenger/Tests/Command/FailedMessagesShowCommandTest.php') ) ->setCacheFile('.php-cs-fixer.cache') ; diff --git a/CHANGELOG-5.4.md b/CHANGELOG-5.4.md new file mode 100644 index 0000000000000..7bf10c7c6623c --- /dev/null +++ b/CHANGELOG-5.4.md @@ -0,0 +1,207 @@ +CHANGELOG for 5.4.x +=================== + +This changelog references the relevant changes (bug and security fixes) done +in 5.4 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/v5.4.0...v5.4.1 + +* 5.4.0-BETA1 (2021-11-05) + + * feature #43916 [PropertyInfo] Support the list pseudo-type (derrabus) + * feature #43850 Add completion for DebugConfig name and path arguments (eclairia, Adrien Jourdier) + * feature #43838 feat: add completion for DebugAutowiring search argument (eclairia, Adrien Jourdier) + * feature #38464 [Routing] Add support for aliasing routes (Lctrs) + * feature #43923 [Console] Open CompleteCommand for custom outputs (wouterj) + * feature #43663 [Messenger] Add command completion for failed messages (scyzoryck) + * feature #43857 [Framework] Add completion to debug:container (GromNaN) + * feature #43891 [Messenger] Add completion to command messenger:consume (GromNaN) + * feature #42471 Add generic types to traversable implementations (derrabus) + * feature #43898 [Security] Make the abstract Voter class implement CacheableVoterInterface (javiereguiluz) + * feature #43848 [FrameworkBundle] Add completion for workflow:dump (StaffNowa) + * feature #43837 [Finder] Add .gitignore nested negated patterns support (julienfalque) + * feature #43754 Determine attribute or annotation type for directories (cinamo) + * feature #43846 Add completion for debug:twig (StaffNowa) + * feature #43138 [FrameworkBundle][HttpKernel] Add the ability to enable the profiler using a parameter (dunglas) + * feature #40457 [PropertyInfo] Add `PhpStanExtractor` (Korbeil) + * feature #40262 [DoctrineBridge] Param as connection in `*.event_subscriber/listener` tags (wbloszyk) + * feature #43354 [Messenger] allow processing messages in batches (nicolas-grekas) + * feature #43835 [SecurityBundle] Deprecate not configuring explicitly a provider for custom_authenticators when there is more than one registered provider (lyrixx) + * feature #43598 [Console] add suggestions for debug commands: firewall, form, messenger, router (IonBazan) + * feature #41993 [Security] Prevent `FormLoginAuthenticator` from responding to requests that should be handled by `JsonLoginAuthenticator` (abunch) + * feature #43751 [WebProfilerBundle] Add a "preview" tab in mailer profiler for HTML email (lyrixx) + * feature #43644 [FrameworkBundle] Add completion to debug:translation command (alexandre-daubois) + * feature #43653 [PasswordHasher] Add autocompletion for security commands (noniagriconomie) + * feature #43676 [FrameworkBundle] Add completion feature on translation:update command (stephenkhoo) + * feature #43672 [Translation] Add completion feature on translation pull and push commands (welcoMattic) + * feature #43060 [RateLimiter] Add support for long intervals (months and years) (alexandre-daubois) + * feature #42177 [Security][SecurityBundle] Implement ADM strategies as dedicated classes (derrabus) + * feature #43804 [DependencyInjection][FrameworkBundle][SecurityBundle][TwigBundle] Deprecate Composer 1 (derrabus) + * feature #43796 [Filesystem] Add third argument `$lockFile` to `Filesystem::appendToFile()` (fwolfsjaeger) + * feature #42414 [Notifier] Add Expo bridge (zairigimad) + * feature #43066 [Security] Cache voters that will always abstain (jderusse) + * feature #43758 [FrameworkBundle] Rename translation:update to translation:extract (welcoMattic) + * feature #41414 Support `statusCode` default param when loading template directly via route (dayallnash) + * feature #42238 [DependencyInjection] Add `SubscribedService` attribute, deprecate current `ServiceSubscriberTrait` usage (kbond) + * feature #38542 [FrameworkBundle][Serializer] Allow serializer default context configuration (soyuka) + * feature #43755 [Dotenv] Add $overrideExistingVars to bootEnv() and loadEnv() and dotenv_overload to SymfonyRuntime (fancyweb) + * feature #43671 add ResponseIsUnprocessable (garak) + * feature #43682 [FrameworkBundle] Add completion for config:dump-reference (StaffNowa) + * feature #43588 [Messenger] Autoconfigurable attributes (alirezamirsepassi) + * feature #43593 [Validator] Add CidrValidator to allow validation of CIDR notations (popsorin) + * feature #43683 [VarDumper] Add completion to server:dump command (alexandre-daubois) + * feature #43677 [RateLimiter] bug #42194 fix: sliding window policy to use microtime (jlekowski) + * feature #43679 [VarDumper] Add support for Fiber (lyrixx) + * feature #43680 Add suggestions for the option 'format' of lints commands: twig, yaml and xliff (makraz) + * feature #43621 Add completion for cache:pool:clear and cache:pool:delete commands (andyexeter) + * feature #43639 [Uid] Allow use autocompletion (StaffNowa) + * feature #43626 [Console] [Framework] Add completion to secrets:set and fix secrets:remove (GromNaN) + * feature #43640 [Console] Add completion to messenger:setup-transports command (Tayfun74) + * feature #43615 feat: add completion for CompletionCommand "shell" argument (dkarlovi) + * feature #43595 [Console] `SymfonyStyle` enhancements (kbond) + * feature #41268 [HttpFoundation] Allow setting session options via DSN (makraz) + * feature #43596 [Console] Add completion to help & list commands (GromNaN) + * feature #43576 [Messenger] subtract handling time from sleep time in worker (nicolas-grekas) + * feature #43386 [DependencyInjection] Extend TaggedIterator and TaggedLocator Attributes with able to specify defaultIndexMethod for #[TaggerIterator] and #[TaggedLocator] (fractalzombie) + * feature #42251 [Console] Bash completion integration (wouterj) + * feature #39402 [Notifier] Add push channel to notifier (norkunas) + * feature #43332 [Lock] Split PdoStore into DoctrineDbalStore (GromNaN) + * feature #43362 [Cache] Split PdoAdapter into DoctrineDbalAdapter (GromNaN) + * feature #42580 [Console][FrameworkBundle] Add DotenvDebugCommand (chr-hertel) + * feature #43411 [HttpFoundation] Deprecate passing null as $requestIp in IpUtils (W0rma) + * feature #43526 Add a warning in WDT when using symfony/symfony (fabpot) + * feature #43481 [String] Add `trimSuffix()` and `trimPrefix()` methods (nicolas-grekas) + * feature #43497 [Notifier] [Twilio] Ensure from/sender is valid via regex (OskarStark) + * feature #43492 Lower log level in case of retry (jderusse) + * feature #43479 [DependencyInjection] autowire union and intersection types (nicolas-grekas) + * feature #43134 [Notifier] Add sms77 Notifier Bridge (matthiez) + * feature #43378 [HttpFoundation] Deprecate upload_progress.* and url_rewriter.tags session options (Matthew Covey) + * feature #42582 [Security] Add authenticators info to the profiler (chalasr) + * feature #42723 [Messenger] Log when worker should stop and when `SIGTERM` is received (ruudk) + * feature #40168 [Validator] Added `CssColor` constraint (welcoMattic) + * feature #43328 [MonologBridge] Deprecate the Swiftmailer handler (fabpot) + * feature #43322 [MonologBridge] Deprecates ResetLoggersWorkerSubscriber (lyrixx) + * feature #43108 [HttpKernel] Add basic support for language negotiation (GregoireHebert) + * feature #41265 [Messenger] Add a middleware to log when transaction has been left open (lyrixx) + * feature #43280 [HttpClient] Add method to set response factory in mock client (greeflas) + * feature #42610 [Dotenv] Reimplementing symfony/flex' dump-env as a Symfony command (abdielcs, nicolas-grekas) + * feature #42244 [HttpKernel] Add support for configuring log level, and status code by exception class (lyrixx) + * feature #43236 [Security] Add alias for FirewallMapInterface to `@security`.firewall.map (lyrixx) + * feature #43150 [Finder] Add recursive .gitignore files support (julienfalque) + * feature #41608 [Runtime] Possibility to define the env and/or debug key (maxhelias) + * feature #42257 [Messenger] Allow using user's serializer for message do not fit the expected JSON structure (welcoMattic) + * feature #43148 [Cache] Throw ValueError in debug mode when serialization fails (nicolas-grekas) + * feature #43139 [Notifier] Mattermost Notifier option to post in an other channel (nathanaelmartel) + * feature #42335 [Messenger] Add `WorkerMetadata` to `Worker` class. (okwinza) + * feature #42712 [Serializer] Save missing arguments in MissingConstructorArgumentsException (BafS) + * feature #43004 [Serializer] Throw NotNormalizableValueException when type is not known or not in body in discriminator map (lyrixx) + * feature #43121 [Notifier] [GoogleChat] remove support for deprecated "threadKey" parameter (IonBazan) + * feature #42338 [DomCrawler] Added Crawler::innerText() method (Bilge) + * feature #43095 [Form] Add the EnumType (derrabus) + * feature #43094 [Console] Add support of RGB functional notation (alexandre-daubois) + * feature #43098 [Validator] Add error's uid to `Count` and `Length` constraints with "exactly" option enabled (VladGapanovich) + * feature #42668 [Yaml] Use more concise float representation in dump (dev97) + * feature #43017 [HttpFoundation] Map `multipart/form-data` as `form` Content-Type (keichinger) + * feature #43015 [DependencyInjection] Allow injecting tagged iterator as service locator arguments (IonBazan) + * feature #42991 [FrameworkBundle] Add configureContainer(), configureRoutes() and getConfigDir() to MicroKernelTrait (nicolas-grekas) + * feature #43018 [Mailer] Adding support for TagHeader and MetadataHeader to the Sendgrid API transport (gnito-org) + * feature #42988 [ErrorHandler] Add helper script to patch type declarations (wouterj) + * feature #42982 Add Session Token to Amazon Mailer (Jubeki) + * feature #42959 [DependencyInjection] Make auto-aliases private by default (nicolas-grekas) + * feature #42957 [RateLimiter][Runtime][Translation] remove ``@experimental`` flag (nicolas-grekas) + * feature #41163 [Mesenger] Add support for reseting container services between 2 messages (lyrixx) + * feature #41858 [Translation] Translate translatable parameters (kylekatarnls) + * feature #42941 Implement Message Stream for Postmark Mailer (driesvints) + * feature #42532 [DependencyInjection] Sort services in service locator according to priority (BoShurik) + * feature #42502 [Serializer] Add support for collecting type error during denormalization (lyrixx) + * feature #40120 [Cache] Add CouchbaseCollectionAdapter compatibility with sdk 3.0.0 (ajcerezo) + * feature #42965 [Cache] Deprecate support for Doctrine Cache (derrabus) + * feature #41615 [Serializer] Add option to skip uninitialized typed properties (vuryss) + * feature #41566 [FrameworkBundle] Introduced new method for getting bundles config path (a-menshchikov) + * feature #42881 [Console] Add more context when CommandIsSuccessful fails (yoannrenard) + * feature #42900 [HttpFoundation] Add a flag to hasSession to distinguished session from factory (jderusse) + * feature #41390 [HttpKernel] Add session cookie handling in cli context (alexander-schranz, Nyholm) + * feature #42800 Display the roles of the logged-in user in the Web Debug Toolbar (NicoHaase) + * feature #42872 [Mime] Update mime types (fabpot) + * feature #42039 [DependencyInjection] Autoconfigurable attributes on methods, properties and parameters (ruudk) + * feature #42710 [Mailer] Added OhMySMTP Bridge (paul-oms) + * feature #40987 [Config] Handle ignoreExtraKeys in config builder (HypeMC) + * feature #42426 [Notifier] Autoconfigure chatter.transport_factory (ismail1432) + * feature #42748 [Notifier] Add Esendex message ID to SentMessage object (benr77) + * feature #42526 [FrameworkBundle] Add BrowserKitAssertionsTrait::assertThatForBrowser (koenreiniers) + * feature #41527 [Ldap] Fixing the behaviour of getting LDAP Attributes (mr-sven) + * feature #42623 [ErrorHandler] Turn return-type annotations into deprecations by default + add mode to turn them into native types (nicolas-grekas) + * feature #42696 [Notifier] Mark Transport as final (fabpot) + * feature #42433 [Notifier] Add more explicit error if a SMSChannel doesn't have a Recipient (ismail1432) + * feature #42619 [Serializer] Deprecate support for returning empty, iterable, countable, raw object when normalizing (lyrixx) + * feature #42662 [Mailer] Consume a PSR-14 event dispatcher (derrabus) + * feature #42625 [DependencyInjection] Add service_closure() to the PHP-DSL (HypeMC) + * feature #42650 [Security] make TokenInterface::getUser() nullable to tell about unauthenticated tokens (nicolas-grekas) + * feature #42632 [Console] Deprecate `HelperSet::setCommand()` and `getCommand()` (derrabus) + * feature #41994 [Validator] Add support of nested attributes (alexandre-daubois) + * feature #42595 Fix incompatibilities with upcoming security 6.0 (wouterj) + * feature #42578 [Security] Deprecate legacy remember me services (wouterj) + * feature #42516 [Security] Deprecate built-in authentication entry points (wouterj) + * feature #42387 [Form] Deprecate calling FormErrorIterator::children() if the current element is not iterable (W0rma) + * feature #39641 [Yaml] Add --exclude and negatable --parse-tags option to lint:yaml command (christingruber) + * feature #42510 [Security] Deprecate remaining anonymous checks (wouterj) + * feature #42423 [Security] Deprecate AnonymousToken, non-UserInterface users, and token credentials (wouterj) + * feature #41954 [Filesystem] Add the Path class (theofidry) + * feature #42442 [FrameworkBundle] Deprecate AbstractController::get() and has() (fabpot) + * feature #42422 Clarify goals of AbstractController (fabpot) + * feature #42420 [Security] Deprecate legacy signatures (wouterj) + * feature #41754 [SecurityBundle] Create a smooth upgrade path for security factories (wouterj) + * feature #42198 [Security] Deprecate `PassportInterface` (chalasr) + * feature #42332 [HttpFoundation] Add `litespeed_finish_request` to `Response` (thomas2411) + * feature #42286 [HttpFoundation] Add `SessionFactoryInterface` (kbond) + * feature #42392 [HttpFoundation] Mark Request::get() internal (ro0NL) + * feature #39601 [Notifier] add `SentMessageEvent` and `FailedMessageEvent` (ismail1432) + * feature #42188 [Notifier] Add FakeChat Logger transport (noniagriconomie) + * feature #41522 [Notifier] Add TurboSms Bridge (fre5h) + * feature #42337 [Validator] Remove internal from `ConstraintViolationAssertion` (jordisala1991) + * feature #42123 [Notifier] Add FakeSMS Logger transport (noniagriconomie) + * feature #42297 [Serializer] Add support for serializing empty array as object (lyrixx) + * feature #42326 [Security] Deprecate remaining `LogoutHandlerInterface` implementations (chalasr) + * feature #42219 [Mailer] Add support of ping_threshold to SesTransportFactory (Tyraelqp) + * feature #40052 [ErrorHandler] Add button to copy the path where error is thrown (lmillucci) + * feature #38495 [Asset] [DX] Option to make asset manifests strict on missing item (GromNaN) + * feature #39828 [Translation] XliffLintCommand supports Github Actions annotations (YaFou) + * feature #39826 [TwigBridge] LintCommand supports Github Actions annotations (YaFou) + * feature #39141 [Notifier] Add Amazon SNS bridge (adrien-chinour) + * feature #42240 [Serializer] Add support for preserving empty object in object property (lyrixx) + * feature #42239 [Notifier] Add Yunpian Notifier Bridge (welcoMattic) + * feature #42195 [WebProfilerBundle] Redesigned the log section (javiereguiluz) + * feature #42163 [Messenger] [Redis] Prepare turning `delete_after_ack` to `true` in 6.0 (chalasr) + * feature #42180 [Notifier] Add bridge for smsc.ru (kozlice) + * feature #42137 [Finder] Make Comparator immutable (derrabus) + * feature #42127 [ExpressionLanguage] Store compiler and evaluator as closures (derrabus) + * feature #42094 [Notifier] [Slack] Throw error if maximum block limit is reached for slack message options (norkunas) + * feature #42050 [Security] Deprecate `TokenInterface::isAuthenticated()` (chalasr) + * feature #42090 [Notifier] [Slack] Include additional errors to slack notifier error message (norkunas) + * feature #41989 [Cache] make `LockRegistry` use semaphores when possible (nicolas-grekas) + * feature #41965 [Security] Deprecate "always authenticate" and "exception on no token" (wouterj) + * feature #41962 add ability to style doubles and integers independently (1ma) + * feature #40830 [Serializer] Add support of PHP backed enumerations (alexandre-daubois) + * feature #40908 [Cache] Deprecate DoctrineProvider (derrabus) + * feature #41717 Allow TranslatableMessage object in form option 'help' (scuben) + * feature #41705 [Notifier] add Mailjet SMS bridge (jnadaud) + * feature #41851 Add TesterTrait::assertCommandIsSuccessful() helper (yoannrenard) + * feature #39623 [Messenger] Added StopWorkerException (lyrixx) + * feature #41292 [Workflow] Add support for getting updated context after a transition (lyrixx) + * feature #41154 [Validator] Add support for `ConstraintViolationList::createFromMessage()` (lyrixx) + * feature #41874 [SecurityBundle] Hide security toolbar if no firewall matched (wouterj) + * feature #41375 [Notifier] Add MessageMedia Bridge (vuphuong87) + * feature #41923 [EventDispatcher] Deprecate configuring tags on RegisterListenersPass (derrabus) + * feature #41802 [Uid] Add NilUlid (fancyweb) + * feature #40738 [Notifier] Add options to Microsoft Teams notifier (OskarStark) + * feature #41172 [Notifier] Add Telnyx notifier bridge (StaffNowa) + * feature #41770 [HttpClient] Add default base_uri to MockHttpClient (nicolas-grekas) + * feature #41205 [TwigBridge] Add `encore_entry_*_tags()` to UndefinedCallableHandler, as no-op (nicolas-grekas) + * feature #41786 [FrameworkBundle] Add commented base64 version of secrets' keys (nicolas-grekas) + * feature #41432 [WebProfilerBundle] Improved the light/dark theme switching (javiereguiluz) + * feature #41540 [VarDumper] Add casters for Symfony UUIDs and ULIDs (fancyweb) + * feature #41530 [FrameworkBundle] Deprecate the public `profiler` service to private (nicolas-grekas) + * feature #41199 [FrameworkBundle] Deprecate the `AdapterInterface` autowiring alias, use `CacheItemPoolInterface` instead (nicolas-grekas) + * feature #41203 [FrameworkBundle] Add autowiring alias for `HttpCache\StoreInterface` (nicolas-grekas) + diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 507ca7e28d68b..1ac10f641e698 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -14,8 +14,8 @@ The Symfony Connect username in parenthesis allows to get more information - Christophe Coevoet (stof) - Wouter De Jong (wouterj) - Jérémy DERUSSÉ (jderusse) - - Maxime Steinhausser (ogizanagi) - Grégoire Pineau (lyrixx) + - Maxime Steinhausser (ogizanagi) - Kévin Dunglas (dunglas) - Jordi Boggiano (seldaek) - Victor Berchet (victor) @@ -53,8 +53,8 @@ The Symfony Connect username in parenthesis allows to get more information - Valentin Udaltsov (vudaltsov) - Iltar van der Berg (kjarli) - Jonathan Wage (jwage) - - Matthias Pigulla (mpdude) - Vasilij Duško (staff) + - Matthias Pigulla (mpdude) - Diego Saint Esteben (dosten) - Grégoire Paris (greg0ire) - Alexandre Salomé (alexandresalome) @@ -68,6 +68,7 @@ The Symfony Connect username in parenthesis allows to get more information - Titouan Galopin (tgalopin) - Laurent VOULLEMIER (lvo) - Vasilij Dusko | CREATION + - Jérôme Tamarelle (gromnan) - Bulat Shakirzyanov (avalanche123) - David Maicher (dmaicher) - gadelat (gadelat) @@ -81,15 +82,14 @@ The Symfony Connect username in parenthesis allows to get more information - Konstantin Kudryashov (everzet) - Vladimir Reznichenko (kalessil) - Bilal Amarni (bamarni) - - Jérôme Tamarelle (gromnan) - Florin Patan (florinpatan) - Jáchym Toušek (enumag) - Alex Pott + - Antoine M (amakdessi) - Michel Weimerskirch (mweimerskirch) - Andrej Hudec (pulzarraider) - Christian Raue - Issei Murasawa (issei_m) - - Antoine M (amakdessi) - Eric Clemmons (ericclemmons) - Charles Sarrazin (csarrazi) - Vasilij Dusko @@ -101,33 +101,35 @@ The Symfony Connect username in parenthesis allows to get more information - Henrik Westphal (snc) - Dariusz Górecki (canni) - Fran Moreno (franmomu) + - Alexander Schranz (alexander-schranz) - Dariusz Ruminski - Jérôme Vasseur (jvasseur) - Lee McDermott - Brandon Turner - Luis Cordova (cordoval) - Daniel Holmes (dholmes) - - Alexander Schranz (alexander-schranz) - Sebastiaan Stok (sstok) - Toni Uebernickel (havvg) - Bart van den Burg (burgov) - Jordan Alliot (jalliot) - John Wards (johnwards) + - Tomas Norkūnas (norkunas) - Baptiste Clavié (talus) - Antoine Hérault (herzult) - Paráda József (paradajozsef) - Vincent Langlet (deviling) + - Massimiliano Arione (garak) - Arnaud Le Blanc (arnaud-lb) - Przemysław Bogusz (przemyslaw-bogusz) - Maxime STEINHAUSSER - - Tomas Norkūnas (norkunas) - Michal Piotrowski (eventhorizon) - Tomáš Votruba (tomas_votruba) - - Massimiliano Arione (garak) - Mathias Arlaud (mtarld) - Tim Nagel (merk) + - Alexandre Daubois (alexandre-daubois) - HypeMC (hypemc) - Chris Wilkinson (thewilkybarkid) + - Julien Falque (julienfalque) - Peter Kokot (maastermedia) - Lars Strojny (lstrojny) - Brice BERNARD (brikou) @@ -141,14 +143,13 @@ The Symfony Connect username in parenthesis allows to get more information - Christian Scheb - Adrien Brault (adrienbrault) - Yanick Witschi (toflar) - - Julien Falque (julienfalque) - Jacob Dreesen (jdreesen) + - Mathieu Santostefano (welcomattic) - Malte Schlüter (maltemaltesich) - Joel Wurtz (brouznouf) - Théo FIDRY (theofidry) - Florian Voutzinos (florianv) - Teoh Han Hui (teohhanhui) - - Alexandre Daubois (alexandre-daubois) - Colin Frei - Javier Spagnoletti (phansys) - Joshua Thijssen @@ -158,27 +159,27 @@ The Symfony Connect username in parenthesis allows to get more information - Gordon Franke (gimler) - Saif Eddin Gmati (azjezz) - Richard van Laak (rvanlaak) + - Maxime Helias (maxhelias) - Jesse Rushlow (geeshoe) - Fabien Pennequin (fabienpennequin) - - Mathieu Santostefano (welcomattic) - Olivier Dolbeau (odolbeau) - Smaine Milianni (ismail1432) - Eric GELOEN (gelo) - Gary PEGEOT (gary-p) - Matthieu Napoli (mnapoli) - - Maxime Helias (maxhelias) + - Ruud Kamphuis (ruudk) - Jannik Zschiesche (apfelbox) - Robert Schönthal (digitalkaoz) - Florian Lonqueu-Brochard (florianlb) - Tigran Azatyan (tigranazatyan) - YaFou - Gabriel Caruso (carusogabriel) - - Ruud Kamphuis (ruudk) - Stefano Sala (stefano.sala) - Andréia Bohner (andreia) - Evgeniy (ewgraf) - Vincent AUBERT (vincent) - Juti Noppornpitak (shiroyuki) + - Simon Berger - Anthony MARTIN (xurudragon) - Sebastian Hörl (blogsh) - Daniel Gomes (danielcsgomes) @@ -205,6 +206,7 @@ The Symfony Connect username in parenthesis allows to get more information - Joe Bennett (kralos) - Mikael Pajunen - Andreas Schempp (aschempp) + - Alessandro Lai (jean85) - Romaric Drigon (romaricdrigon) - Arman Hosseini (arman) - Niels Keurentjes (curry684) @@ -220,9 +222,9 @@ The Symfony Connect username in parenthesis allows to get more information - Rouven Weßling (realityking) - Jérôme Parmentier (lctrs) - Ben Davies (bendavies) - - Alessandro Lai (jean85) - Clemens Tolboom - Helmer Aaviksoo + - Christopher Hertel (chertel) - Remon van de Kamp (rpkamp) - Filippo Tessarotto (slamdunk) - Hiromi Hishida (77web) @@ -242,7 +244,6 @@ The Symfony Connect username in parenthesis allows to get more information - Dmitrii Poddubnyi (karser) - Michael Babker (mbabker) - Tien Vo (tienvx) - - Simon Berger - Timothée Barray (tyx) - James Halsall (jaitsu) - Florent Mata (fmata) @@ -262,7 +263,6 @@ The Symfony Connect username in parenthesis allows to get more information - Richard Miller (mr_r_miller) - Mario A. Alvarez Garcia (nomack84) - Dennis Benkert (denderello) - - Christopher Hertel (chertel) - DQNEO - Hidde Wieringa (hiddewie) - Antonio Pauletich (x-coder264) @@ -274,6 +274,7 @@ The Symfony Connect username in parenthesis allows to get more information - mcfedr (mcfedr) - Ruben Gonzalez (rubenrua) - Benjamin Dulau (dbenjamin) + - zairig imad (zairigimad) - Baptiste Lafontaine (magnetik) - Mathieu Lemoine (lemoinem) - Denis Brumann (dbrumann) @@ -305,6 +306,7 @@ The Symfony Connect username in parenthesis allows to get more information - Dominique Bongiraud - dFayet - Jeremy Livingston (jeremylivingston) + - soyuka - Michael Lee (zerustech) - Matthieu Auger (matthieuauger) - Leszek Prabucki (l3l0) @@ -315,7 +317,6 @@ The Symfony Connect username in parenthesis allows to get more information - Dustin Whittle (dustinwhittle) - jeff - John Kary (johnkary) - - zairig imad (zairigimad) - Justin Hileman (bobthecow) - Blanchon Vincent (blanchonvincent) - Maciej Malarz (malarzm) @@ -345,7 +346,6 @@ The Symfony Connect username in parenthesis allows to get more information - Marcin Szepczynski (czepol) - Rob Frawley 2nd (robfrawley) - Ahmed Raafat - - soyuka - julien pauli (jpauli) - Lorenz Schori - Sébastien Lavoie (lavoiesl) @@ -353,6 +353,7 @@ The Symfony Connect username in parenthesis allows to get more information - Farhad Safarov (safarov) - BoShurik - Thomas Lallement (raziel057) + - Michael Voříšek - Francois Zaninotto - Claude Khedhiri (ck-developer) - Alexander Kotynia (olden) @@ -361,6 +362,7 @@ The Symfony Connect username in parenthesis allows to get more information - Marcos Sánchez - Elnur Abdurrakhimov (elnur) - Manuel Reinhard (sprain) + - fd6130 (fdtvui) - Harm van Tilborg (hvt) - Danny Berger (dpb587) - Antonio J. García Lagar (ajgarlag) @@ -409,7 +411,6 @@ The Symfony Connect username in parenthesis allows to get more information - Mohammad Emran Hasan (phpfour) - Dmitriy Mamontov (mamontovdmitriy) - Ben Ramsey (ramsey) - - Michael Voříšek - Laurent Masforné (heisenberg) - Giorgio Premi - Guillaume (guill) @@ -450,11 +451,14 @@ The Symfony Connect username in parenthesis allows to get more information - Alan Poulain (alanpoulain) - Chris Smith (cs278) - Florian Klein (docteurklein) + - W0rma + - Dāvis Zālītis (k0d3r1s) - Manuel Kiessling (manuelkiessling) - Dimitri Gritsajuk (ottaviano) - Alexey Kopytko (sanmai) - Pol Dellaiera (drupol) - Atsuhiro KUBO (iteman) + - Alireza Mirsepassi (alirezamirsepassi) - rudy onfroy (ronfroy) - Serkan Yildiz (srknyldz) - Andrew Moore (finewolf) @@ -501,7 +505,9 @@ The Symfony Connect username in parenthesis allows to get more information - ivan - Greg Anderson - Tri Pham (phamuyentri) + - Urinbayev Shakhobiddin (shokhaa) - Gennady Telegin (gtelegin) + - Sergey (upyx) - Krystian Marcisz (simivar) - Toni Rudolf (toooni) - Erin Millard @@ -534,13 +540,13 @@ The Symfony Connect username in parenthesis allows to get more information - Tarmo Leppänen (tarlepp) - Martin Auswöger - Robbert Klarenbeek (robbertkl) + - Hamza Makraz (makraz) - Eric Masoero (eric-masoero) - Vitalii Ekert (comrade42) - JhonnyL - hossein zolfi (ocean) - Clément Gautier (clementgautier) - Koen Reiniers (koenre) - - Dāvis Zālītis (k0d3r1s) - Sanpi - Eduardo Gulias (egulias) - giulio de donato (liuggio) @@ -551,10 +557,12 @@ The Symfony Connect username in parenthesis allows to get more information - Grzegorz Zdanowski (kiler129) - Kirill chEbba Chebunin (chebba) - + - Fabien Villepinte - Matthew Grasmick - Greg Thornton (xdissent) - BENOIT POLASZEK (bpolaszek) - Alex Bowers + - Piotr Kugla (piku235) - Philipp Cordes - Jeroen Thora (bolle) - Costin Bereveanu (schniper) @@ -577,6 +585,7 @@ The Symfony Connect username in parenthesis allows to get more information - Daniel Beyer - Manuel Alejandro Paz Cetina - Shein Alexey + - Aleksandar Jakovljevic (ajakov) - Jacek Jędrzejewski (jacek.jedrzejewski) - Romain Gautier (mykiwi) - Stefan Kruppa @@ -596,11 +605,13 @@ The Symfony Connect username in parenthesis allows to get more information - Miha Vrhovnik - Alessandro Desantis - hubert lecorche (hlecorche) + - Vladyslav Loboda - fritzmg - flack (flack) - Marc Morales Valldepérez (kuert) - Jean-Baptiste GOMOND (mjbgo) - Vadim Kharitonov (virtuozzz) + - Jurica Vlahoviček (vjurica) - Oscar Cubo Medina (ocubom) - Karel Souffriau - Christophe L. (christophelau) @@ -610,7 +621,6 @@ The Symfony Connect username in parenthesis allows to get more information - Marc Laporte - Michał Jusięga - Bernd Stellwag - - Alireza Mirsepassi (alirezamirsepassi) - Sébastien Santoro (dereckson) - Gennadi Janzen - Brian King @@ -625,14 +635,13 @@ The Symfony Connect username in parenthesis allows to get more information - Christin Gruber (christingruber) - Andrey Sevastianov - Webnet team (webnet) - - Urinbayev Shakhobiddin (shokhaa) - marie - Jan Schumann - Noémi Salaün (noemi-salaun) - Niklas Fiekas - Philippe Segatori + - Dalibor Karlović (dkarlovi) - Markus Bachmann (baachi) - - fd6130 (fdtvui) - Kévin THERAGE (kevin_therage) - Michel Hunziker - Gunnstein Lye (glye) @@ -656,6 +665,7 @@ The Symfony Connect username in parenthesis allows to get more information - Stefan Gehrig (sgehrig) - vagrant - Aurimas Niekis (gcds) + - Hendrik Luup (hluup) - EdgarPE - Florian Pfitzer (marmelatze) - Asier Illarramendi (doup) @@ -739,7 +749,6 @@ The Symfony Connect username in parenthesis allows to get more information - Pablo Díez (pablodip) - SiD (plbsid) - Michel Roca (mroca) - - Piotr Kugla (piku235) - Kevin McBride - Sergio Santoro - Robin van der Vleuten (robinvdvleuten) @@ -783,6 +792,7 @@ The Symfony Connect username in parenthesis allows to get more information - Erik Trapman (eriktrapman) - De Cock Xavier (xdecock) - Almog Baku (almogbaku) + - Evert Harmeling (evertharmeling) - Scott Arciszewski - Xavier HAUSHERR - Norbert Orzechowicz (norzechowicz) @@ -811,6 +821,7 @@ The Symfony Connect username in parenthesis allows to get more information - Rodrigo Borrego Bernabé (rodrigobb) - Emanuele Iannone - Jörn Lang (j.lang) + - Marcos Rezende (rezehnde) - Denis Gorbachev (starfall) - Peter van Dommelen - Tim van Densen @@ -893,7 +904,6 @@ The Symfony Connect username in parenthesis allows to get more information - vitaliytv - Nicolas Martin (cocorambo) - Adrian Nguyen (vuphuong87) - - Dalibor Karlović (dkarlovi) - Sebastian Blum - Alexis Lefebvre - Laurent Clouet @@ -927,7 +937,6 @@ The Symfony Connect username in parenthesis allows to get more information - Nahuel Cuesta (ncuesta) - Chris Boden (cboden) - Christophe Villeger (seragan) - - Hendrik Luup - Julien Fredon - Jacek Wilczyński (jacekwilczynski) - Xavier Leune (xleune) @@ -973,6 +982,7 @@ The Symfony Connect username in parenthesis allows to get more information - Claus Due (namelesscoder) - adev - Alexandru Patranescu + - Andy Palmer (andyexeter) - Stefan Warman - Tristan Maindron (tmaindron) - Behnoush norouzali (behnoush) @@ -1003,7 +1013,6 @@ The Symfony Connect username in parenthesis allows to get more information - Tomas Javaisis - Ivan Grigoriev - Johann Saunier (prophet777) - - Sergey (upyx) - Fabien Salles (blacked) - Andreas Erhard - John VanDeWeghe @@ -1081,6 +1090,7 @@ The Symfony Connect username in parenthesis allows to get more information - Junaid Farooq (junaidfarooq) - Massimiliano Braglia (massimilianobraglia) - Frankie Wittevrongel + - Jerzy (jlekowski) - Richard Quadling - Raphaëll Roussel - Anton Kroshilin @@ -1202,7 +1212,6 @@ The Symfony Connect username in parenthesis allows to get more information - Ahmadou Waly Ndiaye (waly) - Antonin CLAUZIER (0x346e3730) - moldman - - Evert Harmeling (evertharmeling) - Jonathan Johnson (jrjohnson) - Olivier Maisonneuve (olineuve) - Pedro Miguel Maymone de Resende (pedroresende) @@ -1251,14 +1260,14 @@ The Symfony Connect username in parenthesis allows to get more information - frost-nzcr4 - Taylor Otwell - Sami Mussbach + - Dhananjay Goratela - Kien Nguyen - Foxprodev - Eric Hertwig - Niels Robin-Aubertin + - Achilles Kaloeridis (achilles) - Adrien Wilmet (adrienfr) - - Aleksandar Jakovljevic (ajakov) - Laurent Bassin (lbassin) - - Hamza Makraz (makraz) - Tomasz Ignatiuk - andrey1s - Abhoryo @@ -1267,7 +1276,9 @@ The Symfony Connect username in parenthesis allows to get more information - Stéphan Kochen - Steven Dubois - Arjan Keeman + - siganushka - Alaattin Kahramanlar (alaattin) + - Dadang NH (dadangnh) - Sergey Zolotov (enleur) - Maksim Kotlyar (makasim) - Neil Ferreira @@ -1281,13 +1292,11 @@ The Symfony Connect username in parenthesis allows to get more information - Tony Malzhacker - Pchol - Mathieu MARCHOIS - - W0rma - Cyril Quintin (cyqui) - Cyrille Bourgois (cyrilleb) - Gerard van Helden (drm) - Johnny Peck (johnnypeck) - Jordi Sala Morales (jsala) - - Marcos Rezende (rezehnde) - Roman Anasal - Ivan Menshykov - David Romaní @@ -1314,6 +1323,7 @@ The Symfony Connect username in parenthesis allows to get more information - pdragun - corphi - JoppeDC + - Daniel Tiringer - grizlik - Derek ROTH - Ben Johnson @@ -1328,6 +1338,7 @@ The Symfony Connect username in parenthesis allows to get more information - Simon Leblanc (leblanc_simon) - Matthieu Mota (matthieumota) - Mikhail Prosalov (mprosalov) + - Petr Duda (petrduda) - Ronny López (ronnylt) - abdul malik ikhsan (samsonasik) - Henry Snoek (snoek09) @@ -1432,6 +1443,7 @@ The Symfony Connect username in parenthesis allows to get more information - Htun Htun Htet (ryanhhh91) - Guillaume Gammelin - Valérian Galliat + - Sorin Pop (sorinpop) - d-ph - Renan Taranto (renan-taranto) - Adrien Chinour @@ -1443,6 +1455,7 @@ The Symfony Connect username in parenthesis allows to get more information - The Whole Life to Learn - Mikkel Paulson - ergiegonzaga + - André Matthies - Liverbool (liverbool) - Valentin Nazarov - Jérôme Nadaud (jnadaud) @@ -1452,7 +1465,9 @@ The Symfony Connect username in parenthesis allows to get more information - neghmurken - xaav - Mahmoud Mostafa (mahmoud) + - Fractal Zombie - Ahmed Abdou + - shreyadenny - Daniel Iwaniec - Pieter - Michael Tibben @@ -1461,6 +1476,8 @@ The Symfony Connect username in parenthesis allows to get more information - Albion Bame (abame) - Ganesh Chandrasekaran - Sander Marechal + - Ivan Nemets + - Grégoire Hébert (gregoirehebert) - Franz Wilding (killerpoke) - ProgMiner - Oleg Golovakhin (doc_tr) @@ -1515,10 +1532,12 @@ The Symfony Connect username in parenthesis allows to get more information - Stanislav Kocanda - DerManoMann - Guillaume Royer + - Erfan Bahramali - Artem (digi) - boite - Silvio Ginter - MGDSoft + - Abdiel Carrazana (abdielcs) - Vadim Tyukov (vatson) - Arman - Gabi Udrescu @@ -1576,6 +1595,7 @@ The Symfony Connect username in parenthesis allows to get more information - Maximilian Berghoff (electricmaxxx) - nacho - Piotr Antosik (antek88) + - mwos - Volker Killesreiter (ol0lll) - Vedran Mihočinec (v-m-i) - Sergey Novikov (s12v) @@ -1588,14 +1608,17 @@ The Symfony Connect username in parenthesis allows to get more information - Angel Koilov (po_taka) - RevZer0 (rav) - Dan Finnie + - Marek Binkowski - Ken Marfilla (marfillaster) - benatespina (benatespina) - Denis Kop - Jean-Guilhem Rouel (jean-gui) + - Yoann MOROCUTTI - jfcixmedia - Dominic Tubach - Nikita Konstantinov - Martijn Evers + - Alexander Onatskiy - Philipp Fritsche - tarlepp - Benjamin Paap (benjaminpaap) @@ -1750,6 +1773,7 @@ The Symfony Connect username in parenthesis allows to get more information - Krzysztof Przybyszewski - alexpozzi - Vladimir + - Quentin Devos - Jorge Vahldick (jvahldick) - Frederic Godfrin - Paul Matthews @@ -1890,6 +1914,7 @@ The Symfony Connect username in parenthesis allows to get more information - Vašek Purchart (vasek-purchart) - Janusz Jabłoński (yanoosh) - Fleuv + - Tayfun Aydin - Sandro Hopf - Łukasz Makuch - Arne Groskurth @@ -1908,8 +1933,10 @@ The Symfony Connect username in parenthesis allows to get more information - Philip Frank - David Brooks - Lance McNearney + - Volodymyr Kupriienko (greeflas) - Serhiy Lunak (slunak) - Giorgio Premi + - Sergey Belyshkin - tamcy - Mikko Pesari - ncou @@ -1989,6 +2016,7 @@ The Symfony Connect username in parenthesis allows to get more information - Serhii Smirnov - Robert Queck - Peter Bouwdewijn + - Martins Eglitis - mlively - Wouter Diesveld - Amine Matmati @@ -2009,6 +2037,7 @@ The Symfony Connect username in parenthesis allows to get more information - Ergie Gonzaga - Matthew J Mucklo - AnrDaemon + - Matthew Covey - Anthony Massard (decap94) - Emre Akinci (emre) - Chris Maiden (matason) @@ -2051,6 +2080,7 @@ The Symfony Connect username in parenthesis allows to get more information - Justin (wackymole) - Flavian (2much) - Gautier Deuette + - dsech - mike - Gilbertsoft - tadas @@ -2083,6 +2113,7 @@ The Symfony Connect username in parenthesis allows to get more information - Tobias Genberg (lorceroth) - Nicolas Badey (nico-b) - Shane Preece (shane) + - Stephan Wentz (temp) - Johannes Goslar - Geoff - georaldc @@ -2094,7 +2125,6 @@ The Symfony Connect username in parenthesis allows to get more information - Gavin Staniforth - bahram - Alessandro Tagliapietra (alex88) - - Andy Palmer (andyexeter) - Biji (biji) - Alex Teterin (errogaht) - Gunnar Lium (gunnarlium) @@ -2110,6 +2140,8 @@ The Symfony Connect username in parenthesis allows to get more information - mschop - Martin Eckhardt - natechicago + - Victor + - Andreas Allacher - Alexis - Sergei Gorjunov - Jonathan Poston @@ -2145,14 +2177,18 @@ The Symfony Connect username in parenthesis allows to get more information - Matt Farmer - catch - aetxebeste - - siganushka - Alexandre Segura + - afaricamp - Josef Cech - Glodzienski + - riadh26 + - Konstantinos Alexiou - Andrii Boiko - Harold Iedema + - WaiSkats - Ikhsan Agustian - Arnau González (arnaugm) + - Bahman Mehrdad (bahman) - Simon Bouland (bouland) - Jibé Barth (jibbarth) - Matthew Foster (mfoster) @@ -2208,6 +2244,7 @@ The Symfony Connect username in parenthesis allows to get more information - nuncanada - František Bereň - Kamil Madejski + - G.R.Dalenoort - Jeremiah VALERIE - Mike Francis - Vladimir Khramtsov (chrome) @@ -2291,6 +2328,8 @@ The Symfony Connect username in parenthesis allows to get more information - Aaron Somi - kshida - Michał Dąbrowski (defrag) + - Aryel Tupinamba (dfkimera) + - Hans Höchtl (hhoechtl) - Simone Fumagalli (hpatoio) - Brian Graham (incognito) - Kevin Vergauwen (innocenzo) @@ -2348,6 +2387,7 @@ The Symfony Connect username in parenthesis allows to get more information - Walther Lalk - Adam - Ivo + - Ismo Vuorinen - Sören Bernstein - devel - taiiiraaa @@ -2370,6 +2410,7 @@ The Symfony Connect username in parenthesis allows to get more information - vlakoff - bertillon - thib92 + - Yiorgos Kalligeros - Rudolf Ratusiński - Bertalan Attila - Arek Bochinski @@ -2436,24 +2477,32 @@ The Symfony Connect username in parenthesis allows to get more information - Dan Ordille (dordille) - Jan Eichhorn (exeu) - Grégory Pelletier (ip512) + - Johan Wilfer (johanwilfer) - John Nickell (jrnickell) - Martin Mayer (martin) - Grzegorz Łukaszewicz (newicz) - Jonny Schmid (schmidjon) + - Toby Griffiths (tog) - Götz Gottwald + - Alessandra Lai - Veres Lajos - Ernest Hymel + - Andrea Civita - LoginovIlya - Nick Chiu - grifx - Robert Campbell - Matt Lehner + - Shakhobiddin - Helmut Januschka - Hein Zaw Htet™ - Ruben Kruiswijk - Cosmin-Romeo TANASE + - Ana Raro - Michael J + - youssef saoubou - Joseph Maarek + - Ivan Sarastov - Alexander Menk - Alex Pods - hadriengem @@ -2475,23 +2524,26 @@ The Symfony Connect username in parenthesis allows to get more information - Gerben Wijnja - Emre YILMAZ - Rowan Manning + - Marcos Labad - Per Modin - David Windell - Frank Jogeleit - Ondřej Frei - Gabriel Birke - - Daniel Tiringer - skafandri - Derek Bonner - martijn - Storkeus - Alan Chen - Anton Zagorskii + - ging-dev + - zakaria-amm - insidestyles - Maerlyn - Even André Fiskvik - Agata - dakur + - florian-michael-mast - Александр Ли - Arjan Keeman - Vlad Dumitrache @@ -2524,7 +2576,6 @@ The Symfony Connect username in parenthesis allows to get more information - Daniel González Cerviño - Rafał - Ahmad El-Bardan (absahmad) - - Achilles Kaloeridis (achilles) - Adria Lopez (adlpz) - Aaron Scherer (aequasi) - Rosio (ben-rosio) @@ -2550,6 +2601,7 @@ The Symfony Connect username in parenthesis allows to get more information - Javier Núñez Berrocoso (javiernuber) - Jelle Bekker (jbekker) - Jonathan Sui Lioung Lee Slew (jlslew) + - Johan Vlaar (johjohan) - Giovanni Albero (johntree) - Jorge Martin (jorgemartind) - Joeri Verdeyen (jverdeyen) @@ -2562,13 +2614,13 @@ The Symfony Connect username in parenthesis allows to get more information - Michael Pohlers (mick_the_big) - Misha Klomp (mishaklomp) - mlpo (mlpo) + - Ulrik Nielsen (mrbase) - Marek Šimeček (mssimi) - Dmitriy Tkachenko (neka) - Cayetano Soriano Gallego (neoshadybeat) - Artem (nexim) - Nicolas ASSING (nicolasassing) - Olivier Laviale (olvlvl) - - Petr Duda (petrduda) - Pierre Gasté (pierre_g) - Pablo Monterde Perez (plebs) - Pierre-Olivier Vares (povares) @@ -2579,6 +2631,7 @@ The Symfony Connect username in parenthesis allows to get more information - Wim Godden (wimg) - Yorkie Chadwick (yorkie76) - Maxime Aknin (3m1x4m) + - Geordie - Exploit.cz - GuillaumeVerdon - Philipp Keck @@ -2610,6 +2663,7 @@ The Symfony Connect username in parenthesis allows to get more information - Shrey Puranik - Lars Moelleken - dasmfm + - Claas Augner - Mathias Geat - Arnaud Buathier (arnapou) - chesteroni (chesteroni) @@ -2771,7 +2825,6 @@ The Symfony Connect username in parenthesis allows to get more information - Alex Nostadt - Michael Squires - Egor Gorbachev - - Fabien Villepinte - Derek Stephen McLean - Norman Soetbeer - zorn @@ -2788,6 +2841,7 @@ The Symfony Connect username in parenthesis allows to get more information - jspee - Ilya Bulakh - David Soria Parra + - Egor Taranov - Sergiy Sokolenko - detinkin - Ahmed Abdulrahman @@ -2801,6 +2855,7 @@ The Symfony Connect username in parenthesis allows to get more information - DanSync - Peter Zwosta - parhs + - Harry Wiseman - Diego Campoy - TeLiXj - Oncle Tom @@ -2873,6 +2928,7 @@ The Symfony Connect username in parenthesis allows to get more information - Arash Tabriziyan (ghost098) - Greg Szczotka (greg606) - ibasaw (ibasaw) + - Nathan DIdier (icz) - Vladislav Krupenkin (ideea) - Ilija Tovilo (ilijatovilo) - Peter Orosz (ill_logical) diff --git a/README.md b/README.md index 02417bc9835d2..2a43c34952b2f 100644 --- a/README.md +++ b/README.md @@ -17,11 +17,11 @@ Installation Sponsor ------- -Symfony 5.3 is [backed][27] by [JoliCode][28]. +Symfony 5.4 is [backed][27] by [Private Packagist][28]. -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. +Private Packagist is a fast, reliable, and secure Composer repository for your +private packages. It mirrors all your open-source dependencies for better +availability and monitors them for security vulnerabilities. Help Symfony by [sponsoring][29] its development! @@ -86,5 +86,5 @@ 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://jolicode.com/ +[28]: https://packagist.com/ [29]: https://symfony.com/sponsor diff --git a/UPGRADE-5.4.md b/UPGRADE-5.4.md new file mode 100644 index 0000000000000..f98966dbf1b2d --- /dev/null +++ b/UPGRADE-5.4.md @@ -0,0 +1,165 @@ +UPGRADE FROM 5.3 to 5.4 +======================= + +Cache +----- + + * Deprecate `DoctrineProvider` and `DoctrineAdapter` because these classes have been added to the `doctrine/cache` package + * Deprecate usage of `PdoAdapter` with a `Doctrine\DBAL\Connection` or a DBAL URL. Use the new `DoctrineDbalAdapter` instead + +Console +------- + + * Deprecate `HelperSet::setCommand()` and `getCommand()` without replacement + +Finder +------ + + * Deprecate `Comparator::setTarget()` and `Comparator::setOperator()` + * Add a constructor to `Comparator` that allows setting target and operator + +Form +------ + + * Deprecate calling `FormErrorIterator::children()` if the current element is not iterable. + +FrameworkBundle +--------------- + + * Deprecate the `framework.translator.enabled_locales` config option, use `framework.enabled_locales` instead + * Deprecate the `AdapterInterface` autowiring alias, use `CacheItemPoolInterface` instead + * Deprecate the public `profiler` service to private + * Deprecate `get()`, `has()`, `getDoctrine()`, and `dispatchMessage()` in `AbstractController`, use method/constructor injection instead + * Deprecate the `cache.adapter.doctrine` service: The Doctrine Cache library is deprecated. Either switch to Symfony Cache or use the PSR-6 adapters provided by Doctrine Cache. + +HttpKernel +---------- + + * Deprecate `AbstractTestSessionListener::getSession` inject a session in the request instead + +HttpFoundation +-------------- + + * Deprecate passing `null` as `$requestIp` to `IpUtils::checkIp()`, `IpUtils::checkIp4()` or `IpUtils::checkIp6()`, pass an empty string instead. + * Mark `Request::get()` internal, use explicit input sources instead + * Deprecate `upload_progress.*` and `url_rewriter.tags` session options + +Lock +---- + + * Deprecate usage of `PdoStore` with a `Doctrine\DBAL\Connection` or a DBAL url, use the new `DoctrineDbalStore` instead + * Deprecate usage of `PostgreSqlStore` with a `Doctrine\DBAL\Connection` or a DBAL url, use the new `DoctrineDbalPostgreSqlStore` instead + +Messenger +--------- + + * Deprecate not setting the `delete_after_ack` config option (or DSN parameter) using the Redis transport, + its default value will change to `true` in 6.0 + * Deprecate not setting the `reset_on_message` config option, its default value will change to `true` in 6.0 + +Monolog +------- + + * Deprecate `ResetLoggersWorkerSubscriber` to reset buffered logs in messenger + workers, use "reset_on_message" option in messenger configuration instead. + +SecurityBundle +-------------- + + * Deprecate `FirewallConfig::getListeners()`, use `FirewallConfig::getAuthenticators()` instead + * Deprecate `security.authentication.basic_entry_point` and `security.authentication.retry_entry_point` services, the logic is moved into the + `HttpBasicAuthenticator` and `ChannelListener` respectively + * Deprecate not setting `$authenticatorManagerEnabled` to `true` in `SecurityDataCollector` and `DebugFirewallCommand` + * Deprecate `SecurityFactoryInterface` and `SecurityExtension::addSecurityListenerFactory()` in favor of + `AuthenticatorFactoryInterface` and `SecurityExtension::addAuthenticatorFactory()` + * Add `AuthenticatorFactoryInterface::getPriority()` which replaces `SecurityFactoryInterface::getPosition()`. + Previous positions are mapped to the following priorities: + + | Position | Constant | Priority | + | ----------- | ----------------------------------------------------- | -------- | + | pre_auth | `RemoteUserFactory::PRIORITY`/`X509Factory::PRIORITY` | -10 | + | form | `FormLoginFactory::PRIORITY` | -30 | + | http | `HttpBasicFactory::PRIORITY` | -50 | + | remember_me | `RememberMeFactory::PRIORITY` | -60 | + | anonymous | n/a | -70 | + + * Deprecate passing an array of arrays as 1st argument to `MainConfiguration`, pass a sorted flat array of + factories instead. + * Deprecate the `always_authenticate_before_granting` option + +Security +-------- + + * Deprecate `AuthenticationEvents::AUTHENTICATION_FAILURE`, use the `LoginFailureEvent` instead + * Deprecate the `$authenticationEntryPoint` argument of `ChannelListener`, and add `$httpPort` and `$httpsPort` arguments + * Deprecate `RetryAuthenticationEntryPoint`, this code is now inlined in the `ChannelListener` + * Deprecate `FormAuthenticationEntryPoint` and `BasicAuthenticationEntryPoint`, in the new system the `FormLoginAuthenticator` + and `HttpBasicAuthenticator` should be used instead + * Deprecate `AbstractRememberMeServices`, `PersistentTokenBasedRememberMeServices`, `RememberMeServicesInterface`, + `TokenBasedRememberMeServices`, use the remember me handler alternatives instead + * Deprecate `AnonymousToken`, as the related authenticator was deprecated in 5.3 + * Deprecate `Token::getCredentials()`, tokens should no longer contain credentials (as they represent authenticated sessions) + * Deprecate not returning an `UserInterface` from `Token::getUser()` + * Deprecate `AuthenticatedVoter::IS_AUTHENTICATED_ANONYMOUSLY` and `AuthenticatedVoter::IS_ANONYMOUS`, + use `AuthenticatedVoter::PUBLIC_ACCESS` instead. + + Before: + ```yaml + # config/packages/security.yaml + security: + # ... + access_control: + - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY } + ``` + + After: + ```yaml + # config/packages/security.yaml + security: + # ... + access_control: + - { path: ^/login, roles: PUBLIC_ACCESS } + ``` + + * Deprecate `AuthenticationTrustResolverInterface::isAnonymous()` and the `is_anonymous()` expression function + as anonymous no longer exists in version 6, use the `isFullFledged()` or the new `isAuthenticated()` instead + if you want to check if the request is (fully) authenticated. + * Deprecate the `$authManager` argument of `AccessListener`, the argument will be removed + * Deprecate the `$authenticationManager` argument of the `AuthorizationChecker` constructor, the argument will be removed + * Deprecate setting the `$alwaysAuthenticate` argument to `true` and not setting the + `$exceptionOnNoToken argument to `false` of `AuthorizationChecker` (this is the default + behavior when using `enable_authenticator_manager: true`) + * Deprecate not setting the `$exceptionOnNoToken` argument of `AccessListener` to `false` + (this is the default behavior when using `enable_authenticator_manager: true`) + * Deprecate `TokenInterface:isAuthenticated()` and `setAuthenticated()` methods, + return `null` from `getUser()` instead when a token is not authenticated + * Deprecate `DeauthenticatedEvent`, use `TokenDeauthenticatedEvent` instead + * Deprecate `CookieClearingLogoutHandler`, `SessionLogoutHandler` and `CsrfTokenClearingLogoutHandler`. + Use `CookieClearingLogoutListener`, `SessionLogoutListener` and `CsrfTokenClearingLogoutListener` instead + * Deprecate `AuthenticatorInterface::createAuthenticatedToken()`, use `AuthenticatorInterface::createToken()` instead + * Deprecate `PassportInterface`, `UserPassportInterface` and `PassportTrait`, use `Passport` instead. + As such, the return type declaration of `AuthenticatorInterface::authenticate()` will change to `Passport` in 6.0 + * Deprecate not configuring explicitly a provider for custom_authenticators when there is more than one registered provider + + Before: + ```php + class MyAuthenticator implements AuthenticatorInterface + { + public function authenticate(Request $request): PassportInterface + { + } + } + ``` + + After: + ```php + class MyAuthenticator implements AuthenticatorInterface + { + public function authenticate(Request $request): Passport + { + } + } + ``` + * Deprecate passing the strategy as string to `AccessDecisionManager`, + pass an instance of `AccessDecisionStrategyInterface` instead + * Flag `AccessDecisionManager` as `@final` diff --git a/UPGRADE-6.0.md b/UPGRADE-6.0.md index 9ec7d84f308d0..afd57a60fafca 100644 --- a/UPGRADE-6.0.md +++ b/UPGRADE-6.0.md @@ -11,6 +11,12 @@ DoctrineBridge * Remove `UserLoaderInterface::loadUserByUsername()` in favor of `UserLoaderInterface::loadUserByIdentifier()` +Cache +----- + + * Remove `DoctrineProvider` and `DoctrineAdapter` because these classes have been added to the `doctrine/cache` package + * `PdoAdapter` does not accept `Doctrine\DBAL\Connection` or DBAL URL. Use the new `DoctrineDbalAdapter` instead + Config ------ @@ -25,6 +31,7 @@ Console * `Command::setHidden()` has a default value (`true`) for `$hidden` parameter * Remove `Helper::strlen()`, use `Helper::width()` instead. * Remove `Helper::strlenWithoutDecoration()`, use `Helper::removeDecoration()` instead. + * Remove `HelperSet::setCommand()` and `getCommand()` without replacement DependencyInjection ------------------- @@ -55,9 +62,16 @@ EventDispatcher * Removed `LegacyEventDispatcherProxy`. Use the event dispatcher without the proxy. +Finder +------ + + * Remove `Comparator::setTarget()` and `Comparator::setOperator()` + * The `$target` parameter of `Comparator::__construct()` is now mandatory + Form ---- + * `FormErrorIterator::children()` throws an exception if the current element is not iterable. * The default value of the `rounding_mode` option of the `PercentType` has been changed to `\NumberFormatter::ROUND_HALFUP`. * The default rounding mode of the `PercentToLocalizedStringTransformer` has been changed to `\NumberFormatter::ROUND_HALFUP`. * Added the `getIsEmptyCallback()` method to the `FormConfigInterface`. @@ -76,19 +90,23 @@ Form FrameworkBundle --------------- + * Remove the `framework.translator.enabled_locales` config option, use `framework.enabled_locales` instead * Remove the `session.storage` alias and `session.storage.*` services, use the `session.storage.factory` alias and `session.storage.factory.*` services instead * Remove `framework.session.storage_id` configuration option, use the `framework.session.storage_factory_id` configuration option instead * Remove the `session` service and the `SessionInterface` alias, use the `\Symfony\Component\HttpFoundation\Request::getSession()` or the new `\Symfony\Component\HttpFoundation\RequestStack::getSession()` methods instead * `MicroKernelTrait::configureRoutes()` is now always called with a `RoutingConfigurator` * The "framework.router.utf8" configuration option defaults to `true` * Removed `session.attribute_bag` service and `session.flash_bag` service. - * The `form.factory`, `form.type.file`, `translator`, `security.csrf.token_manager`, `serializer`, + * The `form.factory`, `form.type.file`, `profiler`, `translator`, `security.csrf.token_manager`, `serializer`, `cache_clearer`, `filesystem` and `validator` services are now private. * Removed the `lock.RESOURCE_NAME` and `lock.RESOURCE_NAME.store` services and the `lock`, `LockInterface`, `lock.store` and `PersistingStoreInterface` aliases, use `lock.RESOURCE_NAME.factory`, `lock.factory` or `LockFactory` instead. * Remove the `KernelTestCase::$container` property, use `KernelTestCase::getContainer()` instead * Registered workflow services are now private * Remove option `--xliff-version` of the `translation:update` command, use e.g. `--output-format=xlf20` instead * Remove option `--output-format` of the `translation:update` command, use e.g. `--output-format=xlf20` instead + * Remove the `AdapterInterface` autowiring alias, use `CacheItemPoolInterface` instead + * Remove `get()`, `has()`, `getDoctrine()`, and `dispatchMessage()` in `AbstractController`, use method/constructor injection instead + * Deprecate the `cache.adapter.doctrine` service: The Doctrine Cache library is deprecated. Either switch to Symfony Cache or use the PSR-6 adapters provided by Doctrine Cache. HttpFoundation -------------- @@ -122,6 +140,8 @@ Lock * Removed the `NotSupportedException`. It shouldn't be thrown anymore. * Removed the `RetryTillSaveStore`. Logic has been moved in `Lock` and is not needed anymore. + * Removed usage of `PdoStore` with a `Doctrine\DBAL\Connection` or a DBAL url, use the new `DoctrineDbalStore` instead + * Removed usage of `PostgreSqlStore` with a `Doctrine\DBAL\Connection` or a DBAL url, use the new `DoctrineDbalPostgreSqlStore` instead Mailer ------ @@ -140,6 +160,8 @@ Messenger * The signature of method `RetryStrategyInterface::getWaitingTime()` has been updated to `RetryStrategyInterface::getWaitingTime(Envelope $message, \Throwable $throwable = null)`. * Removed the `prefetch_count` parameter in the AMQP bridge. * Removed the use of TLS option for Redis Bridge, use `rediss://127.0.0.1` instead of `redis://127.0.0.1?tls=1` + * The `delete_after_ack` config option of the Redis transport now defaults to `true` + * The `reset_on_message` config option now defaults to `true` Mime ---- @@ -151,6 +173,7 @@ Monolog * The `$actionLevel` constructor argument of `Symfony\Bridge\Monolog\Handler\FingersCrossed\NotFoundActivationStrategy` has been replaced by the `$inner` one which expects an ActivationStrategyInterface to decorate instead. `Symfony\Bridge\Monolog\Handler\FingersCrossed\NotFoundActivationStrategy` is now final. * The `$actionLevel` constructor argument of `Symfony\Bridge\Monolog\Handler\FingersCrossed\HttpCodeActivationStrategy` has been replaced by the `$inner` one which expects an ActivationStrategyInterface to decorate instead. `Symfony\Bridge\Monolog\Handler\FingersCrossed\HttpCodeActivationStrategy` is now final. + * Remove `ResetLoggersWorkerSubscriber` in favor of "reset_on_message" option in messenger configuration Notifier -------- @@ -193,6 +216,41 @@ Routing Security -------- + * Remove `AuthenticationEvents::AUTHENTICATION_FAILURE`, use the `LoginFailureEvent` instead + * Remove the `$authenticationEntryPoint` argument of `ChannelListener` + * Remove `RetryAuthenticationEntryPoint`, this code was inlined in the `ChannelListener` + * Remove `FormAuthenticationEntryPoint` and `BasicAuthenticationEntryPoint`, the `FormLoginAuthenticator` and `HttpBasicAuthenticator` should be used instead. + * Remove `AbstractRememberMeServices`, `PersistentTokenBasedRememberMeServices`, `RememberMeServicesInterface`, + `TokenBasedRememberMeServices`, use the remember me handler alternatives instead + * Remove `AnonymousToken` + * Remove `Token::getCredentials()`, tokens should no longer contain credentials (as they represent authenticated sessions) + * Restrict the return type of `Token::getUser()` to `UserInterface` (removing `string|\Stringable`) + * Remove `AuthenticatedVoter::IS_AUTHENTICATED_ANONYMOUSLY` and `AuthenticatedVoter::IS_ANONYMOUS`, + use `AuthenticatedVoter::PUBLIC_ACCESS` instead. + + Before: + ```yaml + # config/packages/security.yaml + security: + # ... + access_control: + - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY } + ``` + + After: + ```yaml + # config/packages/security.yaml + security: + # ... + access_control: + - { path: ^/login, roles: PUBLIC_ACCESS } + ``` + + * Remove `AuthenticationTrustResolverInterface::isAnonymous()` and the `is_anonymous()` expression function + as anonymous no longer exists in version 6, use the `isFullFledged()` or the new `isAuthenticated()` instead + if you want to check if the request is (fully) authenticated. + * Remove the 4th and 5th argument of `AuthorizationChecker` + * Remove the 5th argument of `AccessListener` * Remove class `User`, use `InMemoryUser` or your own implementation instead. If you are using the `isAccountNonLocked()`, `isAccountNonExpired()` or `isCredentialsNonExpired()` method, consider re-implementing them in your own user class as they are not part of the `InMemoryUser` API @@ -308,10 +366,59 @@ Security `UsernamePasswordFormAuthenticationListener`, `UsernamePasswordJsonAuthenticationListener` and `X509AuthenticationListener` from security-http, use the new authenticator system instead * Remove the Guard component, use the new authenticator system instead + * Remove `TokenInterface:isAuthenticated()` and `setAuthenticated()`, + return `null` from `getUser()` instead when a token is not authenticated + * Remove `DeauthenticatedEvent`, use `TokenDeauthenticatedEvent` instead + * Remove `CookieClearingLogoutHandler`, `SessionLogoutHandler` and `CsrfTokenClearingLogoutHandler`. + Use `CookieClearingLogoutListener`, `SessionLogoutListener` and `CsrfTokenClearingLogoutListener` instead + * Remove `AuthenticatorInterface::createAuthenticatedToken()`, use `AuthenticatorInterface::createToken()` instead + * Remove `PassportInterface`, `UserPassportInterface` and `PassportTrait`, use `Passport` instead. + Also, the return type declaration of `AuthenticatorInterface::authenticate()` was changed to `Passport` + + Before: + ```php + class MyAuthenticator implements AuthenticatorInterface + { + public function authenticate(Request $request): PassportInterface + { + } + } + ``` + + After: + ```php + class MyAuthenticator implements AuthenticatorInterface + { + public function authenticate(Request $request): Passport + { + } + } + ``` + * `AccessDecisionManager` does not accept strings as strategy anymore, + pass an instance of `AccessDecisionStrategyInterface` instead SecurityBundle -------------- + * Remove `FirewallConfig::getListeners()`, use `FirewallConfig::getAuthenticators()` instead + * Remove `security.authentication.basic_entry_point` and `security.authentication.retry_entry_point` services, + the logic is moved into the `HttpBasicAuthenticator` and `ChannelListener` respectively + * Remove `SecurityFactoryInterface` and `SecurityExtension::addSecurityListenerFactory()` in favor of + `AuthenticatorFactoryInterface` and `SecurityExtension::addAuthenticatorFactory()` + * Add `AuthenticatorFactoryInterface::getPriority()` which replaces `SecurityFactoryInterface::getPosition()`. + Previous positions are mapped to the following priorities: + + | Position | Constant | Priority | + | ----------- | ----------------------------------------------------- | -------- | + | pre_auth | `RemoteUserFactory::PRIORITY`/`X509Factory::PRIORITY` | -10 | + | form | `FormLoginFactory::PRIORITY` | -30 | + | http | `HttpBasicFactory::PRIORITY` | -50 | + | remember_me | `RememberMeFactory::PRIORITY` | -60 | + | anonymous | n/a | -70 | + + * Remove passing an array of arrays as 1st argument to `MainConfiguration`, pass a sorted flat array of + factories instead. + * Remove the `always_authenticate_before_granting` option * Remove the `UserPasswordEncoderCommand` class and the corresponding `user:encode-password` command, use `UserPasswordHashCommand` and `user:hash-password` instead * Remove the `security.encoder_factory.generic` service, the `security.encoder_factory` and `Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface` aliases, @@ -323,6 +430,7 @@ SecurityBundle * Remove the `security.authentication.provider.*` services, use the new authenticator system instead * Remove the `security.authentication.listener.*` services, use the new authenticator system instead * Remove the Guard component integration, use the new authenticator system instead + * Remove the default provider for custom_authenticators when there is more than one registered provider Serializer ---------- diff --git a/composer.json b/composer.json index 1b2709407654f..4abf43bb4a88e 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "psr/http-client-implementation": "1.0", "psr/link-implementation": "1.0", "psr/log-implementation": "1.0|2.0", - "psr/simple-cache-implementation": "1.0", + "psr/simple-cache-implementation": "1.0|2.0", "symfony/cache-implementation": "1.0|2.0", "symfony/event-dispatcher-implementation": "2.0", "symfony/http-client-implementation": "2.4", @@ -52,7 +52,7 @@ "symfony/polyfill-mbstring": "~1.0", "symfony/polyfill-php73": "^1.11", "symfony/polyfill-php80": "^1.16", - "symfony/polyfill-php81": "^1.22", + "symfony/polyfill-php81": "^1.23", "symfony/polyfill-uuid": "^1.15" }, "replace": { @@ -122,13 +122,14 @@ "amphp/http-tunnel": "^1.0", "async-aws/ses": "^1.0", "async-aws/sqs": "^1.0", + "async-aws/sns": "^1.0", "cache/integration-tests": "dev-master", "composer/package-versions-deprecated": "^1.8", - "doctrine/annotations": "^1.12", - "doctrine/cache": "^1.6|^2.0", + "doctrine/annotations": "^1.13.1", + "doctrine/cache": "^1.11|^2.0", "doctrine/collections": "~1.0", "doctrine/data-fixtures": "^1.1", - "doctrine/dbal": "^2.10|^3.0", + "doctrine/dbal": "^2.13.1|^3.0", "doctrine/orm": "^2.7.3", "guzzlehttp/promises": "^1.4", "masterminds/html5": "^2.6", @@ -137,12 +138,13 @@ "paragonie/sodium_compat": "^1.8", "pda/pheanstalk": "^4.0", "php-http/httplug": "^1.0|^2.0", + "phpstan/phpdoc-parser": "^1.0", "predis/predis": "~1.1", "psr/http-client": "^1.0", - "psr/simple-cache": "^1.0", + "psr/simple-cache": "^1.0|^2.0", "egulias/email-validator": "^2.1.10|^3.1", "symfony/mercure-bundle": "^0.3", - "symfony/phpunit-bridge": "^5.2", + "symfony/phpunit-bridge": "^5.2|^6.0", "symfony/runtime": "self.version", "symfony/security-acl": "~2.8|~3.0", "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", @@ -153,8 +155,8 @@ "conflict": { "ext-psr": "<1.1|>=2", "async-aws/core": "<1.5", - "doctrine/annotations": "<1.12", - "doctrine/dbal": "<2.10", + "doctrine/annotations": "<1.13.1", + "doctrine/dbal": "<2.13.1", "egulias/email-validator": "~3.0.0", "masterminds/html5": "<2.6", "phpdocumentor/reflection-docblock": "<3.2.2", @@ -192,7 +194,7 @@ "url": "src/Symfony/Contracts", "options": { "versions": { - "symfony/contracts": "2.4.x-dev" + "symfony/contracts": "2.5.x-dev" } } }, diff --git a/src/Symfony/Bridge/Doctrine/CHANGELOG.md b/src/Symfony/Bridge/Doctrine/CHANGELOG.md index 6323313ba9b89..d2e0a7761eeef 100644 --- a/src/Symfony/Bridge/Doctrine/CHANGELOG.md +++ b/src/Symfony/Bridge/Doctrine/CHANGELOG.md @@ -1,6 +1,13 @@ CHANGELOG ========= +5.4 +--- + + * Add `DoctrineOpenTransactionLoggerMiddleware` to log when a transaction has been left open + * Deprecate `PdoCacheAdapterDoctrineSchemaSubscriber` and add `DoctrineDbalCacheAdapterSchemaSubscriber` instead + * `UniqueEntity` constraint retrieves a maximum of two entities if the default repository method is used. + 5.3 --- diff --git a/src/Symfony/Bridge/Doctrine/CacheWarmer/ProxyCacheWarmer.php b/src/Symfony/Bridge/Doctrine/CacheWarmer/ProxyCacheWarmer.php index bca2ea2c170da..bbee8cea3f3f8 100644 --- a/src/Symfony/Bridge/Doctrine/CacheWarmer/ProxyCacheWarmer.php +++ b/src/Symfony/Bridge/Doctrine/CacheWarmer/ProxyCacheWarmer.php @@ -34,7 +34,7 @@ public function __construct(ManagerRegistry $registry) /** * This cache warmer is not optional, without proxies fatal error occurs! * - * @return false + * @return bool */ public function isOptional() { diff --git a/src/Symfony/Bridge/Doctrine/DependencyInjection/AbstractDoctrineExtension.php b/src/Symfony/Bridge/Doctrine/DependencyInjection/AbstractDoctrineExtension.php index 868042bc31e6f..2714e27d754a3 100644 --- a/src/Symfony/Bridge/Doctrine/DependencyInjection/AbstractDoctrineExtension.php +++ b/src/Symfony/Bridge/Doctrine/DependencyInjection/AbstractDoctrineExtension.php @@ -89,25 +89,8 @@ protected function loadMappingInformation(array $objectManager, ContainerBuilder if (!$mappingConfig) { continue; } - } elseif (!$mappingConfig['type'] && \PHP_VERSION_ID < 80000) { - $mappingConfig['type'] = 'annotation'; } elseif (!$mappingConfig['type']) { - $mappingConfig['type'] = 'attribute'; - - $glob = new GlobResource($mappingConfig['dir'], '*', true); - $container->addResource($glob); - - foreach ($glob as $file) { - $content = file_get_contents($file); - - if (preg_match('/^#\[.*Entity\b/m', $content)) { - break; - } - if (preg_match('/^ \* @.*Entity\b/m', $content)) { - $mappingConfig['type'] = 'annotation'; - break; - } - } + $mappingConfig['type'] = $this->detectMappingType($mappingConfig['dir'], $container); } $this->assertValidMappingConfiguration($mappingConfig, $objectManager['name']); @@ -280,13 +263,48 @@ protected function detectMetadataDriver(string $dir, ContainerBuilder $container } $container->fileExists($resource, false); - return $container->fileExists($dir.'/'.$this->getMappingObjectDefaultName(), false) ? 'annotation' : null; + if ($container->fileExists($dir.'/'.$this->getMappingObjectDefaultName(), false)) { + return $this->detectMappingType($dir, $container); + } + + return null; } $container->fileExists($dir.'/'.$configPath, false); return $driver; } + /** + * Detects what mapping type to use for the supplied directory. + * + * @return string A mapping type 'attribute' or 'annotation' + */ + private function detectMappingType(string $directory, ContainerBuilder $container): string + { + if (\PHP_VERSION_ID < 80000) { + return 'annotation'; + } + + $type = 'attribute'; + + $glob = new GlobResource($directory, '*', true); + $container->addResource($glob); + + foreach ($glob as $file) { + $content = file_get_contents($file); + + if (preg_match('/^#\[.*Entity\b/m', $content)) { + break; + } + if (preg_match('/^ \* @.*Entity\b/m', $content)) { + $type = 'annotation'; + break; + } + } + + return $type; + } + /** * Loads a configured object manager metadata, query or result cache driver. * @@ -379,7 +397,7 @@ protected function loadCacheDriver(string $cacheName, string $objectManagerName, * * The manager called $autoMappedManager will map all bundles that are not mapped by other managers. * - * @return array The modified version of $managerConfigs + * @return array */ protected function fixManagersAutoMappings(array $managerConfigs, array $bundles) { diff --git a/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/RegisterEventListenersAndSubscribersPass.php b/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/RegisterEventListenersAndSubscribersPass.php index a6853fb4809b4..229bfa15f12b2 100644 --- a/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/RegisterEventListenersAndSubscribersPass.php +++ b/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/RegisterEventListenersAndSubscribersPass.php @@ -77,7 +77,9 @@ private function addTaggedServices(ContainerBuilder $container): array $managerDefs = []; foreach ($taggedServices as $taggedSubscriber) { [$tagName, $id, $tag] = $taggedSubscriber; - $connections = isset($tag['connection']) ? [$tag['connection']] : array_keys($this->connections); + $connections = isset($tag['connection']) + ? [$container->getParameterBag()->resolveValue($tag['connection'])] + : array_keys($this->connections); if ($listenerTag === $tagName && !isset($tag['event'])) { throw new InvalidArgumentException(sprintf('Doctrine event listener "%s" must specify the "event" attribute.', $id)); } diff --git a/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/RegisterMappingsPass.php b/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/RegisterMappingsPass.php index e253720d8026f..6f16eb47a9760 100644 --- a/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/RegisterMappingsPass.php +++ b/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/RegisterMappingsPass.php @@ -165,7 +165,7 @@ public function process(ContainerBuilder $container) * Get the service name of the metadata chain driver that the mappings * should be registered with. * - * @return string The name of the chain driver service + * @return string * * @throws InvalidArgumentException if non of the managerParameters has a * non-empty value @@ -181,7 +181,7 @@ protected function getChainDriverServiceName(ContainerBuilder $container) * @param ContainerBuilder $container Passed on in case an extending class * needs access to the container * - * @return Definition|Reference the metadata driver to add to all chain drivers + * @return Definition|Reference */ protected function getDriver(ContainerBuilder $container) { @@ -228,7 +228,7 @@ private function getManagerName(ContainerBuilder $container): string * This default implementation checks if the class has the enabledParameter * configured and if so if that parameter is present in the container. * - * @return bool whether this compiler pass really should register the mappings + * @return bool */ protected function enabled(ContainerBuilder $container) { diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php index 99be884f34b04..eee4bd576dda0 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php @@ -91,9 +91,18 @@ protected function doLoadChoicesForValues(array $values, ?callable $value): arra trigger_deprecation('symfony/doctrine-bridge', '5.1', 'Not defining explicitly the IdReader as value callback when query can be optimized is deprecated. Don\'t pass the IdReader to "%s" or define the "choice_value" option instead.', __CLASS__); } + $idReader = null; + if (\is_array($value) && $value[0] instanceof IdReader) { + $idReader = $value[0]; + } elseif ($value instanceof \Closure && ($rThis = (new \ReflectionFunction($value))->getClosureThis()) instanceof IdReader) { + $idReader = $rThis; + } elseif ($legacy) { + $idReader = $this->idReader; + } + // Optimize performance in case we have an object loader and // a single-field identifier - if (($legacy || \is_array($value) && $this->idReader === $value[0]) && $this->objectLoader) { + if ($idReader && $this->objectLoader) { $objects = []; $objectsById = []; @@ -101,8 +110,8 @@ protected function doLoadChoicesForValues(array $values, ?callable $value): arra // An alternative approach to the following loop is to add the // "INDEX BY" clause to the Doctrine query in the loader, // but I'm not sure whether that's doable in a generic fashion. - foreach ($this->objectLoader->getEntitiesByIds($this->idReader->getIdField(), $values) as $object) { - $objectsById[$this->idReader->getIdValue($object)] = $object; + foreach ($this->objectLoader->getEntitiesByIds($idReader->getIdField(), $values) as $object) { + $objectsById[$idReader->getIdValue($object)] = $object; } foreach ($values as $i => $id) { diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityLoaderInterface.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityLoaderInterface.php index 8eb5a84484503..3b2b553f02ab5 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityLoaderInterface.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityLoaderInterface.php @@ -21,14 +21,14 @@ interface EntityLoaderInterface /** * Returns an array of entities that are valid choices in the corresponding choice list. * - * @return array The entities + * @return array */ public function getEntities(); /** * Returns an array of entities matching the given identifiers. * - * @return array The entities + * @return array */ public function getEntitiesByIds(string $identifier, array $values); } diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php index 0625e5175ce08..5a6e23d963946 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php @@ -59,9 +59,6 @@ public function __construct(ObjectManager $om, ClassMetadata $classMetadata) /** * Returns whether the class has a single-column ID. - * - * @return bool returns `true` if the class has a single-column ID and - * `false` otherwise */ public function isSingleId(): bool { @@ -70,9 +67,6 @@ public function isSingleId(): bool /** * Returns whether the class has a single-column integer ID. - * - * @return bool returns `true` if the class has a single-column integer ID - * and `false` otherwise */ public function isIntId(): bool { @@ -83,10 +77,8 @@ public function isIntId(): bool * Returns the ID value for an object. * * This method assumes that the object has a single-column ID. - * - * @return string The ID value */ - public function getIdValue(object $object = null) + public function getIdValue(object $object = null): string { if (!$object) { return ''; @@ -111,8 +103,6 @@ public function getIdValue(object $object = null) * Returns the name of the ID field. * * This method assumes that the object has a single-column ID. - * - * @return string The name of the ID field */ public function getIdField(): string { diff --git a/src/Symfony/Bridge/Doctrine/Form/DataTransformer/CollectionToArrayTransformer.php b/src/Symfony/Bridge/Doctrine/Form/DataTransformer/CollectionToArrayTransformer.php index 3202dae97f5c2..e2355b4a60fb8 100644 --- a/src/Symfony/Bridge/Doctrine/Form/DataTransformer/CollectionToArrayTransformer.php +++ b/src/Symfony/Bridge/Doctrine/Form/DataTransformer/CollectionToArrayTransformer.php @@ -24,7 +24,7 @@ class CollectionToArrayTransformer implements DataTransformerInterface /** * Transforms a collection into an array. * - * @return mixed An array of entities + * @return mixed * * @throws TransformationFailedException */ @@ -52,7 +52,7 @@ public function transform($collection) * * @param mixed $array An array of entities * - * @return Collection A collection of entities + * @return Collection */ public function reverseTransform($array) { diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php index 324d5d26d4b06..d37a90fa4a20f 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php @@ -86,9 +86,6 @@ public static function createChoiceName(object $choice, $key, string $value): st * @param object $queryBuilder A query builder, type declaration is not present here as there * is no common base class for the different implementations * - * @return array|null Array with important QueryBuilder parts or null if - * they can't be determined - * * @internal This method is public to be usable as callback. It should not * be used in user code. */ diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php b/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php index 90d6ce8750887..66b574c167ef4 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php @@ -32,7 +32,7 @@ public function configureOptions(OptionsResolver $resolver) $queryBuilder = $queryBuilder($options['em']->getRepository($options['class'])); if (null !== $queryBuilder && !$queryBuilder instanceof QueryBuilder) { - throw new UnexpectedTypeException($queryBuilder, 'Doctrine\ORM\QueryBuilder'); + throw new UnexpectedTypeException($queryBuilder, QueryBuilder::class); } } @@ -40,7 +40,7 @@ public function configureOptions(OptionsResolver $resolver) }; $resolver->setNormalizer('query_builder', $queryBuilderNormalizer); - $resolver->setAllowedTypes('query_builder', ['null', 'callable', 'Doctrine\ORM\QueryBuilder']); + $resolver->setAllowedTypes('query_builder', ['null', 'callable', QueryBuilder::class]); } /** diff --git a/src/Symfony/Bridge/Doctrine/Messenger/DoctrineClearEntityManagerWorkerSubscriber.php b/src/Symfony/Bridge/Doctrine/Messenger/DoctrineClearEntityManagerWorkerSubscriber.php index d702186a713ce..5b7503c2d34f2 100644 --- a/src/Symfony/Bridge/Doctrine/Messenger/DoctrineClearEntityManagerWorkerSubscriber.php +++ b/src/Symfony/Bridge/Doctrine/Messenger/DoctrineClearEntityManagerWorkerSubscriber.php @@ -42,8 +42,10 @@ public function onWorkerMessageFailed() public static function getSubscribedEvents() { - yield WorkerMessageHandledEvent::class => 'onWorkerMessageHandled'; - yield WorkerMessageFailedEvent::class => 'onWorkerMessageFailed'; + return [ + WorkerMessageHandledEvent::class => 'onWorkerMessageHandled', + WorkerMessageFailedEvent::class => 'onWorkerMessageFailed', + ]; } private function clearEntityManagers() diff --git a/src/Symfony/Bridge/Doctrine/Messenger/DoctrineOpenTransactionLoggerMiddleware.php b/src/Symfony/Bridge/Doctrine/Messenger/DoctrineOpenTransactionLoggerMiddleware.php new file mode 100644 index 0000000000000..246f0090e58ef --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Messenger/DoctrineOpenTransactionLoggerMiddleware.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Messenger; + +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\Persistence\ManagerRegistry; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Middleware\StackInterface; + +/** + * Middleware to log when transaction has been left open. + * + * @author Grégoire Pineau + */ +class DoctrineOpenTransactionLoggerMiddleware extends AbstractDoctrineMiddleware +{ + private $logger; + + public function __construct(ManagerRegistry $managerRegistry, string $entityManagerName = null, LoggerInterface $logger = null) + { + parent::__construct($managerRegistry, $entityManagerName); + + $this->logger = $logger ?? new NullLogger(); + } + + protected function handleForManager(EntityManagerInterface $entityManager, Envelope $envelope, StackInterface $stack): Envelope + { + try { + return $stack->next()->handle($envelope, $stack); + } finally { + if ($entityManager->getConnection()->isTransactionActive()) { + $this->logger->error('A handler opened a transaction but did not close it.', [ + 'message' => $envelope->getMessage(), + ]); + } + } + } +} diff --git a/src/Symfony/Bridge/Doctrine/Messenger/DoctrinePingConnectionMiddleware.php b/src/Symfony/Bridge/Doctrine/Messenger/DoctrinePingConnectionMiddleware.php index c6b219aa795ab..de925284d09dc 100644 --- a/src/Symfony/Bridge/Doctrine/Messenger/DoctrinePingConnectionMiddleware.php +++ b/src/Symfony/Bridge/Doctrine/Messenger/DoctrinePingConnectionMiddleware.php @@ -11,8 +11,7 @@ namespace Symfony\Bridge\Doctrine\Messenger; -use Doctrine\DBAL\DBALException; -use Doctrine\DBAL\Exception; +use Doctrine\DBAL\Exception as DBALException; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\Middleware\StackInterface; @@ -40,7 +39,7 @@ private function pingConnection(EntityManagerInterface $entityManager) try { $connection->executeQuery($connection->getDatabasePlatform()->getDummySelectSQL()); - } catch (DBALException | Exception $e) { + } catch (DBALException $e) { $connection->close(); $connection->connect(); } diff --git a/src/Symfony/Bridge/Doctrine/PropertyInfo/DoctrineExtractor.php b/src/Symfony/Bridge/Doctrine/PropertyInfo/DoctrineExtractor.php index 6c0c0b9bc721d..8148ce0d3882d 100644 --- a/src/Symfony/Bridge/Doctrine/PropertyInfo/DoctrineExtractor.php +++ b/src/Symfony/Bridge/Doctrine/PropertyInfo/DoctrineExtractor.php @@ -11,10 +11,12 @@ namespace Symfony\Bridge\Doctrine\PropertyInfo; +use Doctrine\Common\Collections\Collection; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Mapping\ClassMetadataInfo; +use Doctrine\ORM\Mapping\Embedded; use Doctrine\ORM\Mapping\MappingException as OrmMappingException; use Doctrine\Persistence\Mapping\MappingException; use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface; @@ -30,7 +32,6 @@ class DoctrineExtractor implements PropertyListExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface { private $entityManager; - private $classMetadataFactory; public function __construct(EntityManagerInterface $entityManager) { @@ -48,7 +49,7 @@ public function getProperties(string $class, array $context = []) $properties = array_merge($metadata->getFieldNames(), $metadata->getAssociationNames()); - if ($metadata instanceof ClassMetadataInfo && class_exists(\Doctrine\ORM\Mapping\Embedded::class) && $metadata->embeddedClasses) { + if ($metadata instanceof ClassMetadataInfo && class_exists(Embedded::class) && $metadata->embeddedClasses) { $properties = array_filter($properties, function ($property) { return !str_contains($property, '.'); }); @@ -90,7 +91,7 @@ public function getTypes(string $class, string $property, array $context = []) if (isset($associationMapping['indexBy'])) { /** @var ClassMetadataInfo $subMetadata */ - $subMetadata = $this->entityManager ? $this->entityManager->getClassMetadata($associationMapping['targetEntity']) : $this->classMetadataFactory->getMetadataFor($associationMapping['targetEntity']); + $subMetadata = $this->entityManager->getClassMetadata($associationMapping['targetEntity']); // Check if indexBy value is a property $fieldName = $associationMapping['indexBy']; @@ -103,7 +104,7 @@ public function getTypes(string $class, string $property, array $context = []) /** @var ClassMetadataInfo $subMetadata */ $indexProperty = $subMetadata->getSingleAssociationReferencedJoinColumnName($fieldName); - $subMetadata = $this->entityManager ? $this->entityManager->getClassMetadata($associationMapping['targetEntity']) : $this->classMetadataFactory->getMetadataFor($associationMapping['targetEntity']); + $subMetadata = $this->entityManager->getClassMetadata($associationMapping['targetEntity']); //Not a property, maybe a column name? if (null === ($typeOfField = $subMetadata->getTypeOfField($indexProperty))) { @@ -122,14 +123,14 @@ public function getTypes(string $class, string $property, array $context = []) return [new Type( Type::BUILTIN_TYPE_OBJECT, false, - 'Doctrine\Common\Collections\Collection', + Collection::class, true, new Type($collectionKeyType), new Type(Type::BUILTIN_TYPE_OBJECT, false, $class) )]; } - if ($metadata instanceof ClassMetadataInfo && class_exists(\Doctrine\ORM\Mapping\Embedded::class) && isset($metadata->embeddedClasses[$property])) { + if ($metadata instanceof ClassMetadataInfo && class_exists(Embedded::class) && isset($metadata->embeddedClasses[$property])) { return [new Type(Type::BUILTIN_TYPE_OBJECT, false, $metadata->embeddedClasses[$property]['class'])]; } @@ -207,8 +208,8 @@ public function isWritable(string $class, string $property, array $context = []) private function getMetadata(string $class): ?ClassMetadata { try { - return $this->entityManager ? $this->entityManager->getClassMetadata($class) : $this->classMetadataFactory->getMetadataFor($class); - } catch (MappingException | OrmMappingException $exception) { + return $this->entityManager->getClassMetadata($class); + } catch (MappingException|OrmMappingException $exception) { return null; } } diff --git a/src/Symfony/Bridge/Doctrine/SchemaListener/DoctrineDbalCacheAdapterSchemaSubscriber.php b/src/Symfony/Bridge/Doctrine/SchemaListener/DoctrineDbalCacheAdapterSchemaSubscriber.php new file mode 100644 index 0000000000000..e61564807befd --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/SchemaListener/DoctrineDbalCacheAdapterSchemaSubscriber.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\SchemaListener; + +use Doctrine\Common\EventSubscriber; +use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs; +use Doctrine\ORM\Tools\ToolEvents; +use Symfony\Component\Cache\Adapter\DoctrineSchemaConfiguratorInterface; + +/** + * Automatically adds the cache table needed for the DoctrineDbalAdapter of + * the Cache component. + * + * @author Ryan Weaver + */ +final class DoctrineDbalCacheAdapterSchemaSubscriber implements EventSubscriber +{ + private $dbalAdapters; + + /** + * @param iterable $dbalAdapters + */ + public function __construct(iterable $dbalAdapters) + { + $this->dbalAdapters = $dbalAdapters; + } + + public function postGenerateSchema(GenerateSchemaEventArgs $event): void + { + $dbalConnection = $event->getEntityManager()->getConnection(); + foreach ($this->dbalAdapters as $dbalAdapter) { + $dbalAdapter->configureSchema($event->getSchema(), $dbalConnection); + } + } + + public function getSubscribedEvents(): array + { + if (!class_exists(ToolEvents::class)) { + return []; + } + + return [ + ToolEvents::postGenerateSchema, + ]; + } +} diff --git a/src/Symfony/Bridge/Doctrine/SchemaListener/MessengerTransportDoctrineSchemaSubscriber.php b/src/Symfony/Bridge/Doctrine/SchemaListener/MessengerTransportDoctrineSchemaSubscriber.php index 5b3798eb3918a..2999d25afe33f 100644 --- a/src/Symfony/Bridge/Doctrine/SchemaListener/MessengerTransportDoctrineSchemaSubscriber.php +++ b/src/Symfony/Bridge/Doctrine/SchemaListener/MessengerTransportDoctrineSchemaSubscriber.php @@ -31,7 +31,7 @@ final class MessengerTransportDoctrineSchemaSubscriber implements EventSubscribe private $transports; /** - * @param iterable|TransportInterface[] $transports + * @param iterable $transports */ public function __construct(iterable $transports) { diff --git a/src/Symfony/Bridge/Doctrine/SchemaListener/PdoCacheAdapterDoctrineSchemaSubscriber.php b/src/Symfony/Bridge/Doctrine/SchemaListener/PdoCacheAdapterDoctrineSchemaSubscriber.php index 527b055b28078..a46d4fa814cb4 100644 --- a/src/Symfony/Bridge/Doctrine/SchemaListener/PdoCacheAdapterDoctrineSchemaSubscriber.php +++ b/src/Symfony/Bridge/Doctrine/SchemaListener/PdoCacheAdapterDoctrineSchemaSubscriber.php @@ -16,17 +16,21 @@ use Doctrine\ORM\Tools\ToolEvents; use Symfony\Component\Cache\Adapter\PdoAdapter; +trigger_deprecation('symfony/doctrine-bridge', '5.4', 'The "%s" class is deprecated, use "%s" instead.', PdoCacheAdapterDoctrineSchemaSubscriber::class, DoctrineDbalCacheAdapterSchemaSubscriber::class); + /** * Automatically adds the cache table needed for the PdoAdapter. * * @author Ryan Weaver + * + * @deprecated since symfony 5.4 use DoctrineDbalCacheAdapterSchemaSubscriber */ final class PdoCacheAdapterDoctrineSchemaSubscriber implements EventSubscriber { private $pdoAdapters; /** - * @param iterable|PdoAdapter[] $pdoAdapters + * @param iterable $pdoAdapters */ public function __construct(iterable $pdoAdapters) { diff --git a/src/Symfony/Bridge/Doctrine/SchemaListener/RememberMeTokenProviderDoctrineSchemaSubscriber.php b/src/Symfony/Bridge/Doctrine/SchemaListener/RememberMeTokenProviderDoctrineSchemaSubscriber.php index 60a849789ef17..9eec839679ffc 100644 --- a/src/Symfony/Bridge/Doctrine/SchemaListener/RememberMeTokenProviderDoctrineSchemaSubscriber.php +++ b/src/Symfony/Bridge/Doctrine/SchemaListener/RememberMeTokenProviderDoctrineSchemaSubscriber.php @@ -28,7 +28,7 @@ final class RememberMeTokenProviderDoctrineSchemaSubscriber implements EventSubs private $rememberMeHandlers; /** - * @param iterable|RememberMeHandlerInterface[] $rememberMeHandlers + * @param iterable $rememberMeHandlers */ public function __construct(iterable $rememberMeHandlers) { diff --git a/src/Symfony/Bridge/Doctrine/Test/DoctrineTestHelper.php b/src/Symfony/Bridge/Doctrine/Test/DoctrineTestHelper.php index 4821fffc616e3..c26bc875f241e 100644 --- a/src/Symfony/Bridge/Doctrine/Test/DoctrineTestHelper.php +++ b/src/Symfony/Bridge/Doctrine/Test/DoctrineTestHelper.php @@ -25,7 +25,7 @@ * * @author Bernhard Schussek * - * @deprecated in 5.3, will be removed in 6.0. + * @deprecated since Symfony 5.3 */ class DoctrineTestHelper { diff --git a/src/Symfony/Bridge/Doctrine/Test/TestRepositoryFactory.php b/src/Symfony/Bridge/Doctrine/Test/TestRepositoryFactory.php index ed63b6bd03bcb..80964663c340c 100644 --- a/src/Symfony/Bridge/Doctrine/Test/TestRepositoryFactory.php +++ b/src/Symfony/Bridge/Doctrine/Test/TestRepositoryFactory.php @@ -19,7 +19,7 @@ /** * @author Andreas Braun * - * @deprecated in 5.3, will be removed in 6.0. + * @deprecated since Symfony 5.3 */ class TestRepositoryFactory implements RepositoryFactory { diff --git a/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/CompilerPass/RegisterEventListenersAndSubscribersPassTest.php b/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/CompilerPass/RegisterEventListenersAndSubscribersPassTest.php index 358f6693cca92..e6fd198920517 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/CompilerPass/RegisterEventListenersAndSubscribersPassTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/CompilerPass/RegisterEventListenersAndSubscribersPassTest.php @@ -114,6 +114,8 @@ public function testProcessEventListenersWithMultipleConnections() { $container = $this->createBuilder(true); + $container->setParameter('connection_param', 'second'); + $container ->register('a', 'stdClass') ->addTag('doctrine.event_listener', [ @@ -137,6 +139,14 @@ public function testProcessEventListenersWithMultipleConnections() ]) ; + $container + ->register('d', 'stdClass') + ->addTag('doctrine.event_listener', [ + 'event' => 'onFlush', + 'connection' => '%connection_param%', + ]) + ; + $this->process($container); $eventManagerDef = $container->getDefinition('doctrine.dbal.default_connection.event_manager'); @@ -167,6 +177,7 @@ public function testProcessEventListenersWithMultipleConnections() [ [['onFlush'], 'a'], [['onFlush'], 'c'], + [['onFlush'], 'd'], ], $secondEventManagerDef->getArgument(1) ); @@ -178,6 +189,7 @@ public function testProcessEventListenersWithMultipleConnections() [ 'a' => new ServiceClosureArgument(new Reference('a')), 'c' => new ServiceClosureArgument(new Reference('c')), + 'd' => new ServiceClosureArgument(new Reference('d')), ], $serviceLocatorDef->getArgument(0) ); @@ -187,6 +199,8 @@ public function testProcessEventSubscribersWithMultipleConnections() { $container = $this->createBuilder(true); + $container->setParameter('connection_param', 'second'); + $container ->register('a', 'stdClass') ->addTag('doctrine.event_subscriber', [ @@ -210,6 +224,14 @@ public function testProcessEventSubscribersWithMultipleConnections() ]) ; + $container + ->register('d', 'stdClass') + ->addTag('doctrine.event_subscriber', [ + 'event' => 'onFlush', + 'connection' => '%connection_param%', + ]) + ; + $this->process($container); $eventManagerDef = $container->getDefinition('doctrine.dbal.default_connection.event_manager'); @@ -240,6 +262,7 @@ public function testProcessEventSubscribersWithMultipleConnections() [ 'a', 'c', + 'd', ], $eventManagerDef->getArgument(1) ); @@ -250,6 +273,7 @@ public function testProcessEventSubscribersWithMultipleConnections() [ 'a' => new ServiceClosureArgument(new Reference('a')), 'c' => new ServiceClosureArgument(new Reference('c')), + 'd' => new ServiceClosureArgument(new Reference('d')), ], $serviceLocatorDef->getArgument(0) ); diff --git a/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/DoctrineExtensionTest.php b/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/DoctrineExtensionTest.php index 085a37fbff73f..dc86ee22b640e 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/DoctrineExtensionTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/DoctrineExtensionTest.php @@ -171,6 +171,23 @@ public function testFixManagersAutoMappings(array $originalEm1, array $originalE ], $expectedEm2)); } + public function testMappingTypeDetection() + { + $container = $this->createContainer(); + + $reflection = new \ReflectionClass(\get_class($this->extension)); + $method = $reflection->getMethod('detectMappingType'); + $method->setAccessible(true); + + // The ordinary fixtures contain annotation + $mappingType = $method->invoke($this->extension, __DIR__.'/../Fixtures', $container); + $this->assertSame($mappingType, 'annotation'); + + // In the attribute folder, attributes are used + $mappingType = $method->invoke($this->extension, __DIR__.'/../Fixtures/Attribute', $container); + $this->assertSame($mappingType, \PHP_VERSION_ID < 80000 ? 'annotation' : 'attribute'); + } + public function providerBasicDrivers() { return [ diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Attribute/UuidIdEntity.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Attribute/UuidIdEntity.php new file mode 100644 index 0000000000000..3d28d4469c1fb --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Attribute/UuidIdEntity.php @@ -0,0 +1,29 @@ + + * + * 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\Attribute; + +use Doctrine\ORM\Mapping\Column; +use Doctrine\ORM\Mapping\Entity; +use Doctrine\ORM\Mapping\Id; + +#[Entity] +class UuidIdEntity +{ + #[Id] + #[Column("uuid")] + protected $id; + + public function __construct($id) + { + $this->id = $id; + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/IdGenerator/UuidGeneratorTest.php b/src/Symfony/Bridge/Doctrine/Tests/IdGenerator/UuidGeneratorTest.php index bfca276a811ba..34367b0bd7213 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/IdGenerator/UuidGeneratorTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/IdGenerator/UuidGeneratorTest.php @@ -14,7 +14,6 @@ use PHPUnit\Framework\TestCase; use Symfony\Bridge\Doctrine\IdGenerator\UuidGenerator; use Symfony\Component\Uid\Factory\UuidFactory; -use Symfony\Component\Uid\NilUuid; use Symfony\Component\Uid\Uuid; use Symfony\Component\Uid\UuidV4; use Symfony\Component\Uid\UuidV6; @@ -35,7 +34,7 @@ public function testUuidCanBeGenerated() public function testCustomUuidfactory() { - $uuid = new NilUuid(); + $uuid = new UuidV4(); $em = new EntityManager(); $factory = $this->createMock(UuidFactory::class); $factory->expects($this->any()) diff --git a/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrineOpenTransactionLoggerMiddlewareTest.php b/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrineOpenTransactionLoggerMiddlewareTest.php new file mode 100644 index 0000000000000..626c19eb4ceae --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrineOpenTransactionLoggerMiddlewareTest.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\Bridge\Doctrine\Tests\Messenger; + +use Doctrine\DBAL\Connection; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\Persistence\ManagerRegistry; +use Psr\Log\AbstractLogger; +use Symfony\Bridge\Doctrine\Messenger\DoctrineOpenTransactionLoggerMiddleware; +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Test\Middleware\MiddlewareTestCase; + +class DoctrineOpenTransactionLoggerMiddlewareTest extends MiddlewareTestCase +{ + private $logger; + private $connection; + private $entityManager; + private $middleware; + + protected function setUp(): void + { + $this->logger = new class() extends AbstractLogger { + public $logs = []; + + public function log($level, $message, $context = []): void + { + $this->logs[$level][] = $message; + } + }; + + $this->connection = $this->createMock(Connection::class); + + $this->entityManager = $this->createMock(EntityManagerInterface::class); + $this->entityManager->method('getConnection')->willReturn($this->connection); + + $managerRegistry = $this->createMock(ManagerRegistry::class); + $managerRegistry->method('getManager')->willReturn($this->entityManager); + + $this->middleware = new DoctrineOpenTransactionLoggerMiddleware($managerRegistry, null, $this->logger); + } + + public function testMiddlewareWrapsInTransactionAndFlushes() + { + $this->connection->expects($this->exactly(1)) + ->method('isTransactionActive') + ->will($this->onConsecutiveCalls(true, true, false)) + ; + + $this->middleware->handle(new Envelope(new \stdClass()), $this->getStackMock()); + + $this->assertSame(['error' => ['A handler opened a transaction but did not close it.']], $this->logger->logs); + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrinePingConnectionMiddlewareTest.php b/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrinePingConnectionMiddlewareTest.php index be63ef923dfbc..6c7bf67bc08af 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrinePingConnectionMiddlewareTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrinePingConnectionMiddlewareTest.php @@ -12,8 +12,7 @@ namespace Symfony\Bridge\Doctrine\Tests\Messenger; use Doctrine\DBAL\Connection; -use Doctrine\DBAL\DBALException; -use Doctrine\DBAL\Exception; +use Doctrine\DBAL\Exception as DBALException; use Doctrine\ORM\EntityManagerInterface; use Doctrine\Persistence\ManagerRegistry; use Symfony\Bridge\Doctrine\Messenger\DoctrinePingConnectionMiddleware; @@ -50,7 +49,7 @@ public function testMiddlewarePingOk() { $this->connection->expects($this->once()) ->method('getDatabasePlatform') - ->will($this->throwException(class_exists(Exception::class) ? new Exception() : new DBALException())); + ->will($this->throwException(new DBALException())); $this->connection->expects($this->once()) ->method('close') @@ -69,7 +68,7 @@ public function testMiddlewarePingResetEntityManager() { $this->connection->expects($this->once()) ->method('getDatabasePlatform') - ->will($this->throwException(class_exists(Exception::class) ? new Exception() : new DBALException())); + ->will($this->throwException(new DBALException())); $this->entityManager->expects($this->once()) ->method('isOpen') diff --git a/src/Symfony/Bridge/Doctrine/Tests/SchemaListener/DoctrineDbalCacheAdapterSchemaSubscriberTest.php b/src/Symfony/Bridge/Doctrine/Tests/SchemaListener/DoctrineDbalCacheAdapterSchemaSubscriberTest.php new file mode 100644 index 0000000000000..8f1afa99b1319 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/SchemaListener/DoctrineDbalCacheAdapterSchemaSubscriberTest.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\SchemaListener; + +use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Schema\Schema; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs; +use PHPUnit\Framework\TestCase; +use Symfony\Bridge\Doctrine\SchemaListener\DoctrineDbalCacheAdapterSchemaSubscriber; +use Symfony\Component\Cache\Adapter\DoctrineSchemaConfiguratorInterface; + +class DoctrineDbalCacheAdapterSchemaSubscriberTest extends TestCase +{ + public function testPostGenerateSchema() + { + $schema = new Schema(); + $dbalConnection = $this->createMock(Connection::class); + $entityManager = $this->createMock(EntityManagerInterface::class); + $entityManager->expects($this->once()) + ->method('getConnection') + ->willReturn($dbalConnection); + $event = new GenerateSchemaEventArgs($entityManager, $schema); + + $pdoAdapter = $this->createMock(DoctrineSchemaConfiguratorInterface::class); + $pdoAdapter->expects($this->once()) + ->method('configureSchema') + ->with($schema, $dbalConnection); + + $subscriber = new DoctrineDbalCacheAdapterSchemaSubscriber([$pdoAdapter]); + $subscriber->postGenerateSchema($event); + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/SchemaListener/PdoCacheAdapterDoctrineSchemaSubscriberTest.php b/src/Symfony/Bridge/Doctrine/Tests/SchemaListener/PdoCacheAdapterDoctrineSchemaSubscriberTest.php index 9cf70e943ed25..90b76328db9f9 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/SchemaListener/PdoCacheAdapterDoctrineSchemaSubscriberTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/SchemaListener/PdoCacheAdapterDoctrineSchemaSubscriberTest.php @@ -19,6 +19,9 @@ use Symfony\Bridge\Doctrine\SchemaListener\PdoCacheAdapterDoctrineSchemaSubscriber; use Symfony\Component\Cache\Adapter\PdoAdapter; +/** + * @group legacy + */ class PdoCacheAdapterDoctrineSchemaSubscriberTest extends TestCase { public function testPostGenerateSchema() diff --git a/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php b/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php index 4996848143b73..c1caadd9df3dd 100644 --- a/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php +++ b/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php @@ -134,7 +134,18 @@ public function validate($entity, Constraint $constraint) $repository = $em->getRepository(\get_class($entity)); } - $result = $repository->{$constraint->repositoryMethod}($criteria); + $arguments = [$criteria]; + + /* If the default repository method is used, it is always enough to retrieve at most two entities because: + * - No entity returned, the current entity is definitely unique. + * - More than one entity returned, the current entity cannot be unique. + * - One entity returned the uniqueness depends on the current entity. + */ + if ('findBy' === $constraint->repositoryMethod) { + $arguments = [$criteria, null, 2]; + } + + $result = $repository->{$constraint->repositoryMethod}(...$arguments); if ($result instanceof \IteratorAggregate) { $result = $result->getIterator(); diff --git a/src/Symfony/Bridge/Doctrine/composer.json b/src/Symfony/Bridge/Doctrine/composer.json index 49957bd989e7b..c1feaf4e4213b 100644 --- a/src/Symfony/Bridge/Doctrine/composer.json +++ b/src/Symfony/Bridge/Doctrine/composer.json @@ -27,33 +27,35 @@ }, "require-dev": { "composer/package-versions-deprecated": "^1.8", - "symfony/stopwatch": "^4.4|^5.0", - "symfony/cache": "^5.1", - "symfony/config": "^4.4|^5.0", - "symfony/dependency-injection": "^4.4|^5.0", - "symfony/form": "^5.1.3", - "symfony/http-kernel": "^5.0", - "symfony/messenger": "^4.4|^5.0", - "symfony/doctrine-messenger": "^5.1", - "symfony/property-access": "^4.4|^5.0", - "symfony/property-info": "^5.0", - "symfony/proxy-manager-bridge": "^4.4|^5.0", - "symfony/security-core": "^5.3", - "symfony/expression-language": "^4.4|^5.0", - "symfony/uid": "^5.1", - "symfony/validator": "^5.2", - "symfony/translation": "^4.4|^5.0", - "symfony/var-dumper": "^4.4|^5.0", + "symfony/stopwatch": "^4.4|^5.0|^6.0", + "symfony/cache": "^5.4|^6.0", + "symfony/config": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/form": "^5.1.3|^6.0", + "symfony/http-kernel": "^5.0|^6.0", + "symfony/messenger": "^4.4|^5.0|^6.0", + "symfony/doctrine-messenger": "^5.1|^6.0", + "symfony/property-access": "^4.4|^5.0|^6.0", + "symfony/property-info": "^5.0|^6.0", + "symfony/proxy-manager-bridge": "^4.4|^5.0|^6.0", + "symfony/security-core": "^5.3|^6.0", + "symfony/expression-language": "^4.4|^5.0|^6.0", + "symfony/uid": "^5.1|^6.0", + "symfony/validator": "^5.2|^6.0", + "symfony/translation": "^4.4|^5.0|^6.0", + "symfony/var-dumper": "^4.4|^5.0|^6.0", "doctrine/annotations": "^1.10.4", "doctrine/collections": "~1.0", "doctrine/data-fixtures": "^1.1", - "doctrine/dbal": "^2.10|^3.0", - "doctrine/orm": "^2.7.3" + "doctrine/dbal": "^2.13.1|^3.0", + "doctrine/orm": "^2.7.3", + "psr/log": "^1|^2|^3" }, "conflict": { - "doctrine/dbal": "<2.10", + "doctrine/dbal": "<2.13.1", "doctrine/orm": "<2.7.3", "phpunit/phpunit": "<5.4.3", + "symfony/cache": "<5.4", "symfony/dependency-injection": "<4.4", "symfony/form": "<5.1", "symfony/http-kernel": "<5", diff --git a/src/Symfony/Bridge/Monolog/CHANGELOG.md b/src/Symfony/Bridge/Monolog/CHANGELOG.md index 072b742a5f4e1..0c9ce91ffe22c 100644 --- a/src/Symfony/Bridge/Monolog/CHANGELOG.md +++ b/src/Symfony/Bridge/Monolog/CHANGELOG.md @@ -1,6 +1,12 @@ CHANGELOG ========= +5.4 +--- + + * Deprecate `ResetLoggersWorkerSubscriber` to reset buffered logs in messenger + workers, use "reset_on_message" option in messenger configuration instead. + 5.3 --- diff --git a/src/Symfony/Bridge/Monolog/Handler/ElasticsearchLogstashHandler.php b/src/Symfony/Bridge/Monolog/Handler/ElasticsearchLogstashHandler.php index a59825f6ab1f4..080d8e620bace 100644 --- a/src/Symfony/Bridge/Monolog/Handler/ElasticsearchLogstashHandler.php +++ b/src/Symfony/Bridge/Monolog/Handler/ElasticsearchLogstashHandler.php @@ -20,6 +20,7 @@ use Symfony\Component\HttpClient\HttpClient; use Symfony\Contracts\HttpClient\Exception\ExceptionInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; /** * Push logs directly to Elasticsearch and format them according to Logstash specification. @@ -47,6 +48,10 @@ class ElasticsearchLogstashHandler extends AbstractHandler private $endpoint; private $index; private $client; + + /** + * @var \SplObjectStorage + */ private $responses; /** diff --git a/src/Symfony/Bridge/Monolog/Handler/SwiftMailerHandler.php b/src/Symfony/Bridge/Monolog/Handler/SwiftMailerHandler.php index d5470c6b18916..c0e678f30c783 100644 --- a/src/Symfony/Bridge/Monolog/Handler/SwiftMailerHandler.php +++ b/src/Symfony/Bridge/Monolog/Handler/SwiftMailerHandler.php @@ -15,12 +15,16 @@ use Symfony\Component\Console\Event\ConsoleTerminateEvent; use Symfony\Component\HttpKernel\Event\TerminateEvent; +trigger_deprecation('symfony/monolog-bridge', '5.4', '"%s" is deprecated and will be removed in 6.0.', SwiftMailerHandler::class); + /** * Extended SwiftMailerHandler that flushes mail queue if necessary. * * @author Philipp Kräutli * * @final + * + * @deprecated since Symfony 5.4 */ class SwiftMailerHandler extends BaseSwiftMailerHandler { diff --git a/src/Symfony/Bridge/Monolog/Messenger/ResetLoggersWorkerSubscriber.php b/src/Symfony/Bridge/Monolog/Messenger/ResetLoggersWorkerSubscriber.php index ad38c8d67e4ff..c4b1e45cfc93f 100644 --- a/src/Symfony/Bridge/Monolog/Messenger/ResetLoggersWorkerSubscriber.php +++ b/src/Symfony/Bridge/Monolog/Messenger/ResetLoggersWorkerSubscriber.php @@ -16,10 +16,14 @@ use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent; use Symfony\Component\Messenger\Event\WorkerMessageHandledEvent; +trigger_deprecation('symfony/monolog-bridge', '5.4', 'The "%s" class is deprecated, use "reset_on_message" option in messenger configuration instead.', ResetLoggersWorkerSubscriber::class); + /** * Reset loggers between messages being handled to release buffered handler logs. * * @author Laurent VOULLEMIER + * + * @deprecated since Symfony 5.4, use "reset_on_message" option in messenger configuration instead. */ class ResetLoggersWorkerSubscriber implements EventSubscriberInterface { diff --git a/src/Symfony/Bridge/Monolog/Processor/AbstractTokenProcessor.php b/src/Symfony/Bridge/Monolog/Processor/AbstractTokenProcessor.php index 1f37d88aea4e2..ad41b877daeb6 100644 --- a/src/Symfony/Bridge/Monolog/Processor/AbstractTokenProcessor.php +++ b/src/Symfony/Bridge/Monolog/Processor/AbstractTokenProcessor.php @@ -42,7 +42,7 @@ public function __invoke(array $record): array if (null !== $token = $this->getToken()) { $record['extra'][$this->getKey()] = [ - 'authenticated' => $token->isAuthenticated(), + 'authenticated' => method_exists($token, 'isAuthenticated') ? $token->isAuthenticated(false) : (bool) $token->getUser(), 'roles' => $token->getRoleNames(), ]; diff --git a/src/Symfony/Bridge/Monolog/Processor/DebugProcessor.php b/src/Symfony/Bridge/Monolog/Processor/DebugProcessor.php index 7b7bf23c1a219..98b1d229220fe 100644 --- a/src/Symfony/Bridge/Monolog/Processor/DebugProcessor.php +++ b/src/Symfony/Bridge/Monolog/Processor/DebugProcessor.php @@ -32,8 +32,17 @@ public function __invoke(array $record) { $hash = $this->requestStack && ($request = $this->requestStack->getCurrentRequest()) ? spl_object_hash($request) : ''; + $timestamp = $timestampRfc3339 = false; + if ($record['datetime'] instanceof \DateTimeInterface) { + $timestamp = $record['datetime']->getTimestamp(); + $timestampRfc3339 = $record['datetime']->format(\DateTimeInterface::RFC3339_EXTENDED); + } elseif (false !== $timestamp = strtotime($record['datetime'])) { + $timestampRfc3339 = (new \DateTimeImmutable($record['datetime']))->format(\DateTimeInterface::RFC3339_EXTENDED); + } + $this->records[$hash][] = [ - 'timestamp' => $record['datetime'] instanceof \DateTimeInterface ? $record['datetime']->getTimestamp() : strtotime($record['datetime']), + 'timestamp' => $timestamp, + 'timestamp_rfc3339' => $timestampRfc3339, 'message' => $record['message'], 'priority' => $record['level'], 'priorityName' => $record['level_name'], diff --git a/src/Symfony/Bridge/Monolog/Tests/Handler/MailerHandlerTest.php b/src/Symfony/Bridge/Monolog/Tests/Handler/MailerHandlerTest.php index 24aaa6b95cdd9..daec7676c9e99 100644 --- a/src/Symfony/Bridge/Monolog/Tests/Handler/MailerHandlerTest.php +++ b/src/Symfony/Bridge/Monolog/Tests/Handler/MailerHandlerTest.php @@ -91,10 +91,7 @@ public function testHtmlContent() $handler->handle($this->getRecord(Logger::WARNING, 'message')); } - /** - * @return array Record - */ - protected function getRecord($level = Logger::WARNING, $message = 'test', $context = []) + protected function getRecord($level = Logger::WARNING, $message = 'test', $context = []): array { return [ 'message' => $message, @@ -107,10 +104,7 @@ protected function getRecord($level = Logger::WARNING, $message = 'test', $conte ]; } - /** - * @return array - */ - protected function getMultipleRecords() + protected function getMultipleRecords(): array { return [ $this->getRecord(Logger::DEBUG, 'debug message 1'), diff --git a/src/Symfony/Bridge/Monolog/Tests/Messenger/ResetLoggersWorkerSubscriberTest.php b/src/Symfony/Bridge/Monolog/Tests/Messenger/ResetLoggersWorkerSubscriberTest.php index 23e2f829e1baa..5f4647069e130 100644 --- a/src/Symfony/Bridge/Monolog/Tests/Messenger/ResetLoggersWorkerSubscriberTest.php +++ b/src/Symfony/Bridge/Monolog/Tests/Messenger/ResetLoggersWorkerSubscriberTest.php @@ -25,6 +25,7 @@ use Symfony\Component\Messenger\Transport\Receiver\ReceiverInterface; use Symfony\Component\Messenger\Worker; +/** @group legacy */ class ResetLoggersWorkerSubscriberTest extends TestCase { public function testLogsAreFlushed() diff --git a/src/Symfony/Bridge/Monolog/Tests/Processor/DebugProcessorTest.php b/src/Symfony/Bridge/Monolog/Tests/Processor/DebugProcessorTest.php index 6adec38a0c7f0..c576462d0abfe 100644 --- a/src/Symfony/Bridge/Monolog/Tests/Processor/DebugProcessorTest.php +++ b/src/Symfony/Bridge/Monolog/Tests/Processor/DebugProcessorTest.php @@ -43,6 +43,30 @@ public function providerDatetimeFormatTests(): array ]; } + /** + * @dataProvider providerDatetimeRfc3339FormatTests + */ + public function testDatetimeRfc3339Format(array $record, $expectedTimestamp) + { + $processor = new DebugProcessor(); + $processor($record); + + $records = $processor->getLogs(); + self::assertCount(1, $records); + self::assertSame($expectedTimestamp, $records[0]['timestamp_rfc3339']); + } + + public function providerDatetimeRfc3339FormatTests(): array + { + $record = $this->getRecord(); + + return [ + [array_merge($record, ['datetime' => new \DateTime('2019-01-01T00:01:00+00:00')]), '2019-01-01T00:01:00.000+00:00'], + [array_merge($record, ['datetime' => '2019-01-01T00:01:00+00:00']), '2019-01-01T00:01:00.000+00:00'], + [array_merge($record, ['datetime' => 'foo']), false], + ]; + } + public function testDebugProcessor() { $processor = new DebugProcessor(); diff --git a/src/Symfony/Bridge/Monolog/Tests/Processor/SwitchUserTokenProcessorTest.php b/src/Symfony/Bridge/Monolog/Tests/Processor/SwitchUserTokenProcessorTest.php index 7d9aaede008c4..c6430ee2c66ac 100644 --- a/src/Symfony/Bridge/Monolog/Tests/Processor/SwitchUserTokenProcessorTest.php +++ b/src/Symfony/Bridge/Monolog/Tests/Processor/SwitchUserTokenProcessorTest.php @@ -16,6 +16,8 @@ use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; +use Symfony\Component\Security\Core\User\InMemoryUser; +use Symfony\Component\Security\Core\User\User; /** * Tests the SwitchUserTokenProcessor. @@ -26,8 +28,13 @@ class SwitchUserTokenProcessorTest extends TestCase { public function testProcessor() { - $originalToken = new UsernamePasswordToken('original_user', 'password', 'provider', ['ROLE_SUPER_ADMIN']); - $switchUserToken = new SwitchUserToken('user', 'passsword', 'provider', ['ROLE_USER'], $originalToken); + if (class_exists(InMemoryUser::class)) { + $originalToken = new UsernamePasswordToken(new InMemoryUser('original_user', 'password', ['ROLE_SUPER_ADMIN']), 'provider', ['ROLE_SUPER_ADMIN']); + $switchUserToken = new SwitchUserToken(new InMemoryUser('user', 'passsword', ['ROLE_USER']), 'provider', ['ROLE_USER'], $originalToken); + } else { + $originalToken = new UsernamePasswordToken(new User('original_user', 'password', ['ROLE_SUPER_ADMIN']), null, 'provider', ['ROLE_SUPER_ADMIN']); + $switchUserToken = new SwitchUserToken(new User('user', 'passsword', ['ROLE_USER']), null, 'provider', ['ROLE_USER'], $originalToken); + } $tokenStorage = $this->createMock(TokenStorageInterface::class); $tokenStorage->method('getToken')->willReturn($switchUserToken); diff --git a/src/Symfony/Bridge/Monolog/Tests/Processor/TokenProcessorTest.php b/src/Symfony/Bridge/Monolog/Tests/Processor/TokenProcessorTest.php index b9fa51658e0d4..9f15051a1570e 100644 --- a/src/Symfony/Bridge/Monolog/Tests/Processor/TokenProcessorTest.php +++ b/src/Symfony/Bridge/Monolog/Tests/Processor/TokenProcessorTest.php @@ -15,6 +15,7 @@ use Symfony\Bridge\Monolog\Processor\TokenProcessor; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; +use Symfony\Component\Security\Core\User\InMemoryUser; /** * Tests the TokenProcessor. @@ -23,6 +24,9 @@ */ class TokenProcessorTest extends TestCase { + /** + * @group legacy + */ public function testLegacyProcessor() { if (method_exists(UsernamePasswordToken::class, 'getUserIdentifier')) { @@ -39,7 +43,6 @@ public function testLegacyProcessor() $this->assertArrayHasKey('token', $record['extra']); $this->assertEquals($token->getUsername(), $record['extra']['token']['username']); - $this->assertEquals($token->isAuthenticated(), $record['extra']['token']['authenticated']); $this->assertEquals(['ROLE_USER'], $record['extra']['token']['roles']); } @@ -49,7 +52,7 @@ public function testProcessor() $this->markTestSkipped('This test requires symfony/security-core 5.3+'); } - $token = new UsernamePasswordToken('user', 'password', 'provider', ['ROLE_USER']); + $token = new UsernamePasswordToken(new InMemoryUser('user', 'password', ['ROLE_USER']), 'provider', ['ROLE_USER']); $tokenStorage = $this->createMock(TokenStorageInterface::class); $tokenStorage->method('getToken')->willReturn($token); @@ -59,7 +62,6 @@ public function testProcessor() $this->assertArrayHasKey('token', $record['extra']); $this->assertEquals($token->getUserIdentifier(), $record['extra']['token']['user_identifier']); - $this->assertEquals($token->isAuthenticated(), $record['extra']['token']['authenticated']); $this->assertEquals(['ROLE_USER'], $record['extra']['token']['roles']); } } diff --git a/src/Symfony/Bridge/Monolog/composer.json b/src/Symfony/Bridge/Monolog/composer.json index 73ead5aa0aa1a..8c0a9d239ad35 100644 --- a/src/Symfony/Bridge/Monolog/composer.json +++ b/src/Symfony/Bridge/Monolog/composer.json @@ -19,18 +19,18 @@ "php": ">=7.2.5", "monolog/monolog": "^1.25.1|^2", "symfony/service-contracts": "^1.1|^2", - "symfony/http-kernel": "^5.3", + "symfony/http-kernel": "^5.3|^6.0", "symfony/deprecation-contracts": "^2.1", "symfony/polyfill-php80": "^1.16" }, "require-dev": { - "symfony/console": "^4.4|^5.0", - "symfony/http-client": "^4.4|^5.0", - "symfony/security-core": "^4.4|^5.0", - "symfony/var-dumper": "^4.4|^5.0", - "symfony/mailer": "^4.4|^5.0", - "symfony/mime": "^4.4|^5.0", - "symfony/messenger": "^4.4|^5.0" + "symfony/console": "^4.4|^5.0|^6.0", + "symfony/http-client": "^4.4|^5.0|^6.0", + "symfony/security-core": "^4.4|^5.0|^6.0", + "symfony/var-dumper": "^4.4|^5.0|^6.0", + "symfony/mailer": "^4.4|^5.0|^6.0", + "symfony/mime": "^4.4|^5.0|^6.0", + "symfony/messenger": "^4.4|^5.0|^6.0" }, "conflict": { "symfony/console": "<4.4", diff --git a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/Deprecation.php b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/Deprecation.php index 5a0514e323071..5eda2bafdfb10 100644 --- a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/Deprecation.php +++ b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/Deprecation.php @@ -102,8 +102,11 @@ public function __construct($message, array $trace, $file) } set_error_handler(function () {}); - $parsedMsg = unserialize($this->message); - restore_error_handler(); + try { + $parsedMsg = unserialize($this->message); + } finally { + restore_error_handler(); + } if ($parsedMsg && isset($parsedMsg['deprecation'])) { $this->message = $parsedMsg['deprecation']; $this->originClass = $parsedMsg['class']; @@ -310,7 +313,7 @@ private function getPackage($path) } /** - * @return string[] an array of paths + * @return string[] */ private static function getVendors() { diff --git a/src/Symfony/Bridge/PhpUnit/composer.json b/src/Symfony/Bridge/PhpUnit/composer.json index 00dc40452757c..60b5ac6fb4802 100644 --- a/src/Symfony/Bridge/PhpUnit/composer.json +++ b/src/Symfony/Bridge/PhpUnit/composer.json @@ -22,7 +22,7 @@ "symfony/deprecation-contracts": "^2.1" }, "require-dev": { - "symfony/error-handler": "^4.4|^5.0" + "symfony/error-handler": "^4.4|^5.0|^6.0" }, "suggest": { "symfony/error-handler": "For tracking deprecated interfaces usages at runtime with DebugClassLoader" diff --git a/src/Symfony/Bridge/ProxyManager/composer.json b/src/Symfony/Bridge/ProxyManager/composer.json index 91d93ba7cf421..68ec28540704d 100644 --- a/src/Symfony/Bridge/ProxyManager/composer.json +++ b/src/Symfony/Bridge/ProxyManager/composer.json @@ -19,11 +19,11 @@ "php": ">=7.2.5", "composer/package-versions-deprecated": "^1.8", "friendsofphp/proxy-manager-lts": "^1.0.2", - "symfony/dependency-injection": "^5.0", + "symfony/dependency-injection": "^5.0|^6.0", "symfony/polyfill-php80": "^1.16" }, "require-dev": { - "symfony/config": "^4.4|^5.0" + "symfony/config": "^4.4|^5.0|^6.0" }, "autoload": { "psr-4": { "Symfony\\Bridge\\ProxyManager\\": "" }, diff --git a/src/Symfony/Bridge/Twig/AppVariable.php b/src/Symfony/Bridge/Twig/AppVariable.php index f5a6494ed29bc..23683eb35e427 100644 --- a/src/Symfony/Bridge/Twig/AppVariable.php +++ b/src/Symfony/Bridge/Twig/AppVariable.php @@ -16,6 +16,7 @@ use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\User\UserInterface; /** * Exposes some Symfony parameters and services as an "app" global variable. @@ -68,7 +69,7 @@ public function getToken() /** * Returns the current user. * - * @return object|null + * @return UserInterface|null * * @see TokenInterface::getUser() */ @@ -84,13 +85,14 @@ public function getUser() $user = $token->getUser(); + // @deprecated since Symfony 5.4, $user will always be a UserInterface instance return \is_object($user) ? $user : null; } /** * Returns the current request. * - * @return Request|null The HTTP request object + * @return Request|null */ public function getRequest() { @@ -104,7 +106,7 @@ public function getRequest() /** * Returns the current session. * - * @return Session|null The session + * @return Session|null */ public function getSession() { @@ -119,7 +121,7 @@ public function getSession() /** * Returns the current app environment. * - * @return string The current environment string (e.g 'dev') + * @return string */ public function getEnvironment() { @@ -133,7 +135,7 @@ public function getEnvironment() /** * Returns the current app debug mode. * - * @return bool The current debug mode + * @return bool */ public function getDebug() { diff --git a/src/Symfony/Bridge/Twig/CHANGELOG.md b/src/Symfony/Bridge/Twig/CHANGELOG.md index 16ce6d86a1ed8..535df0c0897b4 100644 --- a/src/Symfony/Bridge/Twig/CHANGELOG.md +++ b/src/Symfony/Bridge/Twig/CHANGELOG.md @@ -1,6 +1,12 @@ CHANGELOG ========= +5.4 +--- + +* Add `github` format & autodetection to render errors as annotations when + running the Twig linter command in a Github Actions environment. + 5.3 --- diff --git a/src/Symfony/Bridge/Twig/Command/DebugCommand.php b/src/Symfony/Bridge/Twig/Command/DebugCommand.php index 887f81b1f4211..d4c78210114d7 100644 --- a/src/Symfony/Bridge/Twig/Command/DebugCommand.php +++ b/src/Symfony/Bridge/Twig/Command/DebugCommand.php @@ -12,6 +12,8 @@ namespace Symfony\Bridge\Twig\Command; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Input\InputArgument; @@ -110,6 +112,17 @@ protected function execute(InputInterface $input, OutputInterface $output) return 0; } + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('name')) { + $suggestions->suggestValues(array_keys($this->getLoaderPaths())); + } + + if ($input->mustSuggestOptionValuesFor('format')) { + $suggestions->suggestValues(['text', 'json']); + } + } + private function displayPathsText(SymfonyStyle $io, string $name) { $file = new \ArrayIterator($this->findTemplateFiles($name)); diff --git a/src/Symfony/Bridge/Twig/Command/LintCommand.php b/src/Symfony/Bridge/Twig/Command/LintCommand.php index a16c771d6b6ae..b91110b34a5bc 100644 --- a/src/Symfony/Bridge/Twig/Command/LintCommand.php +++ b/src/Symfony/Bridge/Twig/Command/LintCommand.php @@ -11,7 +11,10 @@ namespace Symfony\Bridge\Twig\Command; +use Symfony\Component\Console\CI\GithubActionReporter; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Input\InputArgument; @@ -39,6 +42,11 @@ class LintCommand extends Command private $twig; + /** + * @var string|null + */ + private $format; + public function __construct(Environment $twig) { parent::__construct(); @@ -50,7 +58,7 @@ protected function configure() { $this ->setDescription(self::$defaultDescription) - ->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format', 'txt') + ->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format') ->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') ->setHelp(<<<'EOF' @@ -80,6 +88,11 @@ protected function execute(InputInterface $input, OutputInterface $output) $io = new SymfonyStyle($input, $output); $filenames = $input->getArgument('filename'); $showDeprecations = $input->getOption('show-deprecations'); + $this->format = $input->getOption('format'); + + if (null === $this->format) { + $this->format = GithubActionReporter::isGithubActionEnvironment() ? 'github' : 'txt'; + } if (['-'] === $filenames) { return $this->display($input, $output, $io, [$this->validate(file_get_contents('php://stdin'), uniqid('sf_', true))]); @@ -169,26 +182,29 @@ private function validate(string $template, string $file): array private function display(InputInterface $input, OutputInterface $output, SymfonyStyle $io, array $files) { - switch ($input->getOption('format')) { + switch ($this->format) { case 'txt': return $this->displayTxt($output, $io, $files); case 'json': return $this->displayJson($output, $files); + case 'github': + return $this->displayTxt($output, $io, $files, true); default: throw new InvalidArgumentException(sprintf('The format "%s" is not supported.', $input->getOption('format'))); } } - private function displayTxt(OutputInterface $output, SymfonyStyle $io, array $filesInfo) + private function displayTxt(OutputInterface $output, SymfonyStyle $io, array $filesInfo, bool $errorAsGithubAnnotations = false) { $errors = 0; + $githubReporter = $errorAsGithubAnnotations ? new GithubActionReporter($output) : null; foreach ($filesInfo as $info) { if ($info['valid'] && $output->isVerbose()) { $io->comment('OK'.($info['file'] ? sprintf(' in %s', $info['file']) : '')); } elseif (!$info['valid']) { ++$errors; - $this->renderException($io, $info['template'], $info['exception'], $info['file']); + $this->renderException($io, $info['template'], $info['exception'], $info['file'], $githubReporter); } } @@ -220,10 +236,14 @@ private function displayJson(OutputInterface $output, array $filesInfo) return min($errors, 1); } - private function renderException(SymfonyStyle $output, string $template, Error $exception, string $file = null) + private function renderException(SymfonyStyle $output, string $template, Error $exception, string $file = null, GithubActionReporter $githubReporter = null) { $line = $exception->getTemplateLine(); + if ($githubReporter) { + $githubReporter->error($exception->getRawMessage(), $file, $line <= 0 ? null : $line); + } + if ($file) { $output->text(sprintf(' ERROR in %s (line %s)', $file, $line)); } else { @@ -266,4 +286,11 @@ private function getContext(string $template, int $line, int $context = 3) return $result; } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestOptionValuesFor('format')) { + $suggestions->suggestValues(['txt', 'json', 'github']); + } + } } diff --git a/src/Symfony/Bridge/Twig/DataCollector/TwigDataCollector.php b/src/Symfony/Bridge/Twig/DataCollector/TwigDataCollector.php index be432838ff45a..4a469781084e0 100644 --- a/src/Symfony/Bridge/Twig/DataCollector/TwigDataCollector.php +++ b/src/Symfony/Bridge/Twig/DataCollector/TwigDataCollector.php @@ -22,8 +22,6 @@ use Twig\Profiler\Profile; /** - * TwigDataCollector. - * * @author Fabien Potencier * * @final @@ -198,7 +196,7 @@ private function computeData(Profile $profile) /** * {@inheritdoc} */ - public function getName() + public function getName(): string { return 'twig'; } diff --git a/src/Symfony/Bridge/Twig/Extension/CodeExtension.php b/src/Symfony/Bridge/Twig/Extension/CodeExtension.php index 5282557ee2799..15b70693868b8 100644 --- a/src/Symfony/Bridge/Twig/Extension/CodeExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/CodeExtension.php @@ -175,7 +175,7 @@ public function formatFile(string $file, int $line, string $text = null): string /** * Returns the link for a given file/line pair. * - * @return string|false A link or false + * @return string|false */ public function getFileLink(string $file, int $line) { diff --git a/src/Symfony/Bridge/Twig/Extension/ProfilerExtension.php b/src/Symfony/Bridge/Twig/Extension/ProfilerExtension.php index fcc4396f1c9a1..51d6eba2da185 100644 --- a/src/Symfony/Bridge/Twig/Extension/ProfilerExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/ProfilerExtension.php @@ -12,6 +12,7 @@ namespace Symfony\Bridge\Twig\Extension; use Symfony\Component\Stopwatch\Stopwatch; +use Symfony\Component\Stopwatch\StopwatchEvent; use Twig\Extension\ProfilerExtension as BaseProfilerExtension; use Twig\Profiler\Profile; @@ -21,6 +22,10 @@ final class ProfilerExtension extends BaseProfilerExtension { private $stopwatch; + + /** + * @var \SplObjectStorage + */ private $events; public function __construct(Profile $profile, Stopwatch $stopwatch = null) diff --git a/src/Symfony/Bridge/Twig/Extension/WorkflowExtension.php b/src/Symfony/Bridge/Twig/Extension/WorkflowExtension.php index ea7cd17a8fc10..9b5911ec28992 100644 --- a/src/Symfony/Bridge/Twig/Extension/WorkflowExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/WorkflowExtension.php @@ -59,7 +59,7 @@ public function canTransition(object $subject, string $transitionName, string $n /** * Returns all enabled transitions. * - * @return Transition[] All enabled transitions + * @return Transition[] */ public function getEnabledTransitions(object $subject, string $name = null): array { diff --git a/src/Symfony/Bridge/Twig/Form/TwigRendererEngine.php b/src/Symfony/Bridge/Twig/Form/TwigRendererEngine.php index bc3b82d2f595f..b17da340989e1 100644 --- a/src/Symfony/Bridge/Twig/Form/TwigRendererEngine.php +++ b/src/Symfony/Bridge/Twig/Form/TwigRendererEngine.php @@ -70,7 +70,7 @@ public function renderBlock(FormView $view, $resource, string $blockName, array * * @see getResourceForBlock() * - * @return bool True if the resource could be loaded, false otherwise + * @return bool */ protected function loadResourceForBlockName(string $cacheKey, FormView $view, string $blockName) { diff --git a/src/Symfony/Bridge/Twig/Mime/NotificationEmail.php b/src/Symfony/Bridge/Twig/Mime/NotificationEmail.php index 2058b8e67da9a..b5775f3554255 100644 --- a/src/Symfony/Bridge/Twig/Mime/NotificationEmail.php +++ b/src/Symfony/Bridge/Twig/Mime/NotificationEmail.php @@ -69,6 +69,9 @@ public static function asPublicEmail(Headers $headers = null, AbstractPart $body return $email; } + /** + * @return $this + */ public function markAsPublic(): self { $this->context['importance'] = null; diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/tailwind_2_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/tailwind_2_layout.html.twig index b821f5a965f02..7f31e70b796c0 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/tailwind_2_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/tailwind_2_layout.html.twig @@ -1,5 +1,3 @@ -{# @experimental in 5.3 #} - {% use 'form_div_layout.html.twig' %} {%- block form_row -%} diff --git a/src/Symfony/Bridge/Twig/Tests/AppVariableTest.php b/src/Symfony/Bridge/Twig/Tests/AppVariableTest.php index f5fcbeada6562..c2f9a6cb7cbc0 100644 --- a/src/Symfony/Bridge/Twig/Tests/AppVariableTest.php +++ b/src/Symfony/Bridge/Twig/Tests/AppVariableTest.php @@ -95,9 +95,12 @@ public function testGetUser() $this->assertEquals($user, $this->appVariable->getUser()); } + /** + * @group legacy + */ public function testGetUserWithUsernameAsTokenUser() { - $this->setTokenStorage($user = 'username'); + $this->setTokenStorage('username'); $this->assertNull($this->appVariable->getUser()); } diff --git a/src/Symfony/Bridge/Twig/Tests/Command/DebugCommandTest.php b/src/Symfony/Bridge/Twig/Tests/Command/DebugCommandTest.php index 327763b8f28ec..2488a27677af9 100644 --- a/src/Symfony/Bridge/Twig/Tests/Command/DebugCommandTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Command/DebugCommandTest.php @@ -15,6 +15,7 @@ use Symfony\Bridge\Twig\Command\DebugCommand; use Symfony\Component\Console\Application; use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Tester\CommandCompletionTester; use Symfony\Component\Console\Tester\CommandTester; use Twig\Environment; use Twig\Loader\ChainLoader; @@ -293,6 +294,33 @@ public function testWithFilter() $this->assertNotSame($display1, $display2); } + /** + * @dataProvider provideCompletionSuggestions + */ + public function testComplete(array $input, array $expectedSuggestions) + { + if (!class_exists(CommandCompletionTester::class)) { + $this->markTestSkipped('Test command completion requires symfony/console 5.4+.'); + } + + $projectDir = \dirname(__DIR__).\DIRECTORY_SEPARATOR.'Fixtures'; + $loader = new FilesystemLoader([], $projectDir); + $environment = new Environment($loader); + + $application = new Application(); + $application->add(new DebugCommand($environment, $projectDir, [], null, null)); + + $tester = new CommandCompletionTester($application->find('debug:twig')); + $suggestions = $tester->complete($input, 2); + $this->assertSame($expectedSuggestions, $suggestions); + } + + public function provideCompletionSuggestions(): iterable + { + yield 'name' => [['email'], []]; + yield 'option --format' => [['--format', ''], ['text', 'json']]; + } + private function createCommandTester(array $paths = [], array $bundleMetadata = [], string $defaultPath = null, bool $useChainLoader = false, array $globals = []): CommandTester { $projectDir = \dirname(__DIR__).\DIRECTORY_SEPARATOR.'Fixtures'; diff --git a/src/Symfony/Bridge/Twig/Tests/Command/LintCommandTest.php b/src/Symfony/Bridge/Twig/Tests/Command/LintCommandTest.php index 9bb9a9867c745..6a3d640b2d5b2 100644 --- a/src/Symfony/Bridge/Twig/Tests/Command/LintCommandTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Command/LintCommandTest.php @@ -14,7 +14,9 @@ use PHPUnit\Framework\TestCase; use Symfony\Bridge\Twig\Command\LintCommand; use Symfony\Component\Console\Application; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Tester\CommandCompletionTester; use Symfony\Component\Console\Tester\CommandTester; use Twig\Environment; use Twig\Loader\FilesystemLoader; @@ -107,7 +109,58 @@ public function testLintDefaultPaths() self::assertStringContainsString('OK in', trim($tester->getDisplay())); } + public function testLintIncorrectFileWithGithubFormat() + { + $filename = $this->createFile('{{ foo'); + $tester = $this->createCommandTester(); + $tester->execute(['filename' => [$filename], '--format' => 'github'], ['decorated' => false]); + self::assertEquals(1, $tester->getStatusCode(), 'Returns 1 in case of error'); + self::assertStringMatchesFormat('%A::error file=%s,line=1,col=0::Unexpected token "end of template" ("end of print statement" expected).%A', trim($tester->getDisplay())); + } + + public function testLintAutodetectsGithubActionEnvironment() + { + $prev = getenv('GITHUB_ACTIONS'); + putenv('GITHUB_ACTIONS'); + + try { + putenv('GITHUB_ACTIONS=1'); + + $filename = $this->createFile('{{ foo'); + $tester = $this->createCommandTester(); + + $tester->execute(['filename' => [$filename]], ['decorated' => false]); + self::assertStringMatchesFormat('%A::error file=%s,line=1,col=0::Unexpected token "end of template" ("end of print statement" expected).%A', trim($tester->getDisplay())); + } finally { + putenv('GITHUB_ACTIONS'.($prev ? "=$prev" : '')); + } + } + + /** + * @dataProvider provideCompletionSuggestions + */ + public function testComplete(array $input, array $expectedSuggestions) + { + if (!class_exists(CommandCompletionTester::class)) { + $this->markTestSkipped('Test command completion requires symfony/console 5.4+.'); + } + + $tester = new CommandCompletionTester($this->createCommand()); + + $this->assertSame($expectedSuggestions, $tester->complete($input)); + } + + public function provideCompletionSuggestions() + { + yield 'option' => [['--format', ''], ['txt', 'json', 'github']]; + } + private function createCommandTester(): CommandTester + { + return new CommandTester($this->createCommand()); + } + + private function createCommand(): Command { $environment = new Environment(new FilesystemLoader(\dirname(__DIR__).'/Fixtures/templates/')); $environment->addFilter(new TwigFilter('deprecated_filter', function ($v) { @@ -118,9 +171,8 @@ private function createCommandTester(): CommandTester $application = new Application(); $application->add($command); - $command = $application->find('lint:twig'); - return new CommandTester($command); + return $application->find('lint:twig'); } private function createFile($content): string diff --git a/src/Symfony/Bridge/Twig/Tests/Translation/TwigExtractorTest.php b/src/Symfony/Bridge/Twig/Tests/Translation/TwigExtractorTest.php index 8013714d7b40c..4a8b4d19c066e 100644 --- a/src/Symfony/Bridge/Twig/Tests/Translation/TwigExtractorTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Translation/TwigExtractorTest.php @@ -82,7 +82,7 @@ public function getExtractData() ['{{ ("another " ~ "new " ~ "key") | trans() }}', ['another new key' => 'messages']], ['{{ ("new" ~ " key") | trans(domain="domain") }}', ['new key' => 'domain']], ['{{ ("another " ~ "new " ~ "key") | trans(domain="domain") }}', ['another new key' => 'domain']], - // if it has a variable or other expression, we can not extract it + // if it has a variable or other expression, we cannot extract it ['{% set foo = "new" %} {{ ("new " ~ foo ~ "key") | trans() }}', []], ['{{ ("foo " ~ "new"|trans ~ "key") | trans() }}', ['new' => 'messages']], ]; diff --git a/src/Symfony/Bridge/Twig/UndefinedCallableHandler.php b/src/Symfony/Bridge/Twig/UndefinedCallableHandler.php index 73b6fd7d80cf0..608bbaa8e30ad 100644 --- a/src/Symfony/Bridge/Twig/UndefinedCallableHandler.php +++ b/src/Symfony/Bridge/Twig/UndefinedCallableHandler.php @@ -13,6 +13,8 @@ use Symfony\Bundle\FullStack; use Twig\Error\SyntaxError; +use Twig\TwigFilter; +use Twig\TwigFunction; /** * @internal @@ -30,6 +32,8 @@ class UndefinedCallableHandler 'asset' => 'asset', 'asset_version' => 'asset', 'dump' => 'debug-bundle', + 'encore_entry_link_tags' => 'webpack-encore-bundle', + 'encore_entry_script_tags' => 'webpack-encore-bundle', 'expression' => 'expression-language', 'form_widget' => 'form', 'form_errors' => 'form', @@ -64,34 +68,40 @@ class UndefinedCallableHandler 'workflow' => 'enable "framework.workflows"', ]; - public static function onUndefinedFilter(string $name): bool + /** + * @return TwigFilter|false + */ + public static function onUndefinedFilter(string $name) { if (!isset(self::FILTER_COMPONENTS[$name])) { return false; } - self::onUndefined($name, 'filter', self::FILTER_COMPONENTS[$name]); - - return true; + throw new SyntaxError(self::onUndefined($name, 'filter', self::FILTER_COMPONENTS[$name])); } - public static function onUndefinedFunction(string $name): bool + /** + * @return TwigFunction|false + */ + public static function onUndefinedFunction(string $name) { if (!isset(self::FUNCTION_COMPONENTS[$name])) { return false; } - self::onUndefined($name, 'function', self::FUNCTION_COMPONENTS[$name]); + if ('webpack-encore-bundle' === self::FUNCTION_COMPONENTS[$name]) { + return new TwigFunction($name, static function () { return ''; }); + } - return true; + throw new SyntaxError(self::onUndefined($name, 'function', self::FUNCTION_COMPONENTS[$name])); } - private static function onUndefined(string $name, string $type, string $component) + private static function onUndefined(string $name, string $type, string $component): string { if (class_exists(FullStack::class) && isset(self::FULL_STACK_ENABLE[$component])) { - throw new SyntaxError(sprintf('Did you forget to %s? Unknown %s "%s".', self::FULL_STACK_ENABLE[$component], $type, $name)); + return sprintf('Did you forget to %s? Unknown %s "%s".', self::FULL_STACK_ENABLE[$component], $type, $name); } - throw new SyntaxError(sprintf('Did you forget to run "composer require symfony/%s"? Unknown %s "%s".', $component, $type, $name)); + return sprintf('Did you forget to run "composer require symfony/%s"? Unknown %s "%s".', $component, $type, $name); } } diff --git a/src/Symfony/Bridge/Twig/composer.json b/src/Symfony/Bridge/Twig/composer.json index a623c493a16f7..3d79e39ca2244 100644 --- a/src/Symfony/Bridge/Twig/composer.json +++ b/src/Symfony/Bridge/Twig/composer.json @@ -25,29 +25,29 @@ "doctrine/annotations": "^1.12", "egulias/email-validator": "^2.1.10|^3", "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", - "symfony/asset": "^4.4|^5.0", - "symfony/dependency-injection": "^4.4|^5.0", - "symfony/finder": "^4.4|^5.0", - "symfony/form": "^5.3", - "symfony/http-foundation": "^5.3", - "symfony/http-kernel": "^4.4|^5.0", - "symfony/intl": "^4.4|^5.0", - "symfony/mime": "^5.2", + "symfony/asset": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/finder": "^4.4|^5.0|^6.0", + "symfony/form": "^5.3|^6.0", + "symfony/http-foundation": "^5.3|^6.0", + "symfony/http-kernel": "^4.4|^5.0|^6.0", + "symfony/intl": "^4.4|^5.0|^6.0", + "symfony/mime": "^5.2|^6.0", "symfony/polyfill-intl-icu": "~1.0", - "symfony/property-info": "^4.4|^5.1", - "symfony/routing": "^4.4|^5.0", - "symfony/translation": "^5.2", - "symfony/yaml": "^4.4|^5.0", + "symfony/property-info": "^4.4|^5.1|^6.0", + "symfony/routing": "^4.4|^5.0|^6.0", + "symfony/translation": "^5.2|^6.0", + "symfony/yaml": "^4.4|^5.0|^6.0", "symfony/security-acl": "^2.8|^3.0", - "symfony/security-core": "^4.4|^5.0", - "symfony/security-csrf": "^4.4|^5.0", - "symfony/security-http": "^4.4|^5.0", - "symfony/serializer": "^5.2", - "symfony/stopwatch": "^4.4|^5.0", - "symfony/console": "^4.4|^5.0", - "symfony/expression-language": "^4.4|^5.0", - "symfony/web-link": "^4.4|^5.0", - "symfony/workflow": "^5.2", + "symfony/security-core": "^4.4|^5.0|^6.0", + "symfony/security-csrf": "^4.4|^5.0|^6.0", + "symfony/security-http": "^4.4|^5.0|^6.0", + "symfony/serializer": "^5.2|^6.0", + "symfony/stopwatch": "^4.4|^5.0|^6.0", + "symfony/console": "^5.3|^6.0", + "symfony/expression-language": "^4.4|^5.0|^6.0", + "symfony/web-link": "^4.4|^5.0|^6.0", + "symfony/workflow": "^5.2|^6.0", "twig/cssinliner-extra": "^2.12|^3", "twig/inky-extra": "^2.12|^3", "twig/markdown-extra": "^2.12|^3" @@ -55,7 +55,7 @@ "conflict": { "phpdocumentor/reflection-docblock": "<3.2.2", "phpdocumentor/type-resolver": "<1.4.0", - "symfony/console": "<4.4", + "symfony/console": "<5.3", "symfony/form": "<5.3", "symfony/http-foundation": "<5.3", "symfony/http-kernel": "<4.4", diff --git a/src/Symfony/Bundle/DebugBundle/composer.json b/src/Symfony/Bundle/DebugBundle/composer.json index 7c59e89ab3a55..de18bed072b54 100644 --- a/src/Symfony/Bundle/DebugBundle/composer.json +++ b/src/Symfony/Bundle/DebugBundle/composer.json @@ -18,15 +18,15 @@ "require": { "php": ">=7.2.5", "ext-xml": "*", - "symfony/http-kernel": "^4.4|^5.0", + "symfony/http-kernel": "^4.4|^5.0|^6.0", "symfony/polyfill-php80": "^1.16", - "symfony/twig-bridge": "^4.4|^5.0", - "symfony/var-dumper": "^4.4|^5.0" + "symfony/twig-bridge": "^4.4|^5.0|^6.0", + "symfony/var-dumper": "^4.4|^5.0|^6.0" }, "require-dev": { - "symfony/config": "^4.4|^5.0", - "symfony/dependency-injection": "^4.4|^5.0", - "symfony/web-profiler-bundle": "^4.4|^5.0" + "symfony/config": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/web-profiler-bundle": "^4.4|^5.0|^6.0" }, "conflict": { "symfony/config": "<4.4", diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index 4d67ff130d983..21e565cefae1b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -1,6 +1,27 @@ CHANGELOG ========= +5.4 +--- + + * Add `set_locale_from_accept_language` config option to automatically set the request locale based on the `Accept-Language` + HTTP request header and the `framework.enabled_locales` config option + * Add `set_content_language_from_locale` config option to automatically set the `Content-Language` HTTP response header based on the Request locale + * Deprecate the `framework.translator.enabled_locales`, use `framework.enabled_locales` instead + * Add autowiring alias for `HttpCache\StoreInterface` + * Add the ability to enable the profiler using a request query parameter, body parameter or attribute + * Deprecate the `AdapterInterface` autowiring alias, use `CacheItemPoolInterface` instead + * Deprecate the public `profiler` service to private + * Deprecate `get()`, `has()`, `getDoctrine()`, and `dispatchMessage()` in `AbstractController`, use method/constructor injection instead + * Deprecate the `cache.adapter.doctrine` service + * Add support for resetting container services after each messenger message + * Add `configureContainer()`, `configureRoutes()`, `getConfigDir()` and `getBundlesPath()` to `MicroKernelTrait` + * Add support for configuring log level, and status code by exception class + * Bind the `default_context` parameter onto serializer's encoders and normalizers + * Add support for `statusCode` default parameter when loading a template directly from route using the `Symfony\Bundle\FrameworkBundle\Controller\TemplateController` controller + * Deprecate `translation:update` command, use `translation:extract` instead + * Add `PhpStanExtractor` support for the PropertyInfo component + 5.3 --- diff --git a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/AbstractPhpFileCacheWarmer.php b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/AbstractPhpFileCacheWarmer.php index 29276a0dcecce..17e066045a04c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/AbstractPhpFileCacheWarmer.php +++ b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/AbstractPhpFileCacheWarmer.php @@ -49,7 +49,7 @@ public function warmUp(string $cacheDir) spl_autoload_register([ClassExistenceResource::class, 'throwOnRequiredClass']); try { if (!$this->doWarmUp($cacheDir, $arrayAdapter)) { - return; + return []; } } finally { spl_autoload_unregister([ClassExistenceResource::class, 'throwOnRequiredClass']); diff --git a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/AnnotationsCacheWarmer.php b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/AnnotationsCacheWarmer.php index 8ff2416d808f7..55044001798d0 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/AnnotationsCacheWarmer.php +++ b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/AnnotationsCacheWarmer.php @@ -12,12 +12,10 @@ namespace Symfony\Bundle\FrameworkBundle\CacheWarmer; use Doctrine\Common\Annotations\AnnotationException; -use Doctrine\Common\Annotations\CachedReader; use Doctrine\Common\Annotations\PsrCachedReader; use Doctrine\Common\Annotations\Reader; use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Cache\Adapter\PhpArrayAdapter; -use Symfony\Component\Cache\DoctrineProvider; /** * Warms up annotation caches for classes found in composer's autoload class map @@ -54,10 +52,7 @@ protected function doWarmUp(string $cacheDir, ArrayAdapter $arrayAdapter) } $annotatedClasses = include $annotatedClassPatterns; - $reader = class_exists(PsrCachedReader::class) - ? new PsrCachedReader($this->annotationReader, $arrayAdapter, $this->debug) - : new CachedReader($this->annotationReader, new DoctrineProvider($arrayAdapter), $this->debug) - ; + $reader = new PsrCachedReader($this->annotationReader, $arrayAdapter, $this->debug); foreach ($annotatedClasses as $class) { if (null !== $this->excludeRegexp && preg_match($this->excludeRegexp, $class)) { diff --git a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/CachePoolClearerCacheWarmer.php b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/CachePoolClearerCacheWarmer.php index 0e5997996004f..79bca240339b7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/CachePoolClearerCacheWarmer.php +++ b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/CachePoolClearerCacheWarmer.php @@ -28,6 +28,9 @@ final class CachePoolClearerCacheWarmer implements CacheWarmerInterface private $poolClearer; private $pools; + /** + * @param string[] $pools + */ public function __construct(Psr6CacheClearer $poolClearer, array $pools = []) { $this->poolClearer = $poolClearer; diff --git a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/RouterCacheWarmer.php b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/RouterCacheWarmer.php index ec4c5ac1ff801..6cdf176bb33bb 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/RouterCacheWarmer.php +++ b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/RouterCacheWarmer.php @@ -36,10 +36,8 @@ public function __construct(ContainerInterface $container) /** * {@inheritdoc} - * - * @return string[] */ - public function warmUp(string $cacheDir) + public function warmUp(string $cacheDir): array { $router = $this->container->get('router'); @@ -51,9 +49,7 @@ public function warmUp(string $cacheDir) } /** - * Checks whether this warmer is optional or not. - * - * @return bool always true + * {@inheritdoc} */ public function isOptional(): bool { diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/AssetsInstallCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/AssetsInstallCommand.php index a5a30cbbac350..c7a5fd7a62d03 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/AssetsInstallCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/AssetsInstallCommand.php @@ -233,7 +233,7 @@ private function absoluteSymlinkWithFallback(string $originDir, string $targetDi /** * Creates symbolic link. * - * @throws IOException if link can not be created + * @throws IOException if link cannot be created */ private function symlink(string $originDir, string $targetDir, bool $relative = false) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolClearCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolClearCommand.php index 35e9158f4217f..b72924dfa78d6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolClearCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolClearCommand.php @@ -13,6 +13,8 @@ use Psr\Cache\CacheItemPoolInterface; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -31,12 +33,17 @@ final class CachePoolClearCommand extends Command protected static $defaultDescription = 'Clear cache pools'; private $poolClearer; + private $poolNames; - public function __construct(Psr6CacheClearer $poolClearer) + /** + * @param string[]|null $poolNames + */ + public function __construct(Psr6CacheClearer $poolClearer, array $poolNames = null) { parent::__construct(); $this->poolClearer = $poolClearer; + $this->poolNames = $poolNames; } /** @@ -114,4 +121,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 0; } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if (\is_array($this->poolNames) && $input->mustSuggestArgumentValuesFor('pools')) { + $suggestions->suggestValues($this->poolNames); + } + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolDeleteCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolDeleteCommand.php index 35cf1eba77789..b36d48cfe3973 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolDeleteCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolDeleteCommand.php @@ -12,6 +12,8 @@ namespace Symfony\Bundle\FrameworkBundle\Command; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -29,12 +31,17 @@ final class CachePoolDeleteCommand extends Command protected static $defaultDescription = 'Delete an item from a cache pool'; private $poolClearer; + private $poolNames; - public function __construct(Psr6CacheClearer $poolClearer) + /** + * @param string[]|null $poolNames + */ + public function __construct(Psr6CacheClearer $poolClearer, array $poolNames = null) { parent::__construct(); $this->poolClearer = $poolClearer; + $this->poolNames = $poolNames; } /** @@ -81,4 +88,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 0; } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if (\is_array($this->poolNames) && $input->mustSuggestArgumentValuesFor('pool')) { + $suggestions->suggestValues($this->poolNames); + } + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolListCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolListCommand.php index 4a4b1eb2fa49e..0ad33241deb73 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolListCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolListCommand.php @@ -28,6 +28,9 @@ final class CachePoolListCommand extends Command private $poolNames; + /** + * @param string[] $poolNames + */ public function __construct(array $poolNames) { parent::__construct(); diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolPruneCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolPruneCommand.php index bfe4a444d99ac..8d10352942fb5 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolPruneCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolPruneCommand.php @@ -30,7 +30,7 @@ final class CachePoolPruneCommand extends Command private $pools; /** - * @param iterable|PruneableInterface[] $pools + * @param iterable $pools */ public function __construct(iterable $pools) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDebugCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDebugCommand.php index e88de6e31f257..8a8f2355b0365 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDebugCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDebugCommand.php @@ -13,6 +13,8 @@ use Symfony\Component\Config\Definition\ConfigurationInterface; use Symfony\Component\Config\Definition\Processor; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Exception\LogicException; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -94,11 +96,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $extensionAlias = $extension->getAlias(); $container = $this->compileContainer(); - $config = $container->resolveEnvPlaceholders( - $container->getParameterBag()->resolveValue( - $this->getConfigForExtension($extension, $container) - ) - ); + $config = $this->getConfig($extension, $container); if (null === $path = $input->getArgument('path')) { $io->title( @@ -188,4 +186,55 @@ private function getConfigForExtension(ExtensionInterface $extension, ContainerB return (new Processor())->processConfiguration($configuration, $configs); } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('name')) { + $suggestions->suggestValues($this->getAvailableBundles(!preg_match('/^[A-Z]/', $input->getCompletionValue()))); + + return; + } + + if ($input->mustSuggestArgumentValuesFor('path') && null !== $name = $input->getArgument('name')) { + try { + $config = $this->getConfig($this->findExtension($name), $this->compileContainer()); + $paths = array_keys(self::buildPathsCompletion($config)); + $suggestions->suggestValues($paths); + } catch (LogicException $e) { + } + } + } + + private function getAvailableBundles(bool $alias): array + { + $availableBundles = []; + foreach ($this->getApplication()->getKernel()->getBundles() as $bundle) { + $availableBundles[] = $alias ? $bundle->getContainerExtension()->getAlias() : $bundle->getName(); + } + + return $availableBundles; + } + + private function getConfig(ExtensionInterface $extension, ContainerBuilder $container) + { + return $container->resolveEnvPlaceholders( + $container->getParameterBag()->resolveValue( + $this->getConfigForExtension($extension, $container) + ) + ); + } + + private static function buildPathsCompletion(array $paths, string $prefix = ''): array + { + $completionPaths = []; + foreach ($paths as $key => $values) { + if (\is_array($values)) { + $completionPaths = $completionPaths + self::buildPathsCompletion($values, $prefix.$key.'.'); + } else { + $completionPaths[$prefix.$key] = null; + } + } + + return $completionPaths; + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDumpReferenceCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDumpReferenceCommand.php index 0730c87ae5042..7a56ec5abed48 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDumpReferenceCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDumpReferenceCommand.php @@ -14,6 +14,8 @@ use Symfony\Component\Config\Definition\ConfigurationInterface; use Symfony\Component\Config\Definition\Dumper\XmlReferenceDumper; use Symfony\Component\Config\Definition\Dumper\YamlReferenceDumper; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -157,4 +159,32 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 0; } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('name')) { + $suggestions->suggestValues($this->getAvailableBundles()); + } + + if ($input->mustSuggestOptionValuesFor('format')) { + $suggestions->suggestValues($this->getAvailableFormatOptions()); + } + } + + private function getAvailableBundles(): array + { + $bundles = []; + + foreach ($this->getApplication()->getKernel()->getBundles() as $bundle) { + $bundles[] = $bundle->getName(); + $bundles[] = $bundle->getContainerExtension()->getAlias(); + } + + return $bundles; + } + + private function getAvailableFormatOptions(): array + { + return ['yaml', 'xml']; + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerDebugCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerDebugCommand.php index cfc46f109c240..8dfebe4ae86f1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerDebugCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerDebugCommand.php @@ -13,6 +13,8 @@ use Symfony\Bundle\FrameworkBundle\Console\Helper\DescriptorHelper; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -190,6 +192,44 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 0; } + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestOptionValuesFor('format')) { + $helper = new DescriptorHelper(); + $suggestions->suggestValues($helper->getFormats()); + + return; + } + + $kernel = $this->getApplication()->getKernel(); + $object = $this->getContainerBuilder($kernel); + + if ($input->mustSuggestArgumentValuesFor('name') + && !$input->getOption('tag') && !$input->getOption('tags') + && !$input->getOption('parameter') && !$input->getOption('parameters') + && !$input->getOption('env-var') && !$input->getOption('env-vars') + && !$input->getOption('types') && !$input->getOption('deprecations') + ) { + $suggestions->suggestValues($this->findServiceIdsContaining( + $object, + $input->getCompletionValue(), + (bool) $input->getOption('show-hidden') + )); + + return; + } + + if ($input->mustSuggestOptionValuesFor('tag')) { + $suggestions->suggestValues($object->findTags()); + + return; + } + + if ($input->mustSuggestOptionValuesFor('parameter')) { + $suggestions->suggestValues(array_keys($object->getParameterBag()->all())); + } + } + /** * Validates input arguments and options. * @@ -208,9 +248,9 @@ protected function validateInput(InputInterface $input) $name = $input->getArgument('name'); if ((null !== $name) && ($optionsCount > 0)) { - throw new InvalidArgumentException('The options tags, tag, parameters & parameter can not be combined with the service name argument.'); + throw new InvalidArgumentException('The options tags, tag, parameters & parameter cannot be combined with the service name argument.'); } elseif ((null === $name) && $optionsCount > 1) { - throw new InvalidArgumentException('The options tags, tag, parameters & parameter can not be combined together.'); + throw new InvalidArgumentException('The options tags, tag, parameters & parameter cannot be combined together.'); } } @@ -245,7 +285,7 @@ private function findServiceIdsContaining(ContainerBuilder $builder, string $nam if (false !== stripos(str_replace('\\', '', $serviceId), $name)) { $foundServiceIdsIgnoringBackslashes[] = $serviceId; } - if (false !== stripos($serviceId, $name)) { + if ('' === $name || false !== stripos($serviceId, $name)) { $foundServiceIds[] = $serviceId; } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/DebugAutowiringCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/DebugAutowiringCommand.php index 7d0c5f0092513..e1e3c95341de3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/DebugAutowiringCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/DebugAutowiringCommand.php @@ -12,6 +12,8 @@ namespace Symfony\Bundle\FrameworkBundle\Command; use Symfony\Bundle\FrameworkBundle\Console\Descriptor\Descriptor; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Formatter\OutputFormatterStyle; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -81,7 +83,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int $serviceIds = array_filter($serviceIds, [$this, 'filterToServiceTypes']); if ($search = $input->getArgument('search')) { - $searchNormalized = preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', '', $search); + $searchNormalized = preg_replace('/[^a-zA-Z0-9\x7f-\xff $]++/', '', $search); + $serviceIds = array_filter($serviceIds, function ($serviceId) use ($searchNormalized) { return false !== stripos(str_replace('\\', '', $serviceId), $searchNormalized) && !str_starts_with($serviceId, '.'); }); @@ -162,4 +165,13 @@ private function getFileLink(string $class): string return (string) $this->fileLinkFormatter->format($r->getFileName(), $r->getStartLine()); } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('search')) { + $builder = $this->getContainerBuilder($this->getApplication()->getKernel()); + + $suggestions->suggestValues(array_filter($builder->getServiceIds(), [$this, 'filterToServiceTypes'])); + } + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/RouterDebugCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/RouterDebugCommand.php index 1ae5835447e1d..454d59cb3832c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/RouterDebugCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/RouterDebugCommand.php @@ -13,6 +13,8 @@ use Symfony\Bundle\FrameworkBundle\Console\Helper\DescriptorHelper; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -131,4 +133,18 @@ private function findRouteNameContaining(string $name, RouteCollection $routes): return $foundRoutesNames; } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('name')) { + $suggestions->suggestValues(array_keys($this->router->getRouteCollection()->all())); + + return; + } + + if ($input->mustSuggestOptionValuesFor('format')) { + $helper = new DescriptorHelper(); + $suggestions->suggestValues($helper->getFormats()); + } + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/RouterMatchCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/RouterMatchCommand.php index 0edaf661e222b..6cceb945dd96f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/RouterMatchCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/RouterMatchCommand.php @@ -18,6 +18,7 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface; use Symfony\Component\Routing\Matcher\TraceableUrlMatcher; use Symfony\Component\Routing\RouterInterface; @@ -36,6 +37,9 @@ class RouterMatchCommand extends Command private $router; private $expressionLanguageProviders; + /** + * @param iterable $expressionLanguageProviders + */ public function __construct(RouterInterface $router, iterable $expressionLanguageProviders = []) { parent::__construct(); diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsRemoveCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsRemoveCommand.php index 504d28beab97a..0451ef300f634 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsRemoveCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsRemoveCommand.php @@ -13,6 +13,8 @@ use Symfony\Bundle\FrameworkBundle\Secrets\AbstractVault; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -80,4 +82,21 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 0; } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if (!$input->mustSuggestArgumentValuesFor('name')) { + return; + } + + $vaultKeys = array_keys($this->vault->list(false)); + if ($input->getOption('local')) { + if (null === $this->localVault) { + return; + } + $vaultKeys = array_intersect($vaultKeys, array_keys($this->localVault->list(false))); + } + + $suggestions->suggestValues($vaultKeys); + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsSetCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsSetCommand.php index 20b898f073cbc..412247da70636 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsSetCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsSetCommand.php @@ -13,6 +13,8 @@ use Symfony\Bundle\FrameworkBundle\Secrets\AbstractVault; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -137,4 +139,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 0; } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('name')) { + $suggestions->suggestValues(array_keys($this->vault->list(false))); + } + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php index f8fa23fd68afc..d56897d76029e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php @@ -12,6 +12,8 @@ namespace Symfony\Bundle\FrameworkBundle\Command; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -56,8 +58,9 @@ class TranslationDebugCommand extends Command private $defaultViewsPath; private $transPaths; private $codePaths; + private $enabledLocales; - public function __construct(TranslatorInterface $translator, TranslationReaderInterface $reader, ExtractorInterface $extractor, string $defaultTransPath = null, string $defaultViewsPath = null, array $transPaths = [], array $codePaths = []) + public function __construct(TranslatorInterface $translator, TranslationReaderInterface $reader, ExtractorInterface $extractor, string $defaultTransPath = null, string $defaultViewsPath = null, array $transPaths = [], array $codePaths = [], array $enabledLocales = []) { parent::__construct(); @@ -68,6 +71,7 @@ public function __construct(TranslatorInterface $translator, TranslationReaderIn $this->defaultViewsPath = $defaultViewsPath; $this->transPaths = $transPaths; $this->codePaths = $codePaths; + $this->enabledLocales = $enabledLocales; } /** @@ -135,15 +139,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int $kernel = $this->getApplication()->getKernel(); // Define Root Paths - $transPaths = $this->transPaths; - if ($this->defaultTransPath) { - $transPaths[] = $this->defaultTransPath; - } - $codePaths = $this->codePaths; - $codePaths[] = $kernel->getProjectDir().'/src'; - if ($this->defaultViewsPath) { - $codePaths[] = $this->defaultViewsPath; - } + $transPaths = $this->getRootTransPaths(); + $codePaths = $this->getRootCodePaths($kernel); // Override with provided Bundle info if (null !== $input->getArgument('bundle')) { @@ -165,7 +162,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $transPaths = [$path.'/translations']; $codePaths = [$path.'/templates']; - if (!is_dir($transPaths[0]) && !isset($transPaths[1])) { + if (!is_dir($transPaths[0])) { throw new InvalidArgumentException(sprintf('"%s" is neither an enabled bundle nor a directory.', $transPaths[0])); } } @@ -259,6 +256,44 @@ protected function execute(InputInterface $input, OutputInterface $output): int return $exitCode; } + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('locale')) { + $suggestions->suggestValues($this->enabledLocales); + + return; + } + + /** @var KernelInterface $kernel */ + $kernel = $this->getApplication()->getKernel(); + + if ($input->mustSuggestArgumentValuesFor('bundle')) { + $availableBundles = []; + foreach ($kernel->getBundles() as $bundle) { + $availableBundles[] = $bundle->getName(); + + if ($extension = $bundle->getContainerExtension()) { + $availableBundles[] = $extension->getAlias(); + } + } + + $suggestions->suggestValues($availableBundles); + + return; + } + + if ($input->mustSuggestOptionValuesFor('domain')) { + $locale = $input->getArgument('locale'); + + $mergeOperation = new MergeOperation( + $this->extractMessages($locale, $this->getRootCodePaths($kernel)), + $this->loadCurrentMessages($locale, $this->getRootTransPaths()) + ); + + $suggestions->suggestValues($mergeOperation->getDomains()); + } + } + private function formatState(int $state): string { if (self::MESSAGE_MISSING === $state) { @@ -354,4 +389,25 @@ private function loadFallbackCatalogues(string $locale, array $transPaths): arra return $fallbackCatalogues; } + + private function getRootTransPaths(): array + { + $transPaths = $this->transPaths; + if ($this->defaultTransPath) { + $transPaths[] = $this->defaultTransPath; + } + + return $transPaths; + } + + private function getRootCodePaths(KernelInterface $kernel): array + { + $codePaths = $this->codePaths; + $codePaths[] = $kernel->getProjectDir().'/src'; + if ($this->defaultViewsPath) { + $codePaths[] = $this->defaultViewsPath; + } + + return $codePaths; + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php index c849538173d0f..de9332615eb97 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php @@ -12,10 +12,13 @@ namespace Symfony\Bundle\FrameworkBundle\Command; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\HttpKernel\KernelInterface; @@ -40,9 +43,13 @@ class TranslationUpdateCommand extends Command private const ASC = 'asc'; private const DESC = 'desc'; private const SORT_ORDERS = [self::ASC, self::DESC]; + private const FORMATS = [ + 'xlf12' => ['xlf', '1.2'], + 'xlf20' => ['xlf', '2.0'], + ]; - protected static $defaultName = 'translation:update'; - protected static $defaultDescription = 'Update the translation file'; + protected static $defaultName = 'translation:extract|translation:update'; + protected static $defaultDescription = 'Extract missing translations keys from code to translation files.'; private $writer; private $reader; @@ -52,8 +59,9 @@ class TranslationUpdateCommand extends Command private $defaultViewsPath; private $transPaths; private $codePaths; + private $enabledLocales; - public function __construct(TranslationWriterInterface $writer, TranslationReaderInterface $reader, ExtractorInterface $extractor, string $defaultLocale, string $defaultTransPath = null, string $defaultViewsPath = null, array $transPaths = [], array $codePaths = []) + public function __construct(TranslationWriterInterface $writer, TranslationReaderInterface $reader, ExtractorInterface $extractor, string $defaultLocale, string $defaultTransPath = null, string $defaultViewsPath = null, array $transPaths = [], array $codePaths = [], array $enabledLocales = []) { parent::__construct(); @@ -65,6 +73,7 @@ public function __construct(TranslationWriterInterface $writer, TranslationReade $this->defaultViewsPath = $defaultViewsPath; $this->transPaths = $transPaths; $this->codePaths = $codePaths; + $this->enabledLocales = $enabledLocales; } /** @@ -80,9 +89,9 @@ protected function configure() new InputOption('output-format', null, InputOption::VALUE_OPTIONAL, 'Override the default output format (deprecated)'), new InputOption('format', null, InputOption::VALUE_OPTIONAL, 'Override the default output format', 'xlf12'), new InputOption('dump-messages', null, InputOption::VALUE_NONE, 'Should the messages be dumped in the console'), - new InputOption('force', null, InputOption::VALUE_NONE, 'Should the update be done'), + new InputOption('force', null, InputOption::VALUE_NONE, 'Should the extract be done'), new InputOption('clean', null, InputOption::VALUE_NONE, 'Should clean not found messages'), - new InputOption('domain', null, InputOption::VALUE_OPTIONAL, 'Specify the domain to update'), + new InputOption('domain', null, InputOption::VALUE_OPTIONAL, 'Specify the domain to extract'), new InputOption('xliff-version', null, InputOption::VALUE_OPTIONAL, 'Override the default xliff version (deprecated)'), new InputOption('sort', null, InputOption::VALUE_OPTIONAL, 'Return list of messages sorted alphabetically', 'asc'), new InputOption('as-tree', null, InputOption::VALUE_OPTIONAL, 'Dump the messages as a tree-like structure: The given value defines the level where to switch to inline YAML'), @@ -126,6 +135,13 @@ protected function configure() */ protected function execute(InputInterface $input, OutputInterface $output): int { + $io = new SymfonyStyle($input, $output); + $errorIo = $output instanceof ConsoleOutputInterface ? new SymfonyStyle($input, $output->getErrorOutput()) : $io; + + if ('translation:update' === $input->getFirstArgument()) { + $errorIo->caution('Command "translation:update" is deprecated since version 5.4 and will be removed in Symfony 6.0. Use "translation:extract" instead.'); + } + $io = new SymfonyStyle($input, $output); $errorIo = $io->getErrorStyle(); @@ -140,17 +156,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int $xliffVersion = $input->getOption('xliff-version') ?? '1.2'; if ($input->getOption('xliff-version')) { - trigger_deprecation('symfony/framework-bundle', '5.3', 'The "--xliff-version" option is deprecated, use "--format=xlf%d" instead.', 10 * $xliffVersion); + $errorIo->warning(sprintf('The "--xliff-version" option is deprecated since version 5.3, use "--format=xlf%d" instead.', 10 * $xliffVersion)); } if ($input->getOption('output-format')) { - trigger_deprecation('symfony/framework-bundle', '5.3', 'The "--output-format" option is deprecated, use "--format=xlf%d" instead.', 10 * $xliffVersion); + $errorIo->warning(sprintf('The "--output-format" option is deprecated since version 5.3, use "--format=xlf%d" instead.', 10 * $xliffVersion)); } - switch ($format) { - case 'xlf20': $xliffVersion = '2.0'; - // no break - case 'xlf12': $format = 'xlf'; + if (\in_array($format, array_keys(self::FORMATS), true)) { + [$format, $xliffVersion] = self::FORMATS[$format]; } // check format @@ -165,15 +179,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int $kernel = $this->getApplication()->getKernel(); // Define Root Paths - $transPaths = $this->transPaths; - if ($this->defaultTransPath) { - $transPaths[] = $this->defaultTransPath; - } - $codePaths = $this->codePaths; - $codePaths[] = $kernel->getProjectDir().'/src'; - if ($this->defaultViewsPath) { - $codePaths[] = $this->defaultViewsPath; - } + $transPaths = $this->getRootTransPaths(); + $codePaths = $this->getRootCodePaths($kernel); + $currentName = 'default directory'; // Override with provided Bundle info @@ -197,7 +205,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $transPaths = [$path.'/translations']; $codePaths = [$path.'/templates']; - if (!is_dir($transPaths[0]) && !isset($transPaths[1])) { + if (!is_dir($transPaths[0])) { throw new InvalidArgumentException(sprintf('"%s" is neither an enabled bundle nor a directory.', $transPaths[0])); } } @@ -206,24 +214,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int $io->title('Translation Messages Extractor and Dumper'); $io->comment(sprintf('Generating "%s" translation files for "%s"', $input->getArgument('locale'), $currentName)); - // load any messages from templates - $extractedCatalogue = new MessageCatalogue($input->getArgument('locale')); $io->comment('Parsing templates...'); - $this->extractor->setPrefix($input->getOption('prefix')); - foreach ($codePaths as $path) { - if (is_dir($path) || is_file($path)) { - $this->extractor->extract($path, $extractedCatalogue); - } - } + $extractedCatalogue = $this->extractMessages($input->getArgument('locale'), $codePaths, $input->getOption('prefix')); - // load any existing messages from the translation files - $currentCatalogue = new MessageCatalogue($input->getArgument('locale')); $io->comment('Loading translation files...'); - foreach ($transPaths as $path) { - if (is_dir($path)) { - $this->reader->read($path, $currentCatalogue); - } - } + $currentCatalogue = $this->loadCurrentMessages($input->getArgument('locale'), $transPaths); if (null !== $domain = $input->getOption('domain')) { $currentCatalogue = $this->filterCatalogue($currentCatalogue, $domain); @@ -321,6 +316,60 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 0; } + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('locale')) { + $suggestions->suggestValues($this->enabledLocales); + + return; + } + + /** @var KernelInterface $kernel */ + $kernel = $this->getApplication()->getKernel(); + if ($input->mustSuggestArgumentValuesFor('bundle')) { + $bundles = []; + + foreach ($kernel->getBundles() as $bundle) { + $bundles[] = $bundle->getName(); + if ($bundle->getContainerExtension()) { + $bundles[] = $bundle->getContainerExtension()->getAlias(); + } + } + + $suggestions->suggestValues($bundles); + + return; + } + + if ($input->mustSuggestOptionValuesFor('format')) { + $suggestions->suggestValues(array_merge( + $this->writer->getFormats(), + array_keys(self::FORMATS) + )); + + return; + } + + if ($input->mustSuggestOptionValuesFor('domain') && $locale = $input->getArgument('locale')) { + $extractedCatalogue = $this->extractMessages($locale, $this->getRootCodePaths($kernel), $input->getOption('prefix')); + + $currentCatalogue = $this->loadCurrentMessages($locale, $this->getRootTransPaths()); + + // process catalogues + $operation = $input->getOption('clean') + ? new TargetOperation($currentCatalogue, $extractedCatalogue) + : new MergeOperation($currentCatalogue, $extractedCatalogue); + + $suggestions->suggestValues($operation->getDomains()); + + return; + } + + if ($input->mustSuggestOptionValuesFor('sort')) { + $suggestions->suggestValues(self::SORT_ORDERS); + } + } + private function filterCatalogue(MessageCatalogue $catalogue, string $domain): MessageCatalogue { $filteredCatalogue = new MessageCatalogue($catalogue->getLocale()); @@ -353,4 +402,50 @@ private function filterCatalogue(MessageCatalogue $catalogue, string $domain): M return $filteredCatalogue; } + + private function extractMessages(string $locale, array $transPaths, string $prefix): MessageCatalogue + { + $extractedCatalogue = new MessageCatalogue($locale); + $this->extractor->setPrefix($prefix); + foreach ($transPaths as $path) { + if (is_dir($path) || is_file($path)) { + $this->extractor->extract($path, $extractedCatalogue); + } + } + + return $extractedCatalogue; + } + + private function loadCurrentMessages(string $locale, array $transPaths): MessageCatalogue + { + $currentCatalogue = new MessageCatalogue($locale); + foreach ($transPaths as $path) { + if (is_dir($path)) { + $this->reader->read($path, $currentCatalogue); + } + } + + return $currentCatalogue; + } + + private function getRootTransPaths(): array + { + $transPaths = $this->transPaths; + if ($this->defaultTransPath) { + $transPaths[] = $this->defaultTransPath; + } + + return $transPaths; + } + + private function getRootCodePaths(KernelInterface $kernel): array + { + $codePaths = $this->codePaths; + $codePaths[] = $kernel->getProjectDir().'/src'; + if ($this->defaultViewsPath) { + $codePaths[] = $this->defaultViewsPath; + } + + return $codePaths; + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/WorkflowDumpCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/WorkflowDumpCommand.php index 1249406f79188..e50b7493308db 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/WorkflowDumpCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/WorkflowDumpCommand.php @@ -12,6 +12,8 @@ namespace Symfony\Bundle\FrameworkBundle\Command; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -32,6 +34,20 @@ class WorkflowDumpCommand extends Command { protected static $defaultName = 'workflow:dump'; protected static $defaultDescription = 'Dump a workflow'; + private $workflows = []; + + private const DUMP_FORMAT_OPTIONS = [ + 'puml', + 'mermaid', + 'dot', + ]; + + public function __construct(array $workflows) + { + parent::__construct(); + + $this->workflows = $workflows; + } /** * {@inheritdoc} @@ -43,7 +59,7 @@ protected function configure() new InputArgument('name', InputArgument::REQUIRED, 'A workflow name'), new InputArgument('marking', InputArgument::IS_ARRAY, 'A marking (a list of places)'), new InputOption('label', 'l', InputOption::VALUE_REQUIRED, 'Label a graph'), - new InputOption('dump-format', null, InputOption::VALUE_REQUIRED, 'The dump format [dot|puml]', 'dot'), + new InputOption('dump-format', null, InputOption::VALUE_REQUIRED, 'The dump format ['.implode('|', self::DUMP_FORMAT_OPTIONS).']', 'dot'), ]) ->setDescription(self::$defaultDescription) ->setHelp(<<<'EOF' @@ -63,19 +79,14 @@ protected function configure() */ protected function execute(InputInterface $input, OutputInterface $output): int { - $container = $this->getApplication()->getKernel()->getContainer(); - $serviceId = $input->getArgument('name'); - - if ($container->has('workflow.'.$serviceId)) { - $workflow = $container->get('workflow.'.$serviceId); - $type = 'workflow'; - } elseif ($container->has('state_machine.'.$serviceId)) { - $workflow = $container->get('state_machine.'.$serviceId); - $type = 'state_machine'; - } else { - throw new InvalidArgumentException(sprintf('No service found for "workflow.%1$s" nor "state_machine.%1$s".', $serviceId)); + $workflowId = $input->getArgument('name'); + + if (!\in_array($workflowId, array_keys($this->workflows), true)) { + throw new InvalidArgumentException(sprintf('No service found for "workflow.%1$s" nor "state_machine.%1$s".', $workflowId)); } + $type = explode('.', $workflowId)[0]; + switch ($input->getOption('dump-format')) { case 'puml': $transitionType = 'workflow' === $type ? PlantUmlDumper::WORKFLOW_TRANSITION : PlantUmlDumper::STATEMACHINE_TRANSITION; @@ -98,15 +109,28 @@ protected function execute(InputInterface $input, OutputInterface $output): int $marking->mark($place); } + $workflow = $this->workflows[$workflowId]; + $options = [ - 'name' => $serviceId, + 'name' => $workflowId, 'nofooter' => true, 'graph' => [ 'label' => $input->getOption('label'), ], ]; - $output->writeln($dumper->dump($workflow->getDefinition(), $marking, $options)); + $output->writeln($dumper->dump($workflow, $marking, $options)); return 0; } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('name')) { + $suggestions->suggestValues(array_keys($this->workflows)); + } + + if ($input->mustSuggestOptionValuesFor('dump-format')) { + $suggestions->suggestValues(self::DUMP_FORMAT_OPTIONS); + } + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Application.php b/src/Symfony/Bundle/FrameworkBundle/Console/Application.php index 490d8cbb61f3e..7fe7bc937d315 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Application.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Application.php @@ -47,7 +47,7 @@ public function __construct(KernelInterface $kernel) /** * Gets the Kernel associated with this Console. * - * @return KernelInterface A KernelInterface instance + * @return KernelInterface */ public function getKernel() { diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php b/src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php index 81b26b47998d3..40b394607cdf2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php @@ -50,7 +50,7 @@ use Twig\Environment; /** - * Provides common features needed in controllers. + * Provides shortcuts for HTTP-related features in controllers. * * @author Fabien Potencier */ @@ -62,7 +62,6 @@ abstract class AbstractController implements ServiceSubscriberInterface protected $container; /** - * @internal * @required */ public function setContainer(ContainerInterface $container): ?ContainerInterface @@ -97,21 +96,25 @@ public static function getSubscribedServices() 'session' => '?'.SessionInterface::class, 'security.authorization_checker' => '?'.AuthorizationCheckerInterface::class, 'twig' => '?'.Environment::class, - 'doctrine' => '?'.ManagerRegistry::class, + 'doctrine' => '?'.ManagerRegistry::class, // to be removed in 6.0 'form.factory' => '?'.FormFactoryInterface::class, 'security.token_storage' => '?'.TokenStorageInterface::class, 'security.csrf.token_manager' => '?'.CsrfTokenManagerInterface::class, 'parameter_bag' => '?'.ContainerBagInterface::class, - 'message_bus' => '?'.MessageBusInterface::class, - 'messenger.default_bus' => '?'.MessageBusInterface::class, + 'message_bus' => '?'.MessageBusInterface::class, // to be removed in 6.0 + 'messenger.default_bus' => '?'.MessageBusInterface::class, // to be removed in 6.0 ]; } /** * Returns true if the service id is defined. + * + * @deprecated since Symfony 5.4, use method or constructor injection in your controller instead */ protected function has(string $id): bool { + trigger_deprecation('symfony/framework-bundle', '5.4', 'Method "%s()" is deprecated, use method or constructor injection in your controller instead.', __METHOD__); + return $this->container->has($id); } @@ -119,9 +122,13 @@ protected function has(string $id): bool * Gets a container service by its id. * * @return object The service + * + * @deprecated since Symfony 5.4, use method or constructor injection in your controller instead */ protected function get(string $id): object { + trigger_deprecation('symfony/framework-bundle', '5.4', 'Method "%s()" is deprecated, use method or constructor injection in your controller instead.', __METHOD__); + return $this->container->get($id); } @@ -204,7 +211,7 @@ protected function addFlash(string $type, $message): void try { $this->container->get('request_stack')->getSession()->getFlashBag()->add($type, $message); } catch (SessionNotFoundException $e) { - throw new \LogicException('You can not use the addFlash method if sessions are disabled. Enable them in "config/packages/framework.yaml".', 0, $e); + throw new \LogicException('You cannot use the addFlash method if sessions are disabled. Enable them in "config/packages/framework.yaml".', 0, $e); } } @@ -245,7 +252,7 @@ protected function denyAccessUnlessGranted($attribute, $subject = null, string $ protected function renderView(string $view, array $parameters = []): string { if (!$this->container->has('twig')) { - throw new \LogicException('You can not use the "renderView" method if the Twig Bundle is not available. Try running "composer require symfony/twig-bundle".'); + throw new \LogicException('You cannot use the "renderView" method if the Twig Bundle is not available. Try running "composer require symfony/twig-bundle".'); } return $this->container->get('twig')->render($view, $parameters); @@ -303,7 +310,7 @@ protected function renderForm(string $view, array $parameters = [], Response $re protected function stream(string $view, array $parameters = [], StreamedResponse $response = null): StreamedResponse { if (!$this->container->has('twig')) { - throw new \LogicException('You can not use the "stream" method if the Twig Bundle is not available. Try running "composer require symfony/twig-bundle".'); + throw new \LogicException('You cannot use the "stream" method if the Twig Bundle is not available. Try running "composer require symfony/twig-bundle".'); } $twig = $this->container->get('twig'); @@ -345,7 +352,7 @@ protected function createNotFoundException(string $message = 'Not Found', \Throw protected function createAccessDeniedException(string $message = 'Access Denied.', \Throwable $previous = null): AccessDeniedException { if (!class_exists(AccessDeniedException::class)) { - throw new \LogicException('You can not use the "createAccessDeniedException" method if the Security component is not available. Try running "composer require symfony/security-bundle".'); + throw new \LogicException('You cannot use the "createAccessDeniedException" method if the Security component is not available. Try running "composer require symfony/security-bundle".'); } return new AccessDeniedException($message, $previous); @@ -371,9 +378,13 @@ protected function createFormBuilder($data = null, array $options = []): FormBui * Shortcut to return the Doctrine Registry service. * * @throws \LogicException If DoctrineBundle is not available + * + * @deprecated since Symfony 5.4, inject an instance of ManagerRegistry in your controller instead */ protected function getDoctrine(): ManagerRegistry { + trigger_deprecation('symfony/framework-bundle', '5.4', 'Method "%s()" is deprecated, inject an instance of ManagerRegistry in your controller instead.', __METHOD__); + if (!$this->container->has('doctrine')) { throw new \LogicException('The DoctrineBundle is not registered in your application. Try running "composer require symfony/orm-pack".'); } @@ -384,7 +395,7 @@ protected function getDoctrine(): ManagerRegistry /** * Get a user from the Security Token Storage. * - * @return UserInterface|object|null + * @return UserInterface|null * * @throws \LogicException If SecurityBundle is not available * @@ -400,6 +411,7 @@ protected function getUser() return null; } + // @deprecated since 5.4, $user will always be a UserInterface instance if (!\is_object($user = $token->getUser())) { // e.g. anonymous authentication return null; @@ -427,9 +439,13 @@ protected function isCsrfTokenValid(string $id, ?string $token): bool * Dispatches a message to the bus. * * @param object|Envelope $message The message or the message pre-wrapped in an envelope + * + * @deprecated since Symfony 5.4, inject an instance of MessageBusInterface in your controller instead */ protected function dispatchMessage(object $message, array $stamps = []): Envelope { + trigger_deprecation('symfony/framework-bundle', '5.4', 'Method "%s()" is deprecated, inject an instance of MessageBusInterface in your controller instead.', __METHOD__); + if (!$this->container->has('messenger.default_bus')) { $message = class_exists(Envelope::class) ? 'You need to define the "messenger.default_bus" configuration option.' : 'Try running "composer require symfony/messenger".'; throw new \LogicException('The message bus is not enabled in your application. '.$message); @@ -446,7 +462,7 @@ protected function dispatchMessage(object $message, array $stamps = []): Envelop protected function addLink(Request $request, LinkInterface $link): void { if (!class_exists(AddLinkHeaderListener::class)) { - throw new \LogicException('You can not use the "addLink" method if the WebLink component is not available. Try running "composer require symfony/web-link".'); + throw new \LogicException('You cannot use the "addLink" method if the WebLink component is not available. Try running "composer require symfony/web-link".'); } if (null === $linkProvider = $request->attributes->get('_links')) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/TemplateController.php b/src/Symfony/Bundle/FrameworkBundle/Controller/TemplateController.php index ebb6b56f8e410..2283dbc91fccf 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/TemplateController.php +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/TemplateController.php @@ -33,19 +33,20 @@ public function __construct(Environment $twig = null) /** * Renders a template. * - * @param string $template The template name - * @param int|null $maxAge Max age for client caching - * @param int|null $sharedAge Max age for shared (proxy) caching - * @param bool|null $private Whether or not caching should apply for client caches only - * @param array $context The context (arguments) of the template + * @param string $template The template name + * @param int|null $maxAge Max age for client caching + * @param int|null $sharedAge Max age for shared (proxy) caching + * @param bool|null $private Whether or not caching should apply for client caches only + * @param array $context The context (arguments) of the template + * @param int $statusCode The HTTP status code to return with the response. Defaults to 200 */ - public function templateAction(string $template, int $maxAge = null, int $sharedAge = null, bool $private = null, array $context = []): Response + public function templateAction(string $template, int $maxAge = null, int $sharedAge = null, bool $private = null, array $context = [], int $statusCode = 200): Response { if (null === $this->twig) { - throw new \LogicException('You can not use the TemplateController if the Twig Bundle is not available.'); + throw new \LogicException('You cannot use the TemplateController if the Twig Bundle is not available.'); } - $response = new Response($this->twig->render($template, $context)); + $response = new Response($this->twig->render($template, $context), $statusCode); if ($maxAge) { $response->setMaxAge($maxAge); @@ -64,8 +65,8 @@ public function templateAction(string $template, int $maxAge = null, int $shared return $response; } - public function __invoke(string $template, int $maxAge = null, int $sharedAge = null, bool $private = null, array $context = []): Response + public function __invoke(string $template, int $maxAge = null, int $sharedAge = null, bool $private = null, array $context = [], int $statusCode = 200): Response { - return $this->templateAction($template, $maxAge, $sharedAge, $private, $context); + return $this->templateAction($template, $maxAge, $sharedAge, $private, $context, $statusCode); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php index 392a95ce64b5f..a556599e76d0c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php @@ -21,7 +21,7 @@ */ class UnusedTagsPass implements CompilerPassInterface { - private $knownTags = [ + private const KNOWN_TAGS = [ 'annotations.cached_reader', 'assets.package', 'auto_alias', @@ -74,10 +74,10 @@ class UnusedTagsPass implements CompilerPassInterface 'routing.expression_language_provider', 'routing.loader', 'routing.route_loader', + 'security.authenticator.login_linker', 'security.expression_language_provider', 'security.remember_me_aware', 'security.remember_me_handler', - 'security.authenticator.login_linker', 'security.voter', 'serializer.encoder', 'serializer.normalizer', @@ -96,11 +96,11 @@ class UnusedTagsPass implements CompilerPassInterface public function process(ContainerBuilder $container) { - $tags = array_unique(array_merge($container->findTags(), $this->knownTags)); + $tags = array_unique(array_merge($container->findTags(), self::KNOWN_TAGS)); foreach ($container->findUnusedTags() as $tag) { // skip known tags - if (\in_array($tag, $this->knownTags)) { + if (\in_array($tag, self::KNOWN_TAGS)) { continue; } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 68b6db95ee874..76955b05d566f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -15,6 +15,7 @@ use Doctrine\Common\Annotations\PsrCachedReader; use Doctrine\Common\Cache\Cache; use Doctrine\DBAL\Connection; +use Psr\Log\LogLevel; use Symfony\Bundle\FullStack; use Symfony\Component\Asset\Package; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; @@ -60,7 +61,7 @@ public function __construct(bool $debug) /** * Generates the configuration tree builder. * - * @return TreeBuilder The tree builder + * @return TreeBuilder */ public function getConfigTreeBuilder() { @@ -76,6 +77,7 @@ public function getConfigTreeBuilder() return $v; }) ->end() + ->fixXmlConfig('enabled_locale') ->children() ->scalarNode('secret')->end() ->scalarNode('http_method_override') @@ -85,6 +87,18 @@ public function getConfigTreeBuilder() ->scalarNode('ide')->defaultNull()->end() ->booleanNode('test')->end() ->scalarNode('default_locale')->defaultValue('en')->end() + ->booleanNode('set_locale_from_accept_language') + ->info('Whether to use the Accept-Language HTTP header to set the Request locale (only when the "_locale" request attribute is not passed).') + ->defaultFalse() + ->end() + ->booleanNode('set_content_language_from_locale') + ->info('Whether to set the Content-Language HTTP header on the Response using the Request locale.') + ->defaultFalse() + ->end() + ->arrayNode('enabled_locales') + ->info('Defines the possible locales for the application. This list is used for generating translations files, but also to restrict which locales are allowed when it is set from Accept-Language header (using "set_locale_from_accept_language").') + ->prototype('scalar')->end() + ->end() ->arrayNode('trusted_hosts') ->beforeNormalization()->ifString()->then(function ($v) { return [$v]; })->end() ->prototype('scalar')->end() @@ -112,7 +126,7 @@ public function getConfigTreeBuilder() $parentPackages = (array) $parentPackage; $parentPackages[] = 'symfony/framework-bundle'; - return ContainerBuilder::willBeAvailable($package, $class, $parentPackages); + return ContainerBuilder::willBeAvailable($package, $class, $parentPackages, true); }; $enableIfStandalone = static function (string $package, string $class) use ($willBeAvailable) { @@ -139,6 +153,7 @@ public function getConfigTreeBuilder() $this->addPropertyInfoSection($rootNode, $enableIfStandalone); $this->addCacheSection($rootNode, $willBeAvailable); $this->addPhpErrorsSection($rootNode); + $this->addExceptionsSection($rootNode); $this->addWebLinkSection($rootNode, $enableIfStandalone); $this->addLockSection($rootNode, $enableIfStandalone); $this->addMessengerSection($rootNode, $enableIfStandalone); @@ -300,6 +315,7 @@ private function addProfilerSection(ArrayNodeDefinition $rootNode) ->canBeEnabled() ->children() ->booleanNode('collect')->defaultTrue()->end() + ->scalarNode('collect_parameter')->defaultNull()->info('The name of the parameter to use to enable or disable collection on a per request basis')->end() ->booleanNode('only_exceptions')->defaultFalse()->end() ->booleanNode('only_main_requests')->defaultFalse()->end() ->booleanNode('only_master_requests')->setDeprecated('symfony/framework-bundle', '5.3', 'Option "%node%" at "%path%" is deprecated, use "only_main_requests" instead.')->defaultFalse()->end() @@ -696,6 +712,10 @@ private function addAssetsSection(ArrayNodeDefinition $rootNode, callable $enabl ->{$enableIfStandalone('symfony/asset', Package::class)}() ->fixXmlConfig('base_url') ->children() + ->booleanNode('strict_mode') + ->info('Throw an exception if an entry is missing from the manifest.json') + ->defaultFalse() + ->end() ->scalarNode('version_strategy')->defaultNull()->end() ->scalarNode('version')->defaultNull()->end() ->scalarNode('version_format')->defaultValue('%%s?%%s')->end() @@ -733,6 +753,10 @@ private function addAssetsSection(ArrayNodeDefinition $rootNode, callable $enabl ->prototype('array') ->fixXmlConfig('base_url') ->children() + ->booleanNode('strict_mode') + ->info('Throw an exception if an entry is missing from the manifest.json') + ->defaultFalse() + ->end() ->scalarNode('version_strategy')->defaultNull()->end() ->scalarNode('version') ->beforeNormalization() @@ -804,6 +828,7 @@ private function addTranslatorSection(ArrayNodeDefinition $rootNode, callable $e ->prototype('scalar')->end() ->end() ->arrayNode('enabled_locales') + ->setDeprecated('symfony/framework-bundle', '5.3', 'Option "%node%" at "%path%" is deprecated, set the "framework.enabled_locales" option instead.') ->prototype('scalar')->end() ->defaultValue([]) ->end() @@ -838,7 +863,7 @@ private function addTranslatorSection(ArrayNodeDefinition $rootNode, callable $e ->arrayNode('locales') ->prototype('scalar')->end() ->defaultValue([]) - ->info('If not set, all locales listed under framework.translator.enabled_locales are used.') + ->info('If not set, all locales listed under framework.enabled_locales are used.') ->end() ->end() ->end() @@ -981,6 +1006,12 @@ private function addSerializerSection(ArrayNodeDefinition $rootNode, callable $e ->end() ->end() ->end() + ->arrayNode('default_context') + ->normalizeKeys(false) + ->useAttributeAsKey('name') + ->defaultValue([]) + ->prototype('variable')->end() + ->end() ->end() ->end() ->end() @@ -1041,7 +1072,7 @@ private function addCacheSection(ArrayNodeDefinition $rootNode, callable $willBe ->info('System related cache pools configuration') ->defaultValue('cache.adapter.system') ->end() - ->scalarNode('directory')->defaultValue('%kernel.cache_dir%/pools')->end() + ->scalarNode('directory')->defaultValue('%kernel.cache_dir%/pools/app')->end() ->scalarNode('default_doctrine_provider')->end() ->scalarNode('default_psr6_provider')->end() ->scalarNode('default_redis_provider')->defaultValue('redis://localhost')->end() @@ -1155,6 +1186,64 @@ private function addPhpErrorsSection(ArrayNodeDefinition $rootNode) ; } + private function addExceptionsSection(ArrayNodeDefinition $rootNode) + { + $logLevels = (new \ReflectionClass(LogLevel::class))->getConstants(); + + $rootNode + ->children() + ->arrayNode('exceptions') + ->info('Exception handling configuration') + ->beforeNormalization() + ->ifArray() + ->then(function (array $v): array { + if (!\array_key_exists('exception', $v)) { + return $v; + } + + // Fix XML normalization + $data = isset($v['exception'][0]) ? $v['exception'] : [$v['exception']]; + $exceptions = []; + foreach ($data as $exception) { + $config = []; + if (\array_key_exists('log-level', $exception)) { + $config['log_level'] = $exception['log-level']; + } + if (\array_key_exists('status-code', $exception)) { + $config['status_code'] = $exception['status-code']; + } + $exceptions[$exception['name']] = $config; + } + + return $exceptions; + }) + ->end() + ->prototype('array') + ->fixXmlConfig('exception') + ->children() + ->scalarNode('log_level') + ->info('The level of log message. Null to let Symfony decide.') + ->validate() + ->ifTrue(function ($v) use ($logLevels) { return !\in_array($v, $logLevels); }) + ->thenInvalid(sprintf('The log level is not valid. Pick one among "%s".', implode('", "', $logLevels))) + ->end() + ->defaultNull() + ->end() + ->scalarNode('status_code') + ->info('The status code of the response. Null to let Symfony decide.') + ->validate() + ->ifTrue(function ($v) { return $v < 100 || $v > 599; }) + ->thenInvalid('The log level is not valid. Pick a value between 100 and 599.') + ->end() + ->defaultNull() + ->end() + ->end() + ->end() + ->end() + ->end() + ; + } + private function addLockSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone) { $rootNode @@ -1194,15 +1283,13 @@ private function addLockSection(ArrayNodeDefinition $rootNode, callable $enableI ->then(function ($v) { $resources = []; foreach ($v as $resource) { - $resources = array_merge_recursive( - $resources, - \is_array($resource) && isset($resource['name']) - ? [$resource['name'] => $resource['value']] - : ['default' => $resource] - ); + $resources[] = \is_array($resource) && isset($resource['name']) + ? [$resource['name'] => $resource['value']] + : ['default' => $resource] + ; } - return $resources; + return array_merge_recursive([], ...$resources); }) ->end() ->prototype('array') @@ -1362,6 +1449,10 @@ function ($a) { ->defaultNull() ->info('Transport name to send failed messages to (after all retries have failed).') ->end() + ->booleanNode('reset_on_message') + ->defaultNull() + ->info('Reset container services after each message.') + ->end() ->scalarNode('default_bus')->defaultNull()->end() ->arrayNode('buses') ->defaultValue(['messenger.bus.default' => ['default_middleware' => true, 'middleware' => []]]) diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 669a5a9ff5dcb..12a7441895401 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -11,10 +11,12 @@ namespace Symfony\Bundle\FrameworkBundle\DependencyInjection; +use Composer\InstalledVersions; use Doctrine\Common\Annotations\AnnotationRegistry; use Doctrine\Common\Annotations\Reader; use Http\Client\HttpClient; use phpDocumentor\Reflection\DocBlockFactoryInterface; +use PHPStan\PhpDocParser\Parser\PhpDocParser; use Psr\Cache\CacheItemPoolInterface; use Psr\Container\ContainerInterface as PsrContainerInterface; use Psr\EventDispatcher\EventDispatcherInterface as PsrEventDispatcherInterface; @@ -58,6 +60,7 @@ use Symfony\Component\DependencyInjection\Parameter; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\ServiceLocator; +use Symfony\Component\Dotenv\Command\DebugCommand; use Symfony\Component\EventDispatcher\Attribute\AsEventListener; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; @@ -90,11 +93,13 @@ use Symfony\Component\Mailer\Bridge\Mailchimp\Transport\MandrillTransportFactory; use Symfony\Component\Mailer\Bridge\Mailgun\Transport\MailgunTransportFactory; use Symfony\Component\Mailer\Bridge\Mailjet\Transport\MailjetTransportFactory; +use Symfony\Component\Mailer\Bridge\OhMySmtp\Transport\OhMySmtpTransportFactory; use Symfony\Component\Mailer\Bridge\Postmark\Transport\PostmarkTransportFactory; use Symfony\Component\Mailer\Bridge\Sendgrid\Transport\SendgridTransportFactory; use Symfony\Component\Mailer\Bridge\Sendinblue\Transport\SendinblueTransportFactory; use Symfony\Component\Mailer\Mailer; use Symfony\Component\Mercure\HubRegistry; +use Symfony\Component\Messenger\Attribute\AsMessageHandler; use Symfony\Component\Messenger\Bridge\AmazonSqs\Transport\AmazonSqsTransportFactory; use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpTransportFactory; use Symfony\Component\Messenger\Bridge\Beanstalkd\Transport\BeanstalkdTransportFactory; @@ -110,9 +115,11 @@ use Symfony\Component\Mime\MimeTypeGuesserInterface; use Symfony\Component\Mime\MimeTypes; use Symfony\Component\Notifier\Bridge\AllMySms\AllMySmsTransportFactory; +use Symfony\Component\Notifier\Bridge\AmazonSns\AmazonSnsTransportFactory; use Symfony\Component\Notifier\Bridge\Clickatell\ClickatellTransportFactory; use Symfony\Component\Notifier\Bridge\Discord\DiscordTransportFactory; use Symfony\Component\Notifier\Bridge\Esendex\EsendexTransportFactory; +use Symfony\Component\Notifier\Bridge\Expo\ExpoTransportFactory; use Symfony\Component\Notifier\Bridge\FakeChat\FakeChatTransportFactory; use Symfony\Component\Notifier\Bridge\FakeSms\FakeSmsTransportFactory; use Symfony\Component\Notifier\Bridge\Firebase\FirebaseTransportFactory; @@ -124,27 +131,37 @@ use Symfony\Component\Notifier\Bridge\Iqsms\IqsmsTransportFactory; use Symfony\Component\Notifier\Bridge\LightSms\LightSmsTransportFactory; use Symfony\Component\Notifier\Bridge\LinkedIn\LinkedInTransportFactory; +use Symfony\Component\Notifier\Bridge\Mailjet\MailjetTransportFactory as MailjetNotifierTransportFactory; use Symfony\Component\Notifier\Bridge\Mattermost\MattermostTransportFactory; use Symfony\Component\Notifier\Bridge\Mercure\MercureTransportFactory; use Symfony\Component\Notifier\Bridge\MessageBird\MessageBirdTransport; +use Symfony\Component\Notifier\Bridge\MessageMedia\MessageMediaTransportFactory; use Symfony\Component\Notifier\Bridge\MicrosoftTeams\MicrosoftTeamsTransportFactory; use Symfony\Component\Notifier\Bridge\Mobyt\MobytTransportFactory; use Symfony\Component\Notifier\Bridge\Nexmo\NexmoTransportFactory; use Symfony\Component\Notifier\Bridge\Octopush\OctopushTransportFactory; +use Symfony\Component\Notifier\Bridge\OneSignal\OneSignalTransportFactory; use Symfony\Component\Notifier\Bridge\OvhCloud\OvhCloudTransportFactory; use Symfony\Component\Notifier\Bridge\RocketChat\RocketChatTransportFactory; use Symfony\Component\Notifier\Bridge\Sendinblue\SendinblueTransportFactory as SendinblueNotifierTransportFactory; use Symfony\Component\Notifier\Bridge\Sinch\SinchTransportFactory; use Symfony\Component\Notifier\Bridge\Slack\SlackTransportFactory; +use Symfony\Component\Notifier\Bridge\Sms77\Sms77TransportFactory; use Symfony\Component\Notifier\Bridge\Smsapi\SmsapiTransportFactory; use Symfony\Component\Notifier\Bridge\SmsBiuras\SmsBiurasTransportFactory; +use Symfony\Component\Notifier\Bridge\Smsc\SmscTransportFactory; use Symfony\Component\Notifier\Bridge\SpotHit\SpotHitTransportFactory; use Symfony\Component\Notifier\Bridge\Telegram\TelegramTransportFactory; +use Symfony\Component\Notifier\Bridge\Telnyx\TelnyxTransportFactory; +use Symfony\Component\Notifier\Bridge\TurboSms\TurboSmsTransport; use Symfony\Component\Notifier\Bridge\Twilio\TwilioTransportFactory; +use Symfony\Component\Notifier\Bridge\Yunpian\YunpianTransportFactory; use Symfony\Component\Notifier\Bridge\Zulip\ZulipTransportFactory; use Symfony\Component\Notifier\Notifier; use Symfony\Component\Notifier\Recipient\Recipient; +use Symfony\Component\Notifier\Transport\TransportFactoryInterface as NotifierTransportFactoryInterface; use Symfony\Component\PropertyAccess\PropertyAccessor; +use Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor; use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface; use Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface; use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface; @@ -220,6 +237,10 @@ class FrameworkExtension extends Extension */ public function load(array $configs, ContainerBuilder $container) { + if (!class_exists(InstalledVersions::class)) { + trigger_deprecation('symfony/framework-bundle', '5.4', 'Configuring Symfony without the Composer Runtime API is deprecated. Consider upgrading to Composer 2.1 or later.'); + } + $loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__).'/Resources/config')); $loader->load('web.php'); @@ -227,13 +248,13 @@ public function load(array $configs, ContainerBuilder $container) $loader->load('fragment_renderer.php'); $loader->load('error_renderer.php'); - if (ContainerBuilder::willBeAvailable('psr/event-dispatcher', PsrEventDispatcherInterface::class, ['symfony/framework-bundle'])) { + if (ContainerBuilder::willBeAvailable('psr/event-dispatcher', PsrEventDispatcherInterface::class, ['symfony/framework-bundle'], true)) { $container->setAlias(PsrEventDispatcherInterface::class, 'event_dispatcher'); } $container->registerAliasForArgument('parameter_bag', PsrContainerInterface::class); - if (class_exists(Application::class)) { + if ($this->hasConsole()) { $loader->load('console.php'); if (!class_exists(BaseXliffLintCommand::class)) { @@ -242,6 +263,10 @@ public function load(array $configs, ContainerBuilder $container) if (!class_exists(BaseYamlLintCommand::class)) { $container->removeDefinition('console.command.yaml_lint'); } + + if (!class_exists(DebugCommand::class)) { + $container->removeDefinition('console.command.dotenv_debug'); + } } // Load Cache configuration first as it is used by other components @@ -266,12 +291,15 @@ public function load(array $configs, ContainerBuilder $container) } } + $container->getDefinition('locale_listener')->replaceArgument(3, $config['set_locale_from_accept_language']); + $container->getDefinition('response_listener')->replaceArgument(1, $config['set_content_language_from_locale']); + // If the slugger is used but the String component is not available, we should throw an error - if (!ContainerBuilder::willBeAvailable('symfony/string', SluggerInterface::class, ['symfony/framework-bundle'])) { + if (!ContainerBuilder::willBeAvailable('symfony/string', SluggerInterface::class, ['symfony/framework-bundle'], true)) { $container->register('slugger', 'stdClass') ->addError('You cannot use the "slugger" service since the String component is not installed. Try running "composer require symfony/string".'); } else { - if (!ContainerBuilder::willBeAvailable('symfony/translation', LocaleAwareInterface::class, ['symfony/framework-bundle'])) { + if (!ContainerBuilder::willBeAvailable('symfony/translation', LocaleAwareInterface::class, ['symfony/framework-bundle'], true)) { $container->register('slugger', 'stdClass') ->addError('You cannot use the "slugger" service since the Translation contracts are not installed. Try running "composer require symfony/translation".'); } @@ -288,6 +316,7 @@ public function load(array $configs, ContainerBuilder $container) $container->setParameter('kernel.http_method_override', $config['http_method_override']); $container->setParameter('kernel.trusted_hosts', $config['trusted_hosts']); $container->setParameter('kernel.default_locale', $config['default_locale']); + $container->setParameter('kernel.enabled_locales', $config['enabled_locales']); $container->setParameter('kernel.error_controller', $config['error_controller']); if (($config['trusted_proxies'] ?? false) && ($config['trusted_headers'] ?? false)) { @@ -317,9 +346,8 @@ public function load(array $configs, ContainerBuilder $container) $this->sessionConfigEnabled = true; $this->registerSessionConfiguration($config['session'], $container, $loader); - if (!empty($config['test'])) { - $container->getDefinition('test.session.listener')->setArgument(1, '%session.storage.options%'); - } + } elseif (!empty($config['test'])) { + $container->removeDefinition('test.session.listener'); } if ($this->isConfigEnabled($container, $config['request'])) { @@ -327,7 +355,7 @@ public function load(array $configs, ContainerBuilder $container) } if (null === $config['csrf_protection']['enabled']) { - $config['csrf_protection']['enabled'] = $this->sessionConfigEnabled && !class_exists(FullStack::class) && ContainerBuilder::willBeAvailable('symfony/security-csrf', CsrfTokenManagerInterface::class, ['symfony/framework-bundle']); + $config['csrf_protection']['enabled'] = $this->sessionConfigEnabled && !class_exists(FullStack::class) && ContainerBuilder::willBeAvailable('symfony/security-csrf', CsrfTokenManagerInterface::class, ['symfony/framework-bundle'], true); } $this->registerSecurityCsrfConfiguration($config['csrf_protection'], $container, $loader); @@ -339,7 +367,7 @@ public function load(array $configs, ContainerBuilder $container) $this->formConfigEnabled = true; $this->registerFormConfiguration($config, $container, $loader); - if (ContainerBuilder::willBeAvailable('symfony/validator', Validation::class, ['symfony/framework-bundle', 'symfony/form'])) { + if (ContainerBuilder::willBeAvailable('symfony/validator', Validation::class, ['symfony/framework-bundle', 'symfony/form'], true)) { $config['validation']['enabled'] = true; } else { $container->setParameter('validator.translation_domain', 'validators'); @@ -410,15 +438,19 @@ public function load(array $configs, ContainerBuilder $container) $this->registerEsiConfiguration($config['esi'], $container, $loader); $this->registerSsiConfiguration($config['ssi'], $container, $loader); $this->registerFragmentsConfiguration($config['fragments'], $container, $loader); - $this->registerTranslatorConfiguration($config['translator'], $container, $loader, $config['default_locale']); + $this->registerTranslatorConfiguration($config['translator'], $container, $loader, $config['default_locale'], $config['enabled_locales']); $this->registerProfilerConfiguration($config['profiler'], $container, $loader); $this->registerWorkflowConfiguration($config['workflows'], $container, $loader); $this->registerDebugConfiguration($config['php_errors'], $container, $loader); - $this->registerRouterConfiguration($config['router'], $container, $loader, $config['translator']['enabled_locales'] ?? []); + // @deprecated since Symfony 5.4, in 6.0 change to: + // $this->registerRouterConfiguration($config['router'], $container, $loader, $config['enabled_locales']); + $this->registerRouterConfiguration($config['router'], $container, $loader, $config['translator']['enabled_locales'] ?: $config['enabled_locales']); $this->registerAnnotationsConfiguration($config['annotations'], $container, $loader); $this->registerPropertyAccessConfiguration($config['property_access'], $container, $loader); $this->registerSecretsConfiguration($config['secrets'], $container, $loader); + $container->getDefinition('exception_listener')->replaceArgument(3, $config['exceptions']); + if ($this->isConfigEnabled($container, $config['serializer'])) { if (!class_exists(\Symfony\Component\Serializer\Serializer::class)) { throw new LogicException('Serializer support cannot be enabled as the Serializer component is not installed. Try running "composer require symfony/serializer-pack".'); @@ -467,7 +499,7 @@ public function load(array $configs, ContainerBuilder $container) 'Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController', ]); - if (ContainerBuilder::willBeAvailable('symfony/mime', MimeTypes::class, ['symfony/framework-bundle'])) { + if (ContainerBuilder::willBeAvailable('symfony/mime', MimeTypes::class, ['symfony/framework-bundle'], true)) { $loader->load('mime_type.php'); } @@ -548,12 +580,26 @@ public function load(array $configs, ContainerBuilder $container) $container->registerForAutoconfiguration(LoggerAwareInterface::class) ->addMethodCall('setLogger', [new Reference('logger')]); - $container->registerAttributeForAutoconfiguration(AsEventListener::class, static function (ChildDefinition $definition, AsEventListener $attribute): void { - $definition->addTag('kernel.event_listener', get_object_vars($attribute)); + $container->registerAttributeForAutoconfiguration(AsEventListener::class, static function (ChildDefinition $definition, AsEventListener $attribute, \Reflector $reflector) { + $tagAttributes = get_object_vars($attribute); + if ($reflector instanceof \ReflectionMethod) { + if (isset($tagAttributes['method'])) { + throw new LogicException(sprintf('AsEventListener attribute cannot declare a method on "%s::%s()".', $reflector->class, $reflector->name)); + } + $tagAttributes['method'] = $reflector->getName(); + } + $definition->addTag('kernel.event_listener', $tagAttributes); }); $container->registerAttributeForAutoconfiguration(AsController::class, static function (ChildDefinition $definition, AsController $attribute): void { $definition->addTag('controller.service_arguments'); }); + $container->registerAttributeForAutoconfiguration(AsMessageHandler::class, static function (ChildDefinition $definition, AsMessageHandler $attribute): void { + $tagAttributes = get_object_vars($attribute); + $tagAttributes['from_transport'] = $tagAttributes['fromTransport']; + unset($tagAttributes['fromTransport']); + + $definition->addTag('messenger.message_handler', $tagAttributes); + }); if (!$container->getParameter('kernel.debug')) { // remove tagged iterator argument for resource checkers @@ -584,6 +630,11 @@ public function getConfiguration(array $config, ContainerBuilder $container) return new Configuration($container->getParameter('kernel.debug')); } + protected function hasConsole(): bool + { + return class_exists(Application::class); + } + private function registerFormConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader) { $loader->load('form.php'); @@ -603,7 +654,7 @@ private function registerFormConfiguration(array $config, ContainerBuilder $cont $container->setParameter('form.type_extension.csrf.enabled', false); } - if (!ContainerBuilder::willBeAvailable('symfony/translation', Translator::class, ['symfony/framework-bundle', 'symfony/form'])) { + if (!ContainerBuilder::willBeAvailable('symfony/translation', Translator::class, ['symfony/framework-bundle', 'symfony/form'], true)) { $container->removeDefinition('form.type_extension.upload.validator'); } if (!method_exists(CachingFactoryDecorator::class, 'reset')) { @@ -727,6 +778,9 @@ private function registerProfilerConfiguration(array $config, ContainerBuilder $ $container->getDefinition('profiler') ->addArgument($config['collect']) ->addTag('kernel.reset', ['method' => 'reset']); + + $container->getDefinition('profiler_listener') + ->addArgument($config['collect_parameter']); } private function registerWorkflowConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader) @@ -745,6 +799,8 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $ $registryDefinition = $container->getDefinition('workflow.registry'); + $workflows = []; + foreach ($config['workflows'] as $name => $workflow) { $type = $workflow['type']; $workflowId = sprintf('%s.%s', $type, $name); @@ -832,6 +888,8 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $ $definitionDefinition->addArgument($initialMarking); $definitionDefinition->addArgument(new Reference(sprintf('%s.metadata_store', $workflowId))); + $workflows[$workflowId] = $definitionDefinition; + // Create MarkingStore if (isset($workflow['marking_store']['type'])) { $markingStoreDefinition = new ChildDefinition('workflow.marking_store.method'); @@ -923,6 +981,9 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $ $container->setParameter('workflow.has_guard_listeners', true); } } + + $commandDumpDefinition = $container->getDefinition('console.command.workflow_dump'); + $commandDumpDefinition->setArgument(0, $workflows); } private function registerDebugConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader) @@ -994,7 +1055,7 @@ private function registerRouterConfiguration(array $config, ContainerBuilder $co $container->getDefinition('routing.loader')->replaceArgument(2, ['_locale' => $enabledLocales]); } - if (!ContainerBuilder::willBeAvailable('symfony/expression-language', ExpressionLanguage::class, ['symfony/framework-bundle', 'symfony/routing'])) { + if (!ContainerBuilder::willBeAvailable('symfony/expression-language', ExpressionLanguage::class, ['symfony/framework-bundle', 'symfony/routing'], true)) { $container->removeDefinition('router.expression_language_provider'); } @@ -1135,7 +1196,7 @@ private function registerAssetsConfiguration(array $config, ContainerBuilder $co if ($config['version_strategy']) { $defaultVersion = new Reference($config['version_strategy']); } else { - $defaultVersion = $this->createVersion($container, $config['version'], $config['version_format'], $config['json_manifest_path'], '_default'); + $defaultVersion = $this->createVersion($container, $config['version'], $config['version_format'], $config['json_manifest_path'], '_default', $config['strict_mode']); } $defaultPackage = $this->createPackageDefinition($config['base_path'], $config['base_urls'], $defaultVersion); @@ -1151,7 +1212,7 @@ private function registerAssetsConfiguration(array $config, ContainerBuilder $co // let format fallback to main version_format $format = $package['version_format'] ?: $config['version_format']; $version = $package['version'] ?? null; - $version = $this->createVersion($container, $version, $format, $package['json_manifest_path'], $name); + $version = $this->createVersion($container, $version, $format, $package['json_manifest_path'], $name, $package['strict_mode']); } $packageDefinition = $this->createPackageDefinition($package['base_path'], $package['base_urls'], $version) @@ -1180,7 +1241,7 @@ private function createPackageDefinition(?string $basePath, array $baseUrls, Ref return $package; } - private function createVersion(ContainerBuilder $container, ?string $version, ?string $format, ?string $jsonManifestPath, string $name): Reference + private function createVersion(ContainerBuilder $container, ?string $version, ?string $format, ?string $jsonManifestPath, string $name, bool $strictMode): Reference { // Configuration prevents $version and $jsonManifestPath from being set if (null !== $version) { @@ -1197,6 +1258,7 @@ private function createVersion(ContainerBuilder $container, ?string $version, ?s if (null !== $jsonManifestPath) { $def = new ChildDefinition('assets.json_manifest_version_strategy'); $def->replaceArgument(0, $jsonManifestPath); + $def->replaceArgument(2, $strictMode); $container->setDefinition('assets._version_'.$name, $def); return new Reference('assets._version_'.$name); @@ -1205,11 +1267,11 @@ private function createVersion(ContainerBuilder $container, ?string $version, ?s return new Reference('assets.empty_version_strategy'); } - private function registerTranslatorConfiguration(array $config, ContainerBuilder $container, LoaderInterface $loader, string $defaultLocale) + private function registerTranslatorConfiguration(array $config, ContainerBuilder $container, LoaderInterface $loader, string $defaultLocale, array $enabledLocales) { if (!$this->isConfigEnabled($container, $config)) { $container->removeDefinition('console.command.translation_debug'); - $container->removeDefinition('console.command.translation_update'); + $container->removeDefinition('console.command.translation_extract'); $container->removeDefinition('console.command.translation_pull'); $container->removeDefinition('console.command.translation_push'); @@ -1229,7 +1291,9 @@ private function registerTranslatorConfiguration(array $config, ContainerBuilder $defaultOptions['cache_dir'] = $config['cache_dir']; $translator->setArgument(4, $defaultOptions); - $translator->setArgument(5, $config['enabled_locales']); + // @deprecated since Symfony 5.4, in 6.0 change to: + // $translator->setArgument(5, $enabledLocales); + $translator->setArgument(5, $config['enabled_locales'] ?: $enabledLocales); $container->setParameter('translator.logging', $config['logging']); $container->setParameter('translator.default_path', $config['default_path']); @@ -1238,17 +1302,17 @@ private function registerTranslatorConfiguration(array $config, ContainerBuilder $dirs = []; $transPaths = []; $nonExistingDirs = []; - if (ContainerBuilder::willBeAvailable('symfony/validator', Validation::class, ['symfony/framework-bundle', 'symfony/translation'])) { + if (ContainerBuilder::willBeAvailable('symfony/validator', Validation::class, ['symfony/framework-bundle', 'symfony/translation'], true)) { $r = new \ReflectionClass(Validation::class); $dirs[] = $transPaths[] = \dirname($r->getFileName()).'/Resources/translations'; } - if (ContainerBuilder::willBeAvailable('symfony/form', Form::class, ['symfony/framework-bundle', 'symfony/translation'])) { + if (ContainerBuilder::willBeAvailable('symfony/form', Form::class, ['symfony/framework-bundle', 'symfony/translation'], true)) { $r = new \ReflectionClass(Form::class); $dirs[] = $transPaths[] = \dirname($r->getFileName()).'/Resources/translations'; } - if (ContainerBuilder::willBeAvailable('symfony/security-core', AuthenticationException::class, ['symfony/framework-bundle', 'symfony/translation'])) { + if (ContainerBuilder::willBeAvailable('symfony/security-core', AuthenticationException::class, ['symfony/framework-bundle', 'symfony/translation'], true)) { $r = new \ReflectionClass(AuthenticationException::class); $dirs[] = $transPaths[] = \dirname($r->getFileName(), 2).'/Resources/translations'; @@ -1274,8 +1338,8 @@ private function registerTranslatorConfiguration(array $config, ContainerBuilder $container->getDefinition('console.command.translation_debug')->replaceArgument(5, $transPaths); } - if ($container->hasDefinition('console.command.translation_update')) { - $container->getDefinition('console.command.translation_update')->replaceArgument(6, $transPaths); + if ($container->hasDefinition('console.command.translation_extract')) { + $container->getDefinition('console.command.translation_extract')->replaceArgument(6, $transPaths); } if (null === $defaultDir) { @@ -1353,7 +1417,7 @@ private function registerTranslatorConfiguration(array $config, ContainerBuilder foreach ($classToServices as $class => $service) { $package = substr($service, \strlen('translation.provider_factory.')); - if (!$container->hasDefinition('http_client') || !ContainerBuilder::willBeAvailable(sprintf('symfony/%s-translation-provider', $package), $class, $parentPackages)) { + if (!$container->hasDefinition('http_client') || !ContainerBuilder::willBeAvailable(sprintf('symfony/%s-translation-provider', $package), $class, $parentPackages, true)) { $container->removeDefinition($service); } } @@ -1362,7 +1426,9 @@ private function registerTranslatorConfiguration(array $config, ContainerBuilder return; } - $locales = $config['enabled_locales'] ?? []; + // @deprecated since Symfony 5.4, in 6.0 change to: + // $locales = $enabledLocales; + $locales = $config['enabled_locales'] ?: $enabledLocales; foreach ($config['providers'] as $provider) { if ($provider['locales']) { @@ -1468,7 +1534,7 @@ private function registerValidatorMapping(ContainerBuilder $container, array $co $files['yaml' === $extension ? 'yml' : $extension][] = $path; }; - if (ContainerBuilder::willBeAvailable('symfony/form', Form::class, ['symfony/framework-bundle', 'symfony/validator'])) { + if (ContainerBuilder::willBeAvailable('symfony/form', Form::class, ['symfony/framework-bundle', 'symfony/validator'], true)) { $reflClass = new \ReflectionClass(Form::class); $fileRecorder('xml', \dirname($reflClass->getFileName()).'/Resources/config/validation.xml'); } @@ -1543,23 +1609,12 @@ private function registerAnnotationsConfiguration(array $config, ContainerBuilde if ('none' === $config['cache']) { $container->removeDefinition('annotations.cached_reader'); - $container->removeDefinition('annotations.psr_cached_reader'); return; } $cacheService = $config['cache']; if (\in_array($config['cache'], ['php_array', 'file'])) { - $isPsr6Service = $container->hasDefinition('annotations.psr_cached_reader'); - } else { - $isPsr6Service = false; - trigger_deprecation('symfony/framework-bundle', '5.3', 'Using a custom service for "framework.annotation.cache" is deprecated, only values "none", "php_array" and "file" are valid in version 6.0.'); - } - - if ($isPsr6Service) { - $container->removeDefinition('annotations.cached_reader'); - $container->setDefinition('annotations.cached_reader', $container->getDefinition('annotations.psr_cached_reader')); - if ('php_array' === $config['cache']) { $cacheService = 'annotations.cache_adapter'; @@ -1580,31 +1635,7 @@ private function registerAnnotationsConfiguration(array $config, ContainerBuilde ; } } else { - // Legacy code for doctrine/annotations:<1.13 - if (!class_exists(\Doctrine\Common\Cache\CacheProvider::class)) { - throw new LogicException('Annotations cannot be cached as the Doctrine Cache library is not installed. Try running "composer require doctrine/cache".'); - } - - if ('php_array' === $config['cache']) { - $cacheService = 'annotations.cache'; - - // Enable warmer only if PHP array is used for cache - $definition = $container->findDefinition('annotations.cache_warmer'); - $definition->addTag('kernel.cache_warmer'); - } elseif ('file' === $config['cache']) { - $cacheDir = $container->getParameterBag()->resolveValue($config['file_cache_dir']); - - if (!is_dir($cacheDir) && false === @mkdir($cacheDir, 0777, true) && !is_dir($cacheDir)) { - throw new \RuntimeException(sprintf('Could not create cache directory "%s".', $cacheDir)); - } - - $container - ->getDefinition('annotations.filesystem_cache_adapter') - ->replaceArgument(2, $cacheDir) - ; - - $cacheService = 'annotations.filesystem_cache'; - } + trigger_deprecation('symfony/framework-bundle', '5.3', 'Using a custom service for "framework.annotation.cache" is deprecated, only values "none", "php_array" and "file" are valid in version 6.0.'); } $container @@ -1674,7 +1705,7 @@ private function registerSecretsConfiguration(array $config, ContainerBuilder $c throw new InvalidArgumentException(sprintf('Invalid value "%s" set as "decryption_env_var": only "word" characters are allowed.', $config['decryption_env_var'])); } - if (ContainerBuilder::willBeAvailable('symfony/string', LazyString::class, ['symfony/framework-bundle'])) { + if (ContainerBuilder::willBeAvailable('symfony/string', LazyString::class, ['symfony/framework-bundle'], true)) { $container->getDefinition('secrets.decryption_key')->replaceArgument(1, $config['decryption_env_var']); } else { $container->getDefinition('secrets.vault')->replaceArgument(1, "%env({$config['decryption_env_var']})%"); @@ -1800,6 +1831,10 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder $defaultContext += ['max_depth_handler' => new Reference($config['max_depth_handler'])]; $container->getDefinition('serializer.normalizer.object')->replaceArgument(6, $defaultContext); } + + if (isset($config['default_context']) && $config['default_context']) { + $container->setParameter('serializer.default_context', $config['default_context']); + } } private function registerPropertyInfoConfiguration(ContainerBuilder $container, PhpFileLoader $loader) @@ -1810,7 +1845,15 @@ private function registerPropertyInfoConfiguration(ContainerBuilder $container, $loader->load('property_info.php'); - if (ContainerBuilder::willBeAvailable('phpdocumentor/reflection-docblock', DocBlockFactoryInterface::class, ['symfony/framework-bundle', 'symfony/property-info'])) { + if ( + ContainerBuilder::willBeAvailable('phpstan/phpdoc-parser', PhpDocParser::class, ['symfony/framework-bundle', 'symfony/property-info'], true) + && ContainerBuilder::willBeAvailable('phpdocumentor/type-resolver', PhpDocParser::class, ['symfony/framework-bundle', 'symfony/property-info'], true) + ) { + $definition = $container->register('property_info.phpstan_extractor', PhpStanExtractor::class); + $definition->addTag('property_info.type_extractor', ['priority' => -1000]); + } + + if (ContainerBuilder::willBeAvailable('phpdocumentor/reflection-docblock', DocBlockFactoryInterface::class, ['symfony/framework-bundle', 'symfony/property-info'], true)) { $definition = $container->register('property_info.php_doc_extractor', 'Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor'); $definition->addTag('property_info.description_extractor', ['priority' => -1000]); $definition->addTag('property_info.type_extractor', ['priority' => -1001]); @@ -1891,19 +1934,19 @@ private function registerMessengerConfiguration(array $config, ContainerBuilder $loader->load('messenger.php'); - if (ContainerBuilder::willBeAvailable('symfony/amqp-messenger', AmqpTransportFactory::class, ['symfony/framework-bundle', 'symfony/messenger'])) { + if (ContainerBuilder::willBeAvailable('symfony/amqp-messenger', AmqpTransportFactory::class, ['symfony/framework-bundle', 'symfony/messenger'], true)) { $container->getDefinition('messenger.transport.amqp.factory')->addTag('messenger.transport_factory'); } - if (ContainerBuilder::willBeAvailable('symfony/redis-messenger', RedisTransportFactory::class, ['symfony/framework-bundle', 'symfony/messenger'])) { + if (ContainerBuilder::willBeAvailable('symfony/redis-messenger', RedisTransportFactory::class, ['symfony/framework-bundle', 'symfony/messenger'], true)) { $container->getDefinition('messenger.transport.redis.factory')->addTag('messenger.transport_factory'); } - if (ContainerBuilder::willBeAvailable('symfony/amazon-sqs-messenger', AmazonSqsTransportFactory::class, ['symfony/framework-bundle', 'symfony/messenger'])) { + if (ContainerBuilder::willBeAvailable('symfony/amazon-sqs-messenger', AmazonSqsTransportFactory::class, ['symfony/framework-bundle', 'symfony/messenger'], true)) { $container->getDefinition('messenger.transport.sqs.factory')->addTag('messenger.transport_factory'); } - if (ContainerBuilder::willBeAvailable('symfony/beanstalkd-messenger', BeanstalkdTransportFactory::class, ['symfony/framework-bundle', 'symfony/messenger'])) { + if (ContainerBuilder::willBeAvailable('symfony/beanstalkd-messenger', BeanstalkdTransportFactory::class, ['symfony/framework-bundle', 'symfony/messenger'], true)) { $container->getDefinition('messenger.transport.beanstalkd.factory')->addTag('messenger.transport_factory'); } @@ -2095,6 +2138,19 @@ private function registerMessengerConfiguration(array $config, ContainerBuilder $container->removeDefinition('console.command.messenger_failed_messages_show'); $container->removeDefinition('console.command.messenger_failed_messages_remove'); } + + if (false === $config['reset_on_message']) { + throw new LogicException('The "framework.messenger.reset_on_message" configuration option can be set to "true" only. To prevent services resetting after each message you can set the "--no-reset" option in "messenger:consume" command.'); + } + + if (!$container->hasDefinition('console.command.messenger_consume_messages')) { + $container->removeDefinition('messenger.listener.reset_services'); + } elseif (null === $config['reset_on_message']) { + trigger_deprecation('symfony/framework-bundle', '5.4', 'Not setting the "framework.messenger.reset_on_message" configuration option is deprecated, it will default to "true" in version 6.0.'); + + $container->getDefinition('console.command.messenger_consume_messages')->replaceArgument(5, null); + $container->removeDefinition('messenger.listener.reset_services'); + } } private function registerCacheConfiguration(array $config, ContainerBuilder $container) @@ -2216,12 +2272,12 @@ private function registerHttpClientConfiguration(array $config, ContainerBuilder unset($options['retry_failed']); $container->getDefinition('http_client')->setArguments([$options, $config['max_host_connections'] ?? 6]); - if (!$hasPsr18 = ContainerBuilder::willBeAvailable('psr/http-client', ClientInterface::class, ['symfony/framework-bundle', 'symfony/http-client'])) { + if (!$hasPsr18 = ContainerBuilder::willBeAvailable('psr/http-client', ClientInterface::class, ['symfony/framework-bundle', 'symfony/http-client'], true)) { $container->removeDefinition('psr18.http_client'); $container->removeAlias(ClientInterface::class); } - if (!ContainerBuilder::willBeAvailable('php-http/httplug', HttpClient::class, ['symfony/framework-bundle', 'symfony/http-client'])) { + if (!ContainerBuilder::willBeAvailable('php-http/httplug', HttpClient::class, ['symfony/framework-bundle', 'symfony/http-client'], true)) { $container->removeDefinition(HttpClient::class); } @@ -2348,12 +2404,13 @@ private function registerMailerConfiguration(array $config, ContainerBuilder $co SendgridTransportFactory::class => 'mailer.transport_factory.sendgrid', SendinblueTransportFactory::class => 'mailer.transport_factory.sendinblue', SesTransportFactory::class => 'mailer.transport_factory.amazon', + OhMySmtpTransportFactory::class => 'mailer.transport_factory.ohmysmtp', ]; foreach ($classToServices as $class => $service) { $package = substr($service, \strlen('mailer.transport_factory.')); - if (!ContainerBuilder::willBeAvailable(sprintf('symfony/%s-mailer', 'gmail' === $package ? 'google' : $package), $class, ['symfony/framework-bundle', 'symfony/mailer'])) { + if (!ContainerBuilder::willBeAvailable(sprintf('symfony/%s-mailer', 'gmail' === $package ? 'google' : $package), $class, ['symfony/framework-bundle', 'symfony/mailer'], true)) { $container->removeDefinition($service); } } @@ -2416,15 +2473,24 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $ $container->getDefinition('notifier.channel.email')->setArgument(0, null); } $container->getDefinition('notifier.channel.sms')->setArgument(0, null); + $container->getDefinition('notifier.channel.push')->setArgument(0, null); } $container->getDefinition('notifier.channel_policy')->setArgument(0, $config['channel_policy']); + $container->registerForAutoconfiguration(NotifierTransportFactoryInterface::class) + ->addTag('chatter.transport_factory'); + + $container->registerForAutoconfiguration(NotifierTransportFactoryInterface::class) + ->addTag('texter.transport_factory'); + $classToServices = [ AllMySmsTransportFactory::class => 'notifier.transport_factory.allmysms', + AmazonSnsTransportFactory::class => 'notifier.transport_factory.amazonsns', ClickatellTransportFactory::class => 'notifier.transport_factory.clickatell', DiscordTransportFactory::class => 'notifier.transport_factory.discord', EsendexTransportFactory::class => 'notifier.transport_factory.esendex', + ExpoTransportFactory::class => 'notifier.transport_factory.expo', FakeChatTransportFactory::class => 'notifier.transport_factory.fakechat', FakeSmsTransportFactory::class => 'notifier.transport_factory.fakesms', FirebaseTransportFactory::class => 'notifier.transport_factory.firebase', @@ -2436,23 +2502,31 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $ IqsmsTransportFactory::class => 'notifier.transport_factory.iqsms', LightSmsTransportFactory::class => 'notifier.transport_factory.lightsms', LinkedInTransportFactory::class => 'notifier.transport_factory.linkedin', + MailjetNotifierTransportFactory::class => 'notifier.transport_factory.mailjet', MattermostTransportFactory::class => 'notifier.transport_factory.mattermost', MercureTransportFactory::class => 'notifier.transport_factory.mercure', MessageBirdTransport::class => 'notifier.transport_factory.messagebird', + MessageMediaTransportFactory::class => 'notifier.transport_factory.messagemedia', MicrosoftTeamsTransportFactory::class => 'notifier.transport_factory.microsoftteams', MobytTransportFactory::class => 'notifier.transport_factory.mobyt', NexmoTransportFactory::class => 'notifier.transport_factory.nexmo', OctopushTransportFactory::class => 'notifier.transport_factory.octopush', + OneSignalTransportFactory::class => 'notifier.transport_factory.onesignal', OvhCloudTransportFactory::class => 'notifier.transport_factory.ovhcloud', RocketChatTransportFactory::class => 'notifier.transport_factory.rocketchat', SendinblueNotifierTransportFactory::class => 'notifier.transport_factory.sendinblue', SinchTransportFactory::class => 'notifier.transport_factory.sinch', SlackTransportFactory::class => 'notifier.transport_factory.slack', + Sms77TransportFactory::class => 'notifier.transport_factory.sms77', SmsapiTransportFactory::class => 'notifier.transport_factory.smsapi', SmsBiurasTransportFactory::class => 'notifier.transport_factory.smsbiuras', + SmscTransportFactory::class => 'notifier.transport_factory.smsc', SpotHitTransportFactory::class => 'notifier.transport_factory.spothit', TelegramTransportFactory::class => 'notifier.transport_factory.telegram', + TelnyxTransportFactory::class => 'notifier.transport_factory.telnyx', + TurboSmsTransport::class => 'notifier.transport_factory.turbosms', TwilioTransportFactory::class => 'notifier.transport_factory.twilio', + YunpianTransportFactory::class => 'notifier.transport_factory.yunpian', ZulipTransportFactory::class => 'notifier.transport_factory.zulip', ]; @@ -2460,6 +2534,7 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $ foreach ($classToServices as $class => $service) { switch ($package = substr($service, \strlen('notifier.transport_factory.'))) { + case 'amazonsns': $package = 'amazon-sns'; break; case 'fakechat': $package = 'fake-chat'; break; case 'fakesms': $package = 'fake-sms'; break; case 'freemobile': $package = 'free-mobile'; break; @@ -2467,33 +2542,38 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $ case 'lightsms': $package = 'light-sms'; break; case 'linkedin': $package = 'linked-in'; break; case 'messagebird': $package = 'message-bird'; break; + case 'messagemedia': $package = 'message-media'; break; case 'microsoftteams': $package = 'microsoft-teams'; break; + case 'onesignal': $package = 'one-signal'; break; case 'ovhcloud': $package = 'ovh-cloud'; break; case 'rocketchat': $package = 'rocket-chat'; break; case 'smsbiuras': $package = 'sms-biuras'; break; case 'spothit': $package = 'spot-hit'; break; + case 'turbosms': $package = 'turbo-sms'; break; } - if (!ContainerBuilder::willBeAvailable(sprintf('symfony/%s-notifier', $package), $class, $parentPackages)) { + if (!ContainerBuilder::willBeAvailable(sprintf('symfony/%s-notifier', $package), $class, $parentPackages, true)) { $container->removeDefinition($service); } } - if (ContainerBuilder::willBeAvailable('symfony/mercure-notifier', MercureTransportFactory::class, $parentPackages) && ContainerBuilder::willBeAvailable('symfony/mercure-bundle', MercureBundle::class, $parentPackages)) { + if (ContainerBuilder::willBeAvailable('symfony/mercure-notifier', MercureTransportFactory::class, $parentPackages, true) && ContainerBuilder::willBeAvailable('symfony/mercure-bundle', MercureBundle::class, $parentPackages, true)) { $container->getDefinition($classToServices[MercureTransportFactory::class]) ->replaceArgument('$registry', new Reference(HubRegistry::class)); - } elseif (ContainerBuilder::willBeAvailable('symfony/mercure-notifier', MercureTransportFactory::class, $parentPackages)) { + } elseif (ContainerBuilder::willBeAvailable('symfony/mercure-notifier', MercureTransportFactory::class, $parentPackages, true)) { $container->removeDefinition($classToServices[MercureTransportFactory::class]); } - if (ContainerBuilder::willBeAvailable('symfony/fake-chat-notifier', FakeChatTransportFactory::class, ['symfony/framework-bundle', 'symfony/notifier', 'symfony/mailer'])) { + if (ContainerBuilder::willBeAvailable('symfony/fake-chat-notifier', FakeChatTransportFactory::class, ['symfony/framework-bundle', 'symfony/notifier', 'symfony/mailer'], true)) { $container->getDefinition($classToServices[FakeChatTransportFactory::class]) - ->replaceArgument('$mailer', new Reference('mailer')); + ->replaceArgument('$mailer', new Reference('mailer')) + ->replaceArgument('$logger', new Reference('logger')); } - if (ContainerBuilder::willBeAvailable('symfony/fake-sms-notifier', FakeSmsTransportFactory::class, ['symfony/framework-bundle', 'symfony/notifier', 'symfony/mailer'])) { + if (ContainerBuilder::willBeAvailable('symfony/fake-sms-notifier', FakeSmsTransportFactory::class, ['symfony/framework-bundle', 'symfony/notifier', 'symfony/mailer'], true)) { $container->getDefinition($classToServices[FakeSmsTransportFactory::class]) - ->replaceArgument('$mailer', new Reference('mailer')); + ->replaceArgument('$mailer', new Reference('mailer')) + ->replaceArgument('$logger', new Reference('logger')); } if (isset($config['admin_recipients'])) { diff --git a/src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php b/src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php index 35ea73c235771..fe38c4adcaa59 100644 --- a/src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php +++ b/src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php @@ -76,18 +76,24 @@ protected function forward(Request $request, bool $catch = false, Response $entr /** * Returns an array of options to customize the Cache configuration. * - * @return array An array of options + * @return array */ protected function getOptions() { return []; } + /** + * @return SurrogateInterface + */ protected function createSurrogate() { return $this->surrogate ?? new Esi(); } + /** + * @return StoreInterface + */ protected function createStore() { return $this->store ?? new Store($this->cacheDir ?: $this->kernel->getCacheDir().'/http_cache'); diff --git a/src/Symfony/Bundle/FrameworkBundle/Kernel/MicroKernelTrait.php b/src/Symfony/Bundle/FrameworkBundle/Kernel/MicroKernelTrait.php index ff269bcf1ea6b..c25f90d63c9ca 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Kernel/MicroKernelTrait.php +++ b/src/Symfony/Bundle/FrameworkBundle/Kernel/MicroKernelTrait.php @@ -27,41 +27,79 @@ * * @author Ryan Weaver * @author Fabien Potencier - * - * @method void configureRoutes(RoutingConfigurator $routes) - * @method void configureContainer(ContainerConfigurator $container) */ trait MicroKernelTrait { - /** - * Adds or imports routes into your application. - * - * $routes->import($this->getProjectDir().'/config/*.{yaml,php}'); - * $routes - * ->add('admin_dashboard', '/admin') - * ->controller('App\Controller\AdminController::dashboard') - * ; - */ - //abstract protected function configureRoutes(RoutingConfigurator $routes): void; - /** * Configures the container. * * You can register extensions: * - * $c->extension('framework', [ + * $container->extension('framework', [ * 'secret' => '%secret%' * ]); * * Or services: * - * $c->services()->set('halloween', 'FooBundle\HalloweenProvider'); + * $container->services()->set('halloween', 'FooBundle\HalloweenProvider'); * * Or parameters: * - * $c->parameters()->set('halloween', 'lot of fun'); + * $container->parameters()->set('halloween', 'lot of fun'); */ - //abstract protected function configureContainer(ContainerConfigurator $container): void; + private function configureContainer(ContainerConfigurator $container, LoaderInterface $loader, ContainerBuilder $builder): void + { + $configDir = $this->getConfigDir(); + + $container->import($configDir.'/{packages}/*.yaml'); + $container->import($configDir.'/{packages}/'.$this->environment.'/*.yaml'); + + if (is_file($configDir.'/services.yaml')) { + $container->import($configDir.'/services.yaml'); + $container->import($configDir.'/{services}_'.$this->environment.'.yaml'); + } else { + $container->import($configDir.'/{services}.php'); + } + } + + /** + * Adds or imports routes into your application. + * + * $routes->import($this->getConfigDir().'/*.{yaml,php}'); + * $routes + * ->add('admin_dashboard', '/admin') + * ->controller('App\Controller\AdminController::dashboard') + * ; + */ + private function configureRoutes(RoutingConfigurator $routes): void + { + $configDir = $this->getConfigDir(); + + $routes->import($configDir.'/{routes}/'.$this->environment.'/*.yaml'); + $routes->import($configDir.'/{routes}/*.yaml'); + + if (is_file($configDir.'/routes.yaml')) { + $routes->import($configDir.'/routes.yaml'); + } else { + $routes->import($configDir.'/{routes}.php'); + } + } + + /** + * Gets the path to the configuration directory. + */ + private function getConfigDir(): string + { + return $this->getProjectDir().'/config'; + } + + /** + * Gets the path to the bundles configuration file. + */ + private function getBundlesPath(): string + { + return $this->getConfigDir().'/bundles.php'; + } /** * {@inheritdoc} @@ -88,7 +126,7 @@ public function getLogDir(): string */ public function registerBundles(): iterable { - $contents = require $this->getProjectDir().'/config/bundles.php'; + $contents = require $this->getBundlesPath(); foreach ($contents as $class => $envs) { if ($envs[$this->environment] ?? $envs['all'] ?? false) { yield new $class(); @@ -124,25 +162,20 @@ public function registerContainerConfiguration(LoaderInterface $loader) $kernelDefinition->addTag('routing.route_loader'); $container->addObjectResource($this); - $container->fileExists($this->getProjectDir().'/config/bundles.php'); - - try { - $configureContainer = new \ReflectionMethod($this, 'configureContainer'); - } catch (\ReflectionException $e) { - throw new \LogicException(sprintf('"%s" uses "%s", but does not implement the required method "protected function configureContainer(ContainerConfigurator $container): void".', get_debug_type($this), MicroKernelTrait::class), 0, $e); - } + $container->fileExists($this->getBundlesPath()); + $configureContainer = new \ReflectionMethod($this, 'configureContainer'); $configuratorClass = $configureContainer->getNumberOfParameters() > 0 && ($type = $configureContainer->getParameters()[0]->getType()) instanceof \ReflectionNamedType && !$type->isBuiltin() ? $type->getName() : null; if ($configuratorClass && !is_a(ContainerConfigurator::class, $configuratorClass, true)) { - $this->configureContainer($container, $loader); + $configureContainer->getClosure($this)($container, $loader); return; } - // the user has opted into using the ContainerConfigurator + $file = (new \ReflectionObject($this))->getFileName(); /* @var ContainerPhpFileLoader $kernelLoader */ - $kernelLoader = $loader->getResolver()->resolve($file = $configureContainer->getFileName()); + $kernelLoader = $loader->getResolver()->resolve($file); $kernelLoader->setCurrentDir(\dirname($file)); $instanceof = &\Closure::bind(function &() { return $this->instanceof; }, $kernelLoader, $kernelLoader)(); @@ -152,7 +185,7 @@ public function registerContainerConfiguration(LoaderInterface $loader) }; try { - $this->configureContainer(new ContainerConfigurator($container, $kernelLoader, $instanceof, $file, $file, $this->getEnvironment()), $loader); + $configureContainer->getClosure($this)(new ContainerConfigurator($container, $kernelLoader, $instanceof, $file, $file, $this->getEnvironment()), $loader, $container); } finally { $instanceof = []; $kernelLoader->registerAliasesForSinglyImplementedInterfaces(); @@ -165,10 +198,8 @@ public function registerContainerConfiguration(LoaderInterface $loader) /** * @internal - * - * @return RouteCollection */ - public function loadRoutes(LoaderInterface $loader) + public function loadRoutes(LoaderInterface $loader): RouteCollection { $file = (new \ReflectionObject($this))->getFileName(); /* @var RoutingPhpFileLoader $kernelLoader */ @@ -176,12 +207,7 @@ public function loadRoutes(LoaderInterface $loader) $kernelLoader->setCurrentDir(\dirname($file)); $collection = new RouteCollection(); - try { - $configureRoutes = new \ReflectionMethod($this, 'configureRoutes'); - } catch (\ReflectionException $e) { - throw new \LogicException(sprintf('"%s" uses "%s", but does not implement the required method "protected function configureRoutes(RoutingConfigurator $routes): void".', get_debug_type($this), MicroKernelTrait::class), 0, $e); - } - + $configureRoutes = new \ReflectionMethod($this, 'configureRoutes'); $configuratorClass = $configureRoutes->getNumberOfParameters() > 0 && ($type = $configureRoutes->getParameters()[0]->getType()) instanceof \ReflectionNamedType && !$type->isBuiltin() ? $type->getName() : null; if ($configuratorClass && !is_a(RoutingConfigurator::class, $configuratorClass, true)) { @@ -193,7 +219,7 @@ public function loadRoutes(LoaderInterface $loader) return $routes->build(); } - $this->configureRoutes(new RoutingConfigurator($collection, $kernelLoader, $file, $file, $this->getEnvironment())); + $configureRoutes->getClosure($this)(new RoutingConfigurator($collection, $kernelLoader, $file, $file, $this->getEnvironment())); foreach ($collection as $route) { $controller = $route->getDefault('_controller'); diff --git a/src/Symfony/Bundle/FrameworkBundle/KernelBrowser.php b/src/Symfony/Bundle/FrameworkBundle/KernelBrowser.php index 7d4475a7e21ca..7ae8f336b740f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/KernelBrowser.php +++ b/src/Symfony/Bundle/FrameworkBundle/KernelBrowser.php @@ -67,7 +67,7 @@ public function getKernel() /** * Gets the profile associated with the current Response. * - * @return HttpProfile|false|null A Profile instance + * @return HttpProfile|false|null */ public function getProfile() { @@ -111,6 +111,8 @@ public function enableReboot() /** * @param UserInterface $user + * + * @return $this */ public function loginUser(object $user, string $firewallContext = 'main'): self { @@ -123,7 +125,10 @@ public function loginUser(object $user, string $firewallContext = 'main'): self } $token = new TestBrowserToken($user->getRoles(), $user, $firewallContext); - $token->setAuthenticated(true); + // @deprecated since Symfony 5.4 + if (method_exists($token, 'isAuthenticated')) { + $token->setAuthenticated(true, false); + } $container = $this->getContainer(); $container->get('security.untracked_token_storage')->setToken($token); @@ -152,7 +157,7 @@ public function loginUser(object $user, string $firewallContext = 'main'): self * * @return Response */ - protected function doRequest($request) + protected function doRequest(object $request) { // avoid shutting down the Kernel if no request has been performed yet // WebTestCase::createClient() boots the Kernel but do not handle a request @@ -179,7 +184,7 @@ protected function doRequest($request) * * @return Response */ - protected function doRequestInProcess($request) + protected function doRequestInProcess(object $request) { $response = parent::doRequestInProcess($request); @@ -200,7 +205,7 @@ protected function doRequestInProcess($request) * * @return string */ - protected function getScript($request) + protected function getScript(object $request) { $kernel = var_export(serialize($this->kernel), true); $request = var_export(serialize($request), true); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/bin/check-unused-known-tags.php b/src/Symfony/Bundle/FrameworkBundle/Resources/bin/check-unused-known-tags.php index ec9ae1f97c0ff..4920c43ebe182 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/bin/check-unused-known-tags.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/bin/check-unused-known-tags.php @@ -15,5 +15,5 @@ $target = dirname(__DIR__, 2).'/DependencyInjection/Compiler/UnusedTagsPass.php'; $contents = file_get_contents($target); -$contents = preg_replace('{private \$knownTags = \[(.+?)\];}sm', "private \$knownTags = [\n '".implode("',\n '", UnusedTagsPassUtils::getDefinedTags())."',\n ];", $contents); +$contents = preg_replace('{private const KNOWN_TAGS = \[(.+?)\];}sm', "private const KNOWN_TAGS = [\n '".implode("',\n '", UnusedTagsPassUtils::getDefinedTags())."',\n ];", $contents); file_put_contents($target, $contents); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/annotations.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/annotations.php index e202c13f8ab03..a8481258a83fa 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/annotations.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/annotations.php @@ -13,14 +13,13 @@ use Doctrine\Common\Annotations\AnnotationReader; use Doctrine\Common\Annotations\AnnotationRegistry; -use Doctrine\Common\Annotations\CachedReader; use Doctrine\Common\Annotations\PsrCachedReader; use Doctrine\Common\Annotations\Reader; +use Doctrine\Common\Cache\Psr6\DoctrineProvider; use Symfony\Bundle\FrameworkBundle\CacheWarmer\AnnotationsCacheWarmer; use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Cache\Adapter\FilesystemAdapter; use Symfony\Component\Cache\Adapter\PhpArrayAdapter; -use Symfony\Component\Cache\DoctrineProvider; return static function (ContainerConfigurator $container) { $container->services() @@ -33,12 +32,10 @@ ->set('annotations.dummy_registry', AnnotationRegistry::class) ->call('registerUniqueLoader', ['class_exists']) - ->set('annotations.cached_reader', CachedReader::class) + ->set('annotations.cached_reader', PsrCachedReader::class) ->args([ service('annotations.reader'), - inline_service(DoctrineProvider::class)->args([ - inline_service(ArrayAdapter::class), - ]), + inline_service(ArrayAdapter::class), abstract_arg('Debug-Flag'), ]) @@ -50,9 +47,11 @@ ]) ->set('annotations.filesystem_cache', DoctrineProvider::class) + ->factory([DoctrineProvider::class, 'wrap']) ->args([ service('annotations.filesystem_cache_adapter'), ]) + ->deprecate('symfony/framework-bundle', '5.4', '"%service_id% is deprecated"') ->set('annotations.cache_warmer', AnnotationsCacheWarmer::class) ->args([ @@ -71,22 +70,12 @@ ->tag('container.hot_path') ->set('annotations.cache', DoctrineProvider::class) + ->factory([DoctrineProvider::class, 'wrap']) ->args([ service('annotations.cache_adapter'), ]) - ->tag('container.hot_path') + ->deprecate('symfony/framework-bundle', '5.4', '"%service_id% is deprecated"') ->alias('annotation_reader', 'annotations.reader') ->alias(Reader::class, 'annotation_reader'); - - if (class_exists(PsrCachedReader::class)) { - $container->services() - ->set('annotations.psr_cached_reader', PsrCachedReader::class) - ->args([ - service('annotations.reader'), - inline_service(ArrayAdapter::class), - abstract_arg('Debug-Flag'), - ]) - ; - } }; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/assets.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/assets.php index a6f278743a75f..1e250aab4dceb 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/assets.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/assets.php @@ -80,6 +80,7 @@ ->args([ abstract_arg('manifest path'), service('http_client')->nullOnInvalid(), + false, ]) ->set('assets.remote_json_manifest_version_strategy', RemoteJsonManifestVersionStrategy::class) diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.php index b44f3b9fb315d..87e2db3f40197 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.php @@ -77,7 +77,7 @@ '', // namespace 0, // default lifetime abstract_arg('version'), - sprintf('%s/pools', param('kernel.cache_dir')), + sprintf('%s/pools/system', param('kernel.cache_dir')), service('logger')->ignoreOnInvalid(), ]) ->tag('cache.pool', ['clearer' => 'cache.system_clearer', 'reset' => 'reset']) @@ -93,8 +93,10 @@ ->call('setLogger', [service('logger')->ignoreOnInvalid()]) ->tag('cache.pool', ['clearer' => 'cache.default_clearer', 'reset' => 'reset']) ->tag('monolog.logger', ['channel' => 'cache']) + ; - ->set('cache.adapter.doctrine', DoctrineAdapter::class) + if (class_exists(DoctrineAdapter::class)) { + $container->services()->set('cache.adapter.doctrine', DoctrineAdapter::class) ->abstract() ->args([ abstract_arg('Doctrine provider service'), @@ -108,13 +110,17 @@ 'reset' => 'reset', ]) ->tag('monolog.logger', ['channel' => 'cache']) + ->deprecate('symfony/framework-bundle', '5.4', 'The abstract service "%service_id%" is deprecated.') + ; + } + $container->services() ->set('cache.adapter.filesystem', FilesystemAdapter::class) ->abstract() ->args([ '', // namespace 0, // default lifetime - sprintf('%s/pools', param('kernel.cache_dir')), + sprintf('%s/pools/app', param('kernel.cache_dir')), service('cache.default_marshaller')->ignoreOnInvalid(), ]) ->call('setLogger', [service('logger')->ignoreOnInvalid()]) @@ -211,6 +217,7 @@ ->set('cache.default_marshaller', DefaultMarshaller::class) ->args([ null, // use igbinary_serialize() when available + '%kernel.debug%', ]) ->set('cache.early_expiration_handler', EarlyExpirationHandler::class) @@ -238,6 +245,7 @@ ->alias(CacheItemPoolInterface::class, 'cache.app') ->alias(AdapterInterface::class, 'cache.app') + ->deprecate('symfony/framework-bundle', '5.4', 'The "%alias_id%" alias is deprecated, use "%s" instead.', CacheItemPoolInterface::class) ->alias(CacheInterface::class, 'cache.app') diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php index d2db4d4052d59..610a83addec42 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php @@ -39,6 +39,7 @@ use Symfony\Bundle\FrameworkBundle\Command\YamlLintCommand; use Symfony\Bundle\FrameworkBundle\EventListener\SuggestMissingPackageSubscriber; use Symfony\Component\Console\EventListener\ErrorListener; +use Symfony\Component\Dotenv\Command\DebugCommand as DotenvDebugCommand; use Symfony\Component\Messenger\Command\ConsumeMessagesCommand; use Symfony\Component\Messenger\Command\DebugCommand; use Symfony\Component\Messenger\Command\FailedMessagesRemoveCommand; @@ -129,6 +130,13 @@ ]) ->tag('console.command') + ->set('console.command.dotenv_debug', DotenvDebugCommand::class) + ->args([ + param('kernel.environment'), + param('kernel.project_dir'), + ]) + ->tag('console.command') + ->set('console.command.event_dispatcher_debug', EventDispatcherDebugCommand::class) ->args([ tagged_locator('event_dispatcher.dispatcher', 'name'), @@ -142,6 +150,8 @@ service('event_dispatcher'), service('logger')->nullOnInvalid(), [], // Receiver names + service('messenger.listener.reset_services')->nullOnInvalid(), + [], // Bus names ]) ->tag('console.command') ->tag('monolog.logger', ['channel' => 'messenger']) @@ -212,10 +222,11 @@ null, // twig.default_path [], // Translator paths [], // Twig paths + param('kernel.enabled_locales'), ]) ->tag('console.command') - ->set('console.command.translation_update', TranslationUpdateCommand::class) + ->set('console.command.translation_extract', TranslationUpdateCommand::class) ->args([ service('translation.writer'), service('translation.reader'), @@ -225,6 +236,7 @@ null, // twig.default_path [], // Translator paths [], // Twig paths + param('kernel.enabled_locales'), ]) ->tag('console.command') diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug_prod.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug_prod.php index 51492cfe1823f..f381b018f0629 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug_prod.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug_prod.php @@ -25,7 +25,6 @@ null, // Log levels map for enabled error levels param('debug.error_handler.throw_at'), param('kernel.debug'), - service('debug.file_link_formatter'), param('kernel.debug'), service('monolog.logger.deprecation')->nullOnInvalid(), ]) diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.php index 9c330ab2c7333..7bddfa7567cee 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.php @@ -16,6 +16,7 @@ use Symfony\Component\Mailer\Bridge\Mailchimp\Transport\MandrillTransportFactory; use Symfony\Component\Mailer\Bridge\Mailgun\Transport\MailgunTransportFactory; use Symfony\Component\Mailer\Bridge\Mailjet\Transport\MailjetTransportFactory; +use Symfony\Component\Mailer\Bridge\OhMySmtp\Transport\OhMySmtpTransportFactory; use Symfony\Component\Mailer\Bridge\Postmark\Transport\PostmarkTransportFactory; use Symfony\Component\Mailer\Bridge\Sendgrid\Transport\SendgridTransportFactory; use Symfony\Component\Mailer\Bridge\Sendinblue\Transport\SendinblueTransportFactory; @@ -76,6 +77,10 @@ ->parent('mailer.transport_factory.abstract') ->tag('mailer.transport_factory') + ->set('mailer.transport_factory.ohmysmtp', OhMySmtpTransportFactory::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]) diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.php index 61a993b255174..813d503000de4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.php @@ -18,8 +18,10 @@ use Symfony\Component\Messenger\Bridge\Redis\Transport\RedisTransportFactory; use Symfony\Component\Messenger\EventListener\AddErrorDetailsStampListener; use Symfony\Component\Messenger\EventListener\DispatchPcntlSignalListener; +use Symfony\Component\Messenger\EventListener\ResetServicesListener; use Symfony\Component\Messenger\EventListener\SendFailedMessageForRetryListener; use Symfony\Component\Messenger\EventListener\SendFailedMessageToFailureTransportListener; +use Symfony\Component\Messenger\EventListener\StopWorkerOnCustomStopExceptionListener; use Symfony\Component\Messenger\EventListener\StopWorkerOnRestartSignalListener; use Symfony\Component\Messenger\EventListener\StopWorkerOnSigtermSignalListener; use Symfony\Component\Messenger\Middleware\AddBusNameStampMiddleware; @@ -191,8 +193,19 @@ ->tag('monolog.logger', ['channel' => 'messenger']) ->set('messenger.listener.stop_worker_on_sigterm_signal_listener', StopWorkerOnSigtermSignalListener::class) + ->args([ + service('logger')->ignoreOnInvalid(), + ]) + ->tag('kernel.event_subscriber') + + ->set('messenger.listener.stop_worker_on_stop_exception_listener', StopWorkerOnCustomStopExceptionListener::class) ->tag('kernel.event_subscriber') + ->set('messenger.listener.reset_services', ResetServicesListener::class) + ->args([ + service('services_resetter'), + ]) + ->set('messenger.routable_message_bus', RoutableMessageBus::class) ->args([ abstract_arg('message bus locator'), diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier.php index a9c447470b667..73beb2c346698 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier.php @@ -16,12 +16,14 @@ use Symfony\Component\Notifier\Channel\ChannelPolicy; use Symfony\Component\Notifier\Channel\ChatChannel; use Symfony\Component\Notifier\Channel\EmailChannel; +use Symfony\Component\Notifier\Channel\PushChannel; use Symfony\Component\Notifier\Channel\SmsChannel; use Symfony\Component\Notifier\Chatter; use Symfony\Component\Notifier\ChatterInterface; use Symfony\Component\Notifier\EventListener\NotificationLoggerListener; use Symfony\Component\Notifier\EventListener\SendFailedMessageToNotifierListener; use Symfony\Component\Notifier\Message\ChatMessage; +use Symfony\Component\Notifier\Message\PushMessage; use Symfony\Component\Notifier\Message\SmsMessage; use Symfony\Component\Notifier\Messenger\MessageHandler; use Symfony\Component\Notifier\Notifier; @@ -57,6 +59,10 @@ ->args([service('mailer.transports'), service('messenger.default_bus')->ignoreOnInvalid()]) ->tag('notifier.channel', ['channel' => 'email']) + ->set('notifier.channel.push', PushChannel::class) + ->args([service('texter.transports'), service('messenger.default_bus')->ignoreOnInvalid()]) + ->tag('notifier.channel', ['channel' => 'push']) + ->set('notifier.monolog_handler', NotifierHandler::class) ->args([service('notifier')]) @@ -103,6 +109,10 @@ ->args([service('texter.transports')]) ->tag('messenger.message_handler', ['handles' => SmsMessage::class]) + ->set('texter.messenger.push_handler', MessageHandler::class) + ->args([service('texter.transports')]) + ->tag('messenger.message_handler', ['handles' => PushMessage::class]) + ->set('notifier.logger_notification_listener', NotificationLoggerListener::class) ->tag('kernel.event_subscriber') ; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php index eae1e0166acae..d77f395e030e3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php @@ -12,9 +12,11 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; use Symfony\Component\Notifier\Bridge\AllMySms\AllMySmsTransportFactory; +use Symfony\Component\Notifier\Bridge\AmazonSns\AmazonSnsTransportFactory; use Symfony\Component\Notifier\Bridge\Clickatell\ClickatellTransportFactory; use Symfony\Component\Notifier\Bridge\Discord\DiscordTransportFactory; use Symfony\Component\Notifier\Bridge\Esendex\EsendexTransportFactory; +use Symfony\Component\Notifier\Bridge\Expo\ExpoTransportFactory; use Symfony\Component\Notifier\Bridge\FakeChat\FakeChatTransportFactory; use Symfony\Component\Notifier\Bridge\FakeSms\FakeSmsTransportFactory; use Symfony\Component\Notifier\Bridge\Firebase\FirebaseTransportFactory; @@ -26,23 +28,31 @@ use Symfony\Component\Notifier\Bridge\Iqsms\IqsmsTransportFactory; use Symfony\Component\Notifier\Bridge\LightSms\LightSmsTransportFactory; use Symfony\Component\Notifier\Bridge\LinkedIn\LinkedInTransportFactory; +use Symfony\Component\Notifier\Bridge\Mailjet\MailjetTransportFactory; use Symfony\Component\Notifier\Bridge\Mattermost\MattermostTransportFactory; use Symfony\Component\Notifier\Bridge\Mercure\MercureTransportFactory; use Symfony\Component\Notifier\Bridge\MessageBird\MessageBirdTransportFactory; +use Symfony\Component\Notifier\Bridge\MessageMedia\MessageMediaTransportFactory; use Symfony\Component\Notifier\Bridge\MicrosoftTeams\MicrosoftTeamsTransportFactory; use Symfony\Component\Notifier\Bridge\Mobyt\MobytTransportFactory; use Symfony\Component\Notifier\Bridge\Nexmo\NexmoTransportFactory; use Symfony\Component\Notifier\Bridge\Octopush\OctopushTransportFactory; +use Symfony\Component\Notifier\Bridge\OneSignal\OneSignalTransportFactory; use Symfony\Component\Notifier\Bridge\OvhCloud\OvhCloudTransportFactory; use Symfony\Component\Notifier\Bridge\RocketChat\RocketChatTransportFactory; use Symfony\Component\Notifier\Bridge\Sendinblue\SendinblueTransportFactory; use Symfony\Component\Notifier\Bridge\Sinch\SinchTransportFactory; use Symfony\Component\Notifier\Bridge\Slack\SlackTransportFactory; +use Symfony\Component\Notifier\Bridge\Sms77\Sms77TransportFactory; use Symfony\Component\Notifier\Bridge\Smsapi\SmsapiTransportFactory; use Symfony\Component\Notifier\Bridge\SmsBiuras\SmsBiurasTransportFactory; +use Symfony\Component\Notifier\Bridge\Smsc\SmscTransportFactory; use Symfony\Component\Notifier\Bridge\SpotHit\SpotHitTransportFactory; use Symfony\Component\Notifier\Bridge\Telegram\TelegramTransportFactory; +use Symfony\Component\Notifier\Bridge\Telnyx\TelnyxTransportFactory; +use Symfony\Component\Notifier\Bridge\TurboSms\TurboSmsTransportFactory; use Symfony\Component\Notifier\Bridge\Twilio\TwilioTransportFactory; +use Symfony\Component\Notifier\Bridge\Yunpian\YunpianTransportFactory; use Symfony\Component\Notifier\Bridge\Zulip\ZulipTransportFactory; use Symfony\Component\Notifier\Transport\AbstractTransportFactory; use Symfony\Component\Notifier\Transport\NullTransportFactory; @@ -173,6 +183,11 @@ ->parent('notifier.transport_factory.abstract') ->tag('texter.transport_factory') + ->set('notifier.transport_factory.amazonsns', AmazonSnsTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + ->tag('chatter.transport_factory') + ->set('notifier.transport_factory.null', NullTransportFactory::class) ->parent('notifier.transport_factory.abstract') ->tag('chatter.transport_factory') @@ -186,8 +201,44 @@ ->parent('notifier.transport_factory.abstract') ->tag('texter.transport_factory') + ->set('notifier.transport_factory.smsc', SmscTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + ->set('notifier.transport_factory.messagebird', MessageBirdTransportFactory::class) ->parent('notifier.transport_factory.abstract') ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.messagemedia', MessageMediaTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.telnyx', TelnyxTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.mailjet', MailjetTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.yunpian', YunpianTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.turbosms', TurboSmsTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.sms77', Sms77TransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.onesignal', OneSignalTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.expo', ExpoTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('chatter.transport_factory') ; }; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/profiling.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/profiling.php index c022158339683..221217896fe93 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/profiling.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/profiling.php @@ -21,6 +21,7 @@ ->public() ->args([service('profiler.storage'), service('logger')->nullOnInvalid()]) ->tag('monolog.logger', ['channel' => 'profiler']) + ->tag('container.private', ['package' => 'symfony/framework-bundle', 'version' => '5.4']) ->set('profiler.storage', FileProfilerStorage::class) ->args([param('profiler.storage.dsn')]) 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 c86e15751362f..dfbf3e5cf99e7 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 @@ -29,6 +29,7 @@ + @@ -37,12 +38,15 @@ + + + @@ -87,6 +91,7 @@ + @@ -155,6 +160,7 @@ + @@ -168,6 +174,7 @@ + @@ -260,6 +267,7 @@ + @@ -344,6 +352,18 @@ + + + + + + + + + + + + @@ -472,6 +492,7 @@ + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php index e4868c054aea1..dfb2589cba315 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php @@ -32,6 +32,7 @@ use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter; use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; +use Symfony\Component\Serializer\Normalizer\BackedEnumNormalizer; use Symfony\Component\Serializer\Normalizer\ConstraintViolationListNormalizer; use Symfony\Component\Serializer\Normalizer\DataUriNormalizer; use Symfony\Component\Serializer\Normalizer\DateIntervalNormalizer; @@ -209,4 +210,11 @@ ->args([service('request_stack'), param('kernel.debug')]), ]) ; + + if (interface_exists(\BackedEnum::class)) { + $container->services() + ->set('serializer.normalizer.backed_enum', BackedEnumNormalizer::class) + ->tag('serializer.normalizer', ['priority' => -915]) + ; + } }; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php index c78faddba2d25..a26dfb5adc612 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php @@ -34,6 +34,7 @@ use Symfony\Component\HttpKernel\DependencyInjection\ServicesResetter; use Symfony\Component\HttpKernel\EventListener\LocaleAwareListener; use Symfony\Component\HttpKernel\HttpCache\Store; +use Symfony\Component\HttpKernel\HttpCache\StoreInterface; use Symfony\Component\HttpKernel\HttpKernel; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelEvents; @@ -104,6 +105,7 @@ class_exists(WorkflowEvents::class) ? WorkflowEvents::ALIASES : [] ->args([ param('kernel.cache_dir').'/http_cache', ]) + ->alias(StoreInterface::class, 'http_cache.store') ->set('url_helper', UrlHelper::class) ->args([ diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/session.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/session.php index 9dbaff5c829e1..43c0000dded40 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/session.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/session.php @@ -12,6 +12,7 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; use Symfony\Bundle\FrameworkBundle\Session\DeprecatedSessionFactory; +use Symfony\Bundle\FrameworkBundle\Session\ServiceSessionFactory; use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBag; use Symfony\Component\HttpFoundation\Session\Flash\FlashBag; use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface; @@ -31,7 +32,6 @@ use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorageFactory; use Symfony\Component\HttpFoundation\Session\Storage\PhpBridgeSessionStorage; use Symfony\Component\HttpFoundation\Session\Storage\PhpBridgeSessionStorageFactory; -use Symfony\Component\HttpFoundation\Session\Storage\ServiceSessionFactory; use Symfony\Component\HttpFoundation\Session\Storage\SessionStorageInterface; use Symfony\Component\HttpKernel\EventListener\SessionListener; @@ -146,14 +146,17 @@ ->set('session_listener', SessionListener::class) ->args([ service_locator([ + 'session_factory' => service('session.factory')->ignoreOnInvalid(), 'session' => service('.session.do-not-use')->ignoreOnInvalid(), 'initialized_session' => service('.session.do-not-use')->ignoreOnUninitialized(), 'logger' => service('logger')->ignoreOnInvalid(), 'session_collector' => service('data_collector.request.session_collector')->ignoreOnInvalid(), ]), param('kernel.debug'), + param('session.storage.options'), ]) ->tag('kernel.event_subscriber') + ->tag('kernel.reset', ['method' => 'reset']) // for BC ->alias('session.storage.filesystem', 'session.storage.mock_file') diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/test.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/test.php index 61e4052521329..76709595bf4b6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/test.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/test.php @@ -16,7 +16,7 @@ use Symfony\Component\BrowserKit\CookieJar; use Symfony\Component\BrowserKit\History; use Symfony\Component\DependencyInjection\ServiceLocator; -use Symfony\Component\HttpKernel\EventListener\TestSessionListener; +use Symfony\Component\HttpKernel\EventListener\SessionListener; return static function (ContainerConfigurator $container) { $container->parameters()->set('test.client.parameters', []); @@ -35,11 +35,14 @@ ->set('test.client.history', History::class)->share(false) ->set('test.client.cookiejar', CookieJar::class)->share(false) - ->set('test.session.listener', TestSessionListener::class) + ->set('test.session.listener', SessionListener::class) ->args([ service_locator([ 'session' => service('.session.do-not-use')->ignoreOnInvalid(), + 'session_factory' => service('session.factory')->ignoreOnInvalid(), ]), + param('kernel.debug'), + param('session.storage.options'), ]) ->tag('kernel.event_subscriber') diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php index 8d1934e345ed6..00b8d8aafbd5a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php @@ -69,6 +69,8 @@ ->set('response_listener', ResponseListener::class) ->args([ param('kernel.charset'), + abstract_arg('The "set_content_language_from_locale" config value'), + param('kernel.enabled_locales'), ]) ->tag('kernel.event_subscriber') @@ -80,6 +82,8 @@ service('request_stack'), param('kernel.default_locale'), service('router')->ignoreOnInvalid(), + abstract_arg('The "set_locale_from_accept_language" config value'), + param('kernel.enabled_locales'), ]) ->tag('kernel.event_subscriber') @@ -102,6 +106,7 @@ param('kernel.error_controller'), service('logger')->nullOnInvalid(), param('kernel.debug'), + abstract_arg('an exceptions to log & status code mapping'), ]) ->tag('kernel.event_subscriber') ->tag('monolog.logger', ['channel' => 'request']) diff --git a/src/Symfony/Bundle/FrameworkBundle/Routing/AnnotatedRouteControllerLoader.php b/src/Symfony/Bundle/FrameworkBundle/Routing/AnnotatedRouteControllerLoader.php index e708b70ca712e..511e87058b802 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Routing/AnnotatedRouteControllerLoader.php +++ b/src/Symfony/Bundle/FrameworkBundle/Routing/AnnotatedRouteControllerLoader.php @@ -41,14 +41,12 @@ protected function configureRoute(Route $route, \ReflectionClass $class, \Reflec */ protected function getDefaultRouteName(\ReflectionClass $class, \ReflectionMethod $method) { - return preg_replace([ - '/(bundle|controller)_/', - '/action(_\d+)?$/', - '/__/', - ], [ - '_', - '\\1', - '_', - ], parent::getDefaultRouteName($class, $method)); + $name = preg_replace('/(bundle|controller)_/', '_', parent::getDefaultRouteName($class, $method)); + + if (str_ends_with($method->name, 'Action') || str_ends_with($method->name, '_action')) { + $name = preg_replace('/action(_\d+)?$/', '\\1', $name); + } + + return str_replace('__', '_', $name); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Routing/DelegatingLoader.php b/src/Symfony/Bundle/FrameworkBundle/Routing/DelegatingLoader.php index 438ee578efb83..e130bd2fa931f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Routing/DelegatingLoader.php +++ b/src/Symfony/Bundle/FrameworkBundle/Routing/DelegatingLoader.php @@ -14,6 +14,7 @@ use Symfony\Component\Config\Exception\LoaderLoadException; use Symfony\Component\Config\Loader\DelegatingLoader as BaseDelegatingLoader; use Symfony\Component\Config\Loader\LoaderResolverInterface; +use Symfony\Component\Routing\RouteCollection; /** * DelegatingLoader delegates route loading to other loaders using a loader resolver. @@ -42,7 +43,7 @@ public function __construct(LoaderResolverInterface $resolver, array $defaultOpt /** * {@inheritdoc} */ - public function load($resource, string $type = null) + public function load($resource, string $type = null): RouteCollection { if ($this->loading) { // This can happen if a fatal error occurs in parent::load(). diff --git a/src/Symfony/Bundle/FrameworkBundle/Routing/Router.php b/src/Symfony/Bundle/FrameworkBundle/Routing/Router.php index 23d194567959d..585e6ee130deb 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Routing/Router.php +++ b/src/Symfony/Bundle/FrameworkBundle/Routing/Router.php @@ -49,9 +49,9 @@ public function __construct(ContainerInterface $container, $resource, array $opt $this->setOptions($options); if ($parameters) { - $this->paramFetcher = [$parameters, 'get']; + $this->paramFetcher = \Closure::fromCallable([$parameters, 'get']); } elseif ($container instanceof SymfonyContainerInterface) { - $this->paramFetcher = [$container, 'getParameter']; + $this->paramFetcher = \Closure::fromCallable([$container, 'getParameter']); } else { throw new \LogicException(sprintf('You should either pass a "%s" instance or provide the $parameters argument of the "%s" method.', SymfonyContainerInterface::class, __METHOD__)); } @@ -130,15 +130,15 @@ private function resolveParameters(RouteCollection $collection) $schemes = []; foreach ($route->getSchemes() as $scheme) { - $schemes = array_merge($schemes, explode('|', $this->resolve($scheme))); + $schemes[] = explode('|', $this->resolve($scheme)); } - $route->setSchemes($schemes); + $route->setSchemes(array_merge([], ...$schemes)); $methods = []; foreach ($route->getMethods() as $method) { - $methods = array_merge($methods, explode('|', $this->resolve($method))); + $methods[] = explode('|', $this->resolve($method)); } - $route->setMethods($methods); + $route->setMethods(array_merge([], ...$methods)); $route->setCondition($this->resolve($route->getCondition())); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Secrets/SodiumVault.php b/src/Symfony/Bundle/FrameworkBundle/Secrets/SodiumVault.php index 5b11704e7aa98..f7804dade3adb 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Secrets/SodiumVault.php +++ b/src/Symfony/Bundle/FrameworkBundle/Secrets/SodiumVault.php @@ -200,9 +200,10 @@ private function loadKeys(): void private function export(string $file, string $data): void { + $b64 = 'decrypt.private' === $file ? '// SYMFONY_DECRYPTION_SECRET='.base64_encode($data)."\n" : ''; $name = basename($this->pathPrefix.$file); $data = str_replace('%', '\x', rawurlencode($data)); - $data = sprintf("createSecretsDir(); diff --git a/src/Symfony/Bundle/FrameworkBundle/Session/ServiceSessionFactory.php b/src/Symfony/Bundle/FrameworkBundle/Session/ServiceSessionFactory.php new file mode 100644 index 0000000000000..c057375016f25 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Session/ServiceSessionFactory.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Session; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage; +use Symfony\Component\HttpFoundation\Session\Storage\SessionStorageFactoryInterface; +use Symfony\Component\HttpFoundation\Session\Storage\SessionStorageInterface; + +/** + * @author Jérémy Derussé + * + * @internal to be removed in Symfony 6 + */ +final class ServiceSessionFactory implements SessionStorageFactoryInterface +{ + private $storage; + + public function __construct(SessionStorageInterface $storage) + { + $this->storage = $storage; + } + + public function createStorage(?Request $request): SessionStorageInterface + { + if ($this->storage instanceof NativeSessionStorage && $request && $request->isSecure()) { + $this->storage->setOptions(['cookie_secure' => true]); + } + + return $this->storage; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/BrowserKitAssertionsTrait.php b/src/Symfony/Bundle/FrameworkBundle/Test/BrowserKitAssertionsTrait.php index 62bd1b79acc05..55b95f055994f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Test/BrowserKitAssertionsTrait.php +++ b/src/Symfony/Bundle/FrameworkBundle/Test/BrowserKitAssertionsTrait.php @@ -94,19 +94,24 @@ public static function assertResponseCookieValueSame(string $name, string $expec ), $message); } + public static function assertResponseIsUnprocessable(string $message = ''): void + { + self::assertThatForResponse(new ResponseConstraint\ResponseIsUnprocessable(), $message); + } + public static function assertBrowserHasCookie(string $name, string $path = '/', string $domain = null, string $message = ''): void { - self::assertThat(self::getClient(), new BrowserKitConstraint\BrowserHasCookie($name, $path, $domain), $message); + self::assertThatForClient(new BrowserKitConstraint\BrowserHasCookie($name, $path, $domain), $message); } public static function assertBrowserNotHasCookie(string $name, string $path = '/', string $domain = null, string $message = ''): void { - self::assertThat(self::getClient(), new LogicalNot(new BrowserKitConstraint\BrowserHasCookie($name, $path, $domain)), $message); + self::assertThatForClient(new LogicalNot(new BrowserKitConstraint\BrowserHasCookie($name, $path, $domain)), $message); } public static function assertBrowserCookieValueSame(string $name, string $expectedValue, bool $raw = false, string $path = '/', string $domain = null, string $message = ''): void { - self::assertThat(self::getClient(), LogicalAnd::fromConstraints( + self::assertThatForClient(LogicalAnd::fromConstraints( new BrowserKitConstraint\BrowserHasCookie($name, $path, $domain), new BrowserKitConstraint\BrowserCookieValueSame($name, $expectedValue, $raw, $path, $domain) ), $message); @@ -146,6 +151,11 @@ public static function assertThatForResponse(Constraint $constraint, string $mes } } + public static function assertThatForClient(Constraint $constraint, string $message = ''): void + { + self::assertThat(self::getClient(), $constraint, $message); + } + private static function getClient(AbstractBrowser $newClient = null): ?AbstractBrowser { static $client; diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php b/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php index f4c4efd2fd93d..25e057cc49c24 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php +++ b/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php @@ -52,7 +52,7 @@ protected function tearDown(): void } /** - * @return string The Kernel class name + * @return string * * @throws \RuntimeException * @throws \LogicException @@ -73,7 +73,7 @@ protected static function getKernelClass() /** * Boots the Kernel for this test. * - * @return KernelInterface A KernelInterface instance + * @return KernelInterface */ protected static function bootKernel(array $options = []) { @@ -118,7 +118,7 @@ protected static function getContainer(): ContainerInterface * * environment * * debug * - * @return KernelInterface A KernelInterface instance + * @return KernelInterface */ protected static function createKernel(array $options = []) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/WebTestCase.php b/src/Symfony/Bundle/FrameworkBundle/Test/WebTestCase.php index 085eeae94da7a..e1bceae331b32 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Test/WebTestCase.php +++ b/src/Symfony/Bundle/FrameworkBundle/Test/WebTestCase.php @@ -35,7 +35,7 @@ protected function tearDown(): void * @param array $options An array of options to pass to the createKernel method * @param array $server An array of server parameters * - * @return KernelBrowser A KernelBrowser instance + * @return KernelBrowser */ protected static function createClient(array $options = [], array $server = []) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/AnnotationsCacheWarmerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/AnnotationsCacheWarmerTest.php index d0eb678420f44..8253c525df8f6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/AnnotationsCacheWarmerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/AnnotationsCacheWarmerTest.php @@ -3,7 +3,6 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\CacheWarmer; use Doctrine\Common\Annotations\AnnotationReader; -use Doctrine\Common\Annotations\CachedReader; use Doctrine\Common\Annotations\PsrCachedReader; use Doctrine\Common\Annotations\Reader; use PHPUnit\Framework\MockObject\MockObject; @@ -12,7 +11,6 @@ use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Cache\Adapter\NullAdapter; use Symfony\Component\Cache\Adapter\PhpArrayAdapter; -use Symfony\Component\Cache\DoctrineProvider; use Symfony\Component\Filesystem\Filesystem; class AnnotationsCacheWarmerTest extends TestCase @@ -44,16 +42,10 @@ public function testAnnotationsCacheWarmerWithDebugDisabled() $this->assertFileExists($cacheFile); // Assert cache is valid - $reader = class_exists(PsrCachedReader::class) - ? new PsrCachedReader( - $this->getReadOnlyReader(), - new PhpArrayAdapter($cacheFile, new NullAdapter()) - ) - : new CachedReader( - $this->getReadOnlyReader(), - new DoctrineProvider(new PhpArrayAdapter($cacheFile, new NullAdapter())) - ) - ; + $reader = new PsrCachedReader( + $this->getReadOnlyReader(), + new PhpArrayAdapter($cacheFile, new NullAdapter()) + ); $refClass = new \ReflectionClass($this); $reader->getClassAnnotations($refClass); $reader->getMethodAnnotations($refClass->getMethod(__FUNCTION__)); @@ -71,18 +63,11 @@ public function testAnnotationsCacheWarmerWithDebugEnabled() // Assert cache is valid $phpArrayAdapter = new PhpArrayAdapter($cacheFile, new NullAdapter()); - $reader = class_exists(PsrCachedReader::class) - ? new PsrCachedReader( - $this->getReadOnlyReader(), - $phpArrayAdapter, - true - ) - : new CachedReader( - $this->getReadOnlyReader(), - new DoctrineProvider($phpArrayAdapter), - true - ) - ; + $reader = new PsrCachedReader( + $this->getReadOnlyReader(), + $phpArrayAdapter, + true + ); $refClass = new \ReflectionClass($this); $reader->getClassAnnotations($refClass); $reader->getMethodAnnotations($refClass->getMethod(__FUNCTION__)); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/RouterCacheWarmerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/RouterCacheWarmerTest.php index 9e792eb278d5c..61214b039c64a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/RouterCacheWarmerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/RouterCacheWarmerTest.php @@ -28,7 +28,7 @@ public function testWarmUpWithWarmebleInterface() $routerCacheWarmer = new RouterCacheWarmer($containerMock); $routerCacheWarmer->warmUp('/tmp'); - $routerMock->expects($this->any())->method('warmUp')->with('/tmp')->willReturn(''); + $routerMock->expects($this->any())->method('warmUp')->with('/tmp')->willReturn([]); $this->addToAssertionCount(1); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/SerializerCacheWarmerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/SerializerCacheWarmerTest.php index 18eebf21e66b0..e64196f64d5c6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/SerializerCacheWarmerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/SerializerCacheWarmerTest.php @@ -15,18 +15,17 @@ use Symfony\Bundle\FrameworkBundle\Tests\TestCase; use Symfony\Component\Cache\Adapter\NullAdapter; use Symfony\Component\Cache\Adapter\PhpArrayAdapter; +use Symfony\Component\Serializer\Mapping\Loader\LoaderChain; use Symfony\Component\Serializer\Mapping\Loader\XmlFileLoader; use Symfony\Component\Serializer\Mapping\Loader\YamlFileLoader; class SerializerCacheWarmerTest extends TestCase { - public function testWarmUp() + /** + * @dataProvider loaderProvider + */ + public function testWarmUp(array $loaders) { - $loaders = [ - new XmlFileLoader(__DIR__.'/../Fixtures/Serialization/Resources/person.xml'), - new YamlFileLoader(__DIR__.'/../Fixtures/Serialization/Resources/author.yml'), - ]; - $file = sys_get_temp_dir().'/cache-serializer.php'; @unlink($file); @@ -41,6 +40,26 @@ public function testWarmUp() $this->assertTrue($arrayPool->getItem('Symfony_Bundle_FrameworkBundle_Tests_Fixtures_Serialization_Author')->isHit()); } + public function loaderProvider() + { + return [ + [ + [ + new LoaderChain([ + new XmlFileLoader(__DIR__.'/../Fixtures/Serialization/Resources/person.xml'), + new YamlFileLoader(__DIR__.'/../Fixtures/Serialization/Resources/author.yml'), + ]), + ], + ], + [ + [ + new XmlFileLoader(__DIR__.'/../Fixtures/Serialization/Resources/person.xml'), + new YamlFileLoader(__DIR__.'/../Fixtures/Serialization/Resources/author.yml'), + ], + ], + ]; + } + public function testWarmUpWithoutLoader() { $file = sys_get_temp_dir().'/cache-serializer-without-loader.php'; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CachePoolClearCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CachePoolClearCommandTest.php new file mode 100644 index 0000000000000..169fcd8c2d75d --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CachePoolClearCommandTest.php @@ -0,0 +1,75 @@ + + * + * 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\MockObject\MockObject; +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Bundle\FrameworkBundle\Command\CachePoolClearCommand; +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\HttpKernel\CacheClearer\Psr6CacheClearer; +use Symfony\Component\HttpKernel\KernelInterface; + +class CachePoolClearCommandTest extends TestCase +{ + private $cachePool; + + protected function setUp(): void + { + $this->cachePool = $this->createMock(CacheItemPoolInterface::class); + } + + /** + * @dataProvider provideCompletionSuggestions + */ + public function testComplete(array $input, array $expectedSuggestions) + { + $application = new Application($this->getKernel()); + $application->add(new CachePoolClearCommand(new Psr6CacheClearer(['foo' => $this->cachePool]), ['foo'])); + $tester = new CommandCompletionTester($application->get('cache:pool:clear')); + + $suggestions = $tester->complete($input); + + $this->assertSame($expectedSuggestions, $suggestions); + } + + public function provideCompletionSuggestions() + { + yield 'pool_name' => [ + ['f'], + ['foo'], + ]; + } + + /** + * @return MockObject&KernelInterface + */ + private function getKernel(): KernelInterface + { + $container = $this->createMock(ContainerInterface::class); + + $kernel = $this->createMock(KernelInterface::class); + $kernel + ->expects($this->any()) + ->method('getContainer') + ->willReturn($container); + + $kernel + ->expects($this->once()) + ->method('getBundles') + ->willReturn([]); + + return $kernel; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CachePoolDeleteCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CachePoolDeleteCommandTest.php index b840a538cc670..f643bc1259901 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CachePoolDeleteCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CachePoolDeleteCommandTest.php @@ -16,6 +16,7 @@ use Symfony\Bundle\FrameworkBundle\Command\CachePoolDeleteCommand; use Symfony\Bundle\FrameworkBundle\Console\Application; 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\HttpKernel\CacheClearer\Psr6CacheClearer; @@ -83,6 +84,28 @@ public function testCommandDeleteFailed() $tester->execute(['pool' => 'foo', 'key' => 'bar']); } + /** + * @dataProvider provideCompletionSuggestions + */ + public function testComplete(array $input, array $expectedSuggestions) + { + $application = new Application($this->getKernel()); + $application->add(new CachePoolDeleteCommand(new Psr6CacheClearer(['foo' => $this->cachePool]), ['foo'])); + $tester = new CommandCompletionTester($application->get('cache:pool:delete')); + + $suggestions = $tester->complete($input); + + $this->assertSame($expectedSuggestions, $suggestions); + } + + public function provideCompletionSuggestions() + { + yield 'pool_name' => [ + ['f'], + ['foo'], + ]; + } + /** * @return MockObject&KernelInterface */ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/SecretsRemoveCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/SecretsRemoveCommandTest.php new file mode 100644 index 0000000000000..213e639f06698 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/SecretsRemoveCommandTest.php @@ -0,0 +1,37 @@ +createMock(AbstractVault::class); + $vault->method('list')->willReturn(['SECRET' => null, 'OTHER_SECRET' => null]); + if ($withLocalVault) { + $localVault = $this->createMock(AbstractVault::class); + $localVault->method('list')->willReturn(['SECRET' => null]); + } else { + $localVault = null; + } + $command = new SecretsRemoveCommand($vault, $localVault); + $tester = new CommandCompletionTester($command); + $suggestions = $tester->complete($input); + $this->assertSame($expectedSuggestions, $suggestions); + } + + public function provideCompletionSuggestions() + { + yield 'name' => [true, [''], ['SECRET', 'OTHER_SECRET']]; + yield '--local name (with local vault)' => [true, ['--local', ''], ['SECRET']]; + yield '--local name (without local vault)' => [false, ['--local', ''], []]; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/SecretsSetCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/SecretsSetCommandTest.php new file mode 100644 index 0000000000000..4f0d2225d148a --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/SecretsSetCommandTest.php @@ -0,0 +1,31 @@ +createMock(AbstractVault::class); + $vault->method('list')->willReturn(['SECRET' => null, 'OTHER_SECRET' => null]); + $localVault = $this->createMock(AbstractVault::class); + $command = new SecretsSetCommand($vault, $localVault); + $tester = new CommandCompletionTester($command); + $suggestions = $tester->complete($input); + $this->assertSame($expectedSuggestions, $suggestions); + } + + public function provideCompletionSuggestions() + { + yield 'name' => [[''], ['SECRET', 'OTHER_SECRET']]; + yield '--local name (with local vault)' => [['--local', ''], ['SECRET', 'OTHER_SECRET']]; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/TranslationDebugCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/TranslationDebugCommandTest.php index 5cdf62e470066..d755e11e730af 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/TranslationDebugCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/TranslationDebugCommandTest.php @@ -14,6 +14,8 @@ use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\Command\TranslationDebugCommand; use Symfony\Bundle\FrameworkBundle\Console\Application; +use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\ExtensionWithoutConfigTestBundle\ExtensionWithoutConfigTestBundle; +use Symfony\Component\Console\Tester\CommandCompletionTester; use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\Filesystem\Filesystem; @@ -139,7 +141,12 @@ protected function tearDown(): void $this->fs->remove($this->translationDir); } - private function createCommandTester($extractedMessages = [], $loadedMessages = [], $kernel = null, array $transPaths = [], array $codePaths = []): CommandTester + private function createCommandTester(array $extractedMessages = [], array $loadedMessages = [], KernelInterface $kernel = null, array $transPaths = [], array $codePaths = []): CommandTester + { + return new CommandTester($this->createCommand($extractedMessages, $loadedMessages, $kernel, $transPaths, $codePaths)); + } + + private function createCommand(array $extractedMessages = [], array $loadedMessages = [], KernelInterface $kernel = null, array $transPaths = [], array $codePaths = [], ExtractorInterface $extractor = null, array $bundles = [], array $enabledLocales = []): TranslationDebugCommand { $translator = $this->createMock(Translator::class); $translator @@ -147,15 +154,17 @@ private function createCommandTester($extractedMessages = [], $loadedMessages = ->method('getFallbackLocales') ->willReturn(['en']); - $extractor = $this->createMock(ExtractorInterface::class); - $extractor - ->expects($this->any()) - ->method('extract') - ->willReturnCallback( - function ($path, $catalogue) use ($extractedMessages) { - $catalogue->add($extractedMessages); - } - ); + if (!$extractor) { + $extractor = $this->createMock(ExtractorInterface::class); + $extractor + ->expects($this->any()) + ->method('extract') + ->willReturnCallback( + function ($path, $catalogue) use ($extractedMessages) { + $catalogue->add($extractedMessages); + } + ); + } $loader = $this->createMock(TranslationReader::class); $loader @@ -182,7 +191,7 @@ function ($path, $catalogue) use ($loadedMessages) { $kernel ->expects($this->any()) ->method('getBundles') - ->willReturn([]); + ->willReturn($bundles); $container = new Container(); $kernel @@ -190,12 +199,12 @@ function ($path, $catalogue) use ($loadedMessages) { ->method('getContainer') ->willReturn($container); - $command = new TranslationDebugCommand($translator, $loader, $extractor, $this->translationDir.'/translations', $this->translationDir.'/templates', $transPaths, $codePaths); + $command = new TranslationDebugCommand($translator, $loader, $extractor, $this->translationDir.'/translations', $this->translationDir.'/templates', $transPaths, $codePaths, $enabledLocales); $application = new Application($kernel); $application->add($command); - return new CommandTester($application->find('debug:translation')); + return $application->find('debug:translation'); } private function getBundle($path) @@ -209,4 +218,55 @@ private function getBundle($path) return $bundle; } + + /** + * @dataProvider provideCompletionSuggestions + */ + public function testComplete(array $input, array $expectedSuggestions) + { + $extractedMessagesWithDomains = [ + 'messages' => [ + 'foo' => 'foo', + ], + 'validators' => [ + 'foo' => 'foo', + ], + 'custom_domain' => [ + 'foo' => 'foo', + ], + ]; + $extractor = $this->createMock(ExtractorInterface::class); + $extractor + ->expects($this->any()) + ->method('extract') + ->willReturnCallback( + function ($path, $catalogue) use ($extractedMessagesWithDomains) { + foreach ($extractedMessagesWithDomains as $domain => $message) { + $catalogue->add($message, $domain); + } + } + ); + + $tester = new CommandCompletionTester($this->createCommand([], [], null, [], [], $extractor, [new ExtensionWithoutConfigTestBundle()], ['fr', 'nl'])); + $suggestions = $tester->complete($input); + $this->assertSame($expectedSuggestions, $suggestions); + } + + public function provideCompletionSuggestions() + { + yield 'locale' => [ + [''], + ['fr', 'nl'], + ]; + + yield 'bundle' => [ + ['fr', '--domain', 'messages', ''], + ['ExtensionWithoutConfigTestBundle', 'extension_without_config_test'], + ]; + + yield 'option --domain' => [ + ['en', '--domain', ''], + ['messages', 'validators', 'custom_domain'], + ]; + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/TranslationUpdateCommandCompletionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/TranslationUpdateCommandCompletionTest.php new file mode 100644 index 0000000000000..7e09a6ffad897 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/TranslationUpdateCommandCompletionTest.php @@ -0,0 +1,150 @@ + + * + * 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\TranslationUpdateCommand; +use Symfony\Bundle\FrameworkBundle\Console\Application; +use Symfony\Component\Console\Tester\CommandCompletionTester; +use Symfony\Component\DependencyInjection\Container; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\HttpKernel\Bundle\BundleInterface; +use Symfony\Component\HttpKernel\KernelInterface; +use Symfony\Component\HttpKernel\Tests\Fixtures\ExtensionPresentBundle\ExtensionPresentBundle; +use Symfony\Component\Translation\Extractor\ExtractorInterface; +use Symfony\Component\Translation\Reader\TranslationReader; +use Symfony\Component\Translation\Translator; +use Symfony\Component\Translation\Writer\TranslationWriter; + +class TranslationUpdateCommandCompletionTest extends TestCase +{ + private $fs; + private $translationDir; + + /** + * @dataProvider provideCompletionSuggestions + */ + public function testComplete(array $input, array $expectedSuggestions) + { + $tester = $this->createCommandCompletionTester(['messages' => ['foo' => 'foo']]); + + $suggestions = $tester->complete($input); + + $this->assertSame($expectedSuggestions, $suggestions); + } + + public function provideCompletionSuggestions() + { + $bundle = new ExtensionPresentBundle(); + + yield 'locale' => [[''], ['en', 'fr']]; + yield 'bundle' => [['en', ''], [$bundle->getName(), $bundle->getContainerExtension()->getAlias()]]; + yield 'domain with locale' => [['en', '--domain=m'], ['messages']]; + yield 'domain without locale' => [['--domain=m'], []]; + yield 'format' => [['en', '--format='], ['php', 'xlf', 'po', 'mo', 'yml', 'yaml', 'ts', 'csv', 'ini', 'json', 'res', 'xlf12', 'xlf20']]; + yield 'sort' => [['en', '--sort='], ['asc', 'desc']]; + } + + protected function setUp(): void + { + $this->fs = new Filesystem(); + $this->translationDir = sys_get_temp_dir().'/'.uniqid('sf_translation', true); + $this->fs->mkdir($this->translationDir.'/translations'); + $this->fs->mkdir($this->translationDir.'/templates'); + } + + protected function tearDown(): void + { + $this->fs->remove($this->translationDir); + } + + private function createCommandCompletionTester($extractedMessages = [], $loadedMessages = [], KernelInterface $kernel = null, array $transPaths = [], array $codePaths = []): CommandCompletionTester + { + $translator = $this->createMock(Translator::class); + $translator + ->expects($this->any()) + ->method('getFallbackLocales') + ->willReturn(['en']); + + $extractor = $this->createMock(ExtractorInterface::class); + $extractor + ->expects($this->any()) + ->method('extract') + ->willReturnCallback( + function ($path, $catalogue) use ($extractedMessages) { + foreach ($extractedMessages as $domain => $messages) { + $catalogue->add($messages, $domain); + } + } + ); + + $loader = $this->createMock(TranslationReader::class); + $loader + ->expects($this->any()) + ->method('read') + ->willReturnCallback( + function ($path, $catalogue) use ($loadedMessages) { + $catalogue->add($loadedMessages); + } + ); + + $writer = $this->createMock(TranslationWriter::class); + $writer + ->expects($this->any()) + ->method('getFormats') + ->willReturn( + ['php', 'xlf', 'po', 'mo', 'yml', 'yaml', 'ts', 'csv', 'ini', 'json', 'res'] + ); + + if (null === $kernel) { + $returnValues = [ + ['foo', $this->getBundle($this->translationDir)], + ['test', $this->getBundle('test')], + ]; + $kernel = $this->createMock(KernelInterface::class); + $kernel + ->expects($this->any()) + ->method('getBundle') + ->willReturnMap($returnValues); + } + + $kernel + ->expects($this->any()) + ->method('getBundles') + ->willReturn([new ExtensionPresentBundle()]); + + $container = new Container(); + $kernel + ->expects($this->any()) + ->method('getContainer') + ->willReturn($container); + + $command = new TranslationUpdateCommand($writer, $loader, $extractor, 'en', $this->translationDir.'/translations', $this->translationDir.'/templates', $transPaths, $codePaths, ['en', 'fr']); + + $application = new Application($kernel); + $application->add($command); + + return new CommandCompletionTester($application->find('translation:update')); + } + + private function getBundle($path) + { + $bundle = $this->createMock(BundleInterface::class); + $bundle + ->expects($this->any()) + ->method('getPath') + ->willReturn($path) + ; + + return $bundle; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/TranslationUpdateCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/TranslationUpdateCommandTest.php index 35ce89f63887c..5c6fa8ec35ea2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/TranslationUpdateCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/TranslationUpdateCommandTest.php @@ -29,7 +29,7 @@ class TranslationUpdateCommandTest extends TestCase private $fs; private $translationDir; - public function testDumpMessagesAndClean() + public function testDumpMessagesAndCleanWithDeprecatedCommandName() { $tester = $this->createCommandTester(['messages' => ['foo' => 'foo']]); $tester->execute(['command' => 'translation:update', 'locale' => 'en', 'bundle' => 'foo', '--dump-messages' => true, '--clean' => true]); @@ -37,10 +37,18 @@ public function testDumpMessagesAndClean() $this->assertMatchesRegularExpression('/1 message was successfully extracted/', $tester->getDisplay()); } + public function testDumpMessagesAndClean() + { + $tester = $this->createCommandTester(['messages' => ['foo' => 'foo']]); + $tester->execute(['command' => 'translation:extract', 'locale' => 'en', 'bundle' => 'foo', '--dump-messages' => true, '--clean' => true]); + $this->assertMatchesRegularExpression('/foo/', $tester->getDisplay()); + $this->assertMatchesRegularExpression('/1 message was successfully extracted/', $tester->getDisplay()); + } + public function testDumpMessagesAsTreeAndClean() { $tester = $this->createCommandTester(['messages' => ['foo' => 'foo']]); - $tester->execute(['command' => 'translation:update', 'locale' => 'en', 'bundle' => 'foo', '--dump-messages' => true, '--clean' => true, '--as-tree' => 1]); + $tester->execute(['command' => 'translation:extract', 'locale' => 'en', 'bundle' => 'foo', '--dump-messages' => true, '--clean' => true, '--as-tree' => 1]); $this->assertMatchesRegularExpression('/foo/', $tester->getDisplay()); $this->assertMatchesRegularExpression('/1 message was successfully extracted/', $tester->getDisplay()); } @@ -48,7 +56,7 @@ public function testDumpMessagesAsTreeAndClean() public function testDumpSortedMessagesAndClean() { $tester = $this->createCommandTester(['messages' => ['foo' => 'foo', 'test' => 'test', 'bar' => 'bar']]); - $tester->execute(['command' => 'translation:update', 'locale' => 'en', 'bundle' => 'foo', '--dump-messages' => true, '--clean' => true, '--sort' => 'asc']); + $tester->execute(['command' => 'translation:extract', 'locale' => 'en', 'bundle' => 'foo', '--dump-messages' => true, '--clean' => true, '--sort' => 'asc']); $this->assertMatchesRegularExpression("/\*bar\*foo\*test/", preg_replace('/\s+/', '', $tester->getDisplay())); $this->assertMatchesRegularExpression('/3 messages were successfully extracted/', $tester->getDisplay()); } @@ -56,7 +64,7 @@ public function testDumpSortedMessagesAndClean() public function testDumpReverseSortedMessagesAndClean() { $tester = $this->createCommandTester(['messages' => ['foo' => 'foo', 'test' => 'test', 'bar' => 'bar']]); - $tester->execute(['command' => 'translation:update', 'locale' => 'en', 'bundle' => 'foo', '--dump-messages' => true, '--clean' => true, '--sort' => 'desc']); + $tester->execute(['command' => 'translation:extract', 'locale' => 'en', 'bundle' => 'foo', '--dump-messages' => true, '--clean' => true, '--sort' => 'desc']); $this->assertMatchesRegularExpression("/\*test\*foo\*bar/", preg_replace('/\s+/', '', $tester->getDisplay())); $this->assertMatchesRegularExpression('/3 messages were successfully extracted/', $tester->getDisplay()); } @@ -64,7 +72,7 @@ public function testDumpReverseSortedMessagesAndClean() public function testDumpSortWithoutValueAndClean() { $tester = $this->createCommandTester(['messages' => ['foo' => 'foo', 'test' => 'test', 'bar' => 'bar']]); - $tester->execute(['command' => 'translation:update', 'locale' => 'en', 'bundle' => 'foo', '--dump-messages' => true, '--clean' => true, '--sort']); + $tester->execute(['command' => 'translation:extract', 'locale' => 'en', 'bundle' => 'foo', '--dump-messages' => true, '--clean' => true, '--sort']); $this->assertMatchesRegularExpression("/\*bar\*foo\*test/", preg_replace('/\s+/', '', $tester->getDisplay())); $this->assertMatchesRegularExpression('/3 messages were successfully extracted/', $tester->getDisplay()); } @@ -72,7 +80,7 @@ public function testDumpSortWithoutValueAndClean() public function testDumpWrongSortAndClean() { $tester = $this->createCommandTester(['messages' => ['foo' => 'foo', 'test' => 'test', 'bar' => 'bar']]); - $tester->execute(['command' => 'translation:update', 'locale' => 'en', 'bundle' => 'foo', '--dump-messages' => true, '--clean' => true, '--sort' => 'test']); + $tester->execute(['command' => 'translation:extract', 'locale' => 'en', 'bundle' => 'foo', '--dump-messages' => true, '--clean' => true, '--sort' => 'test']); $this->assertMatchesRegularExpression('/\[ERROR\] Wrong sort order/', $tester->getDisplay()); } @@ -84,7 +92,7 @@ public function testDumpMessagesAndCleanInRootDirectory() $this->fs->mkdir($this->translationDir.'/templates'); $tester = $this->createCommandTester(['messages' => ['foo' => 'foo']], [], null, [$this->translationDir.'/trans'], [$this->translationDir.'/views']); - $tester->execute(['command' => 'translation:update', 'locale' => 'en', '--dump-messages' => true, '--clean' => true]); + $tester->execute(['command' => 'translation:extract', 'locale' => 'en', '--dump-messages' => true, '--clean' => true]); $this->assertMatchesRegularExpression('/foo/', $tester->getDisplay()); $this->assertMatchesRegularExpression('/1 message was successfully extracted/', $tester->getDisplay()); } @@ -92,7 +100,7 @@ public function testDumpMessagesAndCleanInRootDirectory() public function testDumpTwoMessagesAndClean() { $tester = $this->createCommandTester(['messages' => ['foo' => 'foo', 'bar' => 'bar']]); - $tester->execute(['command' => 'translation:update', 'locale' => 'en', 'bundle' => 'foo', '--dump-messages' => true, '--clean' => true]); + $tester->execute(['command' => 'translation:extract', 'locale' => 'en', 'bundle' => 'foo', '--dump-messages' => true, '--clean' => true]); $this->assertMatchesRegularExpression('/foo/', $tester->getDisplay()); $this->assertMatchesRegularExpression('/bar/', $tester->getDisplay()); $this->assertMatchesRegularExpression('/2 messages were successfully extracted/', $tester->getDisplay()); @@ -101,7 +109,7 @@ public function testDumpTwoMessagesAndClean() public function testDumpMessagesForSpecificDomain() { $tester = $this->createCommandTester(['messages' => ['foo' => 'foo'], 'mydomain' => ['bar' => 'bar']]); - $tester->execute(['command' => 'translation:update', 'locale' => 'en', 'bundle' => 'foo', '--dump-messages' => true, '--clean' => true, '--domain' => 'mydomain']); + $tester->execute(['command' => 'translation:extract', 'locale' => 'en', 'bundle' => 'foo', '--dump-messages' => true, '--clean' => true, '--domain' => 'mydomain']); $this->assertMatchesRegularExpression('/bar/', $tester->getDisplay()); $this->assertMatchesRegularExpression('/1 message was successfully extracted/', $tester->getDisplay()); } @@ -109,7 +117,7 @@ public function testDumpMessagesForSpecificDomain() public function testWriteMessages() { $tester = $this->createCommandTester(['messages' => ['foo' => 'foo']]); - $tester->execute(['command' => 'translation:update', 'locale' => 'en', 'bundle' => 'foo', '--force' => true]); + $tester->execute(['command' => 'translation:extract', 'locale' => 'en', 'bundle' => 'foo', '--force' => true]); $this->assertMatchesRegularExpression('/Translation files were successfully updated./', $tester->getDisplay()); } @@ -121,14 +129,14 @@ public function testWriteMessagesInRootDirectory() $this->fs->mkdir($this->translationDir.'/templates'); $tester = $this->createCommandTester(['messages' => ['foo' => 'foo']]); - $tester->execute(['command' => 'translation:update', 'locale' => 'en', '--force' => true]); + $tester->execute(['command' => 'translation:extract', 'locale' => 'en', '--force' => true]); $this->assertMatchesRegularExpression('/Translation files were successfully updated./', $tester->getDisplay()); } public function testWriteMessagesForSpecificDomain() { $tester = $this->createCommandTester(['messages' => ['foo' => 'foo'], 'mydomain' => ['bar' => 'bar']]); - $tester->execute(['command' => 'translation:update', 'locale' => 'en', 'bundle' => 'foo', '--force' => true, '--domain' => 'mydomain']); + $tester->execute(['command' => 'translation:extract', 'locale' => 'en', 'bundle' => 'foo', '--force' => true, '--domain' => 'mydomain']); $this->assertMatchesRegularExpression('/Translation files were successfully updated./', $tester->getDisplay()); } @@ -145,10 +153,7 @@ protected function tearDown(): void $this->fs->remove($this->translationDir); } - /** - * @return CommandTester - */ - private function createCommandTester($extractedMessages = [], $loadedMessages = [], KernelInterface $kernel = null, array $transPaths = [], array $codePaths = []) + private function createCommandTester($extractedMessages = [], $loadedMessages = [], KernelInterface $kernel = null, array $transPaths = [], array $codePaths = []): CommandTester { $translator = $this->createMock(Translator::class); $translator @@ -214,7 +219,7 @@ function ($path, $catalogue) use ($loadedMessages) { $application = new Application($kernel); $application->add($command); - return new CommandTester($application->find('translation:update')); + return new CommandTester($application->find('translation:extract')); } private function getBundle($path) diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/WorkflowDumpCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/WorkflowDumpCommandTest.php new file mode 100644 index 0000000000000..13a63b40d97fa --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/WorkflowDumpCommandTest.php @@ -0,0 +1,38 @@ + + * + * 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\WorkflowDumpCommand; +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Tester\CommandCompletionTester; + +class WorkflowDumpCommandTest extends TestCase +{ + /** + * @dataProvider provideCompletionSuggestions + */ + public function testComplete(array $input, array $expectedSuggestions) + { + $application = new Application(); + $application->add(new WorkflowDumpCommand([])); + + $tester = new CommandCompletionTester($application->find('workflow:dump')); + $suggestions = $tester->complete($input, 2); + $this->assertSame($expectedSuggestions, $suggestions); + } + + public function provideCompletionSuggestions(): iterable + { + yield 'option --dump-format' => [['--dump-format', ''], ['puml', 'mermaid', 'dot']]; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/XliffLintCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/XliffLintCommandTest.php index 2121929cba610..2de30d44939e5 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/XliffLintCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/XliffLintCommandTest.php @@ -51,7 +51,7 @@ public function testLintFilesFromBundleDirectory() ['verbosity' => OutputInterface::VERBOSITY_VERBOSE, 'decorated' => false] ); - $this->assertEquals(0, $tester->getStatusCode(), 'Returns 0 in case of success'); + $tester->assertCommandIsSuccessful('Returns 0 in case of success'); $this->assertStringContainsString('[OK] All 0 XLIFF files contain valid syntax', trim($tester->getDisplay())); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/YamlLintCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/YamlLintCommandTest.php index 0644c45ddfbad..37d6d8f7fa888 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/YamlLintCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/YamlLintCommandTest.php @@ -40,7 +40,7 @@ public function testLintCorrectFile() ['verbosity' => OutputInterface::VERBOSITY_VERBOSE, 'decorated' => false] ); - $this->assertEquals(0, $tester->getStatusCode(), 'Returns 0 in case of success'); + $tester->assertCommandIsSuccessful('Returns 0 in case of success'); $this->assertStringContainsString('OK', trim($tester->getDisplay())); } @@ -88,7 +88,7 @@ public function testLintFilesFromBundleDirectory() ['verbosity' => OutputInterface::VERBOSITY_VERBOSE, 'decorated' => false] ); - $this->assertEquals(0, $tester->getStatusCode(), 'Returns 0 in case of success'); + $tester->assertCommandIsSuccessful('Returns 0 in case of success'); $this->assertStringContainsString('[OK] All 0 YAML files contain valid syntax', trim($tester->getDisplay())); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php index 0dae2d3180653..d075ad75f32c1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php @@ -147,7 +147,7 @@ public function testRunOnlyWarnsOnUnregistrableCommand() $tester->run(['command' => 'fine']); $output = $tester->getDisplay(); - $this->assertSame(0, $tester->getStatusCode()); + $tester->assertCommandIsSuccessful(); $this->assertStringContainsString('Some commands could not be registered:', $output); $this->assertStringContainsString('throwing', $output); $this->assertStringContainsString('fine', $output); @@ -204,7 +204,7 @@ public function testRunOnlyWarnsOnUnregistrableCommandAtTheEnd() $tester = new ApplicationTester($application); $tester->run(['command' => 'list']); - $this->assertSame(0, $tester->getStatusCode()); + $tester->assertCommandIsSuccessful(); $display = explode('List commands', $tester->getDisplay()); $this->assertStringContainsString(trim('[WARNING] Some commands could not be registered:'), trim($display[1])); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/AbstractControllerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/AbstractControllerTest.php index c6353acdb75c7..9a5c5510ce14e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/AbstractControllerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/AbstractControllerTest.php @@ -138,7 +138,7 @@ public function testForward() public function testGetUser() { $user = new InMemoryUser('user', 'pass'); - $token = new UsernamePasswordToken($user, 'pass', 'default', ['ROLE_USER']); + $token = new UsernamePasswordToken($user, 'default', ['ROLE_USER']); $controller = $this->createController(); $controller->setContainer($this->getContainerWithTokenStorage($token)); @@ -146,6 +146,9 @@ public function testGetUser() $this->assertSame($controller->getUser(), $user); } + /** + * @group legacy + */ public function testGetUserAnonymousUserConvertedToNull() { $token = new AnonymousToken('default', 'anon.'); @@ -601,6 +604,9 @@ public function testCreateFormBuilder() $this->assertEquals($formBuilder, $controller->createFormBuilder('foo')); } + /** + * @group legacy + */ public function testGetDoctrine() { $doctrine = $this->createMock(ManagerRegistry::class); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/TemplateControllerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/TemplateControllerTest.php index ba31544b06165..e567342ecf4c9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/TemplateControllerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/TemplateControllerTest.php @@ -35,7 +35,7 @@ public function testTwig() public function testNoTwig() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('You can not use the TemplateController if the Twig Bundle is not available.'); + $this->expectExceptionMessage('You cannot use the TemplateController if the Twig Bundle is not available.'); $controller = new TemplateController(); $controller->templateAction('mytemplate')->getContent(); @@ -59,4 +59,19 @@ public function testContext() $this->assertEquals($expected, $controller->templateAction($templateName, null, null, null, $context)->getContent()); $this->assertEquals($expected, $controller($templateName, null, null, null, $context)->getContent()); } + + public function testStatusCode() + { + $templateName = 'template_controller.html.twig'; + $statusCode = 201; + + $loader = new ArrayLoader(); + $loader->setTemplate($templateName, '

{{param}}

'); + + $twig = new Environment($loader); + $controller = new TemplateController($twig); + + $this->assertSame(201, $controller->templateAction($templateName, null, null, null, [], $statusCode)->getStatusCode()); + $this->assertSame(200, $controller->templateAction($templateName)->getStatusCode()); + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/TestServiceContainerRefPassesTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/TestServiceContainerRefPassesTest.php index 7b2adfcf6eadd..7dc9e6f59ec99 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/TestServiceContainerRefPassesTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/TestServiceContainerRefPassesTest.php @@ -55,11 +55,12 @@ public function testProcess() 'Test\private_used_shared_service' => new ServiceClosureArgument(new Reference('Test\private_used_shared_service')), 'Test\private_used_non_shared_service' => new ServiceClosureArgument(new Reference('Test\private_used_non_shared_service')), 'Test\soon_private_service' => new ServiceClosureArgument(new Reference('.container.private.Test\soon_private_service')), - 'Psr\Container\ContainerInterface' => new ServiceClosureArgument(new Reference('service_container')), - 'Symfony\Component\DependencyInjection\ContainerInterface' => new ServiceClosureArgument(new Reference('service_container')), ]; - $this->assertEquals($expected, $container->getDefinition('test.private_services_locator')->getArgument(0)); - $this->assertSame($container, $container->get('test.private_services_locator')->get('Psr\Container\ContainerInterface')); + + $privateServices = $container->getDefinition('test.private_services_locator')->getArgument(0); + unset($privateServices['Symfony\Component\DependencyInjection\ContainerInterface'], $privateServices['Psr\Container\ContainerInterface']); + + $this->assertEquals($expected, $privateServices); $this->assertFalse($container->getDefinition('Test\private_used_non_shared_service')->isShared()); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/UnusedTagsPassTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/UnusedTagsPassTest.php index 89a35285ba234..433b798d81a94 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/UnusedTagsPassTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/UnusedTagsPassTest.php @@ -43,13 +43,13 @@ public function testMissingKnownTags() private function getKnownTags(): array { - // get tags in UnusedTagsPass - $target = \dirname(__DIR__, 3).'/DependencyInjection/Compiler/UnusedTagsPass.php'; - $contents = file_get_contents($target); - preg_match('{private \$knownTags = \[(.+?)\];}sm', $contents, $matches); - $tags = array_values(array_filter(array_map(function ($str) { - return trim(preg_replace('{^ +\'(.+)\',}', '$1', $str)); - }, explode("\n", $matches[1])))); + $tags = \Closure::bind( + static function () { + return UnusedTagsPass::KNOWN_TAGS; + }, + null, + UnusedTagsPass::class + )(); sort($tags); return $tags; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php index 8273beafdcfb9..8e3dc42faffb3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -86,6 +86,7 @@ public function testAssetsCanBeEnabled() 'base_urls' => [], 'packages' => [], 'json_manifest_path' => null, + 'strict_mode' => false, ]; $this->assertEquals($defaultConfig, $config['assets']); @@ -369,6 +370,9 @@ protected static function getBundleDefaultConfig() 'http_method_override' => true, 'ide' => null, 'default_locale' => 'en', + 'enabled_locales' => [], + 'set_locale_from_accept_language' => false, + 'set_content_language_from_locale' => false, 'secret' => 's3cr3t', 'trusted_hosts' => [], 'trusted_headers' => [ @@ -401,6 +405,7 @@ protected static function getBundleDefaultConfig() 'only_main_requests' => false, 'dsn' => 'file:%kernel.cache_dir%/profiler', 'collect' => true, + 'collect_parameter' => null, ], 'translator' => [ 'enabled' => !class_exists(FullStack::class), @@ -442,6 +447,7 @@ protected static function getBundleDefaultConfig() 'enabled' => true, ], 'serializer' => [ + 'default_context' => [], 'enabled' => !class_exists(FullStack::class), 'enable_annotations' => !class_exists(FullStack::class), 'mapping' => ['paths' => []], @@ -489,12 +495,13 @@ protected static function getBundleDefaultConfig() 'base_urls' => [], 'packages' => [], 'json_manifest_path' => null, + 'strict_mode' => false, ], 'cache' => [ 'pools' => [], 'app' => 'cache.adapter.filesystem', 'system' => 'cache.adapter.system', - 'directory' => '%kernel.cache_dir%/pools', + 'directory' => '%kernel.cache_dir%/pools/app', 'default_redis_provider' => 'redis://localhost', 'default_memcached_provider' => 'memcached://localhost', 'default_pdo_provider' => ContainerBuilder::willBeAvailable('doctrine/dbal', Connection::class, ['symfony/framework-bundle']) ? 'database_connection' : null, @@ -533,6 +540,7 @@ class_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphor ], 'default_bus' => null, 'buses' => ['messenger.bus.default' => ['default_middleware' => true, 'middleware' => []]], + 'reset_on_message' => null, ], 'disallow_search_engine_index' => true, 'http_client' => [ @@ -576,6 +584,7 @@ class_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphor 'name_based_uuid_version' => 5, 'time_based_uuid_version' => 6, ], + 'exceptions' => [], ]; } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/assets.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/assets.php index ab16a52e21e9b..f26621001c9ec 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/assets.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/assets.php @@ -36,6 +36,10 @@ 'env_manifest' => [ 'json_manifest_path' => '%env(env_manifest)%', ], + 'strict_manifest_strategy' => [ + 'json_manifest_path' => '/path/to/manifest.json', + 'strict_mode' => true, + ], ], ], ]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/cache.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/cache.php index a060c13f930cd..9ca04b6c63bf9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/cache.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/cache.php @@ -7,11 +7,6 @@ 'adapter' => 'cache.adapter.apcu', 'default_lifetime' => 30, ], - 'cache.bar' => [ - 'adapter' => 'cache.adapter.doctrine', - 'default_lifetime' => 5, - 'provider' => 'app.doctrine_cache_provider', - ], 'cache.baz' => [ 'adapter' => 'cache.adapter.filesystem', 'default_lifetime' => 7, diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/doctrine_cache.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/doctrine_cache.php new file mode 100644 index 0000000000000..f16fbbf2505f3 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/doctrine_cache.php @@ -0,0 +1,13 @@ +loadFromExtension('framework', [ + 'cache' => [ + 'pools' => [ + 'cache.bar' => [ + 'adapter' => 'cache.adapter.doctrine', + 'default_lifetime' => 5, + 'provider' => 'app.doctrine_cache_provider', + ], + ], + ], +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/exceptions.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/exceptions.php new file mode 100644 index 0000000000000..5d0dde0e0ac64 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/exceptions.php @@ -0,0 +1,12 @@ +loadFromExtension('framework', [ + 'exceptions' => [ + BadRequestHttpException::class => [ + 'log_level' => 'info', + 'status_code' => 422, + ], + ], +]); 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 7aa6c50135b80..6c73a4e43d3a7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/full.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/full.php @@ -3,6 +3,7 @@ $container->loadFromExtension('framework', [ 'secret' => 's3cr3t', 'default_locale' => 'fr', + 'enabled_locales' => ['fr', 'en'], 'csrf_protection' => true, 'form' => [ 'csrf_protection' => [ @@ -51,7 +52,6 @@ 'fallback' => 'fr', 'paths' => ['%kernel.project_dir%/Fixtures/translations'], 'cache_dir' => '%kernel.cache_dir%/translations', - 'enabled_locales' => ['fr', 'en'], ], 'validation' => [ 'enabled' => true, @@ -67,6 +67,7 @@ 'name_converter' => 'serializer.name_converter.camel_case_to_snake_case', 'circular_reference_handler' => 'my.circular.reference.handler', 'max_depth_handler' => 'my.max.depth.handler', + 'default_context' => ['enable_max_depth' => true], ], 'property_info' => true, 'ide' => 'file%%link%%format', diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/legacy_translator_enabled_locales.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/legacy_translator_enabled_locales.php new file mode 100644 index 0000000000000..a585c6ee5de6d --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/legacy_translator_enabled_locales.php @@ -0,0 +1,18 @@ +loadFromExtension('framework', [ + 'secret' => 's3cr3t', + 'default_locale' => 'fr', + 'router' => [ + 'resource' => '%kernel.project_dir%/config/routing.xml', + 'type' => 'xml', + 'utf8' => true, + ], + 'translator' => [ + 'enabled' => true, + 'fallback' => 'fr', + 'paths' => ['%kernel.project_dir%/Fixtures/translations'], + 'cache_dir' => '%kernel.cache_dir%/translations', + 'enabled_locales' => ['fr', 'en'], + ], +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger.php index adb8239d04737..73102d522db57 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger.php @@ -5,6 +5,7 @@ $container->loadFromExtension('framework', [ 'messenger' => [ + 'reset_on_message' => true, 'routing' => [ FooMessage::class => ['sender.bar', 'sender.biz'], BarMessage::class => 'sender.foo', diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_middleware_factory_erroneous_format.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_middleware_factory_erroneous_format.php index cb4ee5e5127b9..e84240008a610 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_middleware_factory_erroneous_format.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_middleware_factory_erroneous_format.php @@ -2,6 +2,7 @@ $container->loadFromExtension('framework', [ 'messenger' => [ + 'reset_on_message' => true, 'buses' => [ 'command_bus' => [ 'middleware' => [ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_multiple_buses.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_multiple_buses.php index 627e21f3084e9..bc944c660f79e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_multiple_buses.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_multiple_buses.php @@ -2,6 +2,7 @@ $container->loadFromExtension('framework', [ 'messenger' => [ + 'reset_on_message' => true, 'default_bus' => 'messenger.bus.commands', 'buses' => [ 'messenger.bus.commands' => null, diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_multiple_failure_transports.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_multiple_failure_transports.php index 8f85259aa6908..08d9f95a3106c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_multiple_failure_transports.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_multiple_failure_transports.php @@ -2,6 +2,7 @@ $container->loadFromExtension('framework', [ 'messenger' => [ + 'reset_on_message' => true, 'transports' => [ 'transport_1' => [ 'dsn' => 'null://', diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_multiple_failure_transports_global.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_multiple_failure_transports_global.php index 0cff76887b152..184daa165e17d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_multiple_failure_transports_global.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_multiple_failure_transports_global.php @@ -3,6 +3,7 @@ $container->loadFromExtension('framework', [ 'messenger' => [ 'failure_transport' => 'failure_transport_global', + 'reset_on_message' => true, 'transports' => [ 'transport_1' => [ 'dsn' => 'null://', diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_routing.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_routing.php index eb459509015dd..3aaeaffe36d78 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_routing.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_routing.php @@ -3,6 +3,7 @@ $container->loadFromExtension('framework', [ 'serializer' => true, 'messenger' => [ + 'reset_on_message' => true, 'serializer' => [ 'default_serializer' => 'messenger.transport.symfony_serializer', ], diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_routing_invalid_transport.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_routing_invalid_transport.php index ee77e3a3f2dbf..2d31fe5d0e821 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_routing_invalid_transport.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_routing_invalid_transport.php @@ -3,6 +3,7 @@ $container->loadFromExtension('framework', [ 'serializer' => true, 'messenger' => [ + 'reset_on_message' => true, 'serializer' => [ 'default_serializer' => 'messenger.transport.symfony_serializer', ], diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_routing_single.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_routing_single.php index e58814589b870..594a79171602c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_routing_single.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_routing_single.php @@ -2,6 +2,7 @@ $container->loadFromExtension('framework', [ 'messenger' => [ + 'reset_on_message' => true, 'routing' => [ 'Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\DummyMessage' => ['amqp'], ], diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_transport.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_transport.php index 7baab29dc57ce..352f244a4f201 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_transport.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_transport.php @@ -3,6 +3,7 @@ $container->loadFromExtension('framework', [ 'serializer' => true, 'messenger' => [ + 'reset_on_message' => true, 'serializer' => [ 'default_serializer' => 'messenger.transport.symfony_serializer', 'symfony_serializer' => [ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_transports.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_transports.php index 90c5def3ac100..746415729bb7e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_transports.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_transports.php @@ -4,6 +4,7 @@ 'serializer' => true, 'messenger' => [ 'failure_transport' => 'failed', + 'reset_on_message' => true, 'serializer' => [ 'default_serializer' => 'messenger.transport.symfony_serializer', ], diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_with_disabled_reset_on_message.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_with_disabled_reset_on_message.php new file mode 100644 index 0000000000000..dda2e30108b87 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_with_disabled_reset_on_message.php @@ -0,0 +1,19 @@ +loadFromExtension('framework', [ + 'messenger' => [ + 'reset_on_message' => false, + 'routing' => [ + FooMessage::class => ['sender.bar', 'sender.biz'], + BarMessage::class => 'sender.foo', + ], + 'transports' => [ + 'sender.biz' => 'null://', + 'sender.bar' => 'null://', + 'sender.foo' => 'null://', + ], + ], +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_without_reset_on_message_legacy.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_without_reset_on_message_legacy.php new file mode 100644 index 0000000000000..adb8239d04737 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_without_reset_on_message_legacy.php @@ -0,0 +1,18 @@ +loadFromExtension('framework', [ + 'messenger' => [ + 'routing' => [ + FooMessage::class => ['sender.bar', 'sender.biz'], + BarMessage::class => 'sender.foo', + ], + 'transports' => [ + 'sender.biz' => 'null://', + 'sender.bar' => 'null://', + 'sender.foo' => 'null://', + ], + ], +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/notifier.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/notifier.php index 5ffe142be4dfc..51697db21c3de 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/notifier.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/notifier.php @@ -5,7 +5,8 @@ $container->loadFromExtension('framework', [ 'messenger' => [ - 'enabled' => true + 'enabled' => true, + 'reset_on_message' => true, ], 'mailer' => [ 'dsn' => 'smtp://example.com', diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/notifier_without_mailer.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/notifier_without_mailer.php index 6d51ef98517f4..f6f5366523507 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/notifier_without_mailer.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/notifier_without_mailer.php @@ -9,6 +9,7 @@ ], 'messenger' => [ 'enabled' => true, + 'reset_on_message' => true, ], 'notifier' => [ 'enabled' => true, diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/assets.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/assets.xml index ae0e0e099bc93..dadee4529d8b5 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/assets.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/assets.xml @@ -25,6 +25,7 @@ + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/cache.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/cache.xml index 2750715f6b7e2..7c75178c8cf0a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/cache.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/cache.xml @@ -8,7 +8,6 @@ - diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/doctrine_cache.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/doctrine_cache.xml new file mode 100644 index 0000000000000..3a367716831bd --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/doctrine_cache.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/exceptions.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/exceptions.xml new file mode 100644 index 0000000000000..cc73b8de3ced6 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/exceptions.xml @@ -0,0 +1,13 @@ + + + + + + + + + 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 4641e702677cb..2e115a5aebbc0 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/full.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/full.xml @@ -7,6 +7,8 @@ http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + fr + en @@ -28,12 +30,14 @@ %kernel.project_dir%/Fixtures/translations - fr - en - + + + true + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/legacy_translator_enabled_locales.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/legacy_translator_enabled_locales.xml new file mode 100644 index 0000000000000..91139d9d0af3f --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/legacy_translator_enabled_locales.xml @@ -0,0 +1,17 @@ + + + + + + + + %kernel.project_dir%/Fixtures/translations + fr + en + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger.xml index bacd772dcb6fc..1451bb66f516d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger.xml @@ -6,7 +6,7 @@ http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> - + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_buses.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_buses.xml index 1642e57988505..923b6a9579aa7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_buses.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_buses.xml @@ -6,7 +6,7 @@ http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> - + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_failure_transports.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_failure_transports.xml index b8e9f19759429..439575ccb03fe 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_failure_transports.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_failure_transports.xml @@ -6,7 +6,7 @@ http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> - + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_failure_transports_global.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_failure_transports_global.xml index c6e5c530fda1b..ddd0fa598fab7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_failure_transports_global.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_failure_transports_global.xml @@ -6,7 +6,7 @@ http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> - + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_routing.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_routing.xml index 0b022e78a0c8c..89608adf6b569 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_routing.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_routing.xml @@ -7,7 +7,7 @@ - + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_routing_invalid_transport.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_routing_invalid_transport.xml index 98c487fbf8bfa..63d9035692181 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_routing_invalid_transport.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_routing_invalid_transport.xml @@ -7,7 +7,7 @@ - + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_routing_single.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_routing_single.xml index 349a3728d3935..5ce5029991be1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_routing_single.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_routing_single.xml @@ -6,7 +6,7 @@ http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> - + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_transport.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_transport.xml index e5e60a39823a6..b822ab6b95aa0 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_transport.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_transport.xml @@ -7,7 +7,7 @@ - + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_transports.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_transports.xml index b0510d580ceaf..f6637f891a040 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_transports.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_transports.xml @@ -7,7 +7,7 @@ - + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_with_disabled_reset_on_message.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_with_disabled_reset_on_message.xml new file mode 100644 index 0000000000000..67a2a414e8fcf --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_with_disabled_reset_on_message.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_without_reset_on_message_legacy.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_without_reset_on_message_legacy.xml new file mode 100644 index 0000000000000..bacd772dcb6fc --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_without_reset_on_message_legacy.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/notifier.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/notifier.xml index 47e2e2b0c1b13..0913327ed1771 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/notifier.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/notifier.xml @@ -6,7 +6,7 @@ http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> - + null diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/notifier_without_mailer.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/notifier_without_mailer.xml index 1c62b5265b897..107a235fae369 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/notifier_without_mailer.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/notifier_without_mailer.xml @@ -7,7 +7,7 @@ - + null null diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/assets.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/assets.yml index ab9eb1b610ce8..cfd4f07b04346 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/assets.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/assets.yml @@ -25,6 +25,9 @@ framework: json_manifest_path: '%var_json_manifest_path%' env_manifest: json_manifest_path: '%env(env_manifest)%' + strict_manifest_strategy: + json_manifest_path: '/path/to/manifest.json' + strict_mode: true parameters: var_json_manifest_path: 'https://cdn.example.com/manifest.json' diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/cache.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/cache.yml index 8c9e10b82ee6c..c89c027f5aecf 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/cache.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/cache.yml @@ -4,10 +4,6 @@ framework: cache.foo: adapter: cache.adapter.apcu default_lifetime: 30 - cache.bar: - adapter: cache.adapter.doctrine - default_lifetime: 5 - provider: app.doctrine_cache_provider cache.baz: adapter: cache.adapter.filesystem default_lifetime: 7 diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/doctrine_cache.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/doctrine_cache.yml new file mode 100644 index 0000000000000..4452cd69c847f --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/doctrine_cache.yml @@ -0,0 +1,7 @@ +framework: + cache: + pools: + cache.bar: + adapter: cache.adapter.doctrine + default_lifetime: 5 + provider: app.doctrine_cache_provider diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/exceptions.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/exceptions.yml new file mode 100644 index 0000000000000..82fab4e04a9f9 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/exceptions.yml @@ -0,0 +1,5 @@ +framework: + exceptions: + Symfony\Component\HttpKernel\Exception\BadRequestHttpException: + log_level: info + status_code: 422 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 67a3f1db00fef..9d188641d4037 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/full.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/full.yml @@ -1,6 +1,7 @@ framework: secret: s3cr3t default_locale: fr + enabled_locales: ['fr', 'en'] csrf_protection: true form: csrf_protection: @@ -42,7 +43,6 @@ framework: default_path: '%kernel.project_dir%/translations' cache_dir: '%kernel.cache_dir%/translations' paths: ['%kernel.project_dir%/Fixtures/translations'] - enabled_locales: [fr, en] validation: enabled: true annotations: @@ -55,6 +55,8 @@ framework: name_converter: serializer.name_converter.camel_case_to_snake_case circular_reference_handler: my.circular.reference.handler max_depth_handler: my.max.depth.handler + default_context: + enable_max_depth: true property_info: ~ ide: file%%link%%format request: diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/legacy_translator_enabled_locales.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/legacy_translator_enabled_locales.yml new file mode 100644 index 0000000000000..fd3f574b37164 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/legacy_translator_enabled_locales.yml @@ -0,0 +1,14 @@ +framework: + secret: s3cr3t + default_locale: fr + router: + resource: '%kernel.project_dir%/config/routing.xml' + type: xml + utf8: true + translator: + enabled: true + fallback: fr + default_path: '%kernel.project_dir%/translations' + cache_dir: '%kernel.cache_dir%/translations' + paths: ['%kernel.project_dir%/Fixtures/translations'] + enabled_locales: [ 'fr', 'en' ] diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger.yml index 82fea3b27af23..3bf374f474c75 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger.yml @@ -1,5 +1,6 @@ framework: messenger: + reset_on_message: true routing: 'Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\FooMessage': ['sender.bar', 'sender.biz'] 'Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\BarMessage': 'sender.foo' diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_middleware_factory_erroneous_format.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_middleware_factory_erroneous_format.yml index 74431414ba99c..a55251f4da062 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_middleware_factory_erroneous_format.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_middleware_factory_erroneous_format.yml @@ -1,5 +1,6 @@ framework: messenger: + reset_on_message: true buses: command_bus: middleware: diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_buses.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_buses.yml index 0e67039733272..8b0d2b98ef126 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_buses.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_buses.yml @@ -1,5 +1,6 @@ framework: messenger: + reset_on_message: true default_bus: messenger.bus.commands buses: messenger.bus.commands: ~ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_failure_transports.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_failure_transports.yml index 863f18a7d1a1f..ba296162d6d8f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_failure_transports.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_failure_transports.yml @@ -1,5 +1,6 @@ framework: messenger: + reset_on_message: true transports: transport_1: dsn: 'null://' diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_failure_transports_global.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_failure_transports_global.yml index 10023edb0b9fd..6ca54c277a5d4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_failure_transports_global.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_failure_transports_global.yml @@ -1,5 +1,6 @@ framework: messenger: + reset_on_message: true failure_transport: failure_transport_global transports: transport_1: diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_routing.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_routing.yml index 0e493eb882a02..dcde58a026b51 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_routing.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_routing.yml @@ -1,6 +1,7 @@ framework: serializer: true messenger: + reset_on_message: true serializer: default_serializer: messenger.transport.symfony_serializer routing: diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_routing_invalid_transport.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_routing_invalid_transport.yml index 3bf0f2ddf9c03..65f6de8ffa319 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_routing_invalid_transport.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_routing_invalid_transport.yml @@ -1,6 +1,7 @@ framework: serializer: true messenger: + reset_on_message: true serializer: default_serializer: messenger.transport.symfony_serializer routing: diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_routing_single.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_routing_single.yml index caa88641887c7..6957cb4bf35b2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_routing_single.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_routing_single.yml @@ -1,5 +1,6 @@ framework: messenger: + reset_on_message: true routing: 'Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\DummyMessage': [amqp] diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_transport.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_transport.yml index b51feb73bce95..6df55ccd19941 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_transport.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_transport.yml @@ -1,6 +1,7 @@ framework: serializer: true messenger: + reset_on_message: true serializer: default_serializer: messenger.transport.symfony_serializer symfony_serializer: diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_transports.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_transports.yml index d00f4a65dd37c..555f512daae07 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_transports.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_transports.yml @@ -2,6 +2,7 @@ framework: serializer: true messenger: failure_transport: failed + reset_on_message: true serializer: default_serializer: messenger.transport.symfony_serializer transports: diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_with_disabled_reset_on_message.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_with_disabled_reset_on_message.yml new file mode 100644 index 0000000000000..f67395c85c191 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_with_disabled_reset_on_message.yml @@ -0,0 +1,10 @@ +framework: + messenger: + reset_on_message: false + routing: + 'Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\FooMessage': ['sender.bar', 'sender.biz'] + 'Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\BarMessage': 'sender.foo' + transports: + sender.biz: 'null://' + sender.bar: 'null://' + sender.foo: 'null://' diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_without_reset_on_message_legacy.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_without_reset_on_message_legacy.yml new file mode 100644 index 0000000000000..82fea3b27af23 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_without_reset_on_message_legacy.yml @@ -0,0 +1,9 @@ +framework: + messenger: + routing: + 'Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\FooMessage': ['sender.bar', 'sender.biz'] + 'Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\BarMessage': 'sender.foo' + transports: + sender.biz: 'null://' + sender.bar: 'null://' + sender.foo: 'null://' diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/notifier.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/notifier.yml index 586cb98a4a138..e03dd738b8b96 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/notifier.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/notifier.yml @@ -1,6 +1,7 @@ framework: messenger: enabled: true + reset_on_message: true mailer: dsn: 'smtp://example.com' notifier: diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/notifier_without_mailer.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/notifier_without_mailer.yml index 75fa3cf889825..2582aaf438992 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/notifier_without_mailer.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/notifier_without_mailer.yml @@ -3,6 +3,7 @@ framework: enabled: false messenger: enabled: true + reset_on_message: true notifier: enabled: true notification_on_failed_messages: true diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index bd311d113895b..adcf065338035 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -12,7 +12,6 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection; use Doctrine\Common\Annotations\Annotation; -use Doctrine\Common\Annotations\PsrCachedReader; use Psr\Cache\CacheItemPoolInterface; use Psr\Log\LoggerAwareInterface; use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; @@ -522,6 +521,18 @@ public function testPhpErrorsWithLogLevels() ], $definition->getArgument(2)); } + public function testExceptionsConfig() + { + $container = $this->createContainerFromFile('exceptions'); + + $this->assertSame([ + \Symfony\Component\HttpKernel\Exception\BadRequestHttpException::class => [ + 'log_level' => 'info', + 'status_code' => 422, + ], + ], $container->getDefinition('exception_listener')->getArgument(3)); + } + public function testRouter() { $container = $this->createContainerFromFile('full'); @@ -535,6 +546,22 @@ public function testRouter() $this->assertSame(['_locale' => 'fr|en'], $container->getDefinition('routing.loader')->getArgument(2)); } + /** + * @group legacy + */ + public function testRouterWithLegacyTranslatorEnabledLocales() + { + $container = $this->createContainerFromFile('legacy_translator_enabled_locales'); + + $this->assertTrue($container->has('router'), '->registerRouterConfiguration() loads routing.xml'); + $arguments = $container->findDefinition('router')->getArguments(); + $this->assertEquals($container->getParameter('kernel.project_dir').'/config/routing.xml', $container->getParameter('router.resource'), '->registerRouterConfiguration() sets routing resource'); + $this->assertEquals('%router.resource%', $arguments[1], '->registerRouterConfiguration() sets routing resource'); + $this->assertEquals('xml', $arguments[2]['resource_type'], '->registerRouterConfiguration() sets routing resource type'); + + $this->assertSame(['_locale' => 'fr|en'], $container->getDefinition('routing.loader')->getArgument(2)); + } + public function testRouterRequiresResourceOption() { $this->expectException(InvalidConfigurationException::class); @@ -581,7 +608,7 @@ public function testNullSessionHandler() $this->assertNull($container->getDefinition('session.storage.factory.php_bridge')->getArgument(0)); $this->assertSame('session.handler.native_file', (string) $container->getAlias('session.handler')); - $expected = ['session', 'initialized_session', 'logger', 'session_collector']; + $expected = ['session_factory', 'session', 'initialized_session', 'logger', 'session_collector']; $this->assertEquals($expected, array_keys($container->getDefinition('session_listener')->getArgument(0)->getValues())); $this->assertFalse($container->getDefinition('session.storage.factory.native')->getArgument(3)); } @@ -600,7 +627,7 @@ public function testNullSessionHandlerLegacy() $this->assertNull($container->getDefinition('session.storage.php_bridge')->getArgument(0)); $this->assertSame('session.handler.native_file', (string) $container->getAlias('session.handler')); - $expected = ['session', 'initialized_session', 'logger', 'session_collector']; + $expected = ['session_factory', 'session', 'initialized_session', 'logger', 'session_collector']; $this->assertEquals($expected, array_keys($container->getDefinition('session_listener')->getArgument(0)->getValues())); $this->assertFalse($container->getDefinition('session.storage.factory.native')->getArgument(3)); } @@ -632,7 +659,7 @@ public function testAssets() // packages $packageTags = $container->findTaggedServiceIds('assets.package'); - $this->assertCount(9, $packageTags); + $this->assertCount(10, $packageTags); $packages = []; foreach ($packageTags as $serviceId => $tagAttributes) { @@ -658,6 +685,7 @@ public function testAssets() $versionStrategy = $container->getDefinition((string) $package->getArgument(1)); $this->assertEquals('assets.json_manifest_version_strategy', $versionStrategy->getParent()); $this->assertEquals('/path/to/manifest.json', $versionStrategy->getArgument(0)); + $this->assertFalse($versionStrategy->getArgument(2)); $package = $container->getDefinition($packages['remote_manifest']); $versionStrategy = $container->getDefinition($package->getArgument(1)); @@ -668,11 +696,19 @@ public function testAssets() $versionStrategy = $container->getDefinition($package->getArgument(1)); $this->assertSame('assets.json_manifest_version_strategy', $versionStrategy->getParent()); $this->assertSame('https://cdn.example.com/manifest.json', $versionStrategy->getArgument(0)); + $this->assertFalse($versionStrategy->getArgument(2)); $package = $container->getDefinition($packages['env_manifest']); $versionStrategy = $container->getDefinition($package->getArgument(1)); $this->assertSame('assets.json_manifest_version_strategy', $versionStrategy->getParent()); $this->assertStringMatchesFormat('env_%s', $versionStrategy->getArgument(0)); + $this->assertFalse($versionStrategy->getArgument(2)); + + $package = $container->getDefinition((string) $packages['strict_manifest_strategy']); + $versionStrategy = $container->getDefinition((string) $package->getArgument(1)); + $this->assertEquals('assets.json_manifest_version_strategy', $versionStrategy->getParent()); + $this->assertEquals('/path/to/manifest.json', $versionStrategy->getArgument(0)); + $this->assertTrue($versionStrategy->getArgument(2)); } public function testAssetsDefaultVersionStrategyAsService() @@ -704,6 +740,26 @@ public function testMessengerServicesRemovedWhenDisabled() $this->assertFalse($container->hasDefinition('cache.messenger.restart_workers_signal')); } + /** + * @group legacy + */ + public function testMessengerWithoutResetOnMessageLegacy() + { + $this->expectDeprecation('Since symfony/framework-bundle 5.4: Not setting the "framework.messenger.reset_on_message" configuration option is deprecated, it will default to "true" in version 6.0.'); + + $container = $this->createContainerFromFile('messenger_without_reset_on_message_legacy'); + + $this->assertTrue($container->hasDefinition('console.command.messenger_consume_messages')); + $this->assertTrue($container->hasAlias('messenger.default_bus')); + $this->assertTrue($container->getAlias('messenger.default_bus')->isPublic()); + $this->assertTrue($container->hasDefinition('messenger.transport.amqp.factory')); + $this->assertTrue($container->hasDefinition('messenger.transport.redis.factory')); + $this->assertTrue($container->hasDefinition('messenger.transport_factory')); + $this->assertSame(TransportFactory::class, $container->getDefinition('messenger.transport_factory')->getClass()); + $this->assertFalse($container->hasDefinition('messenger.listener.reset_services')); + $this->assertNull($container->getDefinition('console.command.messenger_consume_messages')->getArgument(5)); + } + public function testMessenger() { $container = $this->createContainerFromFile('messenger'); @@ -714,6 +770,27 @@ public function testMessenger() $this->assertTrue($container->hasDefinition('messenger.transport.redis.factory')); $this->assertTrue($container->hasDefinition('messenger.transport_factory')); $this->assertSame(TransportFactory::class, $container->getDefinition('messenger.transport_factory')->getClass()); + $this->assertTrue($container->hasDefinition('messenger.listener.reset_services')); + $this->assertSame('messenger.listener.reset_services', (string) $container->getDefinition('console.command.messenger_consume_messages')->getArgument(5)); + } + + public function testMessengerWithoutConsole() + { + $extension = $this->createPartialMock(FrameworkExtension::class, ['hasConsole', 'getAlias']); + $extension->method('hasConsole')->willReturn(false); + $extension->method('getAlias')->willReturn((new FrameworkExtension())->getAlias()); + + $container = $this->createContainerFromFile('messenger', [], true, false, $extension); + $container->compile(); + + $this->assertFalse($container->hasDefinition('console.command.messenger_consume_messages')); + $this->assertTrue($container->hasAlias('messenger.default_bus')); + $this->assertTrue($container->getAlias('messenger.default_bus')->isPublic()); + $this->assertTrue($container->hasDefinition('messenger.transport.amqp.factory')); + $this->assertTrue($container->hasDefinition('messenger.transport.redis.factory')); + $this->assertTrue($container->hasDefinition('messenger.transport_factory')); + $this->assertSame(TransportFactory::class, $container->getDefinition('messenger.transport_factory')->getClass()); + $this->assertFalse($container->hasDefinition('messenger.listener.reset_services')); } public function testMessengerMultipleFailureTransports() @@ -943,6 +1020,14 @@ public function testMessengerInvalidTransportRouting() $this->createContainerFromFile('messenger_routing_invalid_transport'); } + public function testMessengerWithDisabledResetOnMessage() + { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('The "framework.messenger.reset_on_message" configuration option can be set to "true" only. To prevent services resetting after each message you can set the "--no-reset" option in "messenger:consume" command.'); + + $this->createContainerFromFile('messenger_with_disabled_reset_on_message'); + } + public function testTranslator() { $container = $this->createContainerFromFile('full'); @@ -1080,7 +1165,7 @@ public function testAnnotations() $container->compile(); $this->assertEquals($container->getParameter('kernel.cache_dir').'/annotations', $container->getDefinition('annotations.filesystem_cache_adapter')->getArgument(2)); - $this->assertSame(class_exists(PsrCachedReader::class) ? 'annotations.filesystem_cache_adapter' : 'annotations.filesystem_cache', (string) $container->getDefinition('annotation_reader')->getArgument(1)); + $this->assertSame('annotations.filesystem_cache_adapter', (string) $container->getDefinition('annotation_reader')->getArgument(1)); } public function testFileLinkFormat() @@ -1510,7 +1595,6 @@ public function testCachePoolServices() $container->compile(); $this->assertCachePoolServiceDefinitionIsCreated($container, 'cache.foo', 'cache.adapter.apcu', 30); - $this->assertCachePoolServiceDefinitionIsCreated($container, 'cache.bar', 'cache.adapter.doctrine', 5); $this->assertCachePoolServiceDefinitionIsCreated($container, 'cache.baz', 'cache.adapter.filesystem', 7); $this->assertCachePoolServiceDefinitionIsCreated($container, 'cache.foobar', 'cache.adapter.psr6', 10); $this->assertCachePoolServiceDefinitionIsCreated($container, 'cache.def', 'cache.app', 'PT11S'); @@ -1552,6 +1636,23 @@ public function testCachePoolServices() } } + /** + * @group legacy + */ + public function testDoctrineCache() + { + if (!class_exists(DoctrineAdapter::class)) { + self::markTestSkipped('This test requires symfony/cache 5.4 or lower.'); + } + + $container = $this->createContainerFromFile('doctrine_cache', [], true, false); + $container->setParameter('cache.prefix.seed', 'test'); + $container->addCompilerPass(new CachePoolPass()); + $container->compile(); + + $this->assertCachePoolServiceDefinitionIsCreated($container, 'cache.bar', 'cache.adapter.doctrine', 5); + } + public function testRedisTagAwareAdapter() { $container = $this->createContainerFromFile('cache', [], true); @@ -1618,7 +1719,7 @@ public function testSessionCookieSecureAuto() { $container = $this->createContainerFromFile('session_cookie_secure_auto'); - $expected = ['session', 'initialized_session', 'logger', 'session_collector']; + $expected = ['session_factory', 'session', 'initialized_session', 'logger', 'session_collector']; $this->assertEquals($expected, array_keys($container->getDefinition('session_listener')->getArgument(0)->getValues())); } @@ -1631,7 +1732,7 @@ public function testSessionCookieSecureAutoLegacy() $container = $this->createContainerFromFile('session_cookie_secure_auto_legacy'); - $expected = ['session', 'initialized_session', 'logger', 'session_collector', 'session_storage', 'request_stack']; + $expected = ['session_factory', 'session', 'initialized_session', 'logger', 'session_collector', 'session_storage', 'request_stack']; $this->assertEquals($expected, array_keys($container->getDefinition('session_listener')->getArgument(0)->getValues())); } @@ -1906,14 +2007,14 @@ protected function createContainer(array $data = []) ], $data))); } - protected function createContainerFromFile($file, $data = [], $resetCompilerPasses = true, $compile = true) + protected function createContainerFromFile($file, $data = [], $resetCompilerPasses = true, $compile = true, FrameworkExtension $extension = null) { $cacheKey = md5(static::class.$file.serialize($data)); if ($compile && isset(self::$containerCache[$cacheKey])) { return self::$containerCache[$cacheKey]; } $container = $this->createContainer($data); - $container->registerExtension(new FrameworkExtension()); + $container->registerExtension($extension ?: new FrameworkExtension()); $this->loadFromFile($container, $file); if ($resetCompilerPasses) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/AnnotatedControllerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/AnnotatedControllerTest.php index c9ede7a9cf646..20d3609c16a84 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/AnnotatedControllerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/AnnotatedControllerTest.php @@ -23,6 +23,10 @@ public function testAnnotatedController($path, $expectedValue) $this->assertSame(200, $client->getResponse()->getStatusCode()); $this->assertSame($expectedValue, $client->getResponse()->getContent()); + + $router = self::$container->get('router'); + + $this->assertSame('/annotated/create-transaction', $router->generate('symfony_framework_tests_functional_test_annotated_createtransaction')); } public function getRoutes() diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/AutowiringTypesTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/AutowiringTypesTest.php index 39707a7e09e0a..e60bb93ea22a6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/AutowiringTypesTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/AutowiringTypesTest.php @@ -12,7 +12,6 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; use Doctrine\Common\Annotations\AnnotationReader; -use Doctrine\Common\Annotations\CachedReader; use Doctrine\Common\Annotations\PsrCachedReader; use Symfony\Component\Cache\Adapter\FilesystemAdapter; use Symfony\Component\EventDispatcher\EventDispatcher; @@ -34,11 +33,7 @@ public function testCachedAnnotationReaderAutowiring() static::bootKernel(); $annotationReader = self::getContainer()->get('test.autowiring_types.autowired_services')->getAnnotationReader(); - if (class_exists(PsrCachedReader::class)) { - $this->assertInstanceOf(PsrCachedReader::class, $annotationReader); - } else { - $this->assertInstanceOf(CachedReader::class, $annotationReader); - } + $this->assertInstanceOf(PsrCachedReader::class, $annotationReader); } public function testEventDispatcherAutowiring() diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/AnnotatedController.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/AnnotatedController.php index 96543ce10f6b4..f2f077786f2b7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/AnnotatedController.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/AnnotatedController.php @@ -48,4 +48,12 @@ public function argumentWithoutDefaultWithRouteParamAndDefaultAction($value) { return new Response($value); } + + /** + * @Route("/create-transaction") + */ + public function createTransaction() + { + return new Response(); + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/CachePoolClearCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/CachePoolClearCommandTest.php index bab251bc8f219..b3885edaa3f36 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/CachePoolClearCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/CachePoolClearCommandTest.php @@ -33,7 +33,7 @@ public function testClearPrivatePool() $tester = $this->createCommandTester(); $tester->execute(['pools' => ['cache.private_pool']], ['decorated' => false]); - $this->assertSame(0, $tester->getStatusCode(), 'cache:pool:clear exits with 0 in case of success'); + $tester->assertCommandIsSuccessful('cache:pool:clear exits with 0 in case of success'); $this->assertStringContainsString('Clearing cache pool: cache.private_pool', $tester->getDisplay()); $this->assertStringContainsString('[OK] Cache was successfully cleared.', $tester->getDisplay()); } @@ -43,7 +43,7 @@ public function testClearPublicPool() $tester = $this->createCommandTester(); $tester->execute(['pools' => ['cache.public_pool']], ['decorated' => false]); - $this->assertSame(0, $tester->getStatusCode(), 'cache:pool:clear exits with 0 in case of success'); + $tester->assertCommandIsSuccessful('cache:pool:clear exits with 0 in case of success'); $this->assertStringContainsString('Clearing cache pool: cache.public_pool', $tester->getDisplay()); $this->assertStringContainsString('[OK] Cache was successfully cleared.', $tester->getDisplay()); } @@ -53,7 +53,7 @@ public function testClearPoolWithCustomClearer() $tester = $this->createCommandTester(); $tester->execute(['pools' => ['cache.pool_with_clearer']], ['decorated' => false]); - $this->assertSame(0, $tester->getStatusCode(), 'cache:pool:clear exits with 0 in case of success'); + $tester->assertCommandIsSuccessful('cache:pool:clear exits with 0 in case of success'); $this->assertStringContainsString('Clearing cache pool: cache.pool_with_clearer', $tester->getDisplay()); $this->assertStringContainsString('[OK] Cache was successfully cleared.', $tester->getDisplay()); } @@ -63,7 +63,7 @@ public function testCallClearer() $tester = $this->createCommandTester(); $tester->execute(['pools' => ['cache.app_clearer']], ['decorated' => false]); - $this->assertSame(0, $tester->getStatusCode(), 'cache:pool:clear exits with 0 in case of success'); + $tester->assertCommandIsSuccessful('cache:pool:clear exits with 0 in case of success'); $this->assertStringContainsString('Calling cache clearer: cache.app_clearer', $tester->getDisplay()); $this->assertStringContainsString('[OK] Cache was successfully cleared.', $tester->getDisplay()); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/CachePoolListCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/CachePoolListCommandTest.php index d7e5aae80c4f8..8e9061845a45e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/CachePoolListCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/CachePoolListCommandTest.php @@ -30,7 +30,7 @@ public function testListPools() $tester = $this->createCommandTester(['cache.app', 'cache.system']); $tester->execute([]); - $this->assertSame(0, $tester->getStatusCode(), 'cache:pool:list exits with 0 in case of success'); + $tester->assertCommandIsSuccessful('cache:pool:list exits with 0 in case of success'); $this->assertStringContainsString('cache.app', $tester->getDisplay()); $this->assertStringContainsString('cache.system', $tester->getDisplay()); } @@ -40,7 +40,7 @@ public function testEmptyList() $tester = $this->createCommandTester([]); $tester->execute([]); - $this->assertSame(0, $tester->getStatusCode(), 'cache:pool:list exits with 0 in case of success'); + $tester->assertCommandIsSuccessful('cache:pool:list exits with 0 in case of success'); } private function createCommandTester(array $poolNames) diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ConfigDebugCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ConfigDebugCommandTest.php index 0df853997c59a..8135f4dcfe419 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ConfigDebugCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ConfigDebugCommandTest.php @@ -11,9 +11,11 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; +use Symfony\Bundle\FrameworkBundle\Command\ConfigDebugCommand; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Output\NullOutput; +use Symfony\Component\Console\Tester\CommandCompletionTester; use Symfony\Component\Console\Tester\CommandTester; /** @@ -111,6 +113,31 @@ public function testDumpThrowsExceptionWhenDefaultConfigFallbackIsImpossible() $tester->execute(['name' => 'ExtensionWithoutConfigTestBundle']); } + /** + * @dataProvider provideCompletionSuggestions + */ + public function testComplete(array $input, array $expectedSuggestions) + { + $this->application->add(new ConfigDebugCommand()); + + $tester = new CommandCompletionTester($this->application->get('debug:config')); + + $suggestions = $tester->complete($input); + + foreach ($expectedSuggestions as $expectedSuggestion) { + $this->assertContains($expectedSuggestion, $suggestions); + } + } + + public function provideCompletionSuggestions(): \Generator + { + yield 'name' => [[''], ['default_config_test', 'extension_without_config_test', 'framework', 'test']]; + + yield 'name (started CamelCase)' => [['Fra'], ['DefaultConfigTestBundle', 'ExtensionWithoutConfigTestBundle', 'FrameworkBundle', 'TestBundle']]; + + yield 'name with existing path' => [['framework', ''], ['secret', 'router.resource', 'router.utf8', 'router.enabled', 'validation.enabled', 'default_locale']]; + } + private function createCommandTester(): CommandTester { $command = $this->application->find('debug:config'); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ConfigDumpReferenceCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ConfigDumpReferenceCommandTest.php index 2a9b05d7015e8..e86a7ff790ef7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ConfigDumpReferenceCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ConfigDumpReferenceCommandTest.php @@ -11,9 +11,11 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; +use Symfony\Bundle\FrameworkBundle\Command\ConfigDumpReferenceCommand; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Output\NullOutput; +use Symfony\Component\Console\Tester\CommandCompletionTester; use Symfony\Component\Console\Tester\CommandTester; /** @@ -81,6 +83,23 @@ public function testDumpAtPathXml() $this->assertStringContainsString('[ERROR] The "path" option is only available for the "yaml" format.', $tester->getDisplay()); } + /** + * @dataProvider provideCompletionSuggestions + */ + public function testComplete(array $input, array $expectedSuggestions) + { + $this->application->add(new ConfigDumpReferenceCommand()); + $tester = new CommandCompletionTester($this->application->get('config:dump-reference')); + $suggestions = $tester->complete($input, 2); + $this->assertSame($expectedSuggestions, $suggestions); + } + + public function provideCompletionSuggestions(): iterable + { + yield 'name' => [[''], ['DefaultConfigTestBundle', 'default_config_test', 'ExtensionWithoutConfigTestBundle', 'extension_without_config_test', 'FrameworkBundle', 'framework', 'TestBundle', 'test']]; + yield 'option --format' => [['--format', ''], ['yaml', 'xml']]; + } + private function createCommandTester(): CommandTester { $command = $this->application->find('config:dump-reference'); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ContainerDebugCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ContainerDebugCommandTest.php index 7489ec6a00b27..ff2cc045b7ac1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ContainerDebugCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ContainerDebugCommandTest.php @@ -14,6 +14,7 @@ use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\Tests\Fixtures\BackslashClass; use Symfony\Component\Console\Tester\ApplicationTester; +use Symfony\Component\Console\Tester\CommandCompletionTester; /** * @group functional @@ -161,7 +162,7 @@ public function testGetDeprecation() $tester = new ApplicationTester($application); $tester->run(['command' => 'debug:container', '--deprecations' => true]); - $this->assertSame(0, $tester->getStatusCode()); + $tester->assertCommandIsSuccessful(); $this->assertStringContainsString('Symfony\Bundle\FrameworkBundle\Controller\Controller', $tester->getDisplay()); $this->assertStringContainsString('/home/hamza/projet/contrib/sf/vendor/symfony/framework-bundle/Controller/Controller.php', $tester->getDisplay()); } @@ -181,7 +182,7 @@ public function testGetDeprecationNone() $tester = new ApplicationTester($application); $tester->run(['command' => 'debug:container', '--deprecations' => true]); - $this->assertSame(0, $tester->getStatusCode()); + $tester->assertCommandIsSuccessful(); $this->assertStringContainsString('[OK] There are no deprecations in the logs!', $tester->getDisplay()); } @@ -199,7 +200,7 @@ public function testGetDeprecationNoFile() $tester = new ApplicationTester($application); $tester->run(['command' => 'debug:container', '--deprecations' => true]); - $this->assertSame(0, $tester->getStatusCode()); + $tester->assertCommandIsSuccessful(); $this->assertStringContainsString('[WARNING] The deprecation file does not exist', $tester->getDisplay()); } @@ -211,4 +212,68 @@ public function provideIgnoreBackslashWhenFindingService() ['\\'.BackslashClass::class], ]; } + + /** + * @dataProvider provideCompletionSuggestions + */ + public function testComplete(array $input, array $expectedSuggestions, array $notExpectedSuggestions = []) + { + static::bootKernel(['test_case' => 'ContainerDebug', 'root_config' => 'config.yml', 'debug' => true]); + + $application = new Application(static::$kernel); + $tester = new CommandCompletionTester($application->find('debug:container')); + $suggestions = $tester->complete($input); + + foreach ($expectedSuggestions as $expectedSuggestion) { + $this->assertContains($expectedSuggestion, $suggestions); + } + foreach ($notExpectedSuggestions as $notExpectedSuggestion) { + $this->assertNotContains($notExpectedSuggestion, $suggestions); + } + } + + public function provideCompletionSuggestions() + { + $serviceId = 'console.command.container_debug'; + $hiddenServiceId = '.console.command.container_debug.lazy'; + $interfaceServiceId = 'Symfony\Component\HttpKernel\HttpKernelInterface'; + + yield 'name' => [ + [''], + [$serviceId, $interfaceServiceId], + [$hiddenServiceId], + ]; + + yield 'name (with hidden)' => [ + ['--show-hidden', ''], + [$serviceId, $interfaceServiceId, $hiddenServiceId], + ]; + + yield 'name (with current value)' => [ + ['--show-hidden', 'console'], + [$serviceId, $hiddenServiceId], + [$interfaceServiceId], + ]; + + yield 'name (no suggestion with --tags)' => [ + ['--tags', ''], + [], + [$serviceId, $interfaceServiceId, $hiddenServiceId], + ]; + + yield 'option --tag' => [ + ['--tag', ''], + ['console.command'], + ]; + + yield 'option --parameter' => [ + ['--parameter', ''], + ['kernel.debug'], + ]; + + yield 'option --format' => [ + ['--format', ''], + ['txt', 'xml', 'json', 'md'], + ]; + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/DebugAutowiringCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/DebugAutowiringCommandTest.php index a0ade821d5165..c3110cc71dcbb 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/DebugAutowiringCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/DebugAutowiringCommandTest.php @@ -11,8 +11,10 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; +use Symfony\Bundle\FrameworkBundle\Command\DebugAutowiringCommand; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Component\Console\Tester\ApplicationTester; +use Symfony\Component\Console\Tester\CommandCompletionTester; /** * @group functional @@ -109,4 +111,26 @@ public function testNotConfusedByClassAliases() $tester->run(['command' => 'debug:autowiring', 'search' => 'ClassAlias']); $this->assertStringContainsString('Symfony\Bundle\FrameworkBundle\Tests\Fixtures\ClassAliasExampleClass', $tester->getDisplay()); } + + /** + * @dataProvider provideCompletionSuggestions + */ + public function testComplete(array $input, array $expectedSuggestions) + { + $kernel = static::bootKernel(['test_case' => 'ContainerDebug', 'root_config' => 'config.yml']); + $command = (new Application($kernel))->add(new DebugAutowiringCommand()); + + $tester = new CommandCompletionTester($command); + + $suggestions = $tester->complete($input); + + foreach ($expectedSuggestions as $expectedSuggestion) { + $this->assertContains($expectedSuggestion, $suggestions); + } + } + + public function provideCompletionSuggestions(): \Generator + { + yield 'search' => [[''], ['SessionHandlerInterface', 'Psr\\Log\\LoggerInterface', 'Psr\\Container\\ContainerInterface $parameterBag']]; + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ProfilerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ProfilerTest.php index 35c2e63b7e04a..7b65ca5c276e8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ProfilerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ProfilerTest.php @@ -36,6 +36,27 @@ public function testProfilerIsDisabled($insulate) $this->assertNull($client->getProfile()); } + /** + * @dataProvider getConfigs + */ + public function testProfilerCollectParameter($insulate) + { + $client = $this->createClient(['test_case' => 'ProfilerCollectParameter', 'root_config' => 'config.yml']); + if ($insulate) { + $client->insulate(); + } + + $client->request('GET', '/profiler'); + $this->assertNull($client->getProfile()); + + // enable the profiler for the next request + $client->request('GET', '/profiler?profile=1'); + $this->assertIsObject($client->getProfile()); + + $client->request('GET', '/profiler'); + $this->assertNull($client->getProfile()); + } + public function getConfigs() { return [ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/RouterDebugCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/RouterDebugCommandTest.php index b7cf74798a232..e9a8cd143b802 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/RouterDebugCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/RouterDebugCommandTest.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; use Symfony\Bundle\FrameworkBundle\Console\Application; +use Symfony\Component\Console\Tester\CommandCompletionTester; use Symfony\Component\Console\Tester\CommandTester; /** @@ -69,6 +70,37 @@ public function testSearchWithThrow() $tester->execute(['name' => 'gerard'], ['interactive' => true]); } + /** + * @dataProvider provideCompletionSuggestions + */ + public function testComplete(array $input, array $expectedSuggestions) + { + if (!class_exists(CommandCompletionTester::class)) { + $this->markTestSkipped('Test command completion requires symfony/console 5.4+.'); + } + + $tester = new CommandCompletionTester($this->application->get('debug:router')); + $this->assertSame($expectedSuggestions, $tester->complete($input)); + } + + public function provideCompletionSuggestions() + { + yield 'option --format' => [ + ['--format', ''], + ['txt', 'xml', 'json', 'md'], + ]; + + yield 'route_name' => [ + [''], + [ + 'routerdebug_session_welcome', + 'routerdebug_session_welcome_name', + 'routerdebug_session_logout', + 'routerdebug_test', + ], + ]; + } + private function createCommandTester(): CommandTester { $command = $this->application->get('debug:router'); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/AppKernel.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/AppKernel.php index a666dfc4778fb..c66074b132695 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/AppKernel.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/AppKernel.php @@ -106,7 +106,7 @@ protected function getKernelParameters(): array return $parameters; } - public function getConfigTreeBuilder() + public function getConfigTreeBuilder(): TreeBuilder { $treeBuilder = new TreeBuilder('foo'); $rootNode = $treeBuilder->getRootNode(); @@ -119,7 +119,7 @@ public function load(array $configs, ContainerBuilder $container) { } - public function getNamespace() + public function getNamespace(): string { return ''; } @@ -129,7 +129,7 @@ public function getXsdValidationBasePath() return false; } - public function getAlias() + public function getAlias(): string { return 'foo'; } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ConfigDump/config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ConfigDump/config.yml index 9d7765d5e583e..6dba635a15555 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ConfigDump/config.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ConfigDump/config.yml @@ -4,6 +4,7 @@ imports: framework: secret: '%secret%' default_locale: '%env(LOCALE)%' + enabled_locales: ['%env(LOCALE)%'] session: storage_factory_id: session.storage.factory.native cookie_httponly: '%env(bool:COOKIE_HTTPONLY)%' diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ProfilerCollectParameter/bundles.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ProfilerCollectParameter/bundles.php new file mode 100644 index 0000000000000..15ff182c6fed5 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ProfilerCollectParameter/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/ProfilerCollectParameter/config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ProfilerCollectParameter/config.yml new file mode 100644 index 0000000000000..67360dd321681 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ProfilerCollectParameter/config.yml @@ -0,0 +1,8 @@ +imports: + - { resource: ../config/default.yml } + +framework: + profiler: + enabled: true + collect: false + collect_parameter: profile diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ProfilerCollectParameter/routing.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ProfilerCollectParameter/routing.yml new file mode 100644 index 0000000000000..d4b77c3f703d9 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ProfilerCollectParameter/routing.yml @@ -0,0 +1,2 @@ +_sessiontest_bundle: + resource: '@TestBundle/Resources/config/routing.yml' diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Slugger/config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Slugger/config.yml index f80091b831e05..669edf5667611 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Slugger/config.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Slugger/config.yml @@ -5,6 +5,7 @@ imports: framework: secret: '%secret%' default_locale: '%env(LOCALE)%' + enabled_locales: ['%env(LOCALE)%'] translator: fallbacks: - '%env(LOCALE)%' diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/TransDebug/config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/TransDebug/config.yml index 7f8815b2942fa..1cd6417b937b1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/TransDebug/config.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/TransDebug/config.yml @@ -5,6 +5,7 @@ imports: framework: secret: '%secret%' default_locale: '%env(LOCALE)%' + enabled_locales: ['%env(LOCALE)%'] translator: fallbacks: - '%env(LOCALE)%' diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/config/framework.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/config/framework.yml index bfe7e24b338d7..81cad57ca92c8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/config/framework.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/config/framework.yml @@ -8,6 +8,7 @@ framework: legacy_error_messages: false test: true default_locale: en + enabled_locales: ['en', 'fr'] session: storage_factory_id: session.storage.factory.mock_file diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/ConcreteMicroKernel.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/ConcreteMicroKernel.php index 758ca34784033..d9dd700efd92e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/ConcreteMicroKernel.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/ConcreteMicroKernel.php @@ -79,7 +79,7 @@ protected function configureRoutes(RoutingConfigurator $routes): void $routes->add('danger', '/danger')->controller('kernel::dangerousAction'); } - protected function configureContainer(ContainerBuilder $c, LoaderInterface $loader) + protected function configureContainer(ContainerBuilder $c, LoaderInterface $loader): void { $c->register('logger', NullLogger::class); $c->loadFromExtension('framework', [ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/MicroKernelTraitTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/MicroKernelTraitTest.php index 8bce44e96fb34..d47ca5a822139 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/MicroKernelTraitTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/MicroKernelTraitTest.php @@ -126,38 +126,6 @@ protected function configureRoutes(RoutingConfigurator $routes): void $this->assertSame('Hello World!', $response->getContent()); } - - public function testMissingConfigureContainer() - { - $kernel = new class('missing_configure_container') extends MinimalKernel { - protected function configureRoutes(RoutingConfigurator $routes): void - { - } - }; - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('"Symfony\Bundle\FrameworkBundle\Tests\Kernel\MinimalKernel@anonymous" uses "Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait", but does not implement the required method "protected function configureContainer(ContainerConfigurator $container): void".'); - - $kernel->boot(); - } - - public function testMissingConfigureRoutes() - { - $kernel = new class('missing_configure_routes') extends MinimalKernel { - protected function configureContainer(ContainerConfigurator $c): void - { - $c->extension('framework', [ - 'router' => ['utf8' => true], - ]); - } - }; - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('"Symfony\Bundle\FrameworkBundle\Tests\Kernel\MinimalKernel@anonymous" uses "Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait", but does not implement the required method "protected function configureRoutes(RoutingConfigurator $routes): void".'); - - $request = Request::create('/'); - $kernel->handle($request, HttpKernelInterface::MAIN_REQUEST, false); - } } abstract class MinimalKernel extends Kernel diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/flex-style/src/FlexStyleMicroKernel.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/flex-style/src/FlexStyleMicroKernel.php index 87008db163e76..d4f2ce121be54 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/flex-style/src/FlexStyleMicroKernel.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/flex-style/src/FlexStyleMicroKernel.php @@ -76,7 +76,7 @@ protected function configureRoutes(RoutingConfigurator $routes): void $routes->add('halloween', '/')->controller([$this, 'halloweenAction']); } - protected function configureContainer(ContainerConfigurator $c) + protected function configureContainer(ContainerConfigurator $c): void { $c->parameters() ->set('halloween', 'Have a great day!'); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RouterTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RouterTest.php index 416695ea765ea..cdcaa490ac423 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RouterTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RouterTest.php @@ -410,16 +410,24 @@ public function testExceptionOnNonStringParameter() public function testExceptionOnNonStringParameterWithSfContainer() { - $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".'); $routes = new RouteCollection(); $routes->add('foo', new Route('/%object%')); $sc = $this->getServiceContainer($routes); - $sc->setParameter('object', new \stdClass()); - $router = new Router($sc, 'foo'); + $pc = $this->createMock(ContainerInterface::class); + $pc + ->expects($this->once()) + ->method('get') + ->willReturn(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'); } @@ -545,6 +553,44 @@ public function testCacheValidityWithContainerParameters($parameter) } } + public function testResolvingSchemes() + { + $routes = new RouteCollection(); + + $route = new Route('/test', [], [], [], '', ['%parameter.http%', '%parameter.https%']); + $routes->add('foo', $route); + + $sc = $this->getPsr11ServiceContainer($routes); + $parameters = $this->getParameterBag([ + 'parameter.http' => 'http', + 'parameter.https' => 'https', + ]); + + $router = new Router($sc, 'foo', [], null, $parameters); + $route = $router->getRouteCollection()->get('foo'); + + $this->assertEquals(['http', 'https'], $route->getSchemes()); + } + + public function testResolvingMethods() + { + $routes = new RouteCollection(); + + $route = new Route('/test', [], [], [], '', [], ['%parameter.get%', '%parameter.post%']); + $routes->add('foo', $route); + + $sc = $this->getPsr11ServiceContainer($routes); + $parameters = $this->getParameterBag([ + 'PARAMETER.GET' => 'GET', + 'PARAMETER.POST' => 'POST', + ]); + + $router = new Router($sc, 'foo', [], null, $parameters); + $route = $router->getRouteCollection()->get('foo'); + + $this->assertEquals(['GET', 'POST'], $route->getMethods()); + } + public function getContainerParameterForRoute() { yield 'String' => ['"foo"']; diff --git a/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php b/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php index f043d53f4e0d8..5173f8a8efb51 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php +++ b/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php @@ -36,7 +36,7 @@ class Translator extends BaseTranslator implements WarmableInterface ]; /** - * @var array + * @var list */ private $resourceLocales; @@ -44,10 +44,13 @@ class Translator extends BaseTranslator implements WarmableInterface * Holds parameters from addResource() calls so we can defer the actual * parent::addResource() calls until initialize() is executed. * - * @var array + * @var array[] */ private $resources = []; + /** + * @var string[][] + */ private $resourceFiles; /** @@ -100,7 +103,7 @@ public function warmUp(string $cacheDir) { // skip warmUp when translator doesn't use cache if (null === $this->options['cache_dir']) { - return; + return []; } $localesToWarmUp = $this->enabledLocales ?: array_merge($this->getFallbackLocales(), [$this->getLocale()], $this->resourceLocales); @@ -152,7 +155,7 @@ protected function initialize() if ($this->resourceFiles) { $this->addResourceFiles(); } - foreach ($this->resources as $key => $params) { + foreach ($this->resources as $params) { [$format, $resource, $locale, $domain] = $params; parent::addResource($format, $resource, $locale, $domain); } @@ -165,13 +168,13 @@ protected function initialize() } } - private function addResourceFiles() + private function addResourceFiles(): void { $filesByLocale = $this->resourceFiles; $this->resourceFiles = []; - foreach ($filesByLocale as $locale => $files) { - foreach ($files as $key => $file) { + foreach ($filesByLocale as $files) { + foreach ($files as $file) { // filename is domain.locale.format $fileNameParts = explode('.', basename($file)); $format = array_pop($fileNameParts); diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index 460249f72a2c3..c797289672f9d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -18,64 +18,65 @@ "require": { "php": ">=7.2.5", "ext-xml": "*", - "symfony/cache": "^5.2", - "symfony/config": "^5.3", - "symfony/dependency-injection": "^5.3", + "symfony/cache": "^5.2|^6.0", + "symfony/config": "^5.3|^6.0", + "symfony/dependency-injection": "^5.3|^6.0", "symfony/deprecation-contracts": "^2.1", - "symfony/event-dispatcher": "^5.1", - "symfony/error-handler": "^4.4.1|^5.0.1", - "symfony/http-foundation": "^5.3", - "symfony/http-kernel": "^5.3", + "symfony/event-dispatcher": "^5.1|^6.0", + "symfony/error-handler": "^4.4.1|^5.0.1|^6.0", + "symfony/http-foundation": "^5.3|^6.0", + "symfony/http-kernel": "^5.4|^6.0", "symfony/polyfill-mbstring": "~1.0", "symfony/polyfill-php80": "^1.16", - "symfony/filesystem": "^4.4|^5.0", - "symfony/finder": "^4.4|^5.0", - "symfony/routing": "^5.3" + "symfony/filesystem": "^4.4|^5.0|^6.0", + "symfony/finder": "^4.4|^5.0|^6.0", + "symfony/routing": "^5.3|^6.0" }, "require-dev": { - "doctrine/annotations": "^1.10.4", - "doctrine/cache": "^1.0|^2.0", + "doctrine/annotations": "^1.13.1", + "doctrine/cache": "^1.11|^2.0", "doctrine/persistence": "^1.3|^2.0", - "symfony/asset": "^5.3", - "symfony/browser-kit": "^4.4|^5.0", - "symfony/console": "^5.2", - "symfony/css-selector": "^4.4|^5.0", - "symfony/dom-crawler": "^4.4.30|^5.3.7", - "symfony/dotenv": "^5.1", + "symfony/asset": "^5.3|^6.0", + "symfony/browser-kit": "^5.4|^6.0", + "symfony/console": "^5.4|^6.0", + "symfony/css-selector": "^4.4|^5.0|^6.0", + "symfony/dom-crawler": "^4.4.30|^5.3.7|^6.0", + "symfony/dotenv": "^5.1|^6.0", "symfony/polyfill-intl-icu": "~1.0", - "symfony/form": "^5.2", - "symfony/expression-language": "^4.4|^5.0", - "symfony/http-client": "^4.4|^5.0", - "symfony/lock": "^4.4|^5.0", - "symfony/mailer": "^5.2", - "symfony/messenger": "^5.2", - "symfony/mime": "^4.4|^5.0", - "symfony/notifier": "^5.3", - "symfony/process": "^4.4|^5.0", - "symfony/rate-limiter": "^5.2", - "symfony/security-bundle": "^5.3", - "symfony/serializer": "^5.2", - "symfony/stopwatch": "^4.4|^5.0", - "symfony/string": "^5.0", - "symfony/translation": "^5.3", - "symfony/twig-bundle": "^4.4|^5.0", - "symfony/validator": "^5.2", - "symfony/workflow": "^5.2", - "symfony/yaml": "^4.4|^5.0", - "symfony/property-info": "^4.4|^5.0", - "symfony/web-link": "^4.4|^5.0", + "symfony/form": "^5.2|^6.0", + "symfony/expression-language": "^4.4|^5.0|^6.0", + "symfony/http-client": "^4.4|^5.0|^6.0", + "symfony/lock": "^4.4|^5.0|^6.0", + "symfony/mailer": "^5.2|^6.0", + "symfony/messenger": "^5.4|^6.0", + "symfony/mime": "^4.4|^5.0|^6.0", + "symfony/notifier": "^5.4|^6.0", + "symfony/process": "^4.4|^5.0|^6.0", + "symfony/rate-limiter": "^5.2|^6.0", + "symfony/security-bundle": "^5.4|^6.0", + "symfony/serializer": "^5.4|^6.0", + "symfony/stopwatch": "^4.4|^5.0|^6.0", + "symfony/string": "^5.0|^6.0", + "symfony/translation": "^5.3|^6.0", + "symfony/twig-bundle": "^4.4|^5.0|^6.0", + "symfony/validator": "^5.2|^6.0", + "symfony/workflow": "^5.2|^6.0", + "symfony/yaml": "^4.4|^5.0|^6.0", + "symfony/property-info": "^4.4|^5.0|^6.0", + "symfony/web-link": "^4.4|^5.0|^6.0", "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", "paragonie/sodium_compat": "^1.8", "twig/twig": "^2.10|^3.0", - "symfony/phpunit-bridge": "^5.3" + "symfony/phpunit-bridge": "^5.3|^6.0" }, "conflict": { + "doctrine/annotations": "<1.13.1", + "doctrine/cache": "<1.11", "doctrine/persistence": "<1.3", "phpdocumentor/reflection-docblock": "<3.2.2", "phpdocumentor/type-resolver": "<1.4.0", "phpunit/phpunit": "<5.4.3", "symfony/asset": "<5.3", - "symfony/browser-kit": "<4.4", "symfony/console": "<5.2.5", "symfony/dotenv": "<5.1", "symfony/dom-crawler": "<4.4", @@ -83,13 +84,13 @@ "symfony/form": "<5.2", "symfony/lock": "<4.4", "symfony/mailer": "<5.2", - "symfony/messenger": "<4.4", + "symfony/messenger": "<5.4", "symfony/mime": "<4.4", "symfony/property-info": "<4.4", "symfony/property-access": "<5.3", "symfony/serializer": "<5.2", + "symfony/service-contracts": ">=3.0", "symfony/security-csrf": "<5.3", - "symfony/security-core": "<5.3", "symfony/stopwatch": "<4.4", "symfony/translation": "<5.3", "symfony/twig-bridge": "<4.4", diff --git a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md index 640f0d2ce3393..4cb8093f88daf 100644 --- a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md @@ -1,6 +1,24 @@ CHANGELOG ========= +5.4 +--- + + * Deprecate `FirewallConfig::getListeners()`, use `FirewallConfig::getAuthenticators()` instead + * Deprecate `security.authentication.basic_entry_point` and `security.authentication.retry_entry_point` services, the logic is moved into the + `HttpBasicAuthenticator` and `ChannelListener` respectively + * Deprecate `FirewallConfig::allowsAnonymous()` and the `allows_anonymous` from the data collector data, there will be no anonymous concept as of version 6. + * Deprecate not setting `$authenticatorManagerEnabled` to `true` in `SecurityDataCollector` and `DebugFirewallCommand` + * Deprecate `SecurityFactoryInterface` and `SecurityExtension::addSecurityListenerFactory()` in favor of + `AuthenticatorFactoryInterface` and `SecurityExtension::addAuthenticatorFactory()` + * Add `AuthenticatorFactoryInterface::getPriority()` which replaces `SecurityFactoryInterface::getPosition()` + * Deprecate passing an array of arrays as 1st argument to `MainConfiguration`, pass a sorted flat array of + factories instead. + * Deprecate the `always_authenticate_before_granting` option + * Display the roles of the logged-in user in the Web Debug Toolbar + * Add the `security.access_decision_manager.strategy_service` option + * Deprecate not configuring explicitly a provider for custom_authenticators when there is more than one registered provider + 5.3 --- @@ -21,6 +39,7 @@ CHANGELOG * Deprecate the `security.authentication.provider.*` services, use the new authenticator system instead * Deprecate the `security.authentication.listener.*` services, use the new authenticator system instead * Deprecate the Guard component integration, use the new authenticator system instead + * Add `form_login.form_only` option 5.2.0 ----- diff --git a/src/Symfony/Bundle/SecurityBundle/CacheWarmer/ExpressionCacheWarmer.php b/src/Symfony/Bundle/SecurityBundle/CacheWarmer/ExpressionCacheWarmer.php index 1ca1f32ecd98e..095286e66538a 100644 --- a/src/Symfony/Bundle/SecurityBundle/CacheWarmer/ExpressionCacheWarmer.php +++ b/src/Symfony/Bundle/SecurityBundle/CacheWarmer/ExpressionCacheWarmer.php @@ -21,7 +21,7 @@ class ExpressionCacheWarmer implements CacheWarmerInterface private $expressionLanguage; /** - * @param iterable|Expression[] $expressions + * @param iterable $expressions */ public function __construct(iterable $expressions, ExpressionLanguage $expressionLanguage) { diff --git a/src/Symfony/Bundle/SecurityBundle/Command/DebugFirewallCommand.php b/src/Symfony/Bundle/SecurityBundle/Command/DebugFirewallCommand.php index 0c562d9fdddcd..b5fe209944ce9 100644 --- a/src/Symfony/Bundle/SecurityBundle/Command/DebugFirewallCommand.php +++ b/src/Symfony/Bundle/SecurityBundle/Command/DebugFirewallCommand.php @@ -15,6 +15,8 @@ use Symfony\Bundle\SecurityBundle\Security\FirewallContext; use Symfony\Bundle\SecurityBundle\Security\LazyFirewallContext; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -43,6 +45,10 @@ final class DebugFirewallCommand extends Command */ public function __construct(array $firewallNames, ContainerInterface $contexts, ContainerInterface $eventDispatchers, array $authenticators, bool $authenticatorManagerEnabled) { + if (!$authenticatorManagerEnabled) { + trigger_deprecation('symfony/security-bundle', '5.4', 'Setting the $authenticatorManagerEnabled argument of "%s" to "false" is deprecated, use the new authenticator system instead.', __METHOD__); + } + $this->firewallNames = $firewallNames; $this->contexts = $contexts; $this->eventDispatchers = $eventDispatchers; @@ -273,4 +279,11 @@ private function getExampleName(): string return $name; } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('name')) { + $suggestions->suggestValues($this->firewallNames); + } + } } diff --git a/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php b/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php index e6456dd05b4da..116e3d029c638 100644 --- a/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php +++ b/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php @@ -48,6 +48,10 @@ class SecurityDataCollector extends DataCollector implements LateDataCollectorIn public function __construct(TokenStorageInterface $tokenStorage = null, RoleHierarchyInterface $roleHierarchy = null, LogoutUrlGenerator $logoutUrlGenerator = null, AccessDecisionManagerInterface $accessDecisionManager = null, FirewallMapInterface $firewallMap = null, TraceableFirewallListener $firewall = null, bool $authenticatorManagerEnabled = false) { + if (!$authenticatorManagerEnabled) { + trigger_deprecation('symfony/security-bundle', '5.4', 'Setting the $authenticatorManagerEnabled argument of "%s" to "false" is deprecated, use the new authenticator system instead.', __METHOD__); + } + $this->tokenStorage = $tokenStorage; $this->roleHierarchy = $roleHierarchy; $this->logoutUrlGenerator = $logoutUrlGenerator; @@ -123,7 +127,7 @@ public function collect(Request $request, Response $response, \Throwable $except $this->data = [ 'enabled' => true, - 'authenticated' => $token->isAuthenticated(), + 'authenticated' => method_exists($token, 'isAuthenticated') ? $token->isAuthenticated(false) : (bool) $token->getUser(), 'impersonated' => null !== $impersonatorUser, 'impersonator_user' => $impersonatorUser, 'impersonation_exit_path' => null, @@ -180,7 +184,7 @@ public function collect(Request $request, Response $response, \Throwable $except if (null !== $firewallConfig) { $this->data['firewall'] = [ 'name' => $firewallConfig->getName(), - 'allows_anonymous' => $firewallConfig->allowsAnonymous(), + 'allows_anonymous' => $this->authenticatorManagerEnabled ? false : $firewallConfig->allowsAnonymous(), 'request_matcher' => $firewallConfig->getRequestMatcher(), 'security_enabled' => $firewallConfig->isSecurityEnabled(), 'stateless' => $firewallConfig->isStateless(), @@ -190,7 +194,7 @@ public function collect(Request $request, Response $response, \Throwable $except 'access_denied_handler' => $firewallConfig->getAccessDeniedHandler(), 'access_denied_url' => $firewallConfig->getAccessDeniedUrl(), 'user_checker' => $firewallConfig->getUserChecker(), - 'listeners' => $firewallConfig->getListeners(), + 'authenticators' => $firewallConfig->getAuthenticators(), ]; // generate exit impersonation path from current request @@ -211,6 +215,7 @@ public function collect(Request $request, Response $response, \Throwable $except } $this->data['authenticator_manager_enabled'] = $this->authenticatorManagerEnabled; + $this->data['authenticators'] = $this->firewall ? $this->firewall->getAuthenticatorsInfo() : []; } /** @@ -228,20 +233,16 @@ public function lateCollect() /** * Checks if security is enabled. - * - * @return bool true if security is enabled, false otherwise */ - public function isEnabled() + public function isEnabled(): bool { return $this->data['enabled']; } /** * Gets the user. - * - * @return string The user */ - public function getUser() + public function getUser(): string { return $this->data['user']; } @@ -269,44 +270,31 @@ public function getInheritedRoles() /** * Checks if the data contains information about inherited roles. Still the inherited * roles can be an empty array. - * - * @return bool true if the profile was contains inherited role information */ - public function supportsRoleHierarchy() + public function supportsRoleHierarchy(): bool { return $this->data['supports_role_hierarchy']; } /** * Checks if the user is authenticated or not. - * - * @return bool true if the user is authenticated, false otherwise */ - public function isAuthenticated() + public function isAuthenticated(): bool { return $this->data['authenticated']; } - /** - * @return bool - */ - public function isImpersonated() + public function isImpersonated(): bool { return $this->data['impersonated']; } - /** - * @return string|null - */ - public function getImpersonatorUser() + public function getImpersonatorUser(): ?string { return $this->data['impersonator_user']; } - /** - * @return string|null - */ - public function getImpersonationExitPath() + public function getImpersonationExitPath(): ?string { return $this->data['impersonation_exit_path']; } @@ -314,7 +302,7 @@ public function getImpersonationExitPath() /** * Get the class name of the security token. * - * @return string|Data|null The token + * @return string|Data|null */ public function getTokenClass() { @@ -323,20 +311,16 @@ public function getTokenClass() /** * Get the full security token class as Data object. - * - * @return Data|null */ - public function getToken() + public function getToken(): ?Data { return $this->data['token']; } /** * Get the logout URL. - * - * @return string|null The logout URL */ - public function getLogoutUrl() + public function getLogoutUrl(): ?string { return $this->data['logout_url']; } @@ -353,10 +337,8 @@ public function getVoters() /** * Returns the strategy configured for the security voters. - * - * @return string */ - public function getVoterStrategy() + public function getVoterStrategy(): string { return $this->data['voter_strategy']; } @@ -389,10 +371,18 @@ public function getListeners() return $this->data['listeners']; } + /** + * @return array|Data + */ + public function getAuthenticators() + { + return $this->data['authenticators']; + } + /** * {@inheritdoc} */ - public function getName() + public function getName(): string { return 'security'; } diff --git a/src/Symfony/Bundle/SecurityBundle/Debug/TraceableFirewallListener.php b/src/Symfony/Bundle/SecurityBundle/Debug/TraceableFirewallListener.php index bc7549a97a34d..e82b47695bad9 100644 --- a/src/Symfony/Bundle/SecurityBundle/Debug/TraceableFirewallListener.php +++ b/src/Symfony/Bundle/SecurityBundle/Debug/TraceableFirewallListener.php @@ -15,32 +15,43 @@ use Symfony\Bundle\SecurityBundle\Security\FirewallContext; use Symfony\Bundle\SecurityBundle\Security\LazyFirewallContext; use Symfony\Component\HttpKernel\Event\RequestEvent; +use Symfony\Component\Security\Http\Authenticator\Debug\TraceableAuthenticatorManagerListener; use Symfony\Component\Security\Http\Firewall\FirewallListenerInterface; /** - * Firewall collecting called listeners. + * Firewall collecting called security listeners and authenticators. * * @author Robin Chalas */ final class TraceableFirewallListener extends FirewallListener { private $wrappedListeners = []; + private $authenticatorsInfo = []; public function getWrappedListeners() { return $this->wrappedListeners; } + public function getAuthenticatorsInfo(): array + { + return $this->authenticatorsInfo; + } + protected function callListeners(RequestEvent $event, iterable $listeners) { $wrappedListeners = []; $wrappedLazyListeners = []; + $authenticatorManagerListener = null; foreach ($listeners as $listener) { if ($listener instanceof LazyFirewallContext) { - \Closure::bind(function () use (&$wrappedLazyListeners, &$wrappedListeners) { + \Closure::bind(function () use (&$wrappedLazyListeners, &$wrappedListeners, &$authenticatorManagerListener) { $listeners = []; foreach ($this->listeners as $listener) { + if (!$authenticatorManagerListener && $listener instanceof TraceableAuthenticatorManagerListener) { + $authenticatorManagerListener = $listener; + } if ($listener instanceof FirewallListenerInterface) { $listener = new WrappedLazyListener($listener); $listeners[] = $listener; @@ -61,6 +72,9 @@ protected function callListeners(RequestEvent $event, iterable $listeners) $wrappedListener = $listener instanceof FirewallListenerInterface ? new WrappedLazyListener($listener) : new WrappedListener($listener); $wrappedListener($event); $wrappedListeners[] = $wrappedListener->getInfo(); + if (!$authenticatorManagerListener && $listener instanceof TraceableAuthenticatorManagerListener) { + $authenticatorManagerListener = $listener; + } } if ($event->hasResponse()) { @@ -75,5 +89,9 @@ protected function callListeners(RequestEvent $event, iterable $listeners) } $this->wrappedListeners = array_merge($this->wrappedListeners, $wrappedListeners); + + if ($authenticatorManagerListener) { + $this->authenticatorsInfo = $authenticatorManagerListener->getAuthenticatorsInfo(); + } } } diff --git a/src/Symfony/Bundle/SecurityBundle/Debug/TraceableListenerTrait.php b/src/Symfony/Bundle/SecurityBundle/Debug/TraceableListenerTrait.php index 691c6659d5384..6581314054dd2 100644 --- a/src/Symfony/Bundle/SecurityBundle/Debug/TraceableListenerTrait.php +++ b/src/Symfony/Bundle/SecurityBundle/Debug/TraceableListenerTrait.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\SecurityBundle\Debug; +use Symfony\Component\Security\Http\Authenticator\Debug\TraceableAuthenticatorManagerListener; use Symfony\Component\VarDumper\Caster\ClassStub; /** @@ -43,7 +44,7 @@ public function getInfo(): array return [ 'response' => $this->response, 'time' => $this->time, - 'stub' => $this->stub ?? $this->stub = ClassStub::wrapCallable($this->listener), + 'stub' => $this->stub ?? $this->stub = ClassStub::wrapCallable($this->listener instanceof TraceableAuthenticatorManagerListener ? $this->listener->getAuthenticatorManagerListener() : $this->listener), ]; } } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php index b9d0a22583d6c..20e6832de69f0 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php @@ -12,11 +12,12 @@ namespace Symfony\Bundle\SecurityBundle\DependencyInjection; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AbstractFactory; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AuthenticatorFactoryInterface; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; -use Symfony\Component\Security\Core\Authorization\AccessDecisionManager; use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface; use Symfony\Component\Security\Http\Event\LogoutEvent; use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategy; @@ -28,11 +29,29 @@ */ class MainConfiguration implements ConfigurationInterface { + /** @internal */ + public const STRATEGY_AFFIRMATIVE = 'affirmative'; + /** @internal */ + public const STRATEGY_CONSENSUS = 'consensus'; + /** @internal */ + public const STRATEGY_UNANIMOUS = 'unanimous'; + /** @internal */ + public const STRATEGY_PRIORITY = 'priority'; + private $factories; private $userProviderFactories; + /** + * @param array $factories + */ public function __construct(array $factories, array $userProviderFactories) { + if (\is_array(current($factories))) { + trigger_deprecation('symfony/security-bundle', '5.4', 'Passing an array of arrays as 1st argument to "%s" is deprecated, pass a sorted array of factories instead.', __METHOD__); + + $factories = array_merge(...array_values($factories)); + } + $this->factories = $factories; $this->userProviderFactories = $userProviderFactories; } @@ -40,7 +59,7 @@ public function __construct(array $factories, array $userProviderFactories) /** * Generates the configuration tree builder. * - * @return TreeBuilder The tree builder + * @return TreeBuilder */ public function getConfigTreeBuilder() { @@ -48,24 +67,6 @@ public function getConfigTreeBuilder() $rootNode = $tb->getRootNode(); $rootNode - ->beforeNormalization() - ->ifTrue(function ($v) { - if (!isset($v['access_decision_manager'])) { - return true; - } - - if (!isset($v['access_decision_manager']['strategy']) && !isset($v['access_decision_manager']['service'])) { - return true; - } - - return false; - }) - ->then(function ($v) { - $v['access_decision_manager']['strategy'] = AccessDecisionManager::STRATEGY_AFFIRMATIVE; - - return $v; - }) - ->end() ->beforeNormalization() ->ifTrue(function ($v) { if ($v['encoders'] ?? false) { @@ -90,7 +91,10 @@ public function getConfigTreeBuilder() ->defaultValue(SessionAuthenticationStrategy::MIGRATE) ->end() ->booleanNode('hide_user_not_found')->defaultTrue()->end() - ->booleanNode('always_authenticate_before_granting')->defaultFalse()->end() + ->booleanNode('always_authenticate_before_granting') + ->defaultFalse() + ->setDeprecated('symfony/security-bundle', '5.4') + ->end() ->booleanNode('erase_credentials')->defaultTrue()->end() ->booleanNode('enable_authenticator_manager')->defaultFalse()->info('Enables the new Symfony Security system based on Authenticators, all used authenticators must support this before enabling this.')->end() ->arrayNode('access_decision_manager') @@ -100,13 +104,22 @@ public function getConfigTreeBuilder() ->values($this->getAccessDecisionStrategies()) ->end() ->scalarNode('service')->end() + ->scalarNode('strategy_service')->end() ->booleanNode('allow_if_all_abstain')->defaultFalse()->end() ->booleanNode('allow_if_equal_granted_denied')->defaultTrue()->end() ->end() ->validate() - ->ifTrue(function ($v) { return isset($v['strategy']) && isset($v['service']); }) + ->ifTrue(function ($v) { return isset($v['strategy'], $v['service']); }) ->thenInvalid('"strategy" and "service" cannot be used together.') ->end() + ->validate() + ->ifTrue(function ($v) { return isset($v['strategy'], $v['strategy_service']); }) + ->thenInvalid('"strategy" and "strategy_service" cannot be used together.') + ->end() + ->validate() + ->ifTrue(function ($v) { return isset($v['service'], $v['strategy_service']); }) + ->thenInvalid('"service" and "strategy_service" cannot be used together.') + ->end() ->end() ->end() ; @@ -184,6 +197,9 @@ private function addAccessControlSection(ArrayNodeDefinition $rootNode) ; } + /** + * @param array $factories + */ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $factories) { $firewallNodeBuilder = $rootNode @@ -294,19 +310,17 @@ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $facto ; $abstractFactoryKeys = []; - foreach ($factories as $factoriesAtPosition) { - foreach ($factoriesAtPosition as $factory) { - $name = str_replace('-', '_', $factory->getKey()); - $factoryNode = $firewallNodeBuilder->arrayNode($name) - ->canBeUnset() - ; - - if ($factory instanceof AbstractFactory) { - $abstractFactoryKeys[] = $name; - } - - $factory->addConfiguration($factoryNode); + foreach ($factories as $factory) { + $name = str_replace('-', '_', $factory->getKey()); + $factoryNode = $firewallNodeBuilder->arrayNode($name) + ->canBeUnset() + ; + + if ($factory instanceof AbstractFactory) { + $abstractFactoryKeys[] = $name; } + + $factory->addConfiguration($factoryNode); } // check for unreachable check paths @@ -495,18 +509,13 @@ private function addPasswordHashersSection(ArrayNodeDefinition $rootNode) ->end(); } - private function getAccessDecisionStrategies() + private function getAccessDecisionStrategies(): array { - $strategies = [ - AccessDecisionManager::STRATEGY_AFFIRMATIVE, - AccessDecisionManager::STRATEGY_CONSENSUS, - AccessDecisionManager::STRATEGY_UNANIMOUS, + return [ + self::STRATEGY_AFFIRMATIVE, + self::STRATEGY_CONSENSUS, + self::STRATEGY_UNANIMOUS, + self::STRATEGY_PRIORITY, ]; - - if (\defined(AccessDecisionManager::class.'::STRATEGY_PRIORITY')) { - $strategies[] = AccessDecisionManager::STRATEGY_PRIORITY; - } - - return $strategies; } } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php index c96dc76d7ba98..e8bfa9412aff7 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php @@ -99,7 +99,7 @@ final public function addOption(string $name, $default = null) * Subclasses must return the id of a service which implements the * AuthenticationProviderInterface. * - * @return string never null, the id of the authentication provider + * @return string */ abstract protected function createAuthProvider(ContainerBuilder $container, string $id, array $config, string $userProviderId); diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AnonymousFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AnonymousFactory.php index ded4a61740d53..13359ee10c9b7 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AnonymousFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AnonymousFactory.php @@ -20,8 +20,6 @@ /** * @author Wouter de Jong * - * @internal - * * @deprecated since Symfony 5.3, use the new authenticator system instead */ class AnonymousFactory implements SecurityFactoryInterface, AuthenticatorFactoryInterface @@ -52,6 +50,11 @@ public function createAuthenticator(ContainerBuilder $container, string $firewal throw new InvalidConfigurationException(sprintf('The authenticator manager no longer has "anonymous" security. Please remove this option under the "%s" firewall'.($config['lazy'] ? ' and add "lazy: true"' : '').'.', $firewallName)); } + public function getPriority() + { + return -60; + } + public function getPosition() { return 'anonymous'; diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AuthenticatorFactoryInterface.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AuthenticatorFactoryInterface.php index d4fef81e247b4..6ecec3e281ae2 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AuthenticatorFactoryInterface.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AuthenticatorFactoryInterface.php @@ -11,9 +11,12 @@ namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory; +use Symfony\Component\Config\Definition\Builder\NodeDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; /** + * @method int getPriority() defines the position at which the authenticator is called + * * @author Wouter de Jong */ interface AuthenticatorFactoryInterface @@ -24,4 +27,14 @@ interface AuthenticatorFactoryInterface * @return string|string[] The authenticator service ID(s) to be used by the firewall */ public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId); + + /** + * Defines the configuration key used to reference the authenticator + * in the firewall configuration. + * + * @return string + */ + public function getKey(); + + public function addConfiguration(NodeDefinition $builder); } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/CustomAuthenticatorFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/CustomAuthenticatorFactory.php index a478de2c8d8a4..94761785d7802 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/CustomAuthenticatorFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/CustomAuthenticatorFactory.php @@ -22,11 +22,16 @@ */ class CustomAuthenticatorFactory implements AuthenticatorFactoryInterface, SecurityFactoryInterface { - public function create(ContainerBuilder $container, string $id, array $config, string $userProvider, ?string $defaultEntryPoint) + public function create(ContainerBuilder $container, string $id, array $config, string $userProvider, ?string $defaultEntryPoint): array { throw new \LogicException('Custom authenticators are not supported when "security.enable_authenticator_manager" is not set to true.'); } + public function getPriority(): int + { + return 0; + } + public function getPosition(): string { return 'pre_auth'; diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php index 3f4f6a16909b1..137e8e53eb2d0 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php @@ -27,6 +27,8 @@ */ class FormLoginFactory extends AbstractFactory implements AuthenticatorFactoryInterface { + public const PRIORITY = -30; + public function __construct() { $this->addOption('username_parameter', '_username'); @@ -35,14 +37,20 @@ public function __construct() $this->addOption('csrf_token_id', 'authenticate'); $this->addOption('enable_csrf', false); $this->addOption('post_only', true); + $this->addOption('form_only', false); + } + + public function getPriority(): int + { + return self::PRIORITY; } - public function getPosition() + public function getPosition(): string { return 'form'; } - public function getKey() + public function getKey(): string { return 'form-login'; } @@ -58,12 +66,12 @@ public function addConfiguration(NodeDefinition $node) ; } - protected function getListenerId() + protected function getListenerId(): string { return 'security.authentication.listener.form'; } - protected function createAuthProvider(ContainerBuilder $container, string $id, array $config, string $userProviderId) + protected function createAuthProvider(ContainerBuilder $container, string $id, array $config, string $userProviderId): string { if ($config['enable_csrf'] ?? false) { throw new InvalidConfigurationException('The "enable_csrf" option of "form_login" is only available when "security.enable_authenticator_manager" is set to "true", use "csrf_token_generator" instead.'); @@ -92,7 +100,7 @@ protected function createListener(ContainerBuilder $container, string $id, array return $listenerId; } - protected function createEntryPoint(ContainerBuilder $container, string $id, array $config, ?string $defaultEntryPointId) + protected function createEntryPoint(ContainerBuilder $container, string $id, array $config, ?string $defaultEntryPointId): ?string { $entryPointId = 'security.authentication.form_entry_point.'.$id; $container diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginLdapFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginLdapFactory.php index 3b58b8bd3f7cb..04c2bc9b27869 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginLdapFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginLdapFactory.php @@ -29,7 +29,7 @@ class FormLoginLdapFactory extends FormLoginFactory { use LdapFactoryTrait; - protected function createAuthProvider(ContainerBuilder $container, string $id, array $config, string $userProviderId) + protected function createAuthProvider(ContainerBuilder $container, string $id, array $config, string $userProviderId): string { $provider = 'security.authentication.provider.ldap_bind.'.$id; $definition = $container @@ -67,9 +67,4 @@ public function addConfiguration(NodeDefinition $node) ->end() ; } - - public function getKey() - { - return 'form-login-ldap'; - } } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardAuthenticationFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardAuthenticationFactory.php index f60666e9dc772..a83a6d987dd52 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardAuthenticationFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardAuthenticationFactory.php @@ -29,12 +29,17 @@ */ class GuardAuthenticationFactory implements SecurityFactoryInterface, AuthenticatorFactoryInterface { - public function getPosition() + public function getPosition(): string { return 'pre_auth'; } - public function getKey() + public function getPriority(): int + { + return 0; + } + + public function getKey(): string { return 'guard'; } @@ -60,7 +65,7 @@ public function addConfiguration(NodeDefinition $node) ; } - public function create(ContainerBuilder $container, string $id, array $config, string $userProvider, ?string $defaultEntryPoint) + public function create(ContainerBuilder $container, string $id, array $config, string $userProvider, ?string $defaultEntryPoint): array { $authenticatorIds = $config['authenticators']; $authenticatorReferences = []; diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php index 784878b9ed775..e35b8a0a49618 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php @@ -25,7 +25,9 @@ */ class HttpBasicFactory implements SecurityFactoryInterface, AuthenticatorFactoryInterface { - public function create(ContainerBuilder $container, string $id, array $config, string $userProvider, ?string $defaultEntryPoint) + public const PRIORITY = -50; + + public function create(ContainerBuilder $container, string $id, array $config, string $userProvider, ?string $defaultEntryPoint): array { $provider = 'security.authentication.provider.dao.'.$id; $container @@ -66,12 +68,17 @@ public function createAuthenticator(ContainerBuilder $container, string $firewal return $authenticatorId; } - public function getPosition() + public function getPriority(): int + { + return self::PRIORITY; + } + + public function getPosition(): string { return 'http'; } - public function getKey() + public function getKey(): string { return 'http-basic'; } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicLdapFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicLdapFactory.php index 4f488e970b3bd..0c63b21c63aaa 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicLdapFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicLdapFactory.php @@ -30,7 +30,7 @@ class HttpBasicLdapFactory extends HttpBasicFactory { use LdapFactoryTrait; - public function create(ContainerBuilder $container, string $id, array $config, string $userProvider, ?string $defaultEntryPoint) + public function create(ContainerBuilder $container, string $id, array $config, string $userProvider, ?string $defaultEntryPoint): array { $provider = 'security.authentication.provider.ldap_bind.'.$id; $definition = $container @@ -84,9 +84,4 @@ public function addConfiguration(NodeDefinition $node) ->end() ; } - - public function getKey() - { - return 'http-basic-ldap'; - } } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/JsonLoginFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/JsonLoginFactory.php index 7458a35b0e6be..b19a696faa4c2 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/JsonLoginFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/JsonLoginFactory.php @@ -24,6 +24,8 @@ */ class JsonLoginFactory extends AbstractFactory implements AuthenticatorFactoryInterface { + public const PRIORITY = -40; + public function __construct() { $this->addOption('username_path', 'username'); @@ -32,10 +34,15 @@ public function __construct() $this->defaultSuccessHandlerOptions = []; } + public function getPriority(): int + { + return self::PRIORITY; + } + /** * {@inheritdoc} */ - public function getPosition() + public function getPosition(): string { return 'form'; } @@ -43,7 +50,7 @@ public function getPosition() /** * {@inheritdoc} */ - public function getKey() + public function getKey(): string { return 'json-login'; } @@ -51,7 +58,7 @@ public function getKey() /** * {@inheritdoc} */ - protected function createAuthProvider(ContainerBuilder $container, string $id, array $config, string $userProviderId) + protected function createAuthProvider(ContainerBuilder $container, string $id, array $config, string $userProviderId): string { $provider = 'security.authentication.provider.dao.'.$id; $container @@ -67,7 +74,7 @@ protected function createAuthProvider(ContainerBuilder $container, string $id, a /** * {@inheritdoc} */ - protected function getListenerId() + protected function getListenerId(): string { return 'security.authentication.listener.json'; } @@ -75,7 +82,7 @@ protected function getListenerId() /** * {@inheritdoc} */ - protected function isRememberMeAware(array $config) + protected function isRememberMeAware(array $config): bool { return false; } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/JsonLoginLdapFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/JsonLoginLdapFactory.php index 9d74f01cffda8..c8b77faff3c01 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/JsonLoginLdapFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/JsonLoginLdapFactory.php @@ -26,12 +26,7 @@ class JsonLoginLdapFactory extends JsonLoginFactory { use LdapFactoryTrait; - public function getKey() - { - return 'json-login-ldap'; - } - - protected function createAuthProvider(ContainerBuilder $container, string $id, array $config, string $userProviderId) + protected function createAuthProvider(ContainerBuilder $container, string $id, array $config, string $userProviderId): string { $provider = 'security.authentication.provider.ldap_bind.'.$id; $definition = $container diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LdapFactoryTrait.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LdapFactoryTrait.php index 434383049de8d..8af8e4424b270 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LdapFactoryTrait.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LdapFactoryTrait.php @@ -27,6 +27,11 @@ */ trait LdapFactoryTrait { + public function getKey(): string + { + return parent::getKey().'-ldap'; + } + public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId): string { $key = str_replace('-', '_', $this->getKey()); diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LoginLinkFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LoginLinkFactory.php index de426df457c5b..5badfb237c5da 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LoginLinkFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LoginLinkFactory.php @@ -27,6 +27,8 @@ */ class LoginLinkFactory extends AbstractFactory implements AuthenticatorFactoryInterface { + public const PRIORITY = -20; + public function addConfiguration(NodeDefinition $node) { /** @var NodeBuilder $builder */ @@ -79,7 +81,7 @@ public function addConfiguration(NodeDefinition $node) } } - public function getKey() + public function getKey(): string { return 'login-link'; } @@ -147,17 +149,22 @@ public function createAuthenticator(ContainerBuilder $container, string $firewal return $authenticatorId; } - public function getPosition() + public function getPriority(): int + { + return self::PRIORITY; + } + + public function getPosition(): string { return 'form'; } - protected function createAuthProvider(ContainerBuilder $container, string $id, array $config, string $userProviderId) + protected function createAuthProvider(ContainerBuilder $container, string $id, array $config, string $userProviderId): string { throw new \Exception('The old authentication system is not supported with login_link.'); } - protected function getListenerId() + protected function getListenerId(): string { throw new \Exception('The old authentication system is not supported with login_link.'); } @@ -167,7 +174,7 @@ protected function createListener(ContainerBuilder $container, string $id, array throw new \Exception('The old authentication system is not supported with login_link.'); } - protected function createEntryPoint(ContainerBuilder $container, string $id, array $config, ?string $defaultEntryPointId) + protected function createEntryPoint(ContainerBuilder $container, string $id, array $config, ?string $defaultEntryPointId): ?string { throw new \Exception('The old authentication system is not supported with login_link.'); } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LoginThrottlingFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LoginThrottlingFactory.php index 111a2a062e1b5..dc829be2edd9e 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LoginThrottlingFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LoginThrottlingFactory.php @@ -29,11 +29,17 @@ */ class LoginThrottlingFactory implements AuthenticatorFactoryInterface, SecurityFactoryInterface { - public function create(ContainerBuilder $container, string $id, array $config, string $userProvider, ?string $defaultEntryPoint) + public function create(ContainerBuilder $container, string $id, array $config, string $userProvider, ?string $defaultEntryPoint): array { throw new \LogicException('Login throttling is not supported when "security.enable_authenticator_manager" is not set to true.'); } + public function getPriority(): int + { + // this factory doesn't register any authenticators, this priority doesn't matter + return 0; + } + public function getPosition(): string { // this factory doesn't register any authenticators, this position doesn't matter diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php index de19f488454f2..b18018e54fe44 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php @@ -21,6 +21,7 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpFoundation\Cookie; @@ -30,8 +31,10 @@ /** * @internal */ -class RememberMeFactory implements SecurityFactoryInterface, AuthenticatorFactoryInterface +class RememberMeFactory implements SecurityFactoryInterface, AuthenticatorFactoryInterface, PrependExtensionInterface { + public const PRIORITY = -50; + protected $options = [ 'name' => 'REMEMBERME', 'lifetime' => 31536000, @@ -44,7 +47,7 @@ class RememberMeFactory implements SecurityFactoryInterface, AuthenticatorFactor 'remember_me_parameter' => '_remember_me', ]; - public function create(ContainerBuilder $container, string $id, array $config, ?string $userProvider, ?string $defaultEntryPoint) + public function create(ContainerBuilder $container, string $id, array $config, ?string $userProvider, ?string $defaultEntryPoint): array { // authentication provider $authProviderId = 'security.authentication.provider.rememberme.'.$id; @@ -176,12 +179,20 @@ public function createAuthenticator(ContainerBuilder $container, string $firewal return $authenticatorId; } - public function getPosition() + public function getPosition(): string { return 'remember_me'; } - public function getKey() + /** + * {@inheritDoc} + */ + public function getPriority(): int + { + return self::PRIORITY; + } + + public function getKey(): string { return 'remember-me'; } @@ -331,4 +342,27 @@ private function createTokenVerifier(ContainerBuilder $container, string $firewa return new Reference($tokenVerifierId, ContainerInterface::NULL_ON_INVALID_REFERENCE); } + + /** + * {@inheritdoc} + */ + public function prepend(ContainerBuilder $container) + { + $rememberMeSecureDefault = false; + $rememberMeSameSiteDefault = null; + + if (!isset($container->getExtensions()['framework'])) { + return; + } + + foreach ($container->getExtensionConfig('framework') as $config) { + if (isset($config['session']) && \is_array($config['session'])) { + $rememberMeSecureDefault = $config['session']['cookie_secure'] ?? $rememberMeSecureDefault; + $rememberMeSameSiteDefault = \array_key_exists('cookie_samesite', $config['session']) ? $config['session']['cookie_samesite'] : $rememberMeSameSiteDefault; + } + } + + $this->options['secure'] = $rememberMeSecureDefault; + $this->options['samesite'] = $rememberMeSameSiteDefault; + } } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RemoteUserFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RemoteUserFactory.php index fc2e49f6f0819..d32cffa0e4c48 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RemoteUserFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RemoteUserFactory.php @@ -26,7 +26,9 @@ */ class RemoteUserFactory implements SecurityFactoryInterface, AuthenticatorFactoryInterface { - public function create(ContainerBuilder $container, string $id, array $config, string $userProvider, ?string $defaultEntryPoint) + public const PRIORITY = -10; + + public function create(ContainerBuilder $container, string $id, array $config, string $userProvider, ?string $defaultEntryPoint): array { $providerId = 'security.authentication.provider.pre_authenticated.'.$id; $container @@ -58,12 +60,17 @@ public function createAuthenticator(ContainerBuilder $container, string $firewal return $authenticatorId; } - public function getPosition() + public function getPriority(): int + { + return self::PRIORITY; + } + + public function getPosition(): string { return 'pre_auth'; } - public function getKey() + public function getKey(): string { return 'remote-user'; } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SecurityFactoryInterface.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SecurityFactoryInterface.php index 4a1497aec1640..4551a6cbcc11b 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SecurityFactoryInterface.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SecurityFactoryInterface.php @@ -18,6 +18,8 @@ * SecurityFactoryInterface is the interface for all security authentication listener. * * @author Fabien Potencier + * + * @deprecated since Symfony 5.3, use AuthenticatorFactoryInterface instead. */ interface SecurityFactoryInterface { diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/X509Factory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/X509Factory.php index 56a25653af9b5..269d36916404a 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/X509Factory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/X509Factory.php @@ -25,7 +25,9 @@ */ class X509Factory implements SecurityFactoryInterface, AuthenticatorFactoryInterface { - public function create(ContainerBuilder $container, string $id, array $config, string $userProvider, ?string $defaultEntryPoint) + public const PRIORITY = -10; + + public function create(ContainerBuilder $container, string $id, array $config, string $userProvider, ?string $defaultEntryPoint): array { $providerId = 'security.authentication.provider.pre_authenticated.'.$id; $container @@ -60,12 +62,17 @@ public function createAuthenticator(ContainerBuilder $container, string $firewal return $authenticatorId; } - public function getPosition() + public function getPriority(): int + { + return self::PRIORITY; + } + + public function getPosition(): string { return 'pre_auth'; } - public function getKey() + public function getKey(): string { return 'x509'; } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index f1ce0a9aabef2..91e0ee80c9692 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -11,10 +11,10 @@ namespace Symfony\Bundle\SecurityBundle\DependencyInjection; +use Composer\InstalledVersions; use Symfony\Bridge\Twig\Extension\LogoutUrlExtension; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AuthenticatorFactoryInterface; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FirewallListenerFactoryInterface; -use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\RememberMeFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\UserProviderFactoryInterface; use Symfony\Bundle\SecurityBundle\Security\LegacyLogoutHandlerListener; @@ -39,12 +39,18 @@ use Symfony\Component\PasswordHasher\Hasher\Pbkdf2PasswordHasher; use Symfony\Component\PasswordHasher\Hasher\PlaintextPasswordHasher; use Symfony\Component\PasswordHasher\Hasher\SodiumPasswordHasher; +use Symfony\Component\Security\Core\Authorization\AccessDecisionManager; +use Symfony\Component\Security\Core\Authorization\Strategy\AffirmativeStrategy; +use Symfony\Component\Security\Core\Authorization\Strategy\ConsensusStrategy; +use Symfony\Component\Security\Core\Authorization\Strategy\PriorityStrategy; +use Symfony\Component\Security\Core\Authorization\Strategy\UnanimousStrategy; use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder; use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder; use Symfony\Component\Security\Core\User\ChainUserProvider; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; +use Symfony\Component\Security\Http\Authenticator\Debug\TraceableAuthenticatorManagerListener; use Symfony\Component\Security\Http\Event\CheckPassportEvent; /** @@ -58,48 +64,30 @@ class SecurityExtension extends Extension implements PrependExtensionInterface private $requestMatchers = []; private $expressions = []; private $contextListeners = []; - private $listenerPositions = ['pre_auth', 'form', 'http', 'remember_me', 'anonymous']; + /** @var list */ private $factories = []; + /** @var list */ + private $sortedFactories = []; private $userProviderFactories = []; private $statelessFirewallKeys = []; private $authenticatorManagerEnabled = false; - public function __construct() - { - foreach ($this->listenerPositions as $position) { - $this->factories[$position] = []; - } - } - public function prepend(ContainerBuilder $container) { - $rememberMeSecureDefault = false; - $rememberMeSameSiteDefault = null; - - if (!isset($container->getExtensions()['framework'])) { - return; - } - foreach ($container->getExtensionConfig('framework') as $config) { - if (isset($config['session']) && \is_array($config['session'])) { - $rememberMeSecureDefault = $config['session']['cookie_secure'] ?? $rememberMeSecureDefault; - $rememberMeSameSiteDefault = \array_key_exists('cookie_samesite', $config['session']) ? $config['session']['cookie_samesite'] : $rememberMeSameSiteDefault; - } - } - foreach ($this->listenerPositions as $position) { - foreach ($this->factories[$position] as $factory) { - if ($factory instanceof RememberMeFactory) { - \Closure::bind(function () use ($rememberMeSecureDefault, $rememberMeSameSiteDefault) { - $this->options['secure'] = $rememberMeSecureDefault; - $this->options['samesite'] = $rememberMeSameSiteDefault; - }, $factory, $factory)(); - } + foreach ($this->getSortedFactories() as $factory) { + if ($factory instanceof PrependExtensionInterface) { + $factory->prepend($container); } } } public function load(array $configs, ContainerBuilder $container) { + if (!class_exists(InstalledVersions::class)) { + trigger_deprecation('symfony/security-bundle', '5.4', 'Configuring Symfony without the Composer Runtime API is deprecated. Consider upgrading to Composer 2.1 or later.'); + } + if (!array_filter($configs)) { return; } @@ -126,16 +114,23 @@ public function load(array $configs, ContainerBuilder $container) // The authenticator system no longer has anonymous tokens. This makes sure AccessListener // and AuthorizationChecker do not throw AuthenticationCredentialsNotFoundException when no // token is available in the token storage. - $container->getDefinition('security.access_listener')->setArgument(4, false); + $container->getDefinition('security.access_listener')->setArgument(3, false); + $container->getDefinition('security.authorization_checker')->setArgument(3, false); $container->getDefinition('security.authorization_checker')->setArgument(4, false); - $container->getDefinition('security.authorization_checker')->setArgument(5, false); } else { trigger_deprecation('symfony/security-bundle', '5.3', 'Not setting the "security.enable_authenticator_manager" config option to true is deprecated.'); + if ($config['always_authenticate_before_granting']) { + $authorizationChecker = $container->getDefinition('security.authorization_checker'); + $authorizationCheckerArgs = $authorizationChecker->getArguments(); + array_splice($authorizationCheckerArgs, 1, 0, [new Reference('security.authentication_manager')]); + $authorizationChecker->setArguments($authorizationCheckerArgs); + } + $loader->load('security_legacy.php'); } - if ($container::willBeAvailable('symfony/twig-bridge', LogoutUrlExtension::class, ['symfony/security-bundle'])) { + if ($container::willBeAvailable('symfony/twig-bridge', LogoutUrlExtension::class, ['symfony/security-bundle'], true)) { $loader->load('templating_twig.php'); } @@ -148,7 +143,7 @@ public function load(array $configs, ContainerBuilder $container) $loader->load('security_debug.php'); } - if (!$container::willBeAvailable('symfony/expression-language', ExpressionLanguage::class, ['symfony/security-bundle'])) { + if (!$container::willBeAvailable('symfony/expression-language', ExpressionLanguage::class, ['symfony/security-bundle'], true)) { $container->removeDefinition('security.expression_language'); $container->removeDefinition('security.access.expression_voter'); } @@ -160,12 +155,18 @@ public function load(array $configs, ContainerBuilder $container) if (isset($config['access_decision_manager']['service'])) { $container->setAlias('security.access.decision_manager', $config['access_decision_manager']['service']); + } elseif (isset($config['access_decision_manager']['strategy_service'])) { + $container + ->getDefinition('security.access.decision_manager') + ->addArgument(new Reference($config['access_decision_manager']['strategy_service'])); } else { $container ->getDefinition('security.access.decision_manager') - ->addArgument($config['access_decision_manager']['strategy']) - ->addArgument($config['access_decision_manager']['allow_if_all_abstain']) - ->addArgument($config['access_decision_manager']['allow_if_equal_granted_denied']); + ->addArgument($this->createStrategyDefinition( + $config['access_decision_manager']['strategy'] ?? AccessDecisionManager::STRATEGY_AFFIRMATIVE, + $config['access_decision_manager']['allow_if_all_abstain'], + $config['access_decision_manager']['allow_if_equal_granted_denied'] + )); } $container->setParameter('security.access.always_authenticate_before_granting', $config['always_authenticate_before_granting']); @@ -206,6 +207,25 @@ public function load(array $configs, ContainerBuilder $container) ->addTag('security.voter'); } + /** + * @throws \InvalidArgumentException if the $strategy is invalid + */ + private function createStrategyDefinition(string $strategy, bool $allowIfAllAbstainDecisions, bool $allowIfEqualGrantedDeniedDecisions): Definition + { + switch ($strategy) { + case MainConfiguration::STRATEGY_AFFIRMATIVE: + return new Definition(AffirmativeStrategy::class, [$allowIfAllAbstainDecisions]); + case MainConfiguration::STRATEGY_CONSENSUS: + return new Definition(ConsensusStrategy::class, [$allowIfAllAbstainDecisions, $allowIfEqualGrantedDeniedDecisions]); + case MainConfiguration::STRATEGY_UNANIMOUS: + return new Definition(UnanimousStrategy::class, [$allowIfAllAbstainDecisions]); + case MainConfiguration::STRATEGY_PRIORITY: + return new Definition(PriorityStrategy::class, [$allowIfAllAbstainDecisions]); + } + + throw new \InvalidArgumentException(sprintf('The strategy "%s" is not supported.', $strategy)); + } + private function createRoleHierarchy(array $config, ContainerBuilder $container) { if (!isset($config['role_hierarchy']) || 0 === \count($config['role_hierarchy'])) { @@ -520,6 +540,14 @@ private function createFirewall(ContainerBuilder $container, string $id, array $ ->replaceArgument(0, new Reference($managerId)) ; + if ($container->hasDefinition('debug.security.firewall') && $this->authenticatorManagerEnabled) { + $container + ->register('debug.security.firewall.authenticator.'.$id, TraceableAuthenticatorManagerListener::class) + ->setDecoratedService('security.firewall.authenticator.'.$id) + ->setArguments([new Reference('debug.security.firewall.authenticator.'.$id.'.inner')]) + ; + } + // user checker listener $container ->setDefinition('security.listener.user_checker.'.$id, new ChildDefinition('security.listener.user_checker')) @@ -556,12 +584,16 @@ private function createFirewall(ContainerBuilder $container, string $id, array $ $container->setAlias('security.user_checker.'.$id, new Alias($firewall['user_checker'], false)); - foreach ($this->factories as $position) { - foreach ($position as $factory) { - $key = str_replace('-', '_', $factory->getKey()); - if (\array_key_exists($key, $firewall)) { - $listenerKeys[] = $key; - } + foreach ($this->getSortedFactories() as $factory) { + $key = str_replace('-', '_', $factory->getKey()); + if ('custom_authenticators' !== $key && \array_key_exists($key, $firewall)) { + $listenerKeys[] = $key; + } + } + + if ($firewall['custom_authenticators'] ?? false) { + foreach ($firewall['custom_authenticators'] as $customAuthenticatorId) { + $listenerKeys[] = $customAuthenticatorId; } } @@ -594,44 +626,42 @@ private function createAuthenticationListeners(ContainerBuilder $container, stri $hasListeners = false; $entryPoints = []; - foreach ($this->listenerPositions as $position) { - foreach ($this->factories[$position] as $factory) { - $key = str_replace('-', '_', $factory->getKey()); + foreach ($this->getSortedFactories() as $factory) { + $key = str_replace('-', '_', $factory->getKey()); - if (isset($firewall[$key])) { - $userProvider = $this->getUserProvider($container, $id, $firewall, $key, $defaultProvider, $providerIds, $contextListenerId); + if (isset($firewall[$key])) { + $userProvider = $this->getUserProvider($container, $id, $firewall, $key, $defaultProvider, $providerIds, $contextListenerId); - if ($this->authenticatorManagerEnabled) { - if (!$factory instanceof AuthenticatorFactoryInterface) { - throw new InvalidConfigurationException(sprintf('Cannot configure AuthenticatorManager as "%s" authentication does not support it, set "security.enable_authenticator_manager" to `false`.', $key)); - } + if ($this->authenticatorManagerEnabled) { + if (!$factory instanceof AuthenticatorFactoryInterface) { + throw new InvalidConfigurationException(sprintf('Cannot configure AuthenticatorManager as "%s" authentication does not support it, set "security.enable_authenticator_manager" to `false`.', $key)); + } - $authenticators = $factory->createAuthenticator($container, $id, $firewall[$key], $userProvider); - if (\is_array($authenticators)) { - foreach ($authenticators as $authenticator) { - $authenticationProviders[] = $authenticator; - $entryPoints[] = $authenticator; - } - } else { - $authenticationProviders[] = $authenticators; - $entryPoints[$key] = $authenticators; + $authenticators = $factory->createAuthenticator($container, $id, $firewall[$key], $userProvider); + if (\is_array($authenticators)) { + foreach ($authenticators as $authenticator) { + $authenticationProviders[] = $authenticator; + $entryPoints[] = $authenticator; } } else { - [$provider, $listenerId, $defaultEntryPoint] = $factory->create($container, $id, $firewall[$key], $userProvider, $defaultEntryPoint); - - $listeners[] = new Reference($listenerId); - $authenticationProviders[] = $provider; + $authenticationProviders[] = $authenticators; + $entryPoints[$key] = $authenticators; } + } else { + [$provider, $listenerId, $defaultEntryPoint] = $factory->create($container, $id, $firewall[$key], $userProvider, $defaultEntryPoint); - if ($factory instanceof FirewallListenerFactoryInterface) { - $firewallListenerIds = $factory->createListeners($container, $id, $firewall[$key]); - foreach ($firewallListenerIds as $firewallListenerId) { - $listeners[] = new Reference($firewallListenerId); - } - } + $listeners[] = new Reference($listenerId); + $authenticationProviders[] = $provider; + } - $hasListeners = true; + if ($factory instanceof FirewallListenerFactoryInterface) { + $firewallListenerIds = $factory->createListeners($container, $id, $firewall[$key]); + foreach ($firewallListenerIds as $firewallListenerId) { + $listeners[] = new Reference($firewallListenerId); + } } + + $hasListeners = true; } } @@ -674,10 +704,14 @@ private function getUserProvider(ContainerBuilder $container, string $id, array } if ('remember_me' === $factoryKey || 'anonymous' === $factoryKey || 'custom_authenticators' === $factoryKey) { + if ('custom_authenticators' === $factoryKey) { + trigger_deprecation('symfony/security-bundle', '5.4', 'Not configuring explicitly the provider for the "%s" listener on "%s" firewall is deprecated because it\'s ambiguous as there is more than one registered provider.', $factoryKey, $id); + } + return 'security.user_providers'; } - throw new InvalidConfigurationException(sprintf('Not configuring explicitly the provider for the "%s" listener on "%s" firewall is ambiguous as there is more than one registered provider.', $factoryKey, $id)); + throw new InvalidConfigurationException(sprintf('Not configuring explicitly the provider for the "%s" %s on "%s" firewall is ambiguous as there is more than one registered provider.', $factoryKey, $this->authenticatorManagerEnabled ? 'authenticator' : 'listener', $id)); } private function createEncoders(array $encoders, ContainerBuilder $container) @@ -1010,7 +1044,7 @@ private function createExpression(ContainerBuilder $container, string $expressio return $this->expressions[$id]; } - if (!$container::willBeAvailable('symfony/expression-language', ExpressionLanguage::class, ['symfony/security-bundle'])) { + if (!$container::willBeAvailable('symfony/expression-language', ExpressionLanguage::class, ['symfony/security-bundle'], true)) { throw new \RuntimeException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed. Try running "composer require symfony/expression-language".'); } @@ -1062,9 +1096,27 @@ private function createRequestMatcher(ContainerBuilder $container, string $path return $this->requestMatchers[$id] = new Reference($id); } + /** + * @deprecated since Symfony 5.4, use "addAuthenticatorFactory()" instead + */ public function addSecurityListenerFactory(SecurityFactoryInterface $factory) { - $this->factories[$factory->getPosition()][] = $factory; + trigger_deprecation('symfony/security-bundle', '5.4', 'Method "%s()" is deprecated, use "addAuthenticatorFactory()" instead.', __METHOD__); + + $this->factories[] = [[ + 'pre_auth' => -10, + 'form' => -30, + 'http' => -40, + 'remember_me' => -50, + 'anonymous' => -60, + ][$factory->getPosition()], $factory]; + $this->sortedFactories = []; + } + + public function addAuthenticatorFactory(AuthenticatorFactoryInterface $factory) + { + $this->factories[] = [method_exists($factory, 'getPriority') ? $factory->getPriority() : 0, $factory]; + $this->sortedFactories = []; } public function addUserProviderFactory(UserProviderFactoryInterface $factory) @@ -1088,7 +1140,7 @@ public function getNamespace() public function getConfiguration(array $config, ContainerBuilder $container) { // first assemble the factories - return new MainConfiguration($this->factories, $this->userProviderFactories); + return new MainConfiguration($this->getSortedFactories(), $this->userProviderFactories); } private function isValidIps($ips): bool @@ -1135,4 +1187,25 @@ private function isValidIp(string $cidr): bool return false; } + + /** + * @return array + */ + private function getSortedFactories(): array + { + if (!$this->sortedFactories) { + $factories = []; + foreach ($this->factories as $i => $factory) { + $factories[] = array_merge($factory, [$i]); + } + + usort($factories, function ($a, $b) { + return $b[0] <=> $a[0] ?: $a[2] <=> $b[2]; + }); + + $this->sortedFactories = array_column($factories, 1); + } + + return $this->sortedFactories; + } } diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/schema/security-1.0.xsd b/src/Symfony/Bundle/SecurityBundle/Resources/config/schema/security-1.0.xsd index 586948d2f73be..8f0c1faffd364 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/schema/security-1.0.xsd +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/schema/security-1.0.xsd @@ -63,6 +63,7 @@ + diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.php b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.php index 34d100193b237..9c44adf0338cb 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.php +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.php @@ -48,6 +48,7 @@ use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; use Symfony\Component\Security\Http\Controller\UserValueResolver; use Symfony\Component\Security\Http\Firewall; +use Symfony\Component\Security\Http\FirewallMapInterface; use Symfony\Component\Security\Http\HttpUtils; use Symfony\Component\Security\Http\Impersonate\ImpersonateUrlGenerator; use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator; @@ -64,7 +65,6 @@ ->public() ->args([ service('security.token_storage'), - service('security.authentication.manager'), service('security.access.decision_manager'), param('security.access.always_authenticate_before_granting'), ]) @@ -189,6 +189,7 @@ abstract_arg('Firewall context locator'), abstract_arg('Request matchers'), ]) + ->alias(FirewallMapInterface::class, 'security.firewall.map') ->set('security.firewall.context', FirewallContext::class) ->abstract() diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.php b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.php index 163e6a63ca041..72129d1bbf865 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.php +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.php @@ -32,19 +32,22 @@ return static function (ContainerConfigurator $container) { $container->services() + ->set('security.authentication.basic_entry_point', BasicAuthenticationEntryPoint::class) + ->deprecate('symfony/security-bundle', '5.4', 'The "%service_id%" service is deprecated, the logic is contained in the authenticators.') + ->set('security.authentication.retry_entry_point', RetryAuthenticationEntryPoint::class) + ->deprecate('symfony/security-bundle', '5.4', 'The "%service_id%" service is deprecated, the logic is integrated directly in "security.channel_listener".') ->args([ inline_service('int')->factory([service('router.request_context'), 'getHttpPort']), inline_service('int')->factory([service('router.request_context'), 'getHttpsPort']), ]) - ->set('security.authentication.basic_entry_point', BasicAuthenticationEntryPoint::class) - ->set('security.channel_listener', ChannelListener::class) ->args([ service('security.access_map'), - service('security.authentication.retry_entry_point'), service('logger')->nullOnInvalid(), + inline_service('int')->factory([service('router.request_context'), 'getHttpPort']), + inline_service('int')->factory([service('router.request_context'), 'getHttpsPort']), ]) ->tag('monolog.logger', ['channel' => 'security']) diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig b/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig index e5a930af62c87..b332ba5ddb596 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig +++ b/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig @@ -3,374 +3,447 @@ {% block page_title 'Security' %} {% block toolbar %} - {% if collector.token %} - {% set is_authenticated = collector.enabled and collector.authenticated %} - {% set color_code = not is_authenticated ? 'yellow' %} - {% elseif collector.enabled %} - {% set color_code = collector.authenticatorManagerEnabled ? 'yellow' : 'red' %} - {% else %} - {% set color_code = '' %} - {% endif %} - - {% set icon %} - {{ include('@Security/Collector/icon.svg') }} - {{ collector.user|default('n/a') }} - {% endset %} - - {% set text %} - {% if collector.impersonated %} -
-
- Impersonator - {{ collector.impersonatorUser }} -
-
+ {% if collector.firewall %} + {% if collector.token %} + {% set is_authenticated = collector.enabled and collector.authenticated %} + {% set color_code = not is_authenticated ? 'yellow' %} + {% elseif collector.enabled %} + {% set color_code = collector.authenticatorManagerEnabled ? 'yellow' : 'red' %} + {% else %} + {% set color_code = '' %} {% endif %} -
- {% if collector.enabled %} - {% if collector.token %} -
- Logged in as - {{ collector.user }} -
+ {% set icon %} + {{ include('@Security/Collector/icon.svg') }} + {{ collector.user|default('n/a') }} + {% endset %} + {% set text %} + {% if collector.impersonated %} +
- Authenticated - {{ is_authenticated ? 'Yes' : 'No' }} + Impersonator + {{ collector.impersonatorUser }}
+
+ {% endif %} -
- Token class - {{ collector.tokenClass|abbr_class }} -
- {% else %} -
- Authenticated - No -
- {% endif %} +
+ {% if collector.enabled %} + {% if collector.token %} +
+ Logged in as + {{ collector.user }} +
+ +
+ Authenticated + {{ is_authenticated ? 'Yes' : 'No' }} +
+ +
+ Roles + + {% set remainingRoles = collector.roles|slice(1) %} + {{ collector.roles|first }} + {% if remainingRoles is not empty %} + + + + {{ remainingRoles|length }} more + + {% endif %} + +
+ +
+ Token class + {{ collector.tokenClass|abbr_class }} +
+ {% else %} +
+ Authenticated + No +
+ {% endif %} - {% if collector.firewall %} -
- Firewall name - {{ collector.firewall.name }} -
- {% endif %} + {% if collector.firewall %} +
+ Firewall name + {{ collector.firewall.name }} +
+ {% endif %} - {% if collector.token and collector.logoutUrl %} + {% if collector.token and collector.logoutUrl %} +
+ Actions + + Logout + {% if collector.impersonated and collector.impersonationExitPath %} + | Exit impersonation + {% endif %} + +
+ {% endif %} + {% else %}
- Actions - - Logout - {% if collector.impersonated and collector.impersonationExitPath %} - | Exit impersonation - {% endif %} - + The security is disabled.
{% endif %} - {% else %} -
- The security is disabled. -
- {% endif %} -
- {% endset %} +
+ {% endset %} - {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: color_code }) }} + {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: color_code }) }} + {% endif %} {% endblock %} {% block menu %} - + {{ include('@Security/Collector/icon.svg') }} Security {% endblock %} {% block panel %} -

Security Token

- +

Security

{% if collector.enabled %} - {% if collector.token %} -
-
- {{ collector.user == 'anon.' ? 'Anonymous' : collector.user }} - Username -
+
+
+

Token

+ +
+ {% if collector.token %} +
+
+ {{ collector.user == 'anon.' ? 'Anonymous' : collector.user }} + Username +
+ +
+ {{ include('@WebProfiler/Icon/' ~ (collector.authenticated ? 'yes' : 'no') ~ '.svg') }} + Authenticated +
+
+ + + + + + + + + + + + + + + {% if collector.supportsRoleHierarchy %} + + + + + {% endif %} -
- {{ include('@WebProfiler/Icon/' ~ (collector.authenticated ? 'yes' : 'no') ~ '.svg') }} - Authenticated + {% if collector.token %} +
+ + + + {% endif %} + +
PropertyValue
Roles + {{ collector.roles is empty ? 'none' : profiler_dump(collector.roles, maxDepth=1) }} + + {% if not collector.authenticated and collector.roles is empty %} +

User is not authenticated probably because they have no roles.

+ {% endif %} +
Inherited Roles{{ collector.inheritedRoles is empty ? 'none' : profiler_dump(collector.inheritedRoles, maxDepth=1) }}
Token{{ profiler_dump(collector.token) }}
+ {% elseif collector.enabled %} +
+

There is no security token.

+
+ {% endif %}
- - - - - - - - - - - - - - {% if collector.supportsRoleHierarchy %} - - - - - {% endif %} - - {% if collector.token %} - - - - + + + {% if collector.firewall.security_enabled %} +

Configuration

+
PropertyValue
Roles - {{ collector.roles is empty ? 'none' : profiler_dump(collector.roles, maxDepth=1) }} - - {% if not collector.authenticated and collector.roles is empty %} -

User is not authenticated probably because they have no roles.

+
+

Firewall

+
+ {% if collector.firewall %} +
+
+ {{ collector.firewall.name }} + Name +
+
+ {{ include('@WebProfiler/Icon/' ~ (collector.firewall.security_enabled ? 'yes' : 'no') ~ '.svg') }} + Security enabled +
+
+ {{ include('@WebProfiler/Icon/' ~ (collector.firewall.stateless ? 'yes' : 'no') ~ '.svg') }} + Stateless +
+ {% if collector.authenticatorManagerEnabled == false %} +
+ {{ include('@WebProfiler/Icon/' ~ (collector.firewall.allows_anonymous ? 'yes' : 'no') ~ '.svg') }} + Allows anonymous +
{% endif %} -
Inherited Roles{{ collector.inheritedRoles is empty ? 'none' : profiler_dump(collector.inheritedRoles, maxDepth=1) }}
Token{{ profiler_dump(collector.token) }}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% if collector.authenticatorManagerEnabled %} + + + + + {% else %} + + + + + {% endif %} + +
KeyValue
provider{{ collector.firewall.provider ?: '(none)' }}
context{{ collector.firewall.context ?: '(none)' }}
entry_point{{ collector.firewall.entry_point ?: '(none)' }}
user_checker{{ collector.firewall.user_checker ?: '(none)' }}
access_denied_handler{{ collector.firewall.access_denied_handler ?: '(none)' }}
access_denied_url{{ collector.firewall.access_denied_url ?: '(none)' }}
authenticators{{ collector.firewall.authenticators is empty ? '(none)' : profiler_dump(collector.firewall.authenticators, maxDepth=1) }}
listeners{{ collector.firewall.listeners is empty ? '(none)' : profiler_dump(collector.firewall.listeners, maxDepth=1) }}
+ {% endif %} {% endif %} - - - {% elseif collector.enabled %} -
-

There is no security token.

+
- {% endif %} +
+

Listeners

+
+ {% if collector.listeners|default([]) is empty %} +
+

No security listeners have been recorded. Check that debugging is enabled in the kernel.

+
+ {% else %} + + + + + + + + -

Security Firewall

+ {% set previous_event = (collector.listeners|first) %} + {% for listener in collector.listeners %} + {% if loop.first or listener != previous_event %} + {% if not loop.first %} + + {% endif %} - {% if collector.firewall %} -
-
- {{ collector.firewall.name }} - Name -
-
- {{ include('@WebProfiler/Icon/' ~ (collector.firewall.security_enabled ? 'yes' : 'no') ~ '.svg') }} - Security enabled -
-
- {{ include('@WebProfiler/Icon/' ~ (collector.firewall.stateless ? 'yes' : 'no') ~ '.svg') }} - Stateless -
- {% if collector.authenticatorManagerEnabled == false %} -
- {{ include('@WebProfiler/Icon/' ~ (collector.firewall.allows_anonymous ? 'yes' : 'no') ~ '.svg') }} - Allows anonymous -
- {% endif %} -
+ + {% set previous_event = listener %} + {% endif %} - {% if collector.firewall.security_enabled %} -

Configuration

- -
ListenerDurationResponse
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
KeyValue
provider{{ collector.firewall.provider ?: '(none)' }}
context{{ collector.firewall.context ?: '(none)' }}
entry_point{{ collector.firewall.entry_point ?: '(none)' }}
user_checker{{ collector.firewall.user_checker ?: '(none)' }}
access_denied_handler{{ collector.firewall.access_denied_handler ?: '(none)' }}
access_denied_url{{ collector.firewall.access_denied_url ?: '(none)' }}
listeners{{ collector.firewall.listeners is empty ? '(none)' : profiler_dump(collector.firewall.listeners, maxDepth=1) }}
- -

Listeners

- - {% if collector.listeners|default([]) is empty %} -
-

No security listeners have been recorded. Check that debugging is enabled in the kernel.

-
- {% else %} - - - - - - - - - - {% set previous_event = (collector.listeners|first) %} - {% for listener in collector.listeners %} - {% if loop.first or listener != previous_event %} - {% if not loop.first %} + + + + + + + {% if loop.last %} {% endif %} + {% endfor %} +
ListenerDurationResponse
{{ profiler_dump(listener.stub) }}{{ '%0.2f'|format(listener.time * 1000) }} ms{{ listener.response ? profiler_dump(listener.response) : '(none)' }}
+ {% endif %} +
+
- - {% set previous_event = listener %} - {% endif %} - +
+

Authenticators

+
+ {% if collector.authenticators|default([]) is not empty %} + + - - - + + + + + - {% if loop.last %} - - {% endif %} - {% endfor %} -
{{ profiler_dump(listener.stub) }}{{ '%0.2f'|format(listener.time * 1000) }} ms{{ listener.response ? profiler_dump(listener.response) : '(none)' }}AuthenticatorSupportsDurationPassport
- {% endif %} - {% endif %} - {% elseif collector.enabled %} -
-

This request was not covered by any firewall.

-
- {% endif %} - {% else %} -
-

The security component is disabled.

-
- {% endif %} - - {% if collector.voters|default([]) is not empty %} -

Security Voters ({{ collector.voters|length }})

+ {% set previous_event = (collector.listeners|first) %} + {% for authenticator in collector.authenticators %} + {% if loop.first or authenticator != previous_event %} + {% if not loop.first %} + + {% endif %} -
-
- {{ collector.voterStrategy|default('unknown') }} - Strategy -
-
+ + {% set previous_event = authenticator %} + {% endif %} - - - - - - - - - - {% for voter in collector.voters %} - - - - - {% endfor %} - -
#Voter class
{{ loop.index }}{{ profiler_dump(voter) }}
- {% endif %} + + {{ profiler_dump(authenticator.stub) }} + {{ include('@WebProfiler/Icon/' ~ (authenticator.supports ? 'yes' : 'no') ~ '.svg') }} + {{ '%0.2f'|format(authenticator.duration * 1000) }} ms + {{ authenticator.passport ? profiler_dump(authenticator.passport) : '(none)' }} + - {% if collector.accessDecisionLog|default([]) is not empty %} -

Access decision log

- - - - - - - - - - - - - - - - - - {% for decision in collector.accessDecisionLog %} - - - - {% endif %} - {% else %} - {{ profiler_dump(decision.attributes) }} - {% endif %} - - - - - - - - {% endfor %} - -
#ResultAttributesObject
{{ loop.index }} - {{ decision.result - ? 'GRANTED' - : 'DENIED' - }} - - {% if decision.attributes|length == 1 %} - {% set attribute = decision.attributes|first %} - {% if attribute.expression is defined %} - Expression:
{{ attribute.expression }}
- {% elseif attribute.type == 'string' %} - {{ attribute }} - {% else %} - {{ profiler_dump(attribute) }} + {% if loop.last %} +
{{ profiler_dump(decision.seek('object')) }}
- {% if decision.voter_details is not empty %} - {% set voter_details_id = 'voter-details-' ~ loop.index %} -
- - - {% for voter_detail in decision.voter_details %} - - - {% if collector.voterStrategy == constant('Symfony\\Component\\Security\\Core\\Authorization\\AccessDecisionManager::STRATEGY_UNANIMOUS') %} - - {% endif %} -
{{ profiler_dump(voter_detail['class']) }}attribute {{ voter_detail['attributes'][0] }} - {% if voter_detail['vote'] == constant('Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface::ACCESS_GRANTED') %} - ACCESS GRANTED - {% elseif voter_detail['vote'] == constant('Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface::ACCESS_ABSTAIN') %} - ACCESS ABSTAIN - {% elseif voter_detail['vote'] == constant('Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface::ACCESS_DENIED') %} - ACCESS DENIED + {% endfor %} +
+ {% else %} +
+

No authenticators have been recorded. Check previous profiles on your authentication endpoint.

+
+ {% endif %} +
+ + +
+

Access Decision

+
+ {% if collector.voters|default([]) is not empty %} +
+
+ {{ collector.voterStrategy|default('unknown') }} + Strategy +
+
+ + + + + + + + + + + {% for voter in collector.voters %} + + + + + {% endfor %} + +
#Voter class
{{ loop.index }}{{ profiler_dump(voter) }}
+ {% endif %} + {% if collector.accessDecisionLog|default([]) is not empty %} +

Access decision log

+ + + + + + + + + + + + + + + + + + {% for decision in collector.accessDecisionLog %} + + + + - - {% endfor %} - -
#ResultAttributesObject
{{ loop.index }} + {{ decision.result + ? 'GRANTED' + : 'DENIED' + }} + + {% if decision.attributes|length == 1 %} + {% set attribute = decision.attributes|first %} + {% if attribute.expression is defined %} + Expression:
{{ attribute.expression }}
+ {% elseif attribute.type == 'string' %} + {{ attribute }} {% else %} - unknown ({{ voter_detail['vote'] }}) + {{ profiler_dump(attribute) }} {% endif %} -
-
- Show voter details - {% endif %} -
+ {% else %} + {{ profiler_dump(decision.attributes) }} + {% endif %} + + {{ profiler_dump(decision.seek('object')) }} + + + + + {% if decision.voter_details is not empty %} + {% set voter_details_id = 'voter-details-' ~ loop.index %} +
+ + + {% for voter_detail in decision.voter_details %} + + + {% if collector.voterStrategy == constant('Symfony\\Component\\Security\\Core\\Authorization\\AccessDecisionManager::STRATEGY_UNANIMOUS') %} + + {% endif %} + + + {% endfor %} + +
{{ profiler_dump(voter_detail['class']) }}attribute {{ voter_detail['attributes'][0] }} + {% if voter_detail['vote'] == constant('Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface::ACCESS_GRANTED') %} + ACCESS GRANTED + {% elseif voter_detail['vote'] == constant('Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface::ACCESS_ABSTAIN') %} + ACCESS ABSTAIN + {% elseif voter_detail['vote'] == constant('Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface::ACCESS_DENIED') %} + ACCESS DENIED + {% else %} + unknown ({{ voter_detail['vote'] }}) + {% endif %} +
+
+ Show voter details + {% endif %} + + + {% endfor %} + + +
+ {% endif %} +
+
{% endif %} {% endblock %} diff --git a/src/Symfony/Bundle/SecurityBundle/Security/FirewallConfig.php b/src/Symfony/Bundle/SecurityBundle/Security/FirewallConfig.php index 1a78dd2f4aa72..c9b1e9ca8f1fb 100644 --- a/src/Symfony/Bundle/SecurityBundle/Security/FirewallConfig.php +++ b/src/Symfony/Bundle/SecurityBundle/Security/FirewallConfig.php @@ -26,10 +26,10 @@ final class FirewallConfig private $entryPoint; private $accessDeniedHandler; private $accessDeniedUrl; - private $listeners; + private $authenticators; private $switchUser; - public function __construct(string $name, string $userChecker, string $requestMatcher = null, bool $securityEnabled = true, bool $stateless = false, string $provider = null, string $context = null, string $entryPoint = null, string $accessDeniedHandler = null, string $accessDeniedUrl = null, array $listeners = [], array $switchUser = null) + public function __construct(string $name, string $userChecker, string $requestMatcher = null, bool $securityEnabled = true, bool $stateless = false, string $provider = null, string $context = null, string $entryPoint = null, string $accessDeniedHandler = null, string $accessDeniedUrl = null, array $authenticators = [], array $switchUser = null) { $this->name = $name; $this->userChecker = $userChecker; @@ -41,7 +41,7 @@ public function __construct(string $name, string $userChecker, string $requestMa $this->entryPoint = $entryPoint; $this->accessDeniedHandler = $accessDeniedHandler; $this->accessDeniedUrl = $accessDeniedUrl; - $this->listeners = $listeners; + $this->authenticators = $authenticators; $this->switchUser = $switchUser; } @@ -64,9 +64,14 @@ public function isSecurityEnabled(): bool return $this->securityEnabled; } + /** + * @deprecated since Symfony 5.4 + */ public function allowsAnonymous(): bool { - return \in_array('anonymous', $this->listeners, true); + trigger_deprecation('symfony/security-bundle', '5.4', 'The "%s()" method is deprecated.', __METHOD__); + + return \in_array('anonymous', $this->authenticators, true); } public function isStateless(): bool @@ -107,9 +112,19 @@ public function getAccessDeniedUrl(): ?string return $this->accessDeniedUrl; } + /** + * @deprecated since Symfony 5.4, use {@see getListeners()} instead + */ public function getListeners(): array { - return $this->listeners; + trigger_deprecation('symfony/security-bundle', '5.4', 'Method "%s()" is deprecated, use "%s::getAuthenticators()" instead.', __METHOD__, __CLASS__); + + return $this->getAuthenticators(); + } + + public function getAuthenticators(): array + { + return $this->authenticators; } public function getSwitchUser(): ?array diff --git a/src/Symfony/Bundle/SecurityBundle/Security/FirewallContext.php b/src/Symfony/Bundle/SecurityBundle/Security/FirewallContext.php index 00754e4363cfc..4ebc9c7de0dc7 100644 --- a/src/Symfony/Bundle/SecurityBundle/Security/FirewallContext.php +++ b/src/Symfony/Bundle/SecurityBundle/Security/FirewallContext.php @@ -27,6 +27,9 @@ class FirewallContext private $logoutListener; private $config; + /** + * @param iterable $listeners + */ public function __construct(iterable $listeners, ExceptionListener $exceptionListener = null, LogoutListener $logoutListener = null, FirewallConfig $config = null) { $this->listeners = $listeners; @@ -40,6 +43,9 @@ public function getConfig() return $this->config; } + /** + * @return iterable + */ public function getListeners(): iterable { return $this->listeners; diff --git a/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php b/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php index 0798a3627da7f..4c2c3046f004f 100644 --- a/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php +++ b/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php @@ -38,6 +38,7 @@ use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\X509Factory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\InMemoryFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\LdapFactory; +use Symfony\Bundle\SecurityBundle\DependencyInjection\SecurityExtension; use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\EventDispatcher\DependencyInjection\AddEventAliasesPass; @@ -56,21 +57,22 @@ public function build(ContainerBuilder $container) { parent::build($container); + /** @var SecurityExtension $extension */ $extension = $container->getExtension('security'); - $extension->addSecurityListenerFactory(new FormLoginFactory()); - $extension->addSecurityListenerFactory(new FormLoginLdapFactory()); - $extension->addSecurityListenerFactory(new JsonLoginFactory()); - $extension->addSecurityListenerFactory(new JsonLoginLdapFactory()); - $extension->addSecurityListenerFactory(new HttpBasicFactory()); - $extension->addSecurityListenerFactory(new HttpBasicLdapFactory()); - $extension->addSecurityListenerFactory(new RememberMeFactory()); - $extension->addSecurityListenerFactory(new X509Factory()); - $extension->addSecurityListenerFactory(new RemoteUserFactory()); - $extension->addSecurityListenerFactory(new GuardAuthenticationFactory()); - $extension->addSecurityListenerFactory(new AnonymousFactory()); - $extension->addSecurityListenerFactory(new CustomAuthenticatorFactory()); - $extension->addSecurityListenerFactory(new LoginThrottlingFactory()); - $extension->addSecurityListenerFactory(new LoginLinkFactory()); + $extension->addAuthenticatorFactory(new FormLoginFactory()); + $extension->addAuthenticatorFactory(new FormLoginLdapFactory()); + $extension->addAuthenticatorFactory(new JsonLoginFactory()); + $extension->addAuthenticatorFactory(new JsonLoginLdapFactory()); + $extension->addAuthenticatorFactory(new HttpBasicFactory()); + $extension->addAuthenticatorFactory(new HttpBasicLdapFactory()); + $extension->addAuthenticatorFactory(new RememberMeFactory()); + $extension->addAuthenticatorFactory(new X509Factory()); + $extension->addAuthenticatorFactory(new RemoteUserFactory()); + $extension->addAuthenticatorFactory(new GuardAuthenticationFactory()); + $extension->addAuthenticatorFactory(new AnonymousFactory()); + $extension->addAuthenticatorFactory(new CustomAuthenticatorFactory()); + $extension->addAuthenticatorFactory(new LoginThrottlingFactory()); + $extension->addAuthenticatorFactory(new LoginLinkFactory()); $extension->addUserProviderFactory(new InMemoryFactory()); $extension->addUserProviderFactory(new LdapFactory()); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php index 855b9fcb18cc3..b5ed1ad849276 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Bundle\SecurityBundle\DataCollector\SecurityDataCollector; use Symfony\Bundle\SecurityBundle\Debug\TraceableFirewallListener; +use Symfony\Bundle\SecurityBundle\DependencyInjection\MainConfiguration; use Symfony\Bundle\SecurityBundle\Security\FirewallConfig; use Symfony\Bundle\SecurityBundle\Security\FirewallMap; use Symfony\Component\EventDispatcher\EventDispatcher; @@ -24,11 +25,11 @@ use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; -use Symfony\Component\Security\Core\Authorization\AccessDecisionManager; use Symfony\Component\Security\Core\Authorization\TraceableAccessDecisionManager; use Symfony\Component\Security\Core\Authorization\Voter\TraceableVoter; use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; use Symfony\Component\Security\Core\Role\RoleHierarchy; +use Symfony\Component\Security\Core\User\InMemoryUser; use Symfony\Component\Security\Http\FirewallMapInterface; use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; @@ -37,7 +38,7 @@ class SecurityDataCollectorTest extends TestCase { public function testCollectWhenSecurityIsDisabled() { - $collector = new SecurityDataCollector(); + $collector = new SecurityDataCollector(null, null, null, null, null, null, true); $collector->collect(new Request(), new Response()); $this->assertSame('security', $collector->getName()); @@ -57,7 +58,7 @@ public function testCollectWhenSecurityIsDisabled() public function testCollectWhenAuthenticationTokenIsNull() { $tokenStorage = new TokenStorage(); - $collector = new SecurityDataCollector($tokenStorage, $this->getRoleHierarchy()); + $collector = new SecurityDataCollector($tokenStorage, $this->getRoleHierarchy(), null, null, null, null, true); $collector->collect(new Request(), new Response()); $this->assertTrue($collector->isEnabled()); @@ -71,16 +72,16 @@ public function testCollectWhenAuthenticationTokenIsNull() $this->assertCount(0, $collector->getInheritedRoles()); $this->assertEmpty($collector->getUser()); $this->assertNull($collector->getFirewall()); - $this->assertFalse($collector->isAuthenticatorManagerEnabled()); + $this->assertTrue($collector->isAuthenticatorManagerEnabled()); } /** @dataProvider provideRoles */ public function testCollectAuthenticationTokenAndRoles(array $roles, array $normalizedRoles, array $inheritedRoles) { $tokenStorage = new TokenStorage(); - $tokenStorage->setToken(new UsernamePasswordToken('hhamon', 'P4$$w0rD', 'provider', $roles)); + $tokenStorage->setToken(new UsernamePasswordToken(new InMemoryUser('hhamon', 'P4$$w0rD', $roles), 'provider', $roles)); - $collector = new SecurityDataCollector($tokenStorage, $this->getRoleHierarchy()); + $collector = new SecurityDataCollector($tokenStorage, $this->getRoleHierarchy(), null, null, null, null, true); $collector->collect(new Request(), new Response()); $collector->lateCollect(); @@ -94,17 +95,17 @@ public function testCollectAuthenticationTokenAndRoles(array $roles, array $norm $this->assertSame($normalizedRoles, $collector->getRoles()->getValue(true)); $this->assertSame($inheritedRoles, $collector->getInheritedRoles()->getValue(true)); $this->assertSame('hhamon', $collector->getUser()); - $this->assertFalse($collector->isAuthenticatorManagerEnabled()); + $this->assertTrue($collector->isAuthenticatorManagerEnabled()); } public function testCollectSwitchUserToken() { - $adminToken = new UsernamePasswordToken('yceruto', 'P4$$w0rD', 'provider', ['ROLE_ADMIN']); + $adminToken = new UsernamePasswordToken(new InMemoryUser('yceruto', 'P4$$w0rD', ['ROLE_ADMIN']), 'provider', ['ROLE_ADMIN']); $tokenStorage = new TokenStorage(); - $tokenStorage->setToken(new SwitchUserToken('hhamon', 'P4$$w0rD', 'provider', ['ROLE_USER', 'ROLE_PREVIOUS_ADMIN'], $adminToken)); + $tokenStorage->setToken(new SwitchUserToken(new InMemoryUser('hhamon', 'P4$$w0rD', ['ROLE_USER', 'ROLE_PREVIOUS_ADMIN']), 'provider', ['ROLE_USER', 'ROLE_PREVIOUS_ADMIN'], $adminToken)); - $collector = new SecurityDataCollector($tokenStorage, $this->getRoleHierarchy()); + $collector = new SecurityDataCollector($tokenStorage, $this->getRoleHierarchy(), null, null, null, null, true); $collector->collect(new Request(), new Response()); $collector->lateCollect(); @@ -140,7 +141,6 @@ public function testGetFirewall() $collected = $collector->getFirewall(); $this->assertSame($firewallConfig->getName(), $collected['name']); - $this->assertSame($firewallConfig->allowsAnonymous(), $collected['allows_anonymous']); $this->assertSame($firewallConfig->getRequestMatcher(), $collected['request_matcher']); $this->assertSame($firewallConfig->isSecurityEnabled(), $collected['security_enabled']); $this->assertSame($firewallConfig->isStateless(), $collected['stateless']); @@ -150,7 +150,7 @@ public function testGetFirewall() $this->assertSame($firewallConfig->getAccessDeniedHandler(), $collected['access_denied_handler']); $this->assertSame($firewallConfig->getAccessDeniedUrl(), $collected['access_denied_url']); $this->assertSame($firewallConfig->getUserChecker(), $collected['user_checker']); - $this->assertSame($firewallConfig->getListeners(), $collected['listeners']->getValue()); + $this->assertSame($firewallConfig->getAuthenticators(), $collected['authenticators']->getValue()); $this->assertTrue($collector->isAuthenticatorManagerEnabled()); } @@ -160,7 +160,7 @@ public function testGetFirewallReturnsNull() $response = new Response(); // Don't inject any firewall map - $collector = new SecurityDataCollector(); + $collector = new SecurityDataCollector(null, null, null, null, null, null, true); $collector->collect($request, $response); $this->assertNull($collector->getFirewall()); @@ -170,7 +170,7 @@ public function testGetFirewallReturnsNull() ->disableOriginalConstructor() ->getMock(); - $collector = new SecurityDataCollector(null, null, null, null, $firewallMap, new TraceableFirewallListener($firewallMap, new EventDispatcher(), new LogoutUrlGenerator())); + $collector = new SecurityDataCollector(null, null, null, null, $firewallMap, new TraceableFirewallListener($firewallMap, new EventDispatcher(), new LogoutUrlGenerator()), true); $collector->collect($request, $response); $this->assertNull($collector->getFirewall()); @@ -180,7 +180,7 @@ public function testGetFirewallReturnsNull() ->disableOriginalConstructor() ->getMock(); - $collector = new SecurityDataCollector(null, null, null, null, $firewallMap, new TraceableFirewallListener($firewallMap, new EventDispatcher(), new LogoutUrlGenerator())); + $collector = new SecurityDataCollector(null, null, null, null, $firewallMap, new TraceableFirewallListener($firewallMap, new EventDispatcher(), new LogoutUrlGenerator()), true); $collector->collect($request, $response); $this->assertNull($collector->getFirewall()); } @@ -214,7 +214,7 @@ public function testGetListeners() $firewall = new TraceableFirewallListener($firewallMap, new EventDispatcher(), new LogoutUrlGenerator()); $firewall->onKernelRequest($event); - $collector = new SecurityDataCollector(null, null, null, null, $firewallMap, $firewall); + $collector = new SecurityDataCollector(null, null, null, null, $firewallMap, $firewall, true); $collector->collect($request, $response); $this->assertNotEmpty($collected = $collector->getListeners()[0]); @@ -231,7 +231,7 @@ public function providerCollectDecisionLog(): \Generator $decoratedVoter1 = new TraceableVoter($voter1, $eventDispatcher); yield [ - AccessDecisionManager::STRATEGY_AFFIRMATIVE, + MainConfiguration::STRATEGY_AFFIRMATIVE, [[ 'attributes' => ['view'], 'object' => new \stdClass(), @@ -255,7 +255,7 @@ public function providerCollectDecisionLog(): \Generator ]; yield [ - AccessDecisionManager::STRATEGY_UNANIMOUS, + MainConfiguration::STRATEGY_UNANIMOUS, [ [ 'attributes' => ['view', 'edit'], @@ -339,7 +339,7 @@ public function testCollectDecisionLog(string $strategy, array $decisionLog, arr ->method('getDecisionLog') ->willReturn($decisionLog); - $dataCollector = new SecurityDataCollector(null, null, null, $accessDecisionManager); + $dataCollector = new SecurityDataCollector(null, null, null, $accessDecisionManager, null, null, true); $dataCollector->collect(new Request(), new Response()); $this->assertEquals($dataCollector->getAccessDecisionLog(), $expectedDecisionLog, 'Wrong value returned by getAccessDecisionLog'); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Debug/TraceableFirewallListenerTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Debug/TraceableFirewallListenerTest.php index 2e69efd08d633..6dad1f3a72913 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Debug/TraceableFirewallListenerTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Debug/TraceableFirewallListenerTest.php @@ -19,6 +19,15 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Http\Authentication\AuthenticatorManager; +use Symfony\Component\Security\Http\Authenticator\Debug\TraceableAuthenticatorManagerListener; +use Symfony\Component\Security\Http\Authenticator\InteractiveAuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\Passport; +use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; +use Symfony\Component\Security\Http\Firewall\AuthenticatorManagerListener; use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator; /** @@ -54,4 +63,78 @@ public function testOnKernelRequestRecordsListeners() $this->assertCount(1, $listeners); $this->assertSame($listener, $listeners[0]['stub']); } + + public function testOnKernelRequestRecordsAuthenticatorsInfo() + { + $request = new Request(); + + $event = new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST); + $event->setResponse($response = new Response()); + + $supportingAuthenticator = $this->createMock(DummyAuthenticator::class); + $supportingAuthenticator + ->method('supports') + ->with($request) + ->willReturn(true); + $supportingAuthenticator + ->expects($this->once()) + ->method('authenticate') + ->with($request) + ->willReturn(new SelfValidatingPassport(new UserBadge('robin', function () {}))); + $supportingAuthenticator + ->expects($this->once()) + ->method('onAuthenticationSuccess') + ->willReturn($response); + $supportingAuthenticator + ->expects($this->once()) + ->method('createToken') + ->willReturn($this->createMock(TokenInterface::class)); + + $notSupportingAuthenticator = $this->createMock(DummyAuthenticator::class); + $notSupportingAuthenticator + ->method('supports') + ->with($request) + ->willReturn(false); + + $tokenStorage = $this->createMock(TokenStorageInterface::class); + $dispatcher = new EventDispatcher(); + $authenticatorManager = new AuthenticatorManager( + [$notSupportingAuthenticator, $supportingAuthenticator], + $tokenStorage, + $dispatcher, + 'main' + ); + + $listener = new TraceableAuthenticatorManagerListener(new AuthenticatorManagerListener($authenticatorManager)); + $firewallMap = $this->createMock(FirewallMap::class); + $firewallMap + ->expects($this->once()) + ->method('getFirewallConfig') + ->with($request) + ->willReturn(null); + $firewallMap + ->expects($this->once()) + ->method('getListeners') + ->with($request) + ->willReturn([[$listener], null, null]); + + $firewall = new TraceableFirewallListener($firewallMap, $dispatcher, new LogoutUrlGenerator()); + $firewall->configureLogoutUrlGenerator($event); + $firewall->onKernelRequest($event); + + $this->assertCount(2, $authenticatorsInfo = $firewall->getAuthenticatorsInfo()); + + $this->assertFalse($authenticatorsInfo[0]['supports']); + $this->assertStringContainsString('DummyAuthenticator', $authenticatorsInfo[0]['stub']); + + $this->assertTrue($authenticatorsInfo[1]['supports']); + $this->assertStringContainsString('DummyAuthenticator', $authenticatorsInfo[1]['stub']); + } +} + +abstract class DummyAuthenticator implements InteractiveAuthenticatorInterface +{ + public function createToken(Passport $passport, string $firewallName): TokenInterface + { + } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/RegisterEntryPointsPassTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/RegisterEntryPointsPassTest.php index 141f637ae9be0..b10b8a810bc7a 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/RegisterEntryPointsPassTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/RegisterEntryPointsPassTest.php @@ -25,7 +25,7 @@ use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Http\Authentication\AuthenticatorManager; use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator; -use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Passport; use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface; use Symfony\Component\Security\Http\Firewall\ExceptionListener; @@ -76,7 +76,7 @@ public function supports(Request $request): ?bool return false; } - public function authenticate(Request $request): PassportInterface + public function authenticate(Request $request): Passport { throw new BadCredentialsException(); } @@ -93,7 +93,7 @@ public function onAuthenticationFailure(Request $request, AuthenticationExceptio ], JsonResponse::HTTP_FORBIDDEN); } - public function start(Request $request, AuthenticationException $authException = null) + public function start(Request $request, AuthenticationException $authException = null): Response { } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php index 7f53f2d3806c0..ae32d474c7cf6 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php @@ -17,12 +17,14 @@ use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\DependencyInjection\Argument\IteratorArgument; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\PasswordHasher\Hasher\NativePasswordHasher; use Symfony\Component\PasswordHasher\Hasher\Pbkdf2PasswordHasher; use Symfony\Component\PasswordHasher\Hasher\PlaintextPasswordHasher; use Symfony\Component\PasswordHasher\Hasher\SodiumPasswordHasher; use Symfony\Component\Security\Core\Authorization\AccessDecisionManager; +use Symfony\Component\Security\Core\Authorization\Strategy\AffirmativeStrategy; use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder; use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder; use Symfony\Component\Security\Http\Authentication\AuthenticatorManager; @@ -1046,7 +1048,7 @@ public function testDefaultAccessDecisionManagerStrategyIsAffirmative() { $container = $this->getContainer('access_decision_manager_default_strategy'); - $this->assertSame(AccessDecisionManager::STRATEGY_AFFIRMATIVE, $container->getDefinition('security.access.decision_manager')->getArgument(1), 'Default vote strategy is affirmative'); + $this->assertEquals((new Definition(AffirmativeStrategy::class, [false])), $container->getDefinition('security.access.decision_manager')->getArgument(1), 'Default vote strategy is affirmative'); } public function testCustomAccessDecisionManagerService() @@ -1069,9 +1071,17 @@ public function testAccessDecisionManagerOptionsAreNotOverriddenByImplicitStrate $accessDecisionManagerDefinition = $container->getDefinition('security.access.decision_manager'); - $this->assertSame(AccessDecisionManager::STRATEGY_AFFIRMATIVE, $accessDecisionManagerDefinition->getArgument(1)); - $this->assertTrue($accessDecisionManagerDefinition->getArgument(2)); - $this->assertFalse($accessDecisionManagerDefinition->getArgument(3)); + $this->assertEquals((new Definition(AffirmativeStrategy::class, [true])), $accessDecisionManagerDefinition->getArgument(1)); + } + + public function testAccessDecisionManagerWithStrategyService() + { + $container = $this->getContainer('access_decision_manager_strategy_service'); + + $accessDecisionManagerDefinition = $container->getDefinition('security.access.decision_manager'); + + $this->assertEquals(AccessDecisionManager::class, $accessDecisionManagerDefinition->getClass()); + $this->assertEquals(new Reference('app.custom_access_decision_strategy'), $accessDecisionManagerDefinition->getArgument(1)); } public function testFirewallUndefinedUserProvider() diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/access_decision_manager_strategy_service.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/access_decision_manager_strategy_service.php new file mode 100644 index 0000000000000..8024e3a72f25b --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/access_decision_manager_strategy_service.php @@ -0,0 +1,20 @@ +loadFromExtension('security', [ + 'enable_authenticator_manager' => true, + 'access_decision_manager' => [ + 'strategy_service' => 'app.custom_access_decision_strategy', + ], + 'providers' => [ + 'default' => [ + 'memory' => [ + 'users' => [ + 'foo' => ['password' => 'foo', 'roles' => 'ROLE_USER'], + ], + ], + ], + ], + 'firewalls' => [ + 'simple' => ['pattern' => '/login', 'security' => false], + ], +]); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/access_decision_manager_strategy_service.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/access_decision_manager_strategy_service.xml new file mode 100644 index 0000000000000..94763b543f4ec --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/access_decision_manager_strategy_service.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/access_decision_manager_strategy_service.yml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/access_decision_manager_strategy_service.yml new file mode 100644 index 0000000000000..907cdfe8410c6 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/access_decision_manager_strategy_service.yml @@ -0,0 +1,11 @@ +security: + enable_authenticator_manager: true + access_decision_manager: + strategy_service: app.custom_access_decision_strategy + providers: + default: + memory: + users: + foo: { password: foo, roles: ROLE_USER } + firewalls: + simple: { pattern: /login, security: false } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/MainConfigurationTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/MainConfigurationTest.php index acdfff8d1639a..1c57997d32432 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/MainConfigurationTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/MainConfigurationTest.php @@ -12,12 +12,17 @@ namespace Symfony\Bundle\SecurityBundle\Tests\DependencyInjection; use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Bundle\SecurityBundle\DependencyInjection\MainConfiguration; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AuthenticatorFactoryInterface; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\Config\Definition\Processor; +use Symfony\Component\Security\Core\Authorization\AccessDecisionManager; class MainConfigurationTest extends TestCase { + use ExpectDeprecationTrait; + /** * The minimal, required config needed to not have any required validation * issues. @@ -113,4 +118,46 @@ public function testUserCheckers() $this->assertEquals('app.henk_checker', $processedConfig['firewalls']['stub']['user_checker']); } + + public function testConfigMergeWithAccessDecisionManager() + { + $config = [ + 'access_decision_manager' => [ + 'strategy' => AccessDecisionManager::STRATEGY_UNANIMOUS, + ], + ]; + $config = array_merge(static::$minimalConfig, $config); + + $config2 = []; + + $processor = new Processor(); + $configuration = new MainConfiguration([], []); + $processedConfig = $processor->processConfiguration($configuration, [$config, $config2]); + + $this->assertSame(AccessDecisionManager::STRATEGY_UNANIMOUS, $processedConfig['access_decision_manager']['strategy']); + } + + public function testFirewalls() + { + $factory = $this->createMock(AuthenticatorFactoryInterface::class); + $factory->expects($this->once())->method('addConfiguration'); + $factory->method('getKey')->willReturn('key'); + + $configuration = new MainConfiguration(['stub' => $factory], []); + $configuration->getConfigTreeBuilder(); + } + + /** + * @group legacy + */ + public function testLegacyFirewalls() + { + $factory = $this->createMock(AuthenticatorFactoryInterface::class); + $factory->expects($this->once())->method('addConfiguration'); + + $this->expectDeprecation('Since symfony/security-bundle 5.4: Passing an array of arrays as 1st argument to "Symfony\Bundle\SecurityBundle\DependencyInjection\MainConfiguration::__construct" is deprecated, pass a sorted array of factories instead.'); + + $configuration = new MainConfiguration(['http_basic' => ['stub' => $factory]], []); + $configuration->getConfigTreeBuilder(); + } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php index 3df35509237c9..45169b64112ba 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\SecurityBundle\Tests\DependencyInjection; use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Bundle\FrameworkBundle\DependencyInjection\FrameworkExtension; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FirewallListenerFactoryInterface; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface; @@ -36,12 +37,16 @@ use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Guard\AuthenticatorInterface as GuardAuthenticatorInterface; +use Symfony\Component\Security\Guard\Token\GuardTokenInterface; use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; use Symfony\Component\Security\Http\Authenticator\HttpBasicAuthenticator; +use Symfony\Component\Security\Http\Authenticator\Passport\Passport; use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; class SecurityExtensionTest extends TestCase { + use ExpectDeprecationTrait; + public function testInvalidCheckPath() { $this->expectException(InvalidConfigurationException::class); @@ -220,7 +225,7 @@ public function testPerListenerProvider() public function testMissingProviderForListener() { $this->expectException(InvalidConfigurationException::class); - $this->expectExceptionMessage('Not configuring explicitly the provider for the "http_basic" listener on "ambiguous" firewall is ambiguous as there is more than one registered provider.'); + $this->expectExceptionMessage('Not configuring explicitly the provider for the "http_basic" authenticator on "ambiguous" firewall is ambiguous as there is more than one registered provider.'); $container = $this->getRawContainer(); $container->loadFromExtension('security', [ 'enable_authenticator_manager' => true, @@ -371,6 +376,33 @@ public function testDoNotRegisterTheUserProviderAliasWithMultipleProviders() $this->assertFalse($container->has(UserProviderInterface::class)); } + /** + * @group legacy + */ + public function testFirewallWithNoUserProviderTriggerDeprecation() + { + $container = $this->getRawContainer(); + + $container->loadFromExtension('security', [ + 'enable_authenticator_manager' => true, + + 'providers' => [ + 'first' => ['id' => 'foo'], + 'second' => ['id' => 'foo'], + ], + + 'firewalls' => [ + 'some_firewall' => [ + 'custom_authenticator' => 'my_authenticator', + ], + ], + ]); + + $this->expectDeprecation('Since symfony/security-bundle 5.4: Not configuring explicitly the provider for the "custom_authenticators" listener on "some_firewall" firewall is deprecated because it\'s ambiguous as there is more than one registered provider.'); + + $container->compile(); + } + /** * @dataProvider sessionConfigurationProvider * @group legacy @@ -635,6 +667,9 @@ public function provideEntryPointRequiredData() ]; } + /** + * @group legacy + */ public function testAlwaysAuthenticateBeforeGrantingCannotBeTrueWithAuthenticatorManager() { $this->expectException(InvalidConfigurationException::class); @@ -785,6 +820,26 @@ public function testConfigureCustomFirewallListener() $this->assertContains('custom_firewall_listener_id', $firewallListeners); } + /** + * @group legacy + */ + public function testLegacyAuthorizationManagerSignature() + { + $container = $this->getRawContainer(); + $container->loadFromExtension('security', [ + 'always_authenticate_before_granting' => true, + 'firewalls' => ['main' => ['http_basic' => true]], + ]); + + $container->compile(); + + $args = $container->getDefinition('security.authorization_checker')->getArguments(); + $this->assertEquals('security.token_storage', (string) $args[0]); + $this->assertEquals('security.authentication_manager', (string) $args[1]); + $this->assertEquals('security.access.decision_manager', (string) $args[2]); + $this->assertEquals('%security.access.always_authenticate_before_granting%', (string) $args[3]); + } + protected function getRawContainer() { $container = new ContainerBuilder(); @@ -818,7 +873,7 @@ public function supports(Request $request): ?bool { } - public function authenticate(Request $request): PassportInterface + public function authenticate(Request $request): Passport { } @@ -833,15 +888,19 @@ public function onAuthenticationSuccess(Request $request, TokenInterface $token, public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response { } + + public function createToken(Passport $passport, string $firewallName): TokenInterface + { + } } class NullAuthenticator implements GuardAuthenticatorInterface { - public function start(Request $request, AuthenticationException $authException = null) + public function start(Request $request, AuthenticationException $authException = null): Response { } - public function supports(Request $request) + public function supports(Request $request): bool { } @@ -849,27 +908,27 @@ public function getCredentials(Request $request) { } - public function getUser($credentials, UserProviderInterface $userProvider) + public function getUser($credentials, UserProviderInterface $userProvider): ?UserInterface { } - public function checkCredentials($credentials, UserInterface $user) + public function checkCredentials($credentials, UserInterface $user): bool { } - public function createAuthenticatedToken(UserInterface $user, string $providerKey) + public function createAuthenticatedToken(UserInterface $user, string $providerKey): GuardTokenInterface { } - public function onAuthenticationFailure(Request $request, AuthenticationException $exception) + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response { } - public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey) + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response { } - public function supportsRememberMe() + public function supportsRememberMe(): bool { } } @@ -894,7 +953,7 @@ public function createListeners(ContainerBuilder $container, string $firewallNam return ['custom_firewall_listener_id']; } - public function create(ContainerBuilder $container, string $id, array $config, string $userProvider, ?string $defaultEntryPoint) + public function create(ContainerBuilder $container, string $id, array $config, string $userProvider, ?string $defaultEntryPoint): array { $container->register('provider_id', \stdClass::class); $container->register('listener_id', \stdClass::class); @@ -902,12 +961,12 @@ public function create(ContainerBuilder $container, string $id, array $config, s return ['provider_id', 'listener_id', $defaultEntryPoint]; } - public function getPosition() + public function getPosition(): string { return 'form'; } - public function getKey() + public function getKey(): string { return 'custom_listener'; } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AuthenticatorTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AuthenticatorTest.php index 69f64693c85b7..10eeb39ca8c5e 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AuthenticatorTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AuthenticatorTest.php @@ -14,9 +14,11 @@ class AuthenticatorTest extends AbstractWebTestCase { /** + * @group legacy + * * @dataProvider provideEmails */ - public function testGlobalUserProvider($email) + public function testLegacyGlobalUserProvider($email) { $client = $this->createClient(['test_case' => 'Authenticator', 'root_config' => 'implicit_user_provider.yml']); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AnonymousBundle/AppCustomAuthenticator.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AnonymousBundle/AppCustomAuthenticator.php index 5069fa9cc7fa9..c1d38688ecd25 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AnonymousBundle/AppCustomAuthenticator.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AnonymousBundle/AppCustomAuthenticator.php @@ -21,7 +21,7 @@ class AppCustomAuthenticator extends AbstractGuardAuthenticator { - public function supports(Request $request) + public function supports(Request $request): bool { return false; } @@ -30,28 +30,28 @@ public function getCredentials(Request $request) { } - public function getUser($credentials, UserProviderInterface $userProvider) + public function getUser($credentials, UserProviderInterface $userProvider): ?UserInterface { } - public function checkCredentials($credentials, UserInterface $user) + public function checkCredentials($credentials, UserInterface $user): bool { } - public function onAuthenticationFailure(Request $request, AuthenticationException $exception) + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response { } - public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) + public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey): ?Response { } - public function start(Request $request, AuthenticationException $authException = null) + public function start(Request $request, AuthenticationException $authException = null): Response { return new Response($authException->getMessage(), Response::HTTP_UNAUTHORIZED); } - public function supportsRememberMe() + public function supportsRememberMe(): bool { } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AuthenticatorBundle/ApiAuthenticator.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AuthenticatorBundle/ApiAuthenticator.php index 34a2115e4d407..f0558c5c5f5a6 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AuthenticatorBundle/ApiAuthenticator.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AuthenticatorBundle/ApiAuthenticator.php @@ -20,7 +20,7 @@ use Symfony\Component\Security\Core\User\InMemoryUser; use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; -use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Passport; use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; class ApiAuthenticator extends AbstractAuthenticator @@ -37,7 +37,7 @@ public function supports(Request $request): ?bool return $request->headers->has('X-USER-EMAIL'); } - public function authenticate(Request $request): PassportInterface + public function authenticate(Request $request): Passport { $email = $request->headers->get('X-USER-EMAIL'); if (false === strpos($email, '@')) { diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AuthenticatorBundle/LoginFormAuthenticator.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AuthenticatorBundle/LoginFormAuthenticator.php index 2440b23440f7d..1004ee2c10ba7 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AuthenticatorBundle/LoginFormAuthenticator.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AuthenticatorBundle/LoginFormAuthenticator.php @@ -21,7 +21,6 @@ use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; use Symfony\Component\Security\Http\Authenticator\Passport\Passport; -use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; use Symfony\Component\Security\Http\Util\TargetPathTrait; class LoginFormAuthenticator extends AbstractLoginFormAuthenticator @@ -36,7 +35,7 @@ public function __construct(UrlGeneratorInterface $urlGenerator) $this->urlGenerator = $urlGenerator; } - public function authenticate(Request $request): PassportInterface + public function authenticate(Request $request): Passport { $username = $request->request->get('_username', ''); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/CsrfFormLoginBundle/Controller/LoginController.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/CsrfFormLoginBundle/Controller/LoginController.php index f6f7aca9d5ec2..c77b1e204e0db 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/CsrfFormLoginBundle/Controller/LoginController.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/CsrfFormLoginBundle/Controller/LoginController.php @@ -54,7 +54,7 @@ public function secureAction() /** * {@inheritdoc} */ - public static function getSubscribedServices() + public static function getSubscribedServices(): array { return [ 'form.factory' => FormFactoryInterface::class, diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Controller/LocalizedController.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Controller/LocalizedController.php index 5904183581517..11d00e257e98a 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Controller/LocalizedController.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Controller/LocalizedController.php @@ -71,7 +71,7 @@ public function homepageAction() /** * {@inheritdoc} */ - public static function getSubscribedServices() + public static function getSubscribedServices(): array { return [ 'twig' => Environment::class, diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Controller/LoginController.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Controller/LoginController.php index 99183293fb1e8..db6aacca8cfc2 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Controller/LoginController.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Controller/LoginController.php @@ -63,7 +63,7 @@ public function secureAction() /** * {@inheritdoc} */ - public static function getSubscribedServices() + public static function getSubscribedServices(): array { return [ 'twig' => Environment::class, diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/GuardedBundle/AppCustomAuthenticator.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/GuardedBundle/AppCustomAuthenticator.php index 22d378835e4c0..43e439ecfa9bf 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/GuardedBundle/AppCustomAuthenticator.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/GuardedBundle/AppCustomAuthenticator.php @@ -21,7 +21,7 @@ class AppCustomAuthenticator extends AbstractGuardAuthenticator { - public function supports(Request $request) + public function supports(Request $request): bool { return '/manual_login' !== $request->getPathInfo() && '/profile' !== $request->getPathInfo(); } @@ -31,29 +31,29 @@ public function getCredentials(Request $request) throw new AuthenticationException('This should be hit'); } - public function getUser($credentials, UserProviderInterface $userProvider) + public function getUser($credentials, UserProviderInterface $userProvider): ?UserInterface { } - public function checkCredentials($credentials, UserInterface $user) + public function checkCredentials($credentials, UserInterface $user): bool { } - public function onAuthenticationFailure(Request $request, AuthenticationException $exception) + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response { return new Response('', 418); } - public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) + public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey): ?Response { } - public function start(Request $request, AuthenticationException $authException = null) + public function start(Request $request, AuthenticationException $authException = null): Response { return new Response($authException->getMessage(), Response::HTTP_UNAUTHORIZED); } - public function supportsRememberMe() + public function supportsRememberMe(): bool { } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/LoginLink/TestCustomLoginLinkSuccessHandler.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/LoginLink/TestCustomLoginLinkSuccessHandler.php index a20866c8cfd91..0d1501508b58a 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/LoginLink/TestCustomLoginLinkSuccessHandler.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/LoginLink/TestCustomLoginLinkSuccessHandler.php @@ -4,12 +4,13 @@ use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface; class TestCustomLoginLinkSuccessHandler implements AuthenticationSuccessHandlerInterface { - public function onAuthenticationSuccess(Request $request, TokenInterface $token) + public function onAuthenticationSuccess(Request $request, TokenInterface $token): Response { return new JsonResponse(['message' => sprintf('Welcome %s!', $token->getUserIdentifier())]); } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/RememberMeBundle/Security/StaticTokenProvider.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/RememberMeBundle/Security/StaticTokenProvider.php index 43479ca9cfd4d..a51702eec15b6 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/RememberMeBundle/Security/StaticTokenProvider.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/RememberMeBundle/Security/StaticTokenProvider.php @@ -29,7 +29,7 @@ public function __construct($kernel) } } - public function loadTokenBySeries(string $series) + public function loadTokenBySeries(string $series): PersistentTokenInterface { $token = self::$db[$series] ?? false; if (!$token) { diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/RememberMeBundle/Security/UserChangingUserProvider.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/RememberMeBundle/Security/UserChangingUserProvider.php index a5306b6bf1607..f28bfff393693 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/RememberMeBundle/Security/UserChangingUserProvider.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/RememberMeBundle/Security/UserChangingUserProvider.php @@ -26,7 +26,7 @@ public function __construct(InMemoryUserProvider $inner) $this->inner = $inner; } - public function loadUserByUsername($username) + public function loadUserByUsername($username): UserInterface { return $this->inner->loadUserByUsername($username); } @@ -36,7 +36,7 @@ public function loadUserByIdentifier(string $userIdentifier): UserInterface return $this->inner->loadUserByIdentifier($userIdentifier); } - public function refreshUser(UserInterface $user) + public function refreshUser(UserInterface $user): UserInterface { $user = $this->inner->refreshUser($user); @@ -46,7 +46,7 @@ public function refreshUser(UserInterface $user) return $user; } - public function supportsClass($class) + public function supportsClass($class): bool { return $this->inner->supportsClass($class); } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/RequestTrackerBundle/DependencyInjection/RequestTrackerExtension.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/RequestTrackerBundle/DependencyInjection/RequestTrackerExtension.php new file mode 100644 index 0000000000000..69deb865c2436 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/RequestTrackerBundle/DependencyInjection/RequestTrackerExtension.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\Bundle\SecurityBundle\Tests\Functional\Bundle\RequestTrackerBundle\DependencyInjection; + +use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\RequestTrackerBundle\EventSubscriber\RequestTrackerSubscriber; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Extension\Extension; + +final class RequestTrackerExtension extends Extension +{ + public function load(array $configs, ContainerBuilder $container): void + { + $container->register('request_tracker_subscriber', RequestTrackerSubscriber::class) + ->setPublic(true) + ->addTag('kernel.event_subscriber'); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/RequestTrackerBundle/EventSubscriber/RequestTrackerSubscriber.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/RequestTrackerBundle/EventSubscriber/RequestTrackerSubscriber.php new file mode 100644 index 0000000000000..24a0357d84022 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/RequestTrackerBundle/EventSubscriber/RequestTrackerSubscriber.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\RequestTrackerBundle\EventSubscriber; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Event\RequestEvent; + +final class RequestTrackerSubscriber implements EventSubscriberInterface +{ + /** @var ?Request */ + private $lastRequest; + + public static function getSubscribedEvents(): array + { + return [ + RequestEvent::class => 'onRequest', + ]; + } + + public function onRequest(RequestEvent $event) + { + $this->lastRequest = $event->getRequest(); + } + + public function getLastRequest(): ?Request + { + return $this->lastRequest; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/RequestTrackerBundle/RequestTrackerBundle.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/RequestTrackerBundle/RequestTrackerBundle.php new file mode 100644 index 0000000000000..0497be83c316c --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/RequestTrackerBundle/RequestTrackerBundle.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\RequestTrackerBundle; + +use Symfony\Component\HttpKernel\Bundle\Bundle; + +final class RequestTrackerBundle extends Bundle +{ +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/SecuredPageBundle/Security/Core/User/ArrayUserProvider.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/SecuredPageBundle/Security/Core/User/ArrayUserProvider.php index a5ca99a41b6b7..db9d39e7d6e74 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/SecuredPageBundle/Security/Core/User/ArrayUserProvider.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/SecuredPageBundle/Security/Core/User/ArrayUserProvider.php @@ -29,7 +29,7 @@ public function getUser($username) return $this->users[$username]; } - public function loadUserByUsername($username) + public function loadUserByUsername($username): UserInterface { return $this->loadUserByIdentifier($username); } @@ -48,7 +48,7 @@ public function loadUserByIdentifier(string $identifier): UserInterface return $user; } - public function refreshUser(UserInterface $user) + public function refreshUser(UserInterface $user): UserInterface { if (!$user instanceof UserInterface) { throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_debug_type($user))); @@ -60,7 +60,7 @@ public function refreshUser(UserInterface $user) return new $class($storedUser->getUserIdentifier(), $storedUser->getPassword(), $storedUser->getRoles(), $storedUser->isEnabled()); } - public function supportsClass($class) + public function supportsClass($class): bool { return InMemoryUser::class === $class || UserWithoutEquatable::class === $class; } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/MissingUserProviderTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/MissingUserProviderTest.php index 0f04a0eece141..7308de85362fd 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/MissingUserProviderTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/MissingUserProviderTest.php @@ -28,6 +28,9 @@ public function testUserProviderIsNeeded() ]); } + /** + * @group legacy + */ public function testLegacyUserProviderIsNeeded() { $client = $this->createClient(['test_case' => 'MissingUserProvider', 'root_config' => 'config.yml', 'debug' => true]); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityRoutingIntegrationTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityRoutingIntegrationTest.php index d7037bdce72a3..788599224e47e 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityRoutingIntegrationTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityRoutingIntegrationTest.php @@ -136,7 +136,7 @@ public function testPublicHomepage() $this->assertEquals(200, $client->getResponse()->getStatusCode(), (string) $client->getResponse()); $this->assertTrue($client->getResponse()->headers->getCacheControlDirective('public')); - $this->assertSame(0, self::getContainer()->get('session')->getUsageIndex()); + $this->assertSame(0, self::getContainer()->get('request_tracker_subscriber')->getLastRequest()->getSession()->getUsageIndex()); } /** @@ -274,7 +274,7 @@ public function testLegacyPublicHomepage() $this->assertEquals(200, $client->getResponse()->getStatusCode(), (string) $client->getResponse()); $this->assertTrue($client->getResponse()->headers->getCacheControlDirective('public')); - $this->assertSame(0, self::getContainer()->get('session')->getUsageIndex()); + $this->assertSame(0, self::getContainer()->get('request_tracker_subscriber')->getLastRequest()->getSession()->getUsageIndex()); } private function assertAllowed($client, $path) diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityTest.php index d0ac17b1c9f05..418bb55f14454 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityTest.php @@ -27,7 +27,7 @@ public function testServiceIsFunctional() // put a token into the storage so the final calls can function $user = new InMemoryUser('foo', 'pass'); - $token = new UsernamePasswordToken($user, '', 'provider', ['ROLE_USER']); + $token = new UsernamePasswordToken($user, 'provider', ['ROLE_USER']); $container->get('functional.test.security.token_storage')->setToken($token); $security = $container->get('functional_test.security.helper'); @@ -105,7 +105,7 @@ public function testLegacyServiceIsFunctional() // put a token into the storage so the final calls can function $user = new InMemoryUser('foo', 'pass'); - $token = new UsernamePasswordToken($user, '', 'provider', ['ROLE_USER']); + $token = new UsernamePasswordToken($user, 'provider', ['ROLE_USER']); $container->get('functional.test.security.token_storage')->setToken($token); $security = $container->get('functional_test.security.helper'); @@ -161,7 +161,7 @@ public function __toString() /** * {@inheritdoc} */ - public function getRoles() + public function getRoles(): array { return $this->roles; } @@ -177,20 +177,20 @@ public function getPassword(): ?string /** * {@inheritdoc} */ - public function getSalt() + public function getSalt(): string { - return null; + return ''; } /** * {@inheritdoc} */ - public function getUsername() + public function getUsername(): string { return $this->username; } - public function getUserIdentifier() + public function getUserIdentifier(): string { return $this->username; } @@ -198,7 +198,7 @@ public function getUserIdentifier() /** * {@inheritdoc} */ - public function isAccountNonExpired() + public function isAccountNonExpired(): bool { return $this->accountNonExpired; } @@ -206,7 +206,7 @@ public function isAccountNonExpired() /** * {@inheritdoc} */ - public function isAccountNonLocked() + public function isAccountNonLocked(): bool { return $this->accountNonLocked; } @@ -214,7 +214,7 @@ public function isAccountNonLocked() /** * {@inheritdoc} */ - public function isCredentialsNonExpired() + public function isCredentialsNonExpired(): bool { return $this->credentialsNonExpired; } @@ -222,7 +222,7 @@ public function isCredentialsNonExpired() /** * {@inheritdoc} */ - public function isEnabled() + public function isEnabled(): bool { return $this->enabled; } @@ -230,7 +230,7 @@ public function isEnabled() /** * {@inheritdoc} */ - public function eraseCredentials() + public function eraseCredentials(): void { } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/base_config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/base_config.yml index b0543f9808d88..a243ec5f0a448 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/base_config.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/base_config.yml @@ -39,19 +39,19 @@ security: path: /second/logout access_control: - - { path: ^/en/$, roles: IS_AUTHENTICATED_ANONYMOUSLY } - - { path: ^/unprotected_resource$, roles: IS_AUTHENTICATED_ANONYMOUSLY } - - { path: ^/secure-but-not-covered-by-access-control$, roles: IS_AUTHENTICATED_ANONYMOUSLY } - - { path: ^/secured-by-one-ip$, ip: 10.10.10.10, roles: IS_AUTHENTICATED_ANONYMOUSLY } - - { path: ^/secured-by-two-ips$, ips: [1.1.1.1, 2.2.2.2], roles: IS_AUTHENTICATED_ANONYMOUSLY } + - { path: ^/en/$, roles: PUBLIC_ACCESS } + - { path: ^/unprotected_resource$, roles: PUBLIC_ACCESS } + - { path: ^/secure-but-not-covered-by-access-control$, roles: PUBLIC_ACCESS } + - { path: ^/secured-by-one-ip$, ip: 10.10.10.10, roles: PUBLIC_ACCESS } + - { path: ^/secured-by-two-ips$, ips: [1.1.1.1, 2.2.2.2], roles: PUBLIC_ACCESS } # these real IP addresses are reserved for docs/examples (https://tools.ietf.org/search/rfc5737) - - { path: ^/secured-by-one-real-ip$, ips: 198.51.100.0, roles: IS_AUTHENTICATED_ANONYMOUSLY } - - { path: ^/secured-by-one-real-ip-with-mask$, ips: '203.0.113.0/24', roles: IS_AUTHENTICATED_ANONYMOUSLY } - - { path: ^/secured-by-one-real-ipv6$, ips: 0:0:0:0:0:ffff:c633:6400, roles: IS_AUTHENTICATED_ANONYMOUSLY } - - { path: ^/secured-by-one-env-placeholder$, ips: '%env(APP_IP)%', roles: IS_AUTHENTICATED_ANONYMOUSLY } - - { path: ^/secured-by-one-env-placeholder-multiple-ips$, ips: '%env(APP_IPS)%', roles: IS_AUTHENTICATED_ANONYMOUSLY } - - { path: ^/secured-by-one-env-placeholder-and-one-real-ip$, ips: ['%env(APP_IP)%', 198.51.100.0], roles: IS_AUTHENTICATED_ANONYMOUSLY } - - { path: ^/secured-by-one-env-placeholder-multiple-ips-and-one-real-ip$, ips: ['%env(APP_IPS)%', 198.51.100.0], roles: IS_AUTHENTICATED_ANONYMOUSLY } + - { path: ^/secured-by-one-real-ip$, ips: 198.51.100.0, roles: PUBLIC_ACCESS } + - { path: ^/secured-by-one-real-ip-with-mask$, ips: '203.0.113.0/24', roles: PUBLIC_ACCESS } + - { path: ^/secured-by-one-real-ipv6$, ips: 0:0:0:0:0:ffff:c633:6400, roles: PUBLIC_ACCESS } + - { path: ^/secured-by-one-env-placeholder$, ips: '%env(APP_IP)%', roles: PUBLIC_ACCESS } + - { path: ^/secured-by-one-env-placeholder-multiple-ips$, ips: '%env(APP_IPS)%', roles: PUBLIC_ACCESS } + - { path: ^/secured-by-one-env-placeholder-and-one-real-ip$, ips: ['%env(APP_IP)%', 198.51.100.0], roles: PUBLIC_ACCESS } + - { path: ^/secured-by-one-env-placeholder-multiple-ips-and-one-real-ip$, ips: ['%env(APP_IPS)%', 198.51.100.0], roles: PUBLIC_ACCESS } - { path: ^/highly_protected_resource$, roles: IS_ADMIN } - - { path: ^/protected-via-expression$, allow_if: "(is_anonymous() and request.headers.get('user-agent') matches '/Firefox/i') or is_granted('ROLE_USER')" } + - { path: ^/protected-via-expression$, allow_if: "(!is_authenticated() and request.headers.get('user-agent') matches '/Firefox/i') or is_granted('ROLE_USER')" } - { path: .*, roles: IS_AUTHENTICATED_FULLY } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/bundles.php index cef48bfcc4b46..6237258c8606e 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/bundles.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/bundles.php @@ -12,6 +12,7 @@ use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Bundle\SecurityBundle\SecurityBundle; use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\FormLoginBundle\FormLoginBundle; +use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\RequestTrackerBundle\RequestTrackerBundle; use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle; use Symfony\Bundle\TwigBundle\TwigBundle; @@ -20,5 +21,6 @@ new SecurityBundle(), new TwigBundle(), new FormLoginBundle(), + new RequestTrackerBundle(), new TestBundle(), ]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Security/FirewallConfigTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Security/FirewallConfigTest.php index 99e897aa8ff20..59cb0fcc94e91 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Security/FirewallConfigTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Security/FirewallConfigTest.php @@ -18,7 +18,7 @@ class FirewallConfigTest extends TestCase { public function testGetters() { - $listeners = ['logout', 'remember_me', 'anonymous']; + $authenticators = ['form_login', 'remember_me']; $options = [ 'request_matcher' => 'foo_request_matcher', 'security' => false, @@ -43,7 +43,7 @@ public function testGetters() $options['entry_point'], $options['access_denied_handler'], $options['access_denied_url'], - $listeners, + $authenticators, $options['switch_user'] ); @@ -57,8 +57,7 @@ public function testGetters() $this->assertSame($options['access_denied_handler'], $config->getAccessDeniedHandler()); $this->assertSame($options['access_denied_url'], $config->getAccessDeniedUrl()); $this->assertSame($options['user_checker'], $config->getUserChecker()); - $this->assertTrue($config->allowsAnonymous()); - $this->assertSame($listeners, $config->getListeners()); + $this->assertSame($authenticators, $config->getAuthenticators()); $this->assertSame($options['switch_user'], $config->getSwitchUser()); } } diff --git a/src/Symfony/Bundle/SecurityBundle/composer.json b/src/Symfony/Bundle/SecurityBundle/composer.json index 6cf7991f86d6b..50345333fc550 100644 --- a/src/Symfony/Bundle/SecurityBundle/composer.json +++ b/src/Symfony/Bundle/SecurityBundle/composer.json @@ -18,38 +18,38 @@ "require": { "php": ">=7.2.5", "ext-xml": "*", - "symfony/config": "^4.4|^5.0", - "symfony/dependency-injection": "^5.3", + "symfony/config": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^5.3|^6.0", "symfony/deprecation-contracts": "^2.1", - "symfony/event-dispatcher": "^5.1", - "symfony/http-kernel": "^5.3", - "symfony/http-foundation": "^5.3", - "symfony/password-hasher": "^5.3", + "symfony/event-dispatcher": "^5.1|^6.0", + "symfony/http-kernel": "^5.3|^6.0", + "symfony/http-foundation": "^5.3|^6.0", + "symfony/password-hasher": "^5.3|^6.0", "symfony/polyfill-php80": "^1.16", - "symfony/security-core": "^5.3", - "symfony/security-csrf": "^4.4|^5.0", - "symfony/security-guard": "^5.3", - "symfony/security-http": "^5.3.2" + "symfony/security-core": "^5.4|^6.0", + "symfony/security-csrf": "^4.4|^5.0|^6.0", + "symfony/security-guard": "^5.3|^6.0", + "symfony/security-http": "^5.4|^6.0" }, "require-dev": { "doctrine/annotations": "^1.10.4", - "symfony/asset": "^4.4|^5.0", - "symfony/browser-kit": "^4.4|^5.0", - "symfony/console": "^4.4|^5.0", - "symfony/css-selector": "^4.4|^5.0", - "symfony/dom-crawler": "^4.4|^5.0", - "symfony/expression-language": "^4.4|^5.0", - "symfony/form": "^4.4|^5.0", - "symfony/framework-bundle": "^5.3", - "symfony/ldap": "^5.3", - "symfony/process": "^4.4|^5.0", - "symfony/rate-limiter": "^5.2", - "symfony/serializer": "^4.4|^5.0", - "symfony/translation": "^4.4|^5.0", - "symfony/twig-bundle": "^4.4|^5.0", - "symfony/twig-bridge": "^4.4|^5.0", - "symfony/validator": "^4.4|^5.0", - "symfony/yaml": "^4.4|^5.0", + "symfony/asset": "^4.4|^5.0|^6.0", + "symfony/browser-kit": "^4.4|^5.0|^6.0", + "symfony/console": "^4.4|^5.0|^6.0", + "symfony/css-selector": "^4.4|^5.0|^6.0", + "symfony/dom-crawler": "^4.4|^5.0|^6.0", + "symfony/expression-language": "^4.4|^5.0|^6.0", + "symfony/form": "^4.4|^5.0|^6.0", + "symfony/framework-bundle": "^5.3|^6.0", + "symfony/ldap": "^5.3|^6.0", + "symfony/process": "^4.4|^5.0|^6.0", + "symfony/rate-limiter": "^5.2|^6.0", + "symfony/serializer": "^4.4|^5.0|^6.0", + "symfony/translation": "^4.4|^5.0|^6.0", + "symfony/twig-bundle": "^4.4|^5.0|^6.0", + "symfony/twig-bridge": "^4.4|^5.0|^6.0", + "symfony/validator": "^4.4|^5.0|^6.0", + "symfony/yaml": "^4.4|^5.0|^6.0", "twig/twig": "^2.13|^3.0.4" }, "conflict": { diff --git a/src/Symfony/Bundle/TwigBundle/CacheWarmer/TemplateCacheWarmer.php b/src/Symfony/Bundle/TwigBundle/CacheWarmer/TemplateCacheWarmer.php index 2fc1d390b3647..4a15dcf2faf49 100644 --- a/src/Symfony/Bundle/TwigBundle/CacheWarmer/TemplateCacheWarmer.php +++ b/src/Symfony/Bundle/TwigBundle/CacheWarmer/TemplateCacheWarmer.php @@ -56,8 +56,14 @@ public function warmUp(string $cacheDir) $files[] = (new \ReflectionClass($template->unwrap()))->getFileName(); } } catch (Error $e) { - // problem during compilation, give up - // might be a syntax error or a non-Twig template + /* + * Problem during compilation, give up for this template (e.g. syntax errors). + * Failing silently here allows to ignore templates that rely on functions that aren't available in + * the current environment. For example, the WebProfilerBundle shouldn't be available in the prod + * environment, but some templates that are never used in prod might rely on functions the bundle provides. + * As we can't detect which templates are "really" important, we try to load all of them and ignore + * errors. Error checks may be performed by calling the lint:twig command. + */ } } diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php index c7826cd5ff73b..76faa0107e374 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php @@ -26,7 +26,7 @@ class Configuration implements ConfigurationInterface /** * Generates the configuration tree builder. * - * @return TreeBuilder The tree builder + * @return TreeBuilder */ public function getConfigTreeBuilder() { diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php index f3c8dc2ce9cca..388f35b64d698 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\TwigBundle\DependencyInjection; +use Composer\InstalledVersions; use Symfony\Component\Config\FileLocator; use Symfony\Component\Config\Resource\FileExistenceResource; use Symfony\Component\Console\Application; @@ -35,22 +36,26 @@ class TwigExtension extends Extension { public function load(array $configs, ContainerBuilder $container) { + if (!class_exists(InstalledVersions::class)) { + trigger_deprecation('symfony/twig-bundle', '5.4', 'Configuring Symfony without the Composer Runtime API is deprecated. Consider upgrading to Composer 2.1 or later.'); + } + $loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); $loader->load('twig.php'); - if ($container::willBeAvailable('symfony/form', Form::class, ['symfony/twig-bundle'])) { + if ($container::willBeAvailable('symfony/form', Form::class, ['symfony/twig-bundle'], true)) { $loader->load('form.php'); } - if ($container::willBeAvailable('symfony/console', Application::class, ['symfony/twig-bundle'])) { + if ($container::willBeAvailable('symfony/console', Application::class, ['symfony/twig-bundle'], true)) { $loader->load('console.php'); } - if ($container::willBeAvailable('symfony/mailer', Mailer::class, ['symfony/twig-bundle'])) { + if ($container::willBeAvailable('symfony/mailer', Mailer::class, ['symfony/twig-bundle'], true)) { $loader->load('mailer.php'); } - if (!$container::willBeAvailable('symfony/translation', Translator::class, ['symfony/twig-bundle'])) { + if (!$container::willBeAvailable('symfony/translation', Translator::class, ['symfony/twig-bundle'], true)) { $container->removeDefinition('twig.translation.extractor'); } diff --git a/src/Symfony/Bundle/TwigBundle/TemplateIterator.php b/src/Symfony/Bundle/TwigBundle/TemplateIterator.php index 5871600d5438b..8cc0ffc4df76f 100644 --- a/src/Symfony/Bundle/TwigBundle/TemplateIterator.php +++ b/src/Symfony/Bundle/TwigBundle/TemplateIterator.php @@ -20,6 +20,8 @@ * @author Fabien Potencier * * @internal + * + * @implements \IteratorAggregate */ class TemplateIterator implements \IteratorAggregate { @@ -45,7 +47,7 @@ public function getIterator(): \Traversable return $this->templates; } - $templates = null !== $this->defaultPath ? $this->findTemplatesInDirectory($this->defaultPath, null, ['bundles']) : []; + $templates = null !== $this->defaultPath ? [$this->findTemplatesInDirectory($this->defaultPath, null, ['bundles'])] : []; foreach ($this->kernel->getBundles() as $bundle) { $name = $bundle->getName(); @@ -55,18 +57,17 @@ public function getIterator(): \Traversable $bundleTemplatesDir = is_dir($bundle->getPath().'/Resources/views') ? $bundle->getPath().'/Resources/views' : $bundle->getPath().'/templates'; - $templates = array_merge( - $templates, - $this->findTemplatesInDirectory($bundleTemplatesDir, $name), - null !== $this->defaultPath ? $this->findTemplatesInDirectory($this->defaultPath.'/bundles/'.$bundle->getName(), $name) : [] - ); + $templates[] = $this->findTemplatesInDirectory($bundleTemplatesDir, $name); + if (null !== $this->defaultPath) { + $templates[] = $this->findTemplatesInDirectory($this->defaultPath.'/bundles/'.$bundle->getName(), $name); + } } foreach ($this->paths as $dir => $namespace) { - $templates = array_merge($templates, $this->findTemplatesInDirectory($dir, $namespace)); + $templates[] = $this->findTemplatesInDirectory($dir, $namespace); } - return $this->templates = new \ArrayIterator(array_unique($templates)); + return $this->templates = new \ArrayIterator(array_unique(array_merge([], ...$templates))); } /** diff --git a/src/Symfony/Bundle/TwigBundle/composer.json b/src/Symfony/Bundle/TwigBundle/composer.json index 1c21222550e7d..5635bb430d8c5 100644 --- a/src/Symfony/Bundle/TwigBundle/composer.json +++ b/src/Symfony/Bundle/TwigBundle/composer.json @@ -17,32 +17,33 @@ ], "require": { "php": ">=7.2.5", - "symfony/config": "^4.4|^5.0", - "symfony/twig-bridge": "^5.3", - "symfony/http-foundation": "^4.4|^5.0", - "symfony/http-kernel": "^5.0", + "symfony/config": "^4.4|^5.0|^6.0", + "symfony/twig-bridge": "^5.3|^6.0", + "symfony/http-foundation": "^4.4|^5.0|^6.0", + "symfony/http-kernel": "^5.0|^6.0", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-php80": "^1.16", "twig/twig": "^2.13|^3.0.4" }, "require-dev": { - "symfony/asset": "^4.4|^5.0", - "symfony/stopwatch": "^4.4|^5.0", - "symfony/dependency-injection": "^5.3", - "symfony/expression-language": "^4.4|^5.0", - "symfony/finder": "^4.4|^5.0", - "symfony/form": "^4.4|^5.0", - "symfony/routing": "^4.4|^5.0", - "symfony/translation": "^5.0", - "symfony/yaml": "^4.4|^5.0", - "symfony/framework-bundle": "^5.0", - "symfony/web-link": "^4.4|^5.0", + "symfony/asset": "^4.4|^5.0|^6.0", + "symfony/stopwatch": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^5.3|^6.0", + "symfony/expression-language": "^4.4|^5.0|^6.0", + "symfony/finder": "^4.4|^5.0|^6.0", + "symfony/form": "^4.4|^5.0|^6.0", + "symfony/routing": "^4.4|^5.0|^6.0", + "symfony/translation": "^5.0|^6.0", + "symfony/yaml": "^4.4|^5.0|^6.0", + "symfony/framework-bundle": "^5.0|^6.0", + "symfony/web-link": "^4.4|^5.0|^6.0", "doctrine/annotations": "^1.10.4", "doctrine/cache": "^1.0|^2.0" }, "conflict": { "symfony/dependency-injection": "<5.3", "symfony/framework-bundle": "<5.0", + "symfony/service-contracts": ">=3.0", "symfony/translation": "<5.0" }, "autoload": { diff --git a/src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md b/src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md index 028537ead68cd..f0974a6ed9f1a 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +5.4 +--- + + * Add a "preview" tab in mailer profiler for HTML email + 5.2.0 ----- @@ -33,7 +38,7 @@ CHANGELOG ----- * added information about orphaned events - * made the toolbar auto-update with info from ajax reponses when they set the + * made the toolbar auto-update with info from ajax reponses when they set the `Symfony-Debug-Toolbar-Replace header` to `1` 4.0.0 diff --git a/src/Symfony/Bundle/WebProfilerBundle/Controller/ProfilerController.php b/src/Symfony/Bundle/WebProfilerBundle/Controller/ProfilerController.php index b5abff96d2090..03237f306faba 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Controller/ProfilerController.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Controller/ProfilerController.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\WebProfilerBundle\Controller; +use Symfony\Bundle\FullStack; use Symfony\Bundle\WebProfilerBundle\Csp\ContentSecurityPolicyHandler; use Symfony\Bundle\WebProfilerBundle\Profiler\TemplateManager; use Symfony\Component\HttpFoundation\RedirectResponse; @@ -52,11 +53,9 @@ public function __construct(UrlGeneratorInterface $generator, Profiler $profiler /** * Redirects to the last profiles. * - * @return RedirectResponse A RedirectResponse instance - * * @throws NotFoundHttpException */ - public function homeAction() + public function homeAction(): RedirectResponse { $this->denyAccessIfProfilerDisabled(); @@ -66,11 +65,9 @@ public function homeAction() /** * Renders a profiler panel for the given token. * - * @return Response A Response instance - * * @throws NotFoundHttpException */ - public function panelAction(Request $request, string $token) + public function panelAction(Request $request, string $token): Response { $this->denyAccessIfProfilerDisabled(); @@ -125,11 +122,9 @@ public function panelAction(Request $request, string $token) /** * Renders the Web Debug Toolbar. * - * @return Response A Response instance - * * @throws NotFoundHttpException */ - public function toolbarAction(Request $request, string $token = null) + public function toolbarAction(Request $request, string $token = null): Response { if (null === $this->profiler) { throw new NotFoundHttpException('The profiler must be enabled.'); @@ -158,6 +153,7 @@ public function toolbarAction(Request $request, string $token = null) } return $this->renderWithCspNonces($request, '@WebProfiler/Profiler/toolbar.html.twig', [ + 'full_stack' => class_exists(FullStack::class), 'request' => $request, 'profile' => $profile, 'templates' => $this->getTemplateManager()->getNames($profile), @@ -170,11 +166,9 @@ public function toolbarAction(Request $request, string $token = null) /** * Renders the profiler search bar. * - * @return Response A Response instance - * * @throws NotFoundHttpException */ - public function searchBarAction(Request $request) + public function searchBarAction(Request $request): Response { $this->denyAccessIfProfilerDisabled(); @@ -224,11 +218,9 @@ public function searchBarAction(Request $request) /** * Renders the search results. * - * @return Response A Response instance - * * @throws NotFoundHttpException */ - public function searchResultsAction(Request $request, string $token) + public function searchResultsAction(Request $request, string $token): Response { $this->denyAccessIfProfilerDisabled(); @@ -265,11 +257,9 @@ public function searchResultsAction(Request $request, string $token) /** * Narrows the search bar. * - * @return Response A Response instance - * * @throws NotFoundHttpException */ - public function searchAction(Request $request) + public function searchAction(Request $request): Response { $this->denyAccessIfProfilerDisabled(); @@ -316,11 +306,9 @@ public function searchAction(Request $request) /** * Displays the PHP info. * - * @return Response A Response instance - * * @throws NotFoundHttpException */ - public function phpinfoAction() + public function phpinfoAction(): Response { $this->denyAccessIfProfilerDisabled(); @@ -338,11 +326,9 @@ public function phpinfoAction() /** * Displays the source of a file. * - * @return Response A Response instance - * * @throws NotFoundHttpException */ - public function openAction(Request $request) + public function openAction(Request $request): Response { if (null === $this->baseDir) { throw new NotFoundHttpException('The base dir should be set.'); @@ -370,10 +356,8 @@ public function openAction(Request $request) /** * Gets the Template Manager. - * - * @return TemplateManager The Template Manager */ - protected function getTemplateManager() + protected function getTemplateManager(): TemplateManager { if (null === $this->templateManager) { $this->templateManager = new TemplateManager($this->profiler, $this->twig, $this->templates); diff --git a/src/Symfony/Bundle/WebProfilerBundle/Controller/RouterController.php b/src/Symfony/Bundle/WebProfilerBundle/Controller/RouterController.php index 19c40c13b0ece..50560e0b3ffa1 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Controller/RouterController.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Controller/RouterController.php @@ -24,8 +24,6 @@ use Twig\Environment; /** - * RouterController. - * * @author Fabien Potencier * * @internal @@ -54,11 +52,9 @@ public function __construct(Profiler $profiler = null, Environment $twig, UrlMat /** * Renders the profiler panel for the given token. * - * @return Response A Response instance - * * @throws NotFoundHttpException */ - public function panelAction(string $token) + public function panelAction(string $token): Response { if (null === $this->profiler) { throw new NotFoundHttpException('The profiler must be enabled.'); diff --git a/src/Symfony/Bundle/WebProfilerBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/WebProfilerBundle/DependencyInjection/Configuration.php index f0ac3571278aa..041c3350a61d9 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/WebProfilerBundle/DependencyInjection/Configuration.php @@ -27,7 +27,7 @@ class Configuration implements ConfigurationInterface /** * Generates the configuration tree builder. * - * @return TreeBuilder The tree builder + * @return TreeBuilder */ public function getConfigTreeBuilder() { diff --git a/src/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.php b/src/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.php index 5938594bf774e..b2e7db2696661 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.php +++ b/src/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\WebProfilerBundle\EventListener; +use Symfony\Bundle\FullStack; use Symfony\Bundle\WebProfilerBundle\Csp\ContentSecurityPolicyHandler; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Request; @@ -142,6 +143,7 @@ protected function injectToolbar(Response $response, Request $request, array $no $toolbar = "\n".str_replace("\n", '', $this->twig->render( '@WebProfiler/Profiler/toolbar_js.html.twig', [ + 'full_stack' => class_exists(FullStack::class), 'excluded_ajax_paths' => $this->excludedAjaxPaths, 'token' => $response->headers->get('X-Debug-Token'), 'request' => $request, diff --git a/src/Symfony/Bundle/WebProfilerBundle/Profiler/TemplateManager.php b/src/Symfony/Bundle/WebProfilerBundle/Profiler/TemplateManager.php index 794c118837989..f962e69f1a4ba 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Profiler/TemplateManager.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Profiler/TemplateManager.php @@ -17,8 +17,6 @@ use Twig\Environment; /** - * Profiler Templates Manager. - * * @author Fabien Potencier * @author Artur Wielogórski * @@ -58,11 +56,9 @@ public function getName(Profile $profile, string $panel) /** * Gets template names of templates that are present in the viewed profile. * - * @return array - * * @throws \UnexpectedValueException */ - public function getNames(Profile $profile) + public function getNames(Profile $profile): array { $loader = $this->twig->getLoader(); $templates = []; diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/logger.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/logger.html.twig index 4821f79dafd5a..7b1a35b748dd7 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/logger.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/logger.html.twig @@ -46,189 +46,181 @@ {% block panel %}

Log Messages

- {% if collector.logs is empty %} + {% if collector.processedLogs is empty %}

No log messages available.

{% else %} - {# sort collected logs in groups #} - {% set deprecation_logs, debug_logs, info_and_error_logs, silenced_logs = [], [], [], [] %} - {% set has_error_logs = false %} - {% for log in collector.logs %} - {% if log.scream is defined and not log.scream %} - {% set deprecation_logs = deprecation_logs|merge([log]) %} - {% elseif log.scream is defined and log.scream %} - {% set silenced_logs = silenced_logs|merge([log]) %} - {% elseif log.priorityName == 'DEBUG' %} - {% set debug_logs = debug_logs|merge([log]) %} - {% else %} - {% set info_and_error_logs = info_and_error_logs|merge([log]) %} - {% if log.priorityName != 'INFO' %} - {% set has_error_logs = true %} - {% endif %} - {% endif %} - {% endfor %} - -
-
-

Info. & Errors {{ collector.counterrors ?: info_and_error_logs|length }}

-

Informational and error log messages generated during the execution of the application.

- -
- {% if info_and_error_logs is empty %} -
-

There are no log messages of this level.

-
- {% else %} - {{ helper.render_table(info_and_error_logs, 'info', true) }} - {% endif %} -
+ {% set has_error_logs = collector.processedLogs|column('type')|filter(type => 'error' == type)|length > 0 %} + {% set has_deprecation_logs = collector.processedLogs|column('type')|filter(type => 'deprecation' == type)|length > 0 %} + + {% set filters = collector.filters %} +
+
+
    +
  • + + +
  • + +
  • + + +
  • + +
  • + + +
  • +
-
- {# 'deprecation_logs|length' is not used because deprecations are - now grouped and the group count doesn't match the message count #} -

Deprecations {{ collector.countdeprecations|default(0) }}

-

Log messages generated by using features marked as deprecated.

- -
- {% if deprecation_logs is empty %} -
-

There are no log messages about deprecated features.

+
+ + {{ include('@WebProfiler/Icon/filter.svg') }} + Level ({{ filters.priority|length - 1 }}) + + +
+ + + {% for label, value in filters.priority %} +
+ +
- {% else %} - {{ helper.render_table(deprecation_logs, 'deprecation', false, true) }} - {% endif %} + {% endfor %}
-
- -
-

Debug {{ debug_logs|length }}

-

Unimportant log messages generated during the execution of the application.

- -
- {% if debug_logs is empty %} -
-

There are no log messages of this level.

+ + +
+ + {{ include('@WebProfiler/Icon/filter.svg') }} + Channel ({{ filters.channel|length - 1 }}) + + +
+ + + {% for value in filters.channel %} +
+ +
- {% else %} - {{ helper.render_table(debug_logs, 'debug') }} - {% endif %} + {% endfor %}
-
- -
-

PHP Notices {{ collector.countscreams|default(0) }}

-

Log messages generated by PHP notices silenced with the @ operator.

+ +
-
- {% if silenced_logs is empty %} -
-

There are no log messages of this level.

-
- {% else %} - {{ helper.render_table(silenced_logs, 'silenced') }} - {% endif %} -
-
+ + + + + + + + + + + + + {% for log in collector.processedLogs %} + {% set css_class = 'error' == log.type ? 'error' + : (log.priorityName == 'WARNING' or 'deprecation' == log.type) ? 'warning' + : 'silenced' == log.type ? 'silenced' + %} + + - {% set compilerLogTotal = 0 %} - {% for logs in collector.compilerLogs %} - {% set compilerLogTotal = compilerLogTotal + logs|length %} - {% endfor %} -
-

Container {{ compilerLogTotal }}

-

Log messages generated during the compilation of the service container.

- -
- {% if collector.compilerLogs is empty %} -
-

There are no compiler log messages.

-
- {% else %} -
TimeMessage
+ + + {% if log.type in ['error', 'deprecation', 'silenced'] or 'WARNING' == log.priorityName %} + + {% if 'error' == log.type or 'WARNING' == log.priorityName %} + {{ log.priorityName|lower }} + {% else %} + {{ log.type|lower }} + {% endif %} + + {% endif %} +
- - - - - - - - - {% for class, logs in collector.compilerLogs %} - - - - - {% endfor %} - -
ClassMessages
- {% set context_id = 'context-compiler-' ~ loop.index %} - - {{ class }} - -
-
    - {% for log in logs %} -
  • {{ profiler_dump_log(log.message) }}
  • - {% endfor %} -
-
-
{{ logs|length }}
- {% endif %} -
-
+ + {{ helper.render_log_message('debug', loop.index, log) }} + + + {% endfor %} + + +
+

There are no log messages.

- + {% endif %} -{% endblock %} -{% macro render_table(logs, category = '', show_level = false, is_deprecation = false) %} - {% import _self as helper %} - {% set channel_is_defined = (logs|first).channel is defined %} - {% set filter = show_level or channel_is_defined %} - - - - - {% if show_level %}{% else %}{% endif %} - {% if channel_is_defined %}{% endif %} - - - - - - {% for log in logs %} - {% set css_class = not is_deprecation - ? log.priorityName in ['CRITICAL', 'ERROR', 'ALERT', 'EMERGENCY'] ? 'status-error' - : log.priorityName == 'WARNING' ? 'status-warning' - %} - - - - {% if channel_is_defined %} - + {% set compilerLogTotal = 0 %} + {% for logs in collector.compilerLogs %} + {% set compilerLogTotal = compilerLogTotal + logs|length %} + {% endfor %} - {% endif %} +
+ +

Container Compilation Logs ({{ compilerLogTotal }})

+

Log messages generated during the compilation of the service container.

+
-
+ {% if collector.compilerLogs is empty %} +
+

There are no compiler log messages.

+
+ {% else %} +
LevelTimeChannelMessage
- {% if show_level %} - {{ log.priorityName }} - {% endif %} - - - {% if log.channel is null %}n/a{% else %}{{ log.channel }}{% endif %} - {% if log.errorCount is defined and log.errorCount > 1 %} - ({{ log.errorCount }} times) - {% endif %} - {{ helper.render_log_message(category, loop.index, log) }}
+ + + + - {% endfor %} - -
MessagesClass
-{% endmacro %} + + + + {% for class, logs in collector.compilerLogs %} + + {{ logs|length }} + + {% set context_id = 'context-compiler-' ~ loop.index %} + + {{ class }} + +
+
    + {% for log in logs %} +
  • {{ profiler_dump_log(log.message) }}
  • + {% endfor %} +
+
+ + + {% endfor %} + + + {% endif %} + +{% endblock %} {% macro render_log_message(category, log_index, log) %} {% set has_context = log.context is defined and log.context is not empty %} @@ -238,26 +230,41 @@ {{ profiler_dump_log(log.message) }} {% else %} {{ profiler_dump_log(log.message, log.context) }} + {% endif %} -
+ + {% if has_trace %} + {% set trace_id = 'trace-' ~ category ~ '-' ~ log_index %} + Show trace -
- {{ profiler_dump(log.context, maxDepth=1) }} -
+
+ {{ profiler_dump(log.context.exception.trace, maxDepth=1) }} +
+ {% endif %} + + {% if has_context %} +
+ {{ profiler_dump(log.context, maxDepth=1) }} +
+ {% endif %} {% if has_trace %}
{{ profiler_dump(log.context.exception.trace, maxDepth=1) }}
{% endif %} - {% endif %} +
{% endmacro %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/mailer.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/mailer.html.twig index cc85c413fcc52..bdca4eb968fbd 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/mailer.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/mailer.html.twig @@ -141,6 +141,18 @@
{% if message.htmlBody is defined %} {# Email instance #} +
+

HTML preview

+
+
+                                                            
+                                                        
+
+

HTML Content

diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/filter.svg b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/filter.svg new file mode 100644 index 0000000000000..8800f1c05d75c --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/filter.svg @@ -0,0 +1 @@ + diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base.html.twig index 0b13f57509a25..0850414d8c4af 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base.html.twig @@ -15,10 +15,18 @@ {% block body '' %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig index 2dfa26918f420..4b234d24b354c 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig @@ -38,12 +38,23 @@ if (typeof Sfjs === 'undefined' || typeof Sfjs.loadToolbar === 'undefined') { }; } + if (navigator.clipboard) { + document.querySelectorAll('[data-clipboard-text]').forEach(function(element) { + removeClass(element, 'hidden'); + element.addEventListener('click', function() { + navigator.clipboard.writeText(element.getAttribute('data-clipboard-text')); + }) + }); + } + var request = function(url, onSuccess, onError, payload, options, tries) { var xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP'); options = options || {}; - options.maxTries = options.maxTries || 0; + options.retry = options.retry || false; tries = tries || 1; - var delay = Math.pow(2, tries - 1) * 1000; + /* this delays for 125, 375, 625, 875, and 1000, ... */ + var delay = tries < 5 ? (tries - 0.5) * 250 : 1000; + xhr.open(options.method || 'GET', url, true); xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); xhr.onreadystatechange = function(state) { @@ -51,9 +62,11 @@ if (typeof Sfjs === 'undefined' || typeof Sfjs.loadToolbar === 'undefined') { return null; } - if (xhr.status == 404 && options.maxTries > 1) { - setTimeout(function(){ - options.maxTries--; + if (xhr.status == 404 && options.retry && !options.stop) { + setTimeout(function() { + if (options.stop) { + return; + } request(url, onSuccess, onError, payload, options, tries + 1); }, delay); @@ -66,6 +79,11 @@ if (typeof Sfjs === 'undefined' || typeof Sfjs.loadToolbar === 'undefined') { (onError || noop)(xhr); } }; + + if (options.onSend) { + options.onSend(tries); + } + xhr.send(payload || ''); }; @@ -193,7 +211,7 @@ if (typeof Sfjs === 'undefined' || typeof Sfjs.loadToolbar === 'undefined') { row.appendChild(durationCell); request.liveDurationHandle = setInterval(function() { - durationCell.textContent = (new Date() - request.start) + 'ms'; + durationCell.textContent = (new Date() - request.start) + ' ms'; }, 100); row.className = 'sf-ajax-request sf-ajax-request-loading'; @@ -257,7 +275,7 @@ if (typeof Sfjs === 'undefined' || typeof Sfjs.loadToolbar === 'undefined') { } if (request.duration) { - durationCell.textContent = request.duration + 'ms'; + durationCell.textContent = request.duration + ' ms'; } if (request.profilerUrl) { @@ -421,8 +439,94 @@ if (typeof Sfjs === 'undefined' || typeof Sfjs.loadToolbar === 'undefined') { return this; }, + showToolbar: function(token) { + var sfwdt = document.getElementById('sfwdt' + token); + removeClass(sfwdt, 'sf-display-none'); + + if (getPreference('toolbar/displayState') == 'none') { + document.getElementById('sfToolbarMainContent-' + token).style.display = 'none'; + document.getElementById('sfToolbarClearer-' + token).style.display = 'none'; + document.getElementById('sfMiniToolbar-' + token).style.display = 'block'; + } else { + document.getElementById('sfToolbarMainContent-' + token).style.display = 'block'; + document.getElementById('sfToolbarClearer-' + token).style.display = 'block'; + document.getElementById('sfMiniToolbar-' + token).style.display = 'none'; + } + }, + + hideToolbar: function(token) { + var sfwdt = document.getElementById('sfwdt' + token); + addClass(sfwdt, 'sf-display-none'); + }, + + initToolbar: function(token) { + this.showToolbar(token); + + var hideButton = document.getElementById('sfToolbarHideButton-' + token); + var hideButtonSvg = hideButton.querySelector('svg'); + hideButtonSvg.setAttribute('aria-hidden', 'true'); + hideButtonSvg.setAttribute('focusable', 'false'); + addEventListener(hideButton, 'click', function (event) { + event.preventDefault(); + + var p = this.parentNode; + p.style.display = 'none'; + (p.previousElementSibling || p.previousSibling).style.display = 'none'; + document.getElementById('sfMiniToolbar-' + token).style.display = 'block'; + setPreference('toolbar/displayState', 'none'); + }); + + var showButton = document.getElementById('sfToolbarMiniToggler-' + token); + var showButtonSvg = showButton.querySelector('svg'); + showButtonSvg.setAttribute('aria-hidden', 'true'); + showButtonSvg.setAttribute('focusable', 'false'); + addEventListener(showButton, 'click', function (event) { + event.preventDefault(); + + var elem = this.parentNode; + if (elem.style.display == 'none') { + document.getElementById('sfToolbarMainContent-' + token).style.display = 'none'; + document.getElementById('sfToolbarClearer-' + token).style.display = 'none'; + elem.style.display = 'block'; + } else { + document.getElementById('sfToolbarMainContent-' + token).style.display = 'block'; + document.getElementById('sfToolbarClearer-' + token).style.display = 'block'; + elem.style.display = 'none' + } + + setPreference('toolbar/displayState', 'block'); + }); + }, + loadToolbar: function(token, newToken) { + var that = this; + var triesCounter = document.getElementById('sfLoadCounter-' + token); + + var options = { + retry: true, + onSend: function (count) { + if (count === 3) { + that.initToolbar(token); + } + + if (triesCounter) { + triesCounter.textContent = count; + } + }, + }; + + var cancelButton = document.getElementById('sfLoadCancel-' + token); + if (cancelButton) { + addEventListener(cancelButton, 'click', function (event) { + event.preventDefault(); + + options.stop = true; + that.hideToolbar(token); + }); + } + newToken = (newToken || token); + this.load( 'sfwdt' + token, '{{ url("_wdt", { "token": "xxxxxx" })|escape('js') }}'.replace(/xxxxxx/, newToken), @@ -444,15 +548,7 @@ if (typeof Sfjs === 'undefined' || typeof Sfjs.loadToolbar === 'undefined') { return; } - if (getPreference('toolbar/displayState') == 'none') { - document.getElementById('sfToolbarMainContent-' + newToken).style.display = 'none'; - document.getElementById('sfToolbarClearer-' + newToken).style.display = 'none'; - document.getElementById('sfMiniToolbar-' + newToken).style.display = 'block'; - } else { - document.getElementById('sfToolbarMainContent-' + newToken).style.display = 'block'; - document.getElementById('sfToolbarClearer-' + newToken).style.display = 'block'; - document.getElementById('sfMiniToolbar-' + newToken).style.display = 'none'; - } + that.initToolbar(newToken); /* Handle toolbar-info position */ var toolbarBlocks = [].slice.call(el.querySelectorAll('.sf-toolbar-block')); @@ -480,39 +576,7 @@ if (typeof Sfjs === 'undefined' || typeof Sfjs.loadToolbar === 'undefined') { } }; } - var hideButton = document.getElementById('sfToolbarHideButton-' + newToken); - var hideButtonSvg = hideButton.querySelector('svg'); - hideButtonSvg.setAttribute('aria-hidden', 'true'); - hideButtonSvg.setAttribute('focusable', 'false'); - addEventListener(hideButton, 'click', function (event) { - event.preventDefault(); - var p = this.parentNode; - p.style.display = 'none'; - (p.previousElementSibling || p.previousSibling).style.display = 'none'; - document.getElementById('sfMiniToolbar-' + newToken).style.display = 'block'; - setPreference('toolbar/displayState', 'none'); - }); - var showButton = document.getElementById('sfToolbarMiniToggler-' + newToken); - var showButtonSvg = showButton.querySelector('svg'); - showButtonSvg.setAttribute('aria-hidden', 'true'); - showButtonSvg.setAttribute('focusable', 'false'); - addEventListener(showButton, 'click', function (event) { - event.preventDefault(); - - var elem = this.parentNode; - if (elem.style.display == 'none') { - document.getElementById('sfToolbarMainContent-' + newToken).style.display = 'none'; - document.getElementById('sfToolbarClearer-' + newToken).style.display = 'none'; - elem.style.display = 'block'; - } else { - document.getElementById('sfToolbarMainContent-' + newToken).style.display = 'block'; - document.getElementById('sfToolbarClearer-' + newToken).style.display = 'block'; - elem.style.display = 'none' - } - - setPreference('toolbar/displayState', 'block'); - }); renderAjaxRequests(); addEventListener(document.querySelector('.sf-toolbar-ajax-clear'), 'click', function() { requestStack = []; @@ -541,7 +605,7 @@ if (typeof Sfjs === 'undefined' || typeof Sfjs.loadToolbar === 'undefined') { } }, function(xhr) { - if (xhr.status !== 0) { + if (xhr.status !== 0 && !options.stop) { var sfwdt = document.getElementById('sfwdt' + token); sfwdt.innerHTML = '\
\ @@ -552,7 +616,7 @@ if (typeof Sfjs === 'undefined' || typeof Sfjs.loadToolbar === 'undefined') { sfwdt.setAttribute('class', 'sf-toolbar sf-error-toolbar'); } }, - { maxTries: 5 } + options ); return this; @@ -705,108 +769,90 @@ if (typeof Sfjs === 'undefined' || typeof Sfjs.loadToolbar === 'undefined') { }); } + /* Prevents from disallowing clicks on "copy to clipboard" elements inside toggles */ + var copyToClipboardElements = toggles[i].querySelectorAll('span[data-clipboard-text]'); + for (var k = 0; k < copyToClipboardElements.length; k++) { + addEventListener(copyToClipboardElements[k], 'click', function(e) { + e.stopPropagation(); + }); + } + toggles[i].setAttribute('data-processed', 'true'); } }, - createFilters: function() { - document.querySelectorAll('[data-filters] [data-filter]').forEach(function (filter) { - var filters = filter.closest('[data-filters]'), - type = 'choice', - name = filter.dataset.filter, - ucName = name.charAt(0).toUpperCase()+name.slice(1), - list = document.createElement('ul'), - values = filters.dataset['filter'+ucName] || filters.querySelectorAll('[data-filter-'+name+']'), - labels = {}, - defaults = null, - indexed = {}, - processed = {}; - if (typeof values === 'string') { - type = 'level'; - labels = values.split(','); - values = values.toLowerCase().split(','); - defaults = values.length - 1; - } - addClass(list, 'filter-list'); - addClass(list, 'filter-list-'+type); - values.forEach(function (value, i) { - if (value instanceof HTMLElement) { - value = value.dataset['filter'+ucName]; - } - if (value in processed) { - return; - } - var option = document.createElement('li'), - label = i in labels ? labels[i] : value, - active = false, - matches; - if ('' === label) { - option.innerHTML = '(none)'; - } else { - option.innerText = label; - } - option.dataset.filter = value; - option.setAttribute('title', 1 === (matches = filters.querySelectorAll('[data-filter-'+name+'="'+value+'"]').length) ? 'Matches 1 row' : 'Matches '+matches+' rows'); - indexed[value] = i; - list.appendChild(option); - addEventListener(option, 'click', function () { - if ('choice' === type) { - filters.querySelectorAll('[data-filter-'+name+']').forEach(function (row) { - if (option.dataset.filter === row.dataset['filter'+ucName]) { - toggleClass(row, 'filter-hidden-'+name); - } - }); - toggleClass(option, 'active'); - } else if ('level' === type) { - if (i === this.parentNode.querySelectorAll('.active').length - 1) { - return; - } - this.parentNode.querySelectorAll('li').forEach(function (currentOption, j) { - if (j <= i) { - addClass(currentOption, 'active'); - if (i === j) { - addClass(currentOption, 'last-active'); - } else { - removeClass(currentOption, 'last-active'); - } - } else { - removeClass(currentOption, 'active'); - removeClass(currentOption, 'last-active'); - } - }); - filters.querySelectorAll('[data-filter-'+name+']').forEach(function (row) { - if (i < indexed[row.dataset['filter'+ucName]]) { - addClass(row, 'filter-hidden-'+name); - } else { - removeClass(row, 'filter-hidden-'+name); - } - }); - } + initializeLogsTable: function() { + Sfjs.updateLogsTable(); + + document.querySelectorAll('.log-filter input').forEach((input) => { + input.addEventListener('change', () => { Sfjs.updateLogsTable(); }); + }); + + document.querySelectorAll('.filter-select-all-or-none a').forEach((link) => { + link.addEventListener('click', () => { + const selectAll = link.classList.contains('select-all'); + link.closest('.log-filter-content').querySelectorAll('input').forEach((input) => { + input.checked = selectAll; }); - if ('choice' === type) { - active = null === defaults || 0 <= defaults.indexOf(value); - } else if ('level' === type) { - active = i <= defaults; - if (active && i === defaults) { - addClass(option, 'last-active'); - } - } - if (active) { - addClass(option, 'active'); - } else { - filters.querySelectorAll('[data-filter-'+name+'="'+value+'"]').forEach(function (row) { - toggleClass(row, 'filter-hidden-'+name); - }); + + Sfjs.updateLogsTable(); + }); + }); + + document.body.addEventListener('click', (event) => { + document.querySelectorAll('details.log-filter').forEach((filterElement) => { + if (!filterElement.contains(event.target) && filterElement.open) { + filterElement.open = false; } - processed[value] = true; }); + }); + }, + + updateLogsTable: function() { + const selectedType = document.querySelector('#log-filter-type input:checked').value; + const priorities = document.querySelectorAll('#log-filter-priority input'); + const selectedPriorities = Array.from(priorities).filter((input) => input.checked).map((input) => input.value); + const channels = document.querySelectorAll('#log-filter-channel input'); + const selectedChannels = Array.from(channels).filter((input) => input.checked).map((input) => input.value); + + const logs = document.querySelector('table.logs'); + if (null === logs) { + return; + } - if (1 < list.childNodes.length) { - filter.appendChild(list); - filter.dataset.filtered = ''; + /* hide rows that don't match the current filters */ + let numVisibleRows = 0; + logs.querySelectorAll('tbody tr').forEach((row) => { + if ('all' !== selectedType && selectedType !== row.getAttribute('data-type')) { + row.style.display = 'none'; + return; } + + if (false === selectedPriorities.includes(row.getAttribute('data-priority'))) { + row.style.display = 'none'; + return; + } + + if ('' !== row.getAttribute('data-channel') && false === selectedChannels.includes(row.getAttribute('data-channel'))) { + row.style.display = 'none'; + return; + } + + row.style.display = 'table-row'; + numVisibleRows++; }); - } + + document.querySelector('table.logs').style.display = 0 === numVisibleRows ? 'none' : 'table'; + document.querySelector('.no-logs-message').style.display = 0 === numVisibleRows ? 'block' : 'none'; + + /* update the selected totals of all filters */ + document.querySelector('#log-filter-priority .filter-active-num').innerText = (priorities.length === selectedPriorities.length) ? 'All' : selectedPriorities.length; + document.querySelector('#log-filter-channel .filter-active-num').innerText = (channels.length === selectedChannels.length) ? 'All' : selectedChannels.length; + + /* update the currently selected "log type" tab */ + document.querySelectorAll('#log-filter-type li').forEach((tab) => tab.classList.remove('active')); + document.querySelector(`#log-filter-type input[value="${selectedType}"]`).parentElement.classList.add('active'); + }, }; })(); diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/cancel.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/cancel.html.twig new file mode 100644 index 0000000000000..6f1763d3a2937 --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/cancel.html.twig @@ -0,0 +1,25 @@ +{% block toolbar %} + {% set icon %} + {{ include('@WebProfiler/Icon/symfony.svg') }} + + + Loading… + + {% endset %} + + {% set text %} +
+ Loading the web debug toolbar… +
+
+ Attempt # +
+
+ + + +
+ {% endset %} + + {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url }) }} +{% endblock %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig index 6bb39de5beb40..7f7db6cf1239c 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig @@ -10,16 +10,28 @@ --page-background: #f9f9f9; --color-text: #222; --color-muted: #999; + --color-link: #218BC3; /* when updating any of these colors, do the same in toolbar.css.twig */ --color-success: #4f805d; --color-warning: #a46a1f; --color-error: #b0413e; + --badge-background: #f5f5f5; + --badge-color: #666; + --badge-warning-background: #FEF3C7; + --badge-warning-color: #B45309; + --badge-danger-background: #FEE2E2; + --badge-danger-color: #B91C1C; --tab-background: #fff; --tab-color: #444; --tab-active-background: #666; --tab-active-color: #fafafa; --tab-disabled-background: #f5f5f5; --tab-disabled-color: #999; + --log-filter-button-background: #fff; + --log-filter-button-border: #999; + --log-filter-button-color: #555; + --log-filter-active-num-color: #2563EB; + --log-timestamp-color: #555; --metric-value-background: #fff; --metric-value-color: inherit; --metric-unit-color: #999; @@ -54,13 +66,25 @@ --page-background: #36393e; --color-text: #e0e0e0; --color-muted: #777; + --color-link: #93C5FD; --color-error: #d43934; + --badge-background: #555; + --badge-color: #ddd; + --badge-warning-background: #B45309; + --badge-warning-color: #FEF3C7; + --badge-danger-background: #B91C1C; + --badge-danger-color: #FEE2E2; --tab-background: #555; --tab-color: #ccc; --tab-active-background: #888; --tab-active-color: #fafafa; --tab-disabled-background: var(--page-background); --tab-disabled-color: #777; + --log-filter-button-background: #555; + --log-filter-button-border: #999; + --log-filter-button-color: #ccc; + --log-filter-active-num-color: #93C5FD; + --log-timestamp-color: #ccc; --metric-value-background: #555; --metric-value-color: inherit; --metric-unit-color: #999; @@ -139,7 +163,7 @@ p { } a { - color: #218BC3; + color: var(--color-link); text-decoration: none; } a:hover { @@ -204,7 +228,7 @@ button { } .btn-link { border-color: transparent; - color: #218BC3; + color: var(--color-link); text-decoration: none; background-color: transparent; outline: none; @@ -1011,6 +1035,118 @@ tr.status-warning td { {# Logger panel ========================================================================= #} +.badge { + background: var(--badge-background); + border-radius: 4px; + color: var(--badge-color); + font-size: 12px; + font-weight: bold; + padding: 1px 4px; +} +.badge-warning { + background: var(--badge-warning-background); + color: var(--badge-warning-color); +} + +.log-filters { + display: flex; +} +.log-filters .log-filter { + position: relative; +} +.log-filters .log-filter + .log-filter { + margin-left: 15px; +} +.log-filters .log-filter summary { + align-items: center; + background: var(--log-filter-button-background); + border-radius: 2px; + border: 1px solid var(--log-filter-button-border); + color: var(--log-filter-button-color); + cursor: pointer; + display: flex; + padding: 5px 8px; +} +.log-filters .log-filter summary .icon { + height: 18px; + width: 18px; + margin: 0 7px 0 0; +} +.log-filters .log-filter summary svg { + height: 18px; + width: 18px; + opacity: 0.7; +} +.log-filters .log-filter summary .filter-active-num { + color: var(--log-filter-active-num-color); + font-weight: bold; + padding: 0 1px; +} +.log-filter .tab-navigation { + margin-bottom: 0; +} +.log-filter .tab-navigation li:first-child { + border-top-left-radius: 2px; + border-bottom-left-radius: 2px; +} +.log-filter .tab-navigation li:last-child { + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; +} +.log-filter .tab-navigation li { + border-color: var(--log-filter-button-border); + padding: 0; +} +.log-filter .tab-navigation li + li { + margin-left: -5px; +} +.log-filter .tab-navigation li .badge { + font-size: 13px; + padding: 0 6px; +} +.log-filter .tab-navigation li input { + display: none; +} +.log-filter .tab-navigation li label { + align-items: center; + cursor: pointer; + padding: 5px 10px; + display: inline-flex; + font-size: 14px; +} + +.log-filters .log-filter .log-filter-content { + background: var(--base-0); + border: 1px solid var(--table-border); + border-radius: 2px; + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + padding: 15px; + position: absolute; + left: 0; + top: 36px; + max-width: 400px; + min-width: 200px; + z-index: 9999; +} +.log-filters .log-filter .log-filter-content .log-filter-option { + align-items: center; + display: flex; +} +.log-filter .filter-select-all-or-none { + margin-bottom: 10px; +} +.log-filter .filter-select-all-or-none a + a { + margin-left: 15px; +} +.log-filters .log-filter .log-filter-content .log-filter-option + .log-filter-option { + margin: 7px 0 0; +} +.log-filters .log-filter .log-filter-content .log-filter-option label { + cursor: pointer; + flex: 1; + padding-left: 10px; +} + table.logs .metadata { display: block; font-size: 12px; @@ -1018,6 +1154,75 @@ table.logs .metadata { .theme-dark tr.status-error td, .theme-dark tr.status-warning td { border-bottom: unset; border-top: unset; } +table.logs .log-timestamp { + color: var(--log-timestamp-color); +} +table.logs .log-metadata { + margin: 8px 0 0; +} +table.logs .log-metadata span { + display: inline-block; +} +table.logs .log-metadata span + span { + margin-left: 10px; +} +table.logs .log-metadata .log-channel { + color: var(--base-1); + font-size: 13px; + font-weight: bold; +} +table.logs .log-metadata .log-num-occurrences { + color: var(--color-muted); + font-size: 13px; +} +.log-type-badge { + display: inline-block; + font-family: var(--font-sans-serif); + margin-top: 5px; +} +.log-type-badge.badge-deprecation { + background: var(--badge-warning-background); + color: var(--badge-warning-color); +} +.log-type-badge.badge-error { + background: var(--badge-danger-background); + color: var(--badge-danger-color); +} +.log-type-badge.badge-silenced { + background: #EDE9FE; + color: #6D28D9; +} +.theme-dark .log-type-badge.badge-silenced { + background: #5B21B6; + color: #EDE9FE; +} + +tr.log-status-warning { + border-left: 4px solid #F59E0B; +} +tr.log-status-error { + border-left: 4px solid #EF4444; +} +tr.log-status-silenced { + border-left: 4px solid #A78BFA; +} + +.container-compilation-logs { + background: var(--table-background); + border: 1px solid var(--base-2); + margin-top: 30px; + padding: 15px; +} +.container-compilation-logs summary { + cursor: pointer; +} +.container-compilation-logs summary h4 { + margin: 0 0 5px; +} +.container-compilation-logs summary p { + margin: 0; +} + {# Doctrine panel ========================================================================= #} .sql-runnable { diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/settings.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/settings.html.twig index 348d4a1d8bf29..4b2394687f190 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/settings.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/settings.html.twig @@ -116,21 +116,25 @@ @@ -139,27 +143,38 @@