diff --git a/.github/patch-types.php b/.github/patch-types.php index 70fea35aaae3e..3c91c7f580b17 100644 --- a/.github/patch-types.php +++ b/.github/patch-types.php @@ -22,18 +22,25 @@ case false !== strpos($file, '/src/Symfony/Component/Debug/Tests/Fixtures/'): case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Tests/Compiler/OptionalServiceClass.php'): case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes.php'): + case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes_80.php'): case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/uniontype_classes.php'): case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Tests/Fixtures/ParentNotExists.php'): case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Prototype/BadClasses/MissingParent.php'): case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Tests/Fixtures/WitherStaticReturnType.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/HttpKernel/Tests/Fixtures/Controller/AttributeController.php'): 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/PropertyInfo/Tests/Fixtures/Php80Dummy.php'): + case false !== strpos($file, '/src/Symfony/Component/Routing/Tests/Fixtures/AttributeFixtures'): case false !== strpos($file, '/src/Symfony/Component/Serializer/Tests/Normalizer/Features/ObjectOuter.php'): + case false !== strpos($file, '/src/Symfony/Component/VarDumper/Tests/Fixtures/LotsOfAttributes.php'): + case false !== strpos($file, '/src/Symfony/Component/Validator/Tests/Fixtures/Attribute/'): + case false !== strpos($file, '/src/Symfony/Component/VarDumper/Tests/Fixtures/MyAttribute.php'): case false !== strpos($file, '/src/Symfony/Component/VarDumper/Tests/Fixtures/NotLoadableClass.php'): case false !== strpos($file, '/src/Symfony/Component/VarDumper/Tests/Fixtures/Php74.php') && \PHP_VERSION_ID < 70400: + case false !== strpos($file, '/src/Symfony/Component/VarDumper/Tests/Fixtures/RepeatableAttribute.php'): continue 2; } diff --git a/.php_cs.dist b/.php_cs.dist index 0fd2436e791f7..8b691441a3e36 100644 --- a/.php_cs.dist +++ b/.php_cs.dist @@ -37,6 +37,8 @@ return PhpCsFixer\Config::create() ->notPath('#Symfony/Bridge/PhpUnit/.*Legacy#') // file content autogenerated by `var_export` ->notPath('Symfony/Component/Translation/Tests/fixtures/resources.php') + // file content autogenerated by `VarExporter::export` + ->notPath('Symfony/Component/Serializer/Tests/Fixtures/serializer.class.metadata.php') // test template ->notPath('Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Custom/_name_entry_label.html.php') // explicit trigger_error tests diff --git a/CHANGELOG-5.2.md b/CHANGELOG-5.2.md new file mode 100644 index 0000000000000..59ab4dcbb1575 --- /dev/null +++ b/CHANGELOG-5.2.md @@ -0,0 +1,193 @@ +CHANGELOG for 5.2.x +=================== + +This changelog references the relevant changes (bug and security fixes) done +in 5.2 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.2.0...v5.2.1 + +* 5.2.0-BETA1 (2020-10-05) + + * feature #38382 [Validator] Use comparison constraints as attributes (derrabus) + * feature #38369 [HttpFoundation] Expired cookies string representation consistency & tests (iquito) + * feature #38407 [Mime] Prefer .jpg instead of .jpeg (fabpot) + * feature #36479 [Notifier][WebProfilerBundle][FrameworkBundle] Add notifier section to profiler (jschaedl) + * feature #38395 [lock] Prevent user serializing the key when store does not support it. (jderusse) + * feature #38307 [Form] Implement Twig helpers to get field variables (tgalopin) + * feature #38177 [Security] Magic login link authentication (weaverryan) + * feature #38224 [HttpFoundation] Add Request::toArray() for JSON content (Nyholm) + * feature #38323 [Mime] Allow multiple parts with the same name in FormDataPart (HypeMC) + * feature #38354 [RateLimiter] add Limit::ensureAccepted() which throws RateLimitExceededException if not accepted (kbond) + * feature #32904 [Messenger] Added ErrorDetailsStamp (TimoBakx) + * feature #36152 [Messenger] dispatch event when a message is retried (nikophil) + * feature #38361 Can define ChatMessage transport to null (odolbeau) + * feature #38289 [HttpClient] provide response body to the RetryDecider (jderusse) + * feature #38308 [Security][RateLimiter] Added request rate limiter to prevent breadth-first attacks (wouterj) + * feature #38257 [RateLimiter] Add limit object on RateLimiter consume method (Valentin, vasilvestre) + * feature #38309 [Validator] Constraints as php 8 Attributes (derrabus) + * feature #38332 [Validator] Add support for UUIDv6 in Uuid constraint (nicolas-grekas) + * feature #38330 [Contracts] add TranslatableInterface (nicolas-grekas) + * feature #38322 [Validator] Add Ulid constraint and validator (Laurent Clouet) + * feature #38333 [Uid] make UUIDv6 always return truly random nodes to prevent leaking the MAC of the host (nicolas-grekas) + * feature #38296 [lock] Provides default implementation when store does not supports the behavior (jderusse) + * feature #38298 [Notifier] Add Sendinblue notifier. (ptondereau) + * feature #38305 [PhpUnitBridge] Enable a maximum PHPUnit version to be set via SYMFONY_MAX_PHPUNIT_VERSION (stevegrunwell) + * feature #38288 [DomCrawler] Add `assertFormValue()` in `WebTestCase` (mnapoli) + * feature #38287 [DomCrawler] Add checkbox assertions for functional tests (mnapoli) + * feature #36326 [Validator] Add invalid datetime message in Range validator (przemyslaw-bogusz) + * feature #38243 [HttpKernel] Auto-register kernel as an extension (HypeMC) + * feature #38277 [Mailer] Added Sendinblue bridge (drixs6o9) + * feature #35956 [Form] Added "html5" option to both MoneyType and PercentType (romaricdrigon) + * feature #38269 [String] allow passing null to string functions (kbond) + * feature #38176 [Config] Adding the "info" to a missing option error messages (weaverryan) + * feature #38167 [VarDumper] Support for ReflectionAttribute (derrabus) + * feature #35740 [MonologBridge] Use composition instead of inheritance in monolog bridge (pvgnd, mm-pvgnd) + * feature #38182 [HttpClient] Added RetryHttpClient (jderusse) + * feature #38204 [Security] Added login throttling feature (wouterj) + * feature #38007 [Amqp] Add amqps support (vasilvestre-OS) + * feature #37546 [RFC] Introduce a RateLimiter component (wouterj) + * feature #38193 Make RetryTillSaveStore implements the SharedLockStoreInterface (jderusse) + * feature #37968 [Form] Add new way of mapping data using callback functions (yceruto) + * feature #38198 [String] allow translit rules to be given as closure (nicolas-grekas) + * feature #37829 [RFC][HttpKernel][Security] Allowed adding attributes on controller arguments that will be passed to argument resolvers. (jvasseur) + * feature #37752 [RFC][lock] Introduce Shared Lock (or Read/Write Lock) (jderusse) + * feature #37759 [Messenger] - Add option to confirm message delivery in Amqp connection (scyzoryck) + * feature #30572 [Cache] add integration with Messenger to allow computing cached values in a worker (nicolas-grekas) + * feature #38149 [SecurityBundle] Comma separated ips for security.access_control (a-menshchikov) + * feature #38151 [Serializer] add UidNormalizer (guillbdx, norkunas) + * feature #37976 [Messenger] Don't prevent dispatch out of another bus (ogizanagi) + * feature #38134 [Lock] Fix wrong interface for MongoDbStore (jderusse) + * feature #38135 [AmazonSqsMessenger] Added the count message awareness on the transport (raphahardt) + * feature #38026 [HttpClient] Allow to provide additional curl options to CurlHttpClient (pizzaminded) + * feature #37559 [PropertyInfo] fix array types with keys (array) (digilist) + * feature #37519 [Process] allow setting options esp. "create_new_console" to detach a subprocess (andrei0x309) + * feature #37704 [MonologBridge] Added SwitchUserTokenProcessor to log the impersonator (IgorTimoshenko) + * feature #37545 [DependencyInjection] Add the Required attribute (derrabus) + * feature #37474 [RFC][Routing] Added the Route attribute (derrabus) + * feature #38068 [Notifier] Register NotificationDataCollector and NotificationLoggerListener service (jschaedl) + * feature #32841 Create impersonation_exit_path() and *_url() functions (dFayet) + * feature #37706 [Validator] Debug validator command (loic425, fabpot) + * feature #38052 Increase HttpBrowser::getHeaders() visibility to protected (iansltx) + * feature #36727 [Messenger] Add option to prevent Redis from deleting messages on rejection (Steveb-p) + * feature #37678 [DoctrineBridge] Ulid and Uuid as Doctrine Types (gennadigennadigennadi) + * feature #38037 Translate failure messages of json authentication (Malte Schlüter) + * feature #35890 [Cache] give control over cache prefix seed (Tobion) + * feature #37337 [Security] Configurable execution order for firewall listeners (scheb) + * feature #33850 [Serializer] fix denormalization of basic property-types in XML and CSV (mkrauser) + * feature #38017 [PHPUnitBridge] deprecations not disabled anymore when disabled=0 (l-vo) + * feature #33381 [Form] dispatch submit events for disabled forms too (xabbuh) + * feature #35338 Added support for using the "{{ label }}" placeholder in constraint messages (a-menshchikov) + * feature #34790 [Console] Remove restriction for choices to be strings (LordZardeck, YaFou, ogizanagi) + * feature #37979 [Workflow] Expose the Metadata Store in the DIC (lyrixx) + * feature #37371 [Translation] Add support for calling 'trans' with ICU formatted messages (someonewithpc) + * feature #37670 [Translation] Translatable objects (natewiebe13) + * feature #37432 [Mailer] Implement additional mailer transport options (fritzmg) + * feature #35893 [HttpClient][DI] Add an option to use the MockClient in functional tests (GaryPEGEOT) + * feature #35780 [Semaphore] Added the component (lyrixx) + * feature #37846 [Security] Lazily load the user during the check passport event (wouterj) + * feature #37934 [Mailer] Mailjet Add ability to pass custom headers to API (tcheymol) + * feature #37942 [Security] Renamed provider key to firewall name (wouterj) + * feature #37951 [FrameworkBundle] Make AbstractPhpFileCacheWarmer public (ossinkine) + * feature #30335 [PropertyInfo] ConstructorExtractor which has higher priority than PhpDocExtractor and ReflectionExtractor (karser) + * feature #37926 [lock] Lazy create table in lock PDO store (jderusse) + * feature #36573 [Notifier] Add Esendex bridge (odolbeau) + * feature #33540 [Serializer] Add special '*' serialization group that allows any group (nrobinaubertin) + * feature #37708 Allow Drupal to wrap the Symfony test listener (alexpott) + * feature #36211 [Serializer] Adds FormErrorNormalizer (YaFou) + * feature #37087 [Messenger] Add FlattenException Normalizer (monteiro) + * feature #37917 [Security] Pass Passport to LoginFailureEvent (ihmels) + * feature #37734 [HttpFoundation] add support for X_FORWARDED_PREFIX header (jeff1985) + * feature #37897 [Mailer] Support Amazon SES ConfigurationSetName (cvmiert) + * feature #37915 Improve link script with rollback when using symlink (noniagriconomie) + * feature #37889 Toolbar toggler accessibility (Chi-teck) + * feature #36515 [HttpKernel] Add `$kernel->getBuildDir()` to separate it from the cache directory (mnapoli) + * feature #32133 [PropertyAccess] Allow to disable magic __get & __set (ogizanagi) + * feature #36016 [Translation] Add a pseudo localization translator (fancyweb) + * feature #37755 Fix #37740: Cast all Request parameter values to string (rgeraads) + * feature #36541 ✨ [Mailer] Add Mailjet bridge (tcheymol) + * feature #36940 [Notifier] add support for smsapi-notifier (szepczynski) + * feature #37830 [Notifier] Add LinkedIn provider (ismail1432) + * feature #37867 [Messenger] Add message timestamp to amqp connection (Bartłomiej Zając) + * feature #36925 [Security] Verifying if the password field is null (Mbechezi Nawo) + * feature #37847 [Serializer][Mime] Fix Mime message serialization (fabpot) + * feature #37338 [Console] added TableCellStyle (khoptynskyi) + * feature #37840 [VarDumper] Support PHPUnit --colors option (ogizanagi) + * feature #37138 [Notifier][Slack] Use Slack Web API chat.postMessage instead of WebHooks (xavierbriand) + * feature #37827 [Console] Rework the signal integration (lyrixx) + * feature #36131 [Mailer] Add a transport that uses php.ini settings for configuration (l-vo) + * feature #36596 Add cache.adapter.redis_tag_aware to use RedisCacheAwareAdapter (l-vo) + * feature #36582 [Messenger] Add Beanstalkd bridge (X-Coder264) + * feature #35967 [VarDumper] Add VAR_DUMPER_FORMAT=server format (ogizanagi) + * feature #37815 [Workflow] Choose which Workflow events should be dispatched (stewartmalik, lyrixx) + * feature #20054 [Console] Different approach on merging application definition (ro0NL) + * feature #36648 [Notifier] Add Mobyt bridge (Deamon) + * feature #37332 [FrameworkBundle] Allow to leverage autoconfiguration for DataCollectors with template (l-vo) + * feature #37359 [Security] Add event to inspect authenticated token before it becomes effective (scheb) + * feature #37539 [Workflow] Added Context to Workflow Event (epitre) + * feature #37683 [Console] allow multiline responses to console questions (ramsey) + * feature #29117 [Serializer] Add CompiledClassMetadataFactory (fbourigault) + * feature #37676 [Stopwatch] Add name property to the stopwatchEvent (AhmedRaafat14) + * feature #34704 [Messenger] Add method HandlerFailedException::getNestedExceptionOfClass (tyx) + * feature #37793 Revert "[DependencyInjection] Resolve parameters in tag arguments" (rpkamp) + * feature #37537 [HttpKernel] Provide status code in fragment handler exception (gonzalovilaseca) + * feature #36480 [Notifier] Add Infobip bridge (jeremyFreeAgent) + * feature #36496 [Notifier] added telegram options (krasilnikovm) + * feature #37754 [FrameworkBundle] Add days before expiration in "about" command (noniagriconomie) + * feature #35773 [Notifier] Change notifier recipient handling (jschaedl) + * feature #36488 [Notifier] Add Google Chat bridge (GromNaN) + * feature #36692 [HttpClient] add EventSourceHttpClient to consume Server-Sent Events (soyuka) + * feature #36616 [Notifier] Add Zulip notifier bridge (phpfour) + * feature #37747 [Notifier] Make Freemobile config more flexible (fabpot) + * feature #37718 [Security] class Security implements AuthorizationCheckerInterface (SimonHeimberg) + * feature #37732 [Console] Allow testing single command apps using CommandTester (chalasr) + * feature #37480 [Messenger] add redeliveredAt in RedeliveryStamp construct (qkdreyer) + * feature #37565 [Validator] Add Isin validator constraint (lmasforne) + * feature #37712 [Mailer] Prevent MessageLoggerListener from leaking in env=prod (vudaltsov) + * feature #33729 [Console] Add signal event (marie) + * feature #36352 [Validator] Added support for cascade validation on typed properties (HeahDude) + * feature #37243 [DependencyInjection] Resolve parameters in tag arguments (rpkamp) + * feature #37415 [Console] added info method to symfony style (titospeakap, titomiguelcosta) + * feature #36691 [FrameworkBundle] Deprecate some public services to private (fancyweb) + * feature #36929 Added a FrenchInflector for the String component (Alexandre-T) + * feature #37620 [Security] Use NullToken while checking authorization (wouterj) + * feature #37711 [Router] allow to use \A and \z as regex start and end (zlodes) + * feature #37703 Update StopwatchPeriod.php (ThomasLandauer) + * feature #37696 [Routing] Allow inline definition of requirements and defaults for host (julienfalque) + * feature #37567 [PhpUnitBridge] Polyfill new phpunit 9.1 assertions (phpfour) + * feature #37479 [HttpFoundation] Added File::getContent() (lyrixx) + * feature #36178 [Mime] allow non-ASCII characters in local part of email (dmaicher) + * feature #30931 [Form] Improve invalid messages for form types (hiddewie) + * feature #37492 [ErrorHandler] Allow override of the default non-debug template (PhilETaylor) + * feature #37482 [HttpClient] always yield a LastChunk in AsyncResponse on destruction (nicolas-grekas) + * feature #36364 [HttpKernel][WebProfilerBundle] Add session profiling (mtarld) + * feature #37428 [Workflow] Added Function (and Twig extension) to retrieve a specific transition (Carlos Pereira De Amorim) + * feature #36487 [HttpClient] Add MockResponse::getRequestMethod() and getRequestUrl() to allow inspecting which request has been sent (javespi) + * feature #37295 Move event alias mappings to their components (derrabus) + * feature #37443 [HttpClient] add StreamableInterface to ease turning responses into PHP streams (nicolas-grekas) + * feature #36739 [TwigBundle] Deprecate the public "twig" service to private (fancyweb) + * feature #37272 [HttpFoundation] add `HeaderUtils::parseQuery()`: it does the same as `parse_str()` but preserves dots in variable names (nicolas-grekas) + * feature #37403 [Notifier] Return SentMessage from the Notifier message handler (fabpot) + * feature #36611 Add Notifier SentMessage (jeremyFreeAgent) + * feature #37357 [FrameworkBundle] allow configuring trusted proxies using semantic configuration (nicolas-grekas) + * feature #37373 [DI] deprecate Definition/Alias::setPrivate() (nicolas-grekas) + * feature #37351 [FrameworkBundle] allow enabling the HTTP cache using semantic configuration (nicolas-grekas) + * feature #37306 [Messenger] added support for Amazon SQS QueueUrl as DSN (starred-gijs) + * feature #37336 [Security] Let security factories add firewall listeners (scheb) + * feature #37318 [Security] Add attributes on Passport (fabpot) + * feature #37241 [Console] Fix Docblock for CommandTester::getExitCode (Jean85) + * feature #35834 [Notifier] Remove default transport property in Transports class (jschaedl) + * feature #37198 [FrameworkBundle] Add support for tagged_iterator/tagged_locator in unused tags util (fabpot) + * feature #37165 [Mime] Add DKIM support (fabpot) + * feature #36778 Use PHP instead of XML as the prefered service/route configuration in core (fabpot) + * feature #36802 [Console] Add support for true colors (fabpot) + * feature #36775 [DependencyInjection] Add abstract_arg() and param() (fabpot) + * feature #37040 [PropertyInfo] Support using the SerializerExtractor with no group check (GuilhemN) + * feature #37175 [Mime] Deprecate Address::fromString() (fabpot) + * feature #37114 Provides a way to override cache and log folders from the ENV (Plopix) + * feature #36736 [FrameworkBundle][Mailer] Add a way to configure some email headers from semantic configuration (fabpot) + * feature #37136 [HttpClient] added support for pausing responses with a new `pause_handler` callable exposed as an info item (nicolas-grekas) + * feature #36779 [HttpClient] add AsyncDecoratorTrait to ease processing responses without breaking async (nicolas-grekas) + * feature #36790 Bump Doctrine DBAL to 2.10+ (fabpot) + * feature #36818 [Validator] deprecate the "allowEmptyString" option (xabbuh) + diff --git a/UPGRADE-5.2.md b/UPGRADE-5.2.md new file mode 100644 index 0000000000000..e5e486972774a --- /dev/null +++ b/UPGRADE-5.2.md @@ -0,0 +1,126 @@ +UPGRADE FROM 5.1 to 5.2 +======================= + +DependencyInjection +------------------- + + * Deprecated `Definition::setPrivate()` and `Alias::setPrivate()`, use `setPublic()` instead + +FrameworkBundle +--------------- + + * Deprecated the public `form.factory`, `form.type.file`, `translator`, `security.csrf.token_manager`, `serializer`, + `cache_clearer`, `filesystem` and `validator` services to private. + * If you configured the `framework.cache.prefix_seed` option, you might want to add the `%kernel.environment%` to its value to + keep cache namespaces separated by environment of the app. The `%kernel.container_class%` (which includes the environment) + used to be added by default to the seed, which is not the case anymore. This allows sharing caches between + apps or different environments. + +Form +---- + + * Deprecated `PropertyPathMapper` in favor of `DataMapper` and `PropertyPathAccessor`. + + Before: + + ```php + use Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper; + + $builder->setDataMapper(new PropertyPathMapper()); + ``` + + After: + + ```php + use Symfony\Component\Form\Extension\Core\DataAccessor\PropertyPathAccessor; + use Symfony\Component\Form\Extension\Core\DataMapper\DataMapper; + + $builder->setDataMapper(new DataMapper(new PropertyPathAccessor())); + ``` + +Lock +---- + + * `MongoDbStore` does not implement `BlockingStoreInterface` anymore, typehint against `PersistingStoreInterface` instead. + * deprecated `NotSupportedException`, it shouldn't be thrown anymore. + * deprecated `RetryTillSaveStore`, logic has been moved in `Lock` and is not needed anymore. + +Mime +---- + + * Deprecated `Address::fromString()`, use `Address::create()` instead + +Monolog +------- + + * The `$actionLevel` constructor argument of `Symfony\Bridge\Monolog\Handler\FingersCrossed\NotFoundActivationStrategy` has been deprecated and replaced by the `$inner` one which expects an ActivationStrategyInterface to decorate instead. `Symfony\Bridge\Monolog\Handler\FingersCrossed\NotFoundActivationStrategy` will become final in 6.0. + * The `$actionLevel` constructor argument of `Symfony\Bridge\Monolog\Handler\FingersCrossed\HttpCodeActivationStrategy` has been deprecated and replaced by the `$inner` one which expects an ActivationStrategyInterface to decorate instead. `Symfony\Bridge\Monolog\Handler\FingersCrossed\HttpCodeActivationStrategy` will become final in 6.0 + +PropertyAccess +-------------- + + * Deprecated passing a boolean as the first argument of `PropertyAccessor::__construct()`. + Pass a combination of bitwise flags instead. + +PropertyInfo +------------ + + * Deprecated the `enable_magic_call_extraction` context option in `ReflectionExtractor::getWriteInfo()` and `ReflectionExtractor::getReadInfo()` in favor of `enable_magic_methods_extraction`. + +TwigBundle +---------- + + * Deprecated the public `twig` service to private. + +TwigBridge +---------- + + * Changed 2nd argument type of `TranslationExtension::__construct()` to `TranslationNodeVisitor` + +Validator +--------- + + * Deprecated the `allowEmptyString` option of the `Length` constraint. + + Before: + + ```php + use Symfony\Component\Validator\Constraints as Assert; + + /** + * @Assert\Length(min=5, allowEmptyString=true) + */ + ``` + + After: + + ```php + use Symfony\Component\Validator\Constraints as Assert; + + /** + * @Assert\AtLeastOneOf({ + * @Assert\Blank(), + * @Assert\Length(min=5) + * }) + */ + ``` + + * Deprecated the `NumberConstraintTrait` trait. + +Security +-------- + + * [BC break] In the experimental authenticator-based system, * `TokenInterface::getUser()` + returns `null` in case of unauthenticated session. + + * [BC break] `AccessListener::PUBLIC_ACCESS` has been removed in favor of + `AuthenticatedVoter::PUBLIC_ACCESS`. + + * Deprecated `setProviderKey()`/`getProviderKey()` in favor of `setFirewallName()/getFirewallName()` + in `PreAuthenticatedToken`, `RememberMeToken`, `SwitchUserToken`, `UsernamePasswordToken`, + `DefaultAuthenticationSuccessHandler`, the old methods will be removed in 6.0. + + * Deprecated the `AbstractRememberMeServices::$providerKey` property in favor of + `AbstractRememberMeServices::$firewallName`, the old property will be removed + in 6.0. + diff --git a/UPGRADE-6.0.md b/UPGRADE-6.0.md index 1d8243eff7d8a..69db8ccddbb48 100644 --- a/UPGRADE-6.0.md +++ b/UPGRADE-6.0.md @@ -26,6 +26,7 @@ DependencyInjection * Removed `Alias::getDeprecationMessage()`, use `Alias::getDeprecation()` instead. * The `inline()` function from the PHP-DSL has been removed, use `inline_service()` instead. * The `ref()` function from the PHP-DSL has been removed, use `service()` instead. + * Removed `Definition::setPrivate()` and `Alias::setPrivate()`, use `setPublic()` instead Dotenv ------ @@ -47,6 +48,7 @@ Form * Added argument `callable|null $filter` to `ChoiceListFactoryInterface::createListFromChoices()` and `createListFromLoader()`. * The `Symfony\Component\Form\Extension\Validator\Util\ServerParams` class has been removed, use its parent `Symfony\Component\Form\Util\ServerParams` instead. * The `NumberToLocalizedStringTransformer::ROUND_*` constants have been removed, use `\NumberFormatter::ROUND_*` instead. + * Removed `PropertyPathMapper` in favor of `DataMapper` and `PropertyPathAccessor`. FrameworkBundle --------------- @@ -54,6 +56,8 @@ FrameworkBundle * `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`, + `cache_clearer`, `filesystem` and `validator` services are now private. HttpFoundation -------------- @@ -73,6 +77,12 @@ Inflector * The component has been removed, use `EnglishInflector` from the String component instead. +Lock +---- + + * Removed the `NotSupportedException`. It shouldn't be thrown anymore. + * Removed the `RetryTillSaveStore`. Logic has been moved in `Lock` and is not needed anymore. + Mailer ------ @@ -89,6 +99,17 @@ Messenger * The signature of method `RetryStrategyInterface::isRetryable()` has been updated to `RetryStrategyInterface::isRetryable(Envelope $message, \Throwable $throwable = null)`. * The signature of method `RetryStrategyInterface::getWaitingTime()` has been updated to `RetryStrategyInterface::getWaitingTime(Envelope $message, \Throwable $throwable = null)`. +Mime +---- + + * Removed `Address::fromString()`, use `Address::create()` instead + +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. + OptionsResolver --------------- @@ -100,6 +121,17 @@ PhpUnitBridge * Removed support for `@expectedDeprecation` annotations, use the `ExpectDeprecationTrait::expectDeprecation()` method instead. +PropertyAccess +-------------- + + * Dropped support of a boolean as the first argument of `PropertyAccessor::__construct()`. + Pass a combination of bitwise flags instead. + +PropertyInfo +------------ + + * Dropped the `enable_magic_call_extraction` context option in `ReflectionExtractor::getWriteInfo()` and `ReflectionExtractor::getReadInfo()` in favor of `enable_magic_methods_extraction`. + Routing ------- @@ -114,6 +146,45 @@ Security * Removed `LogoutSuccessHandlerInterface` and `LogoutHandlerInterface`, register a listener on the `LogoutEvent` event instead. * Removed `DefaultLogoutSuccessHandler` in favor of `DefaultLogoutListener`. * Added a `logout(Request $request, Response $response, TokenInterface $token)` method to the `RememberMeServicesInterface`. + * Removed `setProviderKey()`/`getProviderKey()` in favor of `setFirewallName()/getFirewallName()` + in `PreAuthenticatedToken`, `RememberMeToken`, `SwitchUserToken`, `UsernamePasswordToken`, + `DefaultAuthenticationSuccessHandler`. + * Removed the `AbstractRememberMeServices::$providerKey` property in favor of `AbstractRememberMeServices::$firewallName` + +TwigBundle +---------- + + * The `twig` service is now private. + +Validator +--------- + + * Removed the `allowEmptyString` option from the `Length` constraint. + + Before: + + ```php + use Symfony\Component\Validator\Constraints as Assert; + + /** + * @Assert\Length(min=5, allowEmptyString=true) + */ + ``` + + After: + + ```php + use Symfony\Component\Validator\Constraints as Assert; + + /** + * @Assert\AtLeastOneOf({ + * @Assert\Blank(), + * @Assert\Length(min=5) + * }) + */ + ``` + + * Removed the `NumberConstraintTrait` trait. Yaml ---- diff --git a/composer.json b/composer.json index 0dfefb3475e7a..ee192a5428f89 100644 --- a/composer.json +++ b/composer.json @@ -78,12 +78,14 @@ "symfony/property-access": "self.version", "symfony/property-info": "self.version", "symfony/proxy-manager-bridge": "self.version", + "symfony/rate-limiter": "self.version", "symfony/routing": "self.version", + "symfony/security-bundle": "self.version", "symfony/security-core": "self.version", "symfony/security-csrf": "self.version", "symfony/security-guard": "self.version", "symfony/security-http": "self.version", - "symfony/security-bundle": "self.version", + "symfony/semaphore": "self.version", "symfony/sendgrid-mailer": "self.version", "symfony/serializer": "self.version", "symfony/stopwatch": "self.version", @@ -102,7 +104,8 @@ "symfony/yaml": "self.version" }, "require-dev": { - "amphp/http-client": "^4.2", + "amphp/amp": "^2.5", + "amphp/http-client": "^4.2.1", "amphp/http-tunnel": "^1.0", "async-aws/ses": "^1.0", "async-aws/sqs": "^1.0", @@ -112,7 +115,7 @@ "doctrine/cache": "~1.6", "doctrine/collections": "~1.0", "doctrine/data-fixtures": "^1.1", - "doctrine/dbal": "~2.4|^3.0", + "doctrine/dbal": "^2.10|^3.0", "doctrine/orm": "~2.4,>=2.4.5", "doctrine/reflection": "~1.0", "doctrine/doctrine-bundle": "^2.0", @@ -122,6 +125,7 @@ "nyholm/psr7": "^1.0", "ocramius/proxy-manager": "^2.1", "paragonie/sodium_compat": "^1.8", + "pda/pheanstalk": "^4.0", "php-http/httplug": "^1.0|^2.0", "predis/predis": "~1.1", "psr/http-client": "^1.0", @@ -135,6 +139,7 @@ "twig/markdown-extra": "^2.12" }, "conflict": { + "doctrine/dbal": "<2.10", "masterminds/html5": "<2.6", "phpdocumentor/reflection-docblock": "<3.2.2", "phpdocumentor/type-resolver": "<0.3.0", @@ -172,7 +177,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "5.1-dev" + "dev-master": "5.2-dev" } } } diff --git a/link b/link index c48c64c49d289..639006e2ca43a 100755 --- a/link +++ b/link @@ -47,7 +47,6 @@ $directories = array_merge(...array_values(array_map(function ($part) { }, $braces))); $directories[] = __DIR__.'/src/Symfony/Contracts'; - foreach ($directories as $dir) { if ($filesystem->exists($composer = "$dir/composer.json")) { $sfPackages[json_decode(file_get_contents($composer))->name] = $dir; diff --git a/src/Symfony/Bridge/Doctrine/CHANGELOG.md b/src/Symfony/Bridge/Doctrine/CHANGELOG.md index 78ebd8b4b9e77..574db63d5ec07 100644 --- a/src/Symfony/Bridge/Doctrine/CHANGELOG.md +++ b/src/Symfony/Bridge/Doctrine/CHANGELOG.md @@ -1,6 +1,12 @@ CHANGELOG ========= +5.2.0 +----- + + * added support for symfony/uid as `UlidType`, `UuidType`, `UlidBinaryType` and `UuidBinaryType` as Doctrine types + * added `UlidGenerator`, `UUidV1Generator`, `UuidV4Generator` and `UuidV6Generator` + 5.0.0 ----- diff --git a/src/Symfony/Bridge/Doctrine/DependencyInjection/AbstractDoctrineExtension.php b/src/Symfony/Bridge/Doctrine/DependencyInjection/AbstractDoctrineExtension.php index ac4c5f68d4d1f..211992d149912 100644 --- a/src/Symfony/Bridge/Doctrine/DependencyInjection/AbstractDoctrineExtension.php +++ b/src/Symfony/Bridge/Doctrine/DependencyInjection/AbstractDoctrineExtension.php @@ -296,7 +296,6 @@ protected function loadCacheDriver(string $cacheName, string $objectManagerName, $memcachedPort = !empty($cacheDriver['port']) ? $cacheDriver['port'] : '%'.$this->getObjectManagerElementName('cache.memcached_port').'%'; $cacheDef = new Definition($memcachedClass); $memcachedInstance = new Definition($memcachedInstanceClass); - $memcachedInstance->setPrivate(true); $memcachedInstance->addMethodCall('addServer', [ $memcachedHost, $memcachedPort, ]); @@ -310,7 +309,6 @@ protected function loadCacheDriver(string $cacheName, string $objectManagerName, $redisPort = !empty($cacheDriver['port']) ? $cacheDriver['port'] : '%'.$this->getObjectManagerElementName('cache.redis_port').'%'; $cacheDef = new Definition($redisClass); $redisInstance = new Definition($redisInstanceClass); - $redisInstance->setPrivate(true); $redisInstance->addMethodCall('connect', [ $redisHost, $redisPort, ]); @@ -334,11 +332,12 @@ protected function loadCacheDriver(string $cacheName, string $objectManagerName, if (!isset($cacheDriver['namespace'])) { // generate a unique namespace for the given application if ($container->hasParameter('cache.prefix.seed')) { - $seed = '.'.$container->getParameterBag()->resolveValue($container->getParameter('cache.prefix.seed')); + $seed = $container->getParameterBag()->resolveValue($container->getParameter('cache.prefix.seed')); } else { $seed = '_'.$container->getParameter('kernel.project_dir'); + $seed .= '.'.$container->getParameter('kernel.container_class'); } - $seed .= '.'.$container->getParameter('kernel.container_class'); + $namespace = 'sf_'.$this->getMappingResourceExtension().'_'.$objectManagerName.'_'.ContainerBuilder::hash($seed); $cacheDriver['namespace'] = $namespace; diff --git a/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/RegisterUidTypePass.php b/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/RegisterUidTypePass.php new file mode 100644 index 0000000000000..f5fea85d14e9c --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/RegisterUidTypePass.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\DependencyInjection\CompilerPass; + +use Symfony\Bridge\Doctrine\Types\UlidBinaryType; +use Symfony\Bridge\Doctrine\Types\UlidType; +use Symfony\Bridge\Doctrine\Types\UuidBinaryType; +use Symfony\Bridge\Doctrine\Types\UuidType; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\Uid\AbstractUid; + +final class RegisterUidTypePass implements CompilerPassInterface +{ + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) + { + if (!class_exists(AbstractUid::class)) { + return; + } + + $typeDefinition = $container->getParameter('doctrine.dbal.connection_factory.types'); + + if (!isset($typeDefinition['uuid'])) { + $typeDefinition['uuid'] = ['class' => UuidType::class]; + } + + if (!isset($typeDefinition['ulid'])) { + $typeDefinition['ulid'] = ['class' => UlidType::class]; + } + + if (!isset($typeDefinition['uuid_binary'])) { + $typeDefinition['uuid_binary'] = ['class' => UuidBinaryType::class]; + } + + if (!isset($typeDefinition['ulid_binary'])) { + $typeDefinition['ulid_binary'] = ['class' => UlidBinaryType::class]; + } + + $container->setParameter('doctrine.dbal.connection_factory.types', $typeDefinition); + } +} diff --git a/src/Symfony/Bridge/Doctrine/DependencyInjection/Security/UserProvider/EntityFactory.php b/src/Symfony/Bridge/Doctrine/DependencyInjection/Security/UserProvider/EntityFactory.php index 454c7cc0b9222..aae6a8643868a 100644 --- a/src/Symfony/Bridge/Doctrine/DependencyInjection/Security/UserProvider/EntityFactory.php +++ b/src/Symfony/Bridge/Doctrine/DependencyInjection/Security/UserProvider/EntityFactory.php @@ -52,7 +52,11 @@ public function addConfiguration(NodeDefinition $node) { $node ->children() - ->scalarNode('class')->isRequired()->cannotBeEmpty()->end() + ->scalarNode('class') + ->isRequired() + ->info('The full entity class name of your user class.') + ->cannotBeEmpty() + ->end() ->scalarNode('property')->defaultNull()->end() ->scalarNode('manager_name')->defaultNull()->end() ->end() diff --git a/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php b/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php index ca75617326331..fd8b013185a69 100644 --- a/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php +++ b/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php @@ -11,7 +11,6 @@ namespace Symfony\Bridge\Doctrine\Form; -use Doctrine\DBAL\Types\Type; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping\ClassMetadataInfo; use Doctrine\ORM\Mapping\MappingException as LegacyMappingException; @@ -29,15 +28,9 @@ class DoctrineOrmTypeGuesser implements FormTypeGuesserInterface private $cache = []; - private static $useDeprecatedConstants; - public function __construct(ManagerRegistry $registry) { $this->registry = $registry; - - if (null === self::$useDeprecatedConstants) { - self::$useDeprecatedConstants = !class_exists(Types::class); - } } /** @@ -59,44 +52,39 @@ public function guessType(string $class, string $property) } switch ($metadata->getTypeOfField($property)) { - case self::$useDeprecatedConstants ? Type::TARRAY : Types::ARRAY: - // no break - case self::$useDeprecatedConstants ? Type::SIMPLE_ARRAY : Types::SIMPLE_ARRAY: + case Types::ARRAY: + case Types::SIMPLE_ARRAY: return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\CollectionType', [], Guess::MEDIUM_CONFIDENCE); - case self::$useDeprecatedConstants ? Type::BOOLEAN : Types::BOOLEAN: + case Types::BOOLEAN: return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\CheckboxType', [], Guess::HIGH_CONFIDENCE); - case self::$useDeprecatedConstants ? Type::DATETIME : Types::DATETIME_MUTABLE: - // no break - case self::$useDeprecatedConstants ? Type::DATETIMETZ : Types::DATETIMETZ_MUTABLE: - // no break + case Types::DATETIME_MUTABLE: + case Types::DATETIMETZ_MUTABLE: case 'vardatetime': return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\DateTimeType', [], Guess::HIGH_CONFIDENCE); - case 'datetime_immutable': - case 'datetimetz_immutable': + case Types::DATE_IMMUTABLE: + case Types::DATETIMETZ_IMMUTABLE: return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\DateTimeType', ['input' => 'datetime_immutable'], Guess::HIGH_CONFIDENCE); - case 'dateinterval': + case Types::DATEINTERVAL: return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\DateIntervalType', [], Guess::HIGH_CONFIDENCE); - case self::$useDeprecatedConstants ? Type::DATE : Types::DATE_MUTABLE: + case Types::DATE_MUTABLE: return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\DateType', [], Guess::HIGH_CONFIDENCE); - case 'date_immutable': + case Types::DATE_IMMUTABLE: return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\DateType', ['input' => 'datetime_immutable'], Guess::HIGH_CONFIDENCE); - case self::$useDeprecatedConstants ? Type::TIME : Types::TIME_MUTABLE: + case Types::TIME_MUTABLE: return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\TimeType', [], Guess::HIGH_CONFIDENCE); - case 'time_immutable': + case Types::TIME_IMMUTABLE: return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\TimeType', ['input' => 'datetime_immutable'], Guess::HIGH_CONFIDENCE); - case self::$useDeprecatedConstants ? Type::DECIMAL : Types::DECIMAL: + case Types::DECIMAL: return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\NumberType', ['input' => 'string'], Guess::MEDIUM_CONFIDENCE); - case self::$useDeprecatedConstants ? Type::FLOAT : Types::FLOAT: + case Types::FLOAT: return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\NumberType', [], Guess::MEDIUM_CONFIDENCE); - case self::$useDeprecatedConstants ? Type::INTEGER : Types::INTEGER: - // no break - case self::$useDeprecatedConstants ? Type::BIGINT : Types::BIGINT: - // no break - case self::$useDeprecatedConstants ? Type::SMALLINT : Types::SMALLINT: + case Types::INTEGER: + case Types::BIGINT: + case Types::SMALLINT: return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\IntegerType', [], Guess::MEDIUM_CONFIDENCE); - case self::$useDeprecatedConstants ? Type::STRING : Types::STRING: + case Types::STRING: return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\TextType', [], Guess::MEDIUM_CONFIDENCE); - case self::$useDeprecatedConstants ? Type::TEXT : Types::TEXT: + case Types::TEXT: return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\TextareaType', [], Guess::MEDIUM_CONFIDENCE); default: return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\TextType', [], Guess::LOW_CONFIDENCE); @@ -119,7 +107,7 @@ public function guessRequired(string $class, string $property) // Check whether the field exists and is nullable or not if (isset($classMetadata->fieldMappings[$property])) { - if (!$classMetadata->isNullable($property) && (self::$useDeprecatedConstants ? Type::BOOLEAN : Types::BOOLEAN) !== $classMetadata->getTypeOfField($property)) { + if (!$classMetadata->isNullable($property) && Types::BOOLEAN !== $classMetadata->getTypeOfField($property)) { return new ValueGuess(true, Guess::HIGH_CONFIDENCE); } @@ -156,7 +144,7 @@ public function guessMaxLength(string $class, string $property) return new ValueGuess($mapping['length'], Guess::HIGH_CONFIDENCE); } - if (\in_array($ret[0]->getTypeOfField($property), self::$useDeprecatedConstants ? [Type::DECIMAL, Type::FLOAT] : [Types::DECIMAL, Types::FLOAT])) { + if (\in_array($ret[0]->getTypeOfField($property), [Types::DECIMAL, Types::FLOAT])) { return new ValueGuess(null, Guess::MEDIUM_CONFIDENCE); } } @@ -171,7 +159,7 @@ public function guessPattern(string $class, string $property) { $ret = $this->getMetadata($class); if ($ret && isset($ret[0]->fieldMappings[$property]) && !$ret[0]->hasAssociation($property)) { - if (\in_array($ret[0]->getTypeOfField($property), self::$useDeprecatedConstants ? [Type::DECIMAL, Type::FLOAT] : [Types::DECIMAL, Types::FLOAT])) { + if (\in_array($ret[0]->getTypeOfField($property), [Types::DECIMAL, Types::FLOAT])) { return new ValueGuess(null, Guess::MEDIUM_CONFIDENCE); } } diff --git a/src/Symfony/Bridge/Doctrine/Id/UlidGenerator.php b/src/Symfony/Bridge/Doctrine/Id/UlidGenerator.php new file mode 100644 index 0000000000000..61494b5f30811 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Id/UlidGenerator.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Types; + +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\Id\AbstractIdGenerator; +use Symfony\Component\Uid\Ulid; + +final class UlidGenerator extends AbstractIdGenerator +{ + public function generate(EntityManager $em, $entity): Ulid + { + return new Ulid(); + } +} diff --git a/src/Symfony/Bridge/Doctrine/Id/UuidV1Generator.php b/src/Symfony/Bridge/Doctrine/Id/UuidV1Generator.php new file mode 100644 index 0000000000000..739a10afd691b --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Id/UuidV1Generator.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Types; + +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\Id\AbstractIdGenerator; +use Symfony\Component\Uid\UuidV1; + +final class UuidV1Generator extends AbstractIdGenerator +{ + public function generate(EntityManager $em, $entity): UuidV1 + { + return new UuidV1(); + } +} diff --git a/src/Symfony/Bridge/Doctrine/Id/UuidV4Generator.php b/src/Symfony/Bridge/Doctrine/Id/UuidV4Generator.php new file mode 100644 index 0000000000000..10eef5db64169 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Id/UuidV4Generator.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Types; + +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\Id\AbstractIdGenerator; +use Symfony\Component\Uid\UuidV4; + +final class UuidV4Generator extends AbstractIdGenerator +{ + public function generate(EntityManager $em, $entity): UuidV4 + { + return new UuidV4(); + } +} diff --git a/src/Symfony/Bridge/Doctrine/Id/UuidV6Generator.php b/src/Symfony/Bridge/Doctrine/Id/UuidV6Generator.php new file mode 100644 index 0000000000000..66ea8d728cb97 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Id/UuidV6Generator.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Types; + +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\Id\AbstractIdGenerator; +use Symfony\Component\Uid\UuidV6; + +final class UuidV6Generator extends AbstractIdGenerator +{ + public function generate(EntityManager $em, $entity): UuidV6 + { + return new UuidV6(); + } +} diff --git a/src/Symfony/Bridge/Doctrine/PropertyInfo/DoctrineExtractor.php b/src/Symfony/Bridge/Doctrine/PropertyInfo/DoctrineExtractor.php index 1829cdbcdb747..4f74c2f9492a2 100644 --- a/src/Symfony/Bridge/Doctrine/PropertyInfo/DoctrineExtractor.php +++ b/src/Symfony/Bridge/Doctrine/PropertyInfo/DoctrineExtractor.php @@ -11,7 +11,6 @@ namespace Symfony\Bridge\Doctrine\PropertyInfo; -use Doctrine\DBAL\Types\Type as DBALType; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\ClassMetadata; @@ -32,15 +31,10 @@ class DoctrineExtractor implements PropertyListExtractorInterface, PropertyTypeE { private $entityManager; private $classMetadataFactory; - private static $useDeprecatedConstants; public function __construct(EntityManagerInterface $entityManager) { $this->entityManager = $entityManager; - - if (null === self::$useDeprecatedConstants) { - self::$useDeprecatedConstants = !class_exists(Types::class); - } } /** @@ -141,35 +135,31 @@ public function getTypes(string $class, string $property, array $context = []) switch ($builtinType) { case Type::BUILTIN_TYPE_OBJECT: switch ($typeOfField) { - case self::$useDeprecatedConstants ? DBALType::DATE : Types::DATE_MUTABLE: - // no break - case self::$useDeprecatedConstants ? DBALType::DATETIME : Types::DATETIME_MUTABLE: - // no break - case self::$useDeprecatedConstants ? DBALType::DATETIMETZ : Types::DATETIMETZ_MUTABLE: - // no break + case Types::DATE_MUTABLE: + case Types::DATETIME_MUTABLE: + case Types::DATETIMETZ_MUTABLE: case 'vardatetime': - case self::$useDeprecatedConstants ? DBALType::TIME : Types::TIME_MUTABLE: + case Types::TIME_MUTABLE: return [new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, 'DateTime')]; - case 'date_immutable': - case 'datetime_immutable': - case 'datetimetz_immutable': - case 'time_immutable': + case Types::DATE_IMMUTABLE: + case Types::DATETIME_IMMUTABLE: + case Types::DATETIMETZ_IMMUTABLE: + case Types::TIME_IMMUTABLE: return [new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, 'DateTimeImmutable')]; - case 'dateinterval': + case Types::DATEINTERVAL: return [new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, 'DateInterval')]; } break; case Type::BUILTIN_TYPE_ARRAY: switch ($typeOfField) { - case self::$useDeprecatedConstants ? DBALType::TARRAY : Types::ARRAY: - // no break - case 'json_array': + case Types::ARRAY: + case Types::JSON_ARRAY: return [new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true)]; - case self::$useDeprecatedConstants ? DBALType::SIMPLE_ARRAY : Types::SIMPLE_ARRAY: + case Types::SIMPLE_ARRAY: return [new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING))]; } } @@ -244,56 +234,43 @@ private function isAssociationNullable(array $associationMapping): bool private function getPhpType(string $doctrineType): ?string { switch ($doctrineType) { - case self::$useDeprecatedConstants ? DBALType::SMALLINT : Types::SMALLINT: - // no break - case self::$useDeprecatedConstants ? DBALType::INTEGER : Types::INTEGER: + case Types::SMALLINT: + case Types::INTEGER: return Type::BUILTIN_TYPE_INT; - case self::$useDeprecatedConstants ? DBALType::FLOAT : Types::FLOAT: + case Types::FLOAT: return Type::BUILTIN_TYPE_FLOAT; - case self::$useDeprecatedConstants ? DBALType::BIGINT : Types::BIGINT: - // no break - case self::$useDeprecatedConstants ? DBALType::STRING : Types::STRING: - // no break - case self::$useDeprecatedConstants ? DBALType::TEXT : Types::TEXT: - // no break - case self::$useDeprecatedConstants ? DBALType::GUID : Types::GUID: - // no break - case self::$useDeprecatedConstants ? DBALType::DECIMAL : Types::DECIMAL: + case Types::BIGINT: + case Types::STRING: + case Types::TEXT: + case Types::GUID: + case Types::DECIMAL: return Type::BUILTIN_TYPE_STRING; - case self::$useDeprecatedConstants ? DBALType::BOOLEAN : Types::BOOLEAN: + case Types::BOOLEAN: return Type::BUILTIN_TYPE_BOOL; - case self::$useDeprecatedConstants ? DBALType::BLOB : Types::BLOB: - // no break - case 'binary': + case Types::BLOB: + case Types::BINARY: return Type::BUILTIN_TYPE_RESOURCE; - case self::$useDeprecatedConstants ? DBALType::OBJECT : Types::OBJECT: - // no break - case self::$useDeprecatedConstants ? DBALType::DATE : Types::DATE_MUTABLE: - // no break - case self::$useDeprecatedConstants ? DBALType::DATETIME : Types::DATETIME_MUTABLE: - // no break - case self::$useDeprecatedConstants ? DBALType::DATETIMETZ : Types::DATETIMETZ_MUTABLE: - // no break + case Types::OBJECT: + case Types::DATE_MUTABLE: + case Types::DATETIME_MUTABLE: + case Types::DATETIMETZ_MUTABLE: case 'vardatetime': - case self::$useDeprecatedConstants ? DBALType::TIME : Types::TIME_MUTABLE: - // no break - case 'date_immutable': - case 'datetime_immutable': - case 'datetimetz_immutable': - case 'time_immutable': - case 'dateinterval': + case Types::TIME_MUTABLE: + case Types::DATE_IMMUTABLE: + case Types::DATETIME_IMMUTABLE: + case Types::DATETIMETZ_IMMUTABLE: + case Types::TIME_IMMUTABLE: + case Types::DATEINTERVAL: return Type::BUILTIN_TYPE_OBJECT; - case self::$useDeprecatedConstants ? DBALType::TARRAY : Types::ARRAY: - // no break - case self::$useDeprecatedConstants ? DBALType::SIMPLE_ARRAY : Types::SIMPLE_ARRAY: - // no break - case 'json_array': + case Types::ARRAY: + case Types::SIMPLE_ARRAY: + case Types::JSON_ARRAY: return Type::BUILTIN_TYPE_ARRAY; } diff --git a/src/Symfony/Bridge/Doctrine/Security/RememberMe/DoctrineTokenProvider.php b/src/Symfony/Bridge/Doctrine/Security/RememberMe/DoctrineTokenProvider.php index 2f5faa6d0072e..4116a6c9c6cb8 100644 --- a/src/Symfony/Bridge/Doctrine/Security/RememberMe/DoctrineTokenProvider.php +++ b/src/Symfony/Bridge/Doctrine/Security/RememberMe/DoctrineTokenProvider.php @@ -14,7 +14,6 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\Driver\Result as DriverResult; use Doctrine\DBAL\Result; -use Doctrine\DBAL\Types\Type; use Doctrine\DBAL\Types\Types; use Symfony\Component\Security\Core\Authentication\RememberMe\PersistentToken; use Symfony\Component\Security\Core\Authentication\RememberMe\PersistentTokenInterface; @@ -43,15 +42,9 @@ class DoctrineTokenProvider implements TokenProviderInterface { private $conn; - private static $useDeprecatedConstants; - public function __construct(Connection $conn) { $this->conn = $conn; - - if (null === self::$useDeprecatedConstants) { - self::$useDeprecatedConstants = !class_exists(Types::class); - } } /** @@ -103,7 +96,7 @@ public function updateToken(string $series, string $tokenValue, \DateTime $lastU ]; $paramTypes = [ 'value' => \PDO::PARAM_STR, - 'lastUsed' => self::$useDeprecatedConstants ? Type::DATETIME : Types::DATETIME_MUTABLE, + 'lastUsed' => Types::DATETIME_MUTABLE, 'series' => \PDO::PARAM_STR, ]; if (method_exists($this->conn, 'executeStatement')) { @@ -136,7 +129,7 @@ public function createNewToken(PersistentTokenInterface $token) 'username' => \PDO::PARAM_STR, 'series' => \PDO::PARAM_STR, 'value' => \PDO::PARAM_STR, - 'lastUsed' => self::$useDeprecatedConstants ? Type::DATETIME : Types::DATETIME_MUTABLE, + 'lastUsed' => Types::DATETIME_MUTABLE, ]; if (method_exists($this->conn, 'executeStatement')) { $this->conn->executeStatement($sql, $paramValues, $paramTypes); diff --git a/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/DoctrineExtensionTest.php b/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/DoctrineExtensionTest.php index 3a7fc18058990..4106683661c2f 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/DoctrineExtensionTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/DoctrineExtensionTest.php @@ -263,6 +263,7 @@ protected function createContainer(array $data = []): ContainerBuilder return new ContainerBuilder(new ParameterBag(array_merge([ 'kernel.bundles' => ['FrameworkBundle' => 'Symfony\\Bundle\\FrameworkBundle\\FrameworkBundle'], 'kernel.cache_dir' => __DIR__, + 'kernel.build_dir' => __DIR__, 'kernel.container_class' => 'kernel', 'kernel.project_dir' => __DIR__, ], $data))); diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/DoctrineLoaderEntity.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/DoctrineLoaderEntity.php index 8c0b348e3bf3a..d6aee2d18b0b1 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/DoctrineLoaderEntity.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/DoctrineLoaderEntity.php @@ -36,13 +36,13 @@ class DoctrineLoaderEntity extends DoctrineLoaderParentEntity /** * @ORM\Column(length=20) - * @Assert\Length(min=5, allowEmptyString=true) + * @Assert\Length(min=5) */ public $mergedMaxLength; /** * @ORM\Column(length=20) - * @Assert\Length(min=1, max=10, allowEmptyString=true) + * @Assert\Length(min=1, max=10) */ public $alreadyMappedMaxLength; diff --git a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php index 4ea6a678e3eba..38ede592d9fe8 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php @@ -13,12 +13,11 @@ use Doctrine\Common\Collections\Collection; use Doctrine\DBAL\Types\Type as DBALType; -use Doctrine\DBAL\Types\Types; use Doctrine\ORM\EntityManager; use Doctrine\ORM\Tools\Setup; use PHPUnit\Framework\TestCase; use Symfony\Bridge\Doctrine\PropertyInfo\DoctrineExtractor; -use Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineDummy210; +use Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineDummy; use Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineGeneratedValue; use Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineRelation; use Symfony\Component\PropertyInfo\Type; @@ -58,12 +57,9 @@ public function testGetProperties() 'binary', 'customFoo', 'bigint', + 'json', ]; - if (class_exists(Types::class)) { - $expected[] = 'json'; - } - // Associations $expected = array_merge($expected, [ 'foo', @@ -76,7 +72,7 @@ public function testGetProperties() $this->assertEquals( $expected, - $this->createExtractor()->getProperties(!class_exists(Types::class) ? 'Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineDummy' : DoctrineDummy210::class) + $this->createExtractor()->getProperties(DoctrineDummy::class) ); } @@ -100,7 +96,7 @@ public function testTestGetPropertiesWithEmbedded() */ public function testExtract($property, array $type = null) { - $this->assertEquals($type, $this->createExtractor()->getTypes(!class_exists(Types::class) ? 'Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineDummy' : DoctrineDummy210::class, $property, [])); + $this->assertEquals($type, $this->createExtractor()->getTypes(DoctrineDummy::class, $property, [])); } public function testExtractWithEmbedded() @@ -175,12 +171,9 @@ public function typesProvider() new Type(Type::BUILTIN_TYPE_OBJECT, false, DoctrineRelation::class) )]], ['indexedByCustomType', null], + ['json', null], ]; - if (class_exists(Types::class)) { - $provider[] = ['json', null]; - } - return $provider; } diff --git a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineDummy.php b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineDummy.php index 81264fad27c5f..bd5af9dd48a0b 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineDummy.php +++ b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineDummy.php @@ -122,4 +122,9 @@ class DoctrineDummy * @OneToMany(targetEntity="DoctrineRelation", mappedBy="customType", indexBy="customType") */ private $indexedByCustomType; + + /** + * @Column(type="json", nullable=true) + */ + private $json; } diff --git a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineDummy210.php b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineDummy210.php deleted file mode 100644 index d3916143deab7..0000000000000 --- a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineDummy210.php +++ /dev/null @@ -1,30 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures; - -use Doctrine\ORM\Mapping\Column; -use Doctrine\ORM\Mapping\Entity; -use Doctrine\ORM\Mapping\Id; -use Doctrine\ORM\Mapping\ManyToMany; -use Doctrine\ORM\Mapping\ManyToOne; -use Doctrine\ORM\Mapping\OneToMany; - -/** - * @Entity - */ -final class DoctrineDummy210 extends DoctrineDummy -{ - /** - * @Column(type="json", nullable=true) - */ - private $json; -} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Resources/validator/BaseUser.xml b/src/Symfony/Bridge/Doctrine/Tests/Resources/validator/BaseUser.xml index 40b7a138d437b..bf64b92ca484d 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Resources/validator/BaseUser.xml +++ b/src/Symfony/Bridge/Doctrine/Tests/Resources/validator/BaseUser.xml @@ -13,7 +13,6 @@ - diff --git a/src/Symfony/Bridge/Doctrine/Tests/Types/UlidBinaryTypeTest.php b/src/Symfony/Bridge/Doctrine/Tests/Types/UlidBinaryTypeTest.php new file mode 100644 index 0000000000000..fce1aa6100953 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Types/UlidBinaryTypeTest.php @@ -0,0 +1,119 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\Types; + +use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\DBAL\Types\ConversionException; +use Doctrine\DBAL\Types\Type; +use PHPUnit\Framework\TestCase; +use Symfony\Bridge\Doctrine\Types\UlidBinaryType; +use Symfony\Component\Uid\Ulid; + +class UlidBinaryTypeTest extends TestCase +{ + private const DUMMY_ULID = '01EEDQEK6ZAZE93J8KG5B4MBJC'; + + private $platform; + + /** @var UlidBinaryType */ + private $type; + + public static function setUpBeforeClass(): void + { + Type::addType('ulid_binary', UlidBinaryType::class); + } + + protected function setUp(): void + { + $this->platform = $this->createMock(AbstractPlatform::class); + $this->platform + ->method('getBinaryTypeDeclarationSQL') + ->willReturn('DUMMYBINARY(16)'); + + $this->type = Type::getType('ulid_binary'); + } + + public function testUlidConvertsToDatabaseValue() + { + $uuid = Ulid::fromString(self::DUMMY_ULID); + + $expected = $uuid->toBinary(); + $actual = $this->type->convertToDatabaseValue($uuid, $this->platform); + + $this->assertEquals($expected, $actual); + } + + public function testStringUlidConvertsToDatabaseValue() + { + $expected = Ulid::fromString(self::DUMMY_ULID)->toBinary(); + $actual = $this->type->convertToDatabaseValue(self::DUMMY_ULID, $this->platform); + + $this->assertEquals($expected, $actual); + } + + public function testInvalidUlidConversionForDatabaseValue() + { + $this->expectException(ConversionException::class); + + $this->type->convertToDatabaseValue('abcdefg', $this->platform); + } + + public function testNotSupportedTypeConversionForDatabaseValue() + { + $this->assertNull($this->type->convertToDatabaseValue(new \stdClass(), $this->platform)); + } + + public function testNullConversionForDatabaseValue() + { + $this->assertNull($this->type->convertToDatabaseValue(null, $this->platform)); + } + + public function testUlidConvertsToPHPValue() + { + $uuid = $this->type->convertToPHPValue(self::DUMMY_ULID, $this->platform); + + $this->assertEquals(self::DUMMY_ULID, $uuid->__toString()); + } + + public function testInvalidUlidConversionForPHPValue() + { + $this->expectException(ConversionException::class); + + $this->type->convertToPHPValue('abcdefg', $this->platform); + } + + public function testNullConversionForPHPValue() + { + $this->assertNull($this->type->convertToPHPValue(null, $this->platform)); + } + + public function testReturnValueIfUlidForPHPValue() + { + $uuid = new Ulid(); + $this->assertSame($uuid, $this->type->convertToPHPValue($uuid, $this->platform)); + } + + public function testGetName() + { + $this->assertEquals('ulid_binary', $this->type->getName()); + } + + public function testGetGuidTypeDeclarationSQL() + { + $this->assertEquals('DUMMYBINARY(16)', $this->type->getSqlDeclaration(['length' => 36], $this->platform)); + } + + public function testRequiresSQLCommentHint() + { + $this->assertTrue($this->type->requiresSQLCommentHint($this->platform)); + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Types/UlidTypeTest.php b/src/Symfony/Bridge/Doctrine/Tests/Types/UlidTypeTest.php new file mode 100644 index 0000000000000..638b178b3783e --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Types/UlidTypeTest.php @@ -0,0 +1,144 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\Types; + +use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\DBAL\Types\ConversionException; +use Doctrine\DBAL\Types\Type; +use PHPUnit\Framework\TestCase; +use Symfony\Bridge\Doctrine\Types\UlidType; +use Symfony\Component\Uid\AbstractUid; +use Symfony\Component\Uid\Ulid; + +final class UlidTypeTest extends TestCase +{ + private const DUMMY_ULID = '01EEDQEK6ZAZE93J8KG5B4MBJC'; + + /** @var AbstractPlatform */ + private $platform; + + /** @var UlidType */ + private $type; + + public static function setUpBeforeClass(): void + { + Type::addType('ulid', UlidType::class); + } + + protected function setUp(): void + { + $this->platform = $this->createMock(AbstractPlatform::class); + $this->platform + ->method('getGuidTypeDeclarationSQL') + ->willReturn('DUMMYVARCHAR()'); + + $this->type = Type::getType('ulid'); + } + + public function testUlidConvertsToDatabaseValue(): void + { + $ulid = Ulid::fromString(self::DUMMY_ULID); + + $expected = $ulid->__toString(); + $actual = $this->type->convertToDatabaseValue($ulid, $this->platform); + + $this->assertEquals($expected, $actual); + } + + public function testUlidInterfaceConvertsToDatabaseValue(): void + { + $ulid = $this->createMock(AbstractUid::class); + + $ulid + ->expects($this->once()) + ->method('__toString') + ->willReturn('foo'); + + $actual = $this->type->convertToDatabaseValue($ulid, $this->platform); + + $this->assertEquals('foo', $actual); + } + + public function testUlidStringConvertsToDatabaseValue(): void + { + $actual = $this->type->convertToDatabaseValue(self::DUMMY_ULID, $this->platform); + + $this->assertEquals(self::DUMMY_ULID, $actual); + } + + public function testInvalidUlidConversionForDatabaseValue(): void + { + $this->expectException(ConversionException::class); + + $this->type->convertToDatabaseValue('abcdefg', $this->platform); + } + + public function testNotSupportedTypeConversionForDatabaseValue() + { + $this->assertNull($this->type->convertToDatabaseValue(new \stdClass(), $this->platform)); + } + + public function testNullConversionForDatabaseValue(): void + { + $this->assertNull($this->type->convertToDatabaseValue(null, $this->platform)); + } + + public function testUlidInterfaceConvertsToPHPValue(): void + { + $ulid = $this->createMock(AbstractUid::class); + $actual = $this->type->convertToPHPValue($ulid, $this->platform); + + $this->assertSame($ulid, $actual); + } + + public function testUlidConvertsToPHPValue(): void + { + $ulid = $this->type->convertToPHPValue(self::DUMMY_ULID, $this->platform); + + $this->assertInstanceOf(Ulid::class, $ulid); + $this->assertEquals(self::DUMMY_ULID, $ulid->__toString()); + } + + public function testInvalidUlidConversionForPHPValue(): void + { + $this->expectException(ConversionException::class); + + $this->type->convertToPHPValue('abcdefg', $this->platform); + } + + public function testNullConversionForPHPValue(): void + { + $this->assertNull($this->type->convertToPHPValue(null, $this->platform)); + } + + public function testReturnValueIfUlidForPHPValue(): void + { + $ulid = new Ulid(); + + $this->assertSame($ulid, $this->type->convertToPHPValue($ulid, $this->platform)); + } + + public function testGetName(): void + { + $this->assertEquals('ulid', $this->type->getName()); + } + + public function testGetGuidTypeDeclarationSQL(): void + { + $this->assertEquals('DUMMYVARCHAR()', $this->type->getSqlDeclaration(['length' => 36], $this->platform)); + } + + public function testRequiresSQLCommentHint(): void + { + $this->assertTrue($this->type->requiresSQLCommentHint($this->platform)); + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Types/UuidBinaryTypeTest.php b/src/Symfony/Bridge/Doctrine/Tests/Types/UuidBinaryTypeTest.php new file mode 100644 index 0000000000000..9e68b6caed3a6 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Types/UuidBinaryTypeTest.php @@ -0,0 +1,121 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\Types; + +use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\DBAL\Types\ConversionException; +use Doctrine\DBAL\Types\Type; +use PHPUnit\Framework\TestCase; +use Symfony\Bridge\Doctrine\Types\UuidBinaryType; +use Symfony\Component\Uid\Uuid; + +class UuidBinaryTypeTest extends TestCase +{ + private const DUMMY_UUID = '9f755235-5a2d-4aba-9605-e9962b312e50'; + + private $platform; + + /** @var UuidBinaryType */ + private $type; + + public static function setUpBeforeClass(): void + { + Type::addType('uuid_binary', UuidBinaryType::class); + } + + protected function setUp(): void + { + $this->platform = $this->createMock(AbstractPlatform::class); + $this->platform + ->method('getBinaryTypeDeclarationSQL') + ->willReturn('DUMMYBINARY(16)'); + + $this->type = Type::getType('uuid_binary'); + } + + public function testUuidConvertsToDatabaseValue() + { + $uuid = Uuid::fromString(self::DUMMY_UUID); + + $expected = uuid_parse(self::DUMMY_UUID); + $actual = $this->type->convertToDatabaseValue($uuid, $this->platform); + + $this->assertEquals($expected, $actual); + } + + public function testStringUuidConvertsToDatabaseValue() + { + $uuid = self::DUMMY_UUID; + + $expected = uuid_parse(self::DUMMY_UUID); + $actual = $this->type->convertToDatabaseValue($uuid, $this->platform); + + $this->assertEquals($expected, $actual); + } + + public function testInvalidUuidConversionForDatabaseValue() + { + $this->expectException(ConversionException::class); + + $this->type->convertToDatabaseValue('abcdefg', $this->platform); + } + + public function testNullConversionForDatabaseValue() + { + $this->assertNull($this->type->convertToDatabaseValue(null, $this->platform)); + } + + public function testUuidConvertsToPHPValue() + { + $uuid = $this->type->convertToPHPValue(uuid_parse(self::DUMMY_UUID), $this->platform); + + $this->assertEquals(self::DUMMY_UUID, $uuid->__toString()); + } + + public function testInvalidUuidConversionForPHPValue() + { + $this->expectException(ConversionException::class); + + $this->type->convertToPHPValue('abcdefg', $this->platform); + } + + public function testNotSupportedTypeConversionForDatabaseValue() + { + $this->assertNull($this->type->convertToDatabaseValue(new \stdClass(), $this->platform)); + } + + public function testNullConversionForPHPValue() + { + $this->assertNull($this->type->convertToPHPValue(null, $this->platform)); + } + + public function testReturnValueIfUuidForPHPValue() + { + $uuid = Uuid::v4(); + $this->assertSame($uuid, $this->type->convertToPHPValue($uuid, $this->platform)); + } + + public function testGetName() + { + $this->assertEquals('uuid_binary', $this->type->getName()); + } + + public function testGetGuidTypeDeclarationSQL() + { + $this->assertEquals('DUMMYBINARY(16)', $this->type->getSqlDeclaration(['length' => 36], $this->platform)); + } + + public function testRequiresSQLCommentHint() + { + $this->assertTrue($this->type->requiresSQLCommentHint($this->platform)); + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Types/UuidTypeTest.php b/src/Symfony/Bridge/Doctrine/Tests/Types/UuidTypeTest.php new file mode 100644 index 0000000000000..4ce96fae32fac --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Types/UuidTypeTest.php @@ -0,0 +1,144 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\Types; + +use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\DBAL\Types\ConversionException; +use Doctrine\DBAL\Types\Type; +use PHPUnit\Framework\TestCase; +use Symfony\Bridge\Doctrine\Types\UuidType; +use Symfony\Component\Uid\AbstractUid; +use Symfony\Component\Uid\Uuid; + +final class UuidTypeTest extends TestCase +{ + private const DUMMY_UUID = '9f755235-5a2d-4aba-9605-e9962b312e50'; + + /** @var AbstractPlatform */ + private $platform; + + /** @var UuidType */ + private $type; + + public static function setUpBeforeClass(): void + { + Type::addType('uuid', UuidType::class); + } + + protected function setUp(): void + { + $this->platform = $this->createMock(AbstractPlatform::class); + $this->platform + ->method('getGuidTypeDeclarationSQL') + ->willReturn('DUMMYVARCHAR()'); + + $this->type = Type::getType('uuid'); + } + + public function testUuidConvertsToDatabaseValue(): void + { + $uuid = Uuid::fromString(self::DUMMY_UUID); + + $expected = $uuid->__toString(); + $actual = $this->type->convertToDatabaseValue($uuid, $this->platform); + + $this->assertEquals($expected, $actual); + } + + public function testUuidInterfaceConvertsToDatabaseValue(): void + { + $uuid = $this->createMock(AbstractUid::class); + + $uuid + ->expects($this->once()) + ->method('__toString') + ->willReturn('foo'); + + $actual = $this->type->convertToDatabaseValue($uuid, $this->platform); + + $this->assertEquals('foo', $actual); + } + + public function testUuidStringConvertsToDatabaseValue(): void + { + $actual = $this->type->convertToDatabaseValue(self::DUMMY_UUID, $this->platform); + + $this->assertEquals(self::DUMMY_UUID, $actual); + } + + public function testInvalidUuidConversionForDatabaseValue(): void + { + $this->expectException(ConversionException::class); + + $this->type->convertToDatabaseValue('abcdefg', $this->platform); + } + + public function testNotSupportedTypeConversionForDatabaseValue() + { + $this->assertNull($this->type->convertToDatabaseValue(new \stdClass(), $this->platform)); + } + + public function testNullConversionForDatabaseValue(): void + { + $this->assertNull($this->type->convertToDatabaseValue(null, $this->platform)); + } + + public function testUuidInterfaceConvertsToPHPValue(): void + { + $uuid = $this->createMock(AbstractUid::class); + $actual = $this->type->convertToPHPValue($uuid, $this->platform); + + $this->assertSame($uuid, $actual); + } + + public function testUuidConvertsToPHPValue(): void + { + $uuid = $this->type->convertToPHPValue(self::DUMMY_UUID, $this->platform); + + $this->assertInstanceOf(Uuid::class, $uuid); + $this->assertEquals(self::DUMMY_UUID, $uuid->__toString()); + } + + public function testInvalidUuidConversionForPHPValue(): void + { + $this->expectException(ConversionException::class); + + $this->type->convertToPHPValue('abcdefg', $this->platform); + } + + public function testNullConversionForPHPValue(): void + { + $this->assertNull($this->type->convertToPHPValue(null, $this->platform)); + } + + public function testReturnValueIfUuidForPHPValue(): void + { + $uuid = Uuid::v4(); + + $this->assertSame($uuid, $this->type->convertToPHPValue($uuid, $this->platform)); + } + + public function testGetName(): void + { + $this->assertEquals('uuid', $this->type->getName()); + } + + public function testGetGuidTypeDeclarationSQL(): void + { + $this->assertEquals('DUMMYVARCHAR()', $this->type->getSqlDeclaration(['length' => 36], $this->platform)); + } + + public function testRequiresSQLCommentHint(): void + { + $this->assertTrue($this->type->requiresSQLCommentHint($this->platform)); + } +} diff --git a/src/Symfony/Bridge/Doctrine/Types/AbstractBinaryUidType.php b/src/Symfony/Bridge/Doctrine/Types/AbstractBinaryUidType.php new file mode 100644 index 0000000000000..587f9a54f3f0c --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Types/AbstractBinaryUidType.php @@ -0,0 +1,90 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Types; + +use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\DBAL\Types\ConversionException; +use Doctrine\DBAL\Types\GuidType; +use Symfony\Component\Uid\AbstractUid; + +abstract class AbstractBinaryUidType extends GuidType +{ + abstract protected function getUidClass(): string; + + public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform): string + { + return $platform->getBinaryTypeDeclarationSQL( + [ + 'length' => '16', + 'fixed' => true, + ] + ); + } + + /** + * {@inheritdoc} + * + * @throws ConversionException + */ + public function convertToPHPValue($value, AbstractPlatform $platform): ?AbstractUid + { + if (null === $value || '' === $value) { + return null; + } + + if ($value instanceof AbstractUid) { + return $value; + } + + try { + $uuid = $this->getUidClass()::fromString($value); + } catch (\InvalidArgumentException $e) { + throw ConversionException::conversionFailed($value, $this->getName()); + } + + return $uuid; + } + + /** + * {@inheritdoc} + * + * @throws ConversionException + */ + public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string + { + if (null === $value || '' === $value) { + return null; + } + + if ($value instanceof AbstractUid) { + return $value->toBinary(); + } + + if (!\is_string($value) && !(\is_object($value) && method_exists($value, '__toString'))) { + return null; + } + + try { + return $this->getUidClass()::fromString((string) $value)->toBinary(); + } catch (\InvalidArgumentException $e) { + throw ConversionException::conversionFailed($value, $this->getName()); + } + } + + /** + * {@inheritdoc} + */ + public function requiresSQLCommentHint(AbstractPlatform $platform): bool + { + return true; + } +} diff --git a/src/Symfony/Bridge/Doctrine/Types/AbstractUidType.php b/src/Symfony/Bridge/Doctrine/Types/AbstractUidType.php new file mode 100644 index 0000000000000..b64ad584b8228 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Types/AbstractUidType.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Types; + +use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\DBAL\Types\ConversionException; +use Doctrine\DBAL\Types\GuidType; +use Symfony\Component\Uid\AbstractUid; + +abstract class AbstractUidType extends GuidType +{ + abstract protected function getUidClass(): string; + + /** + * {@inheritdoc} + * + * @throws ConversionException + */ + public function convertToPHPValue($value, AbstractPlatform $platform): ?AbstractUid + { + if (null === $value || '' === $value) { + return null; + } + + if ($value instanceof AbstractUid) { + return $value; + } + + try { + $uuid = $this->getUidClass()::fromString($value); + } catch (\InvalidArgumentException $e) { + throw ConversionException::conversionFailed($value, $this->getName()); + } + + return $uuid; + } + + /** + * {@inheritdoc} + * + * @throws ConversionException + */ + public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string + { + if (null === $value || '' === $value) { + return null; + } + + if ($value instanceof AbstractUid) { + return (string) $value; + } + + if (!\is_string($value) && !(\is_object($value) && method_exists($value, '__toString'))) { + return null; + } + + if ($this->getUidClass()::isValid((string) $value)) { + return (string) $value; + } + + throw ConversionException::conversionFailed($value, $this->getName()); + } + + /** + * {@inheritdoc} + */ + public function requiresSQLCommentHint(AbstractPlatform $platform): bool + { + return true; + } +} diff --git a/src/Symfony/Bridge/Doctrine/Types/UlidBinaryType.php b/src/Symfony/Bridge/Doctrine/Types/UlidBinaryType.php new file mode 100644 index 0000000000000..34077d24494e0 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Types/UlidBinaryType.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Types; + +use Symfony\Component\Uid\Ulid; + +final class UlidBinaryType extends AbstractBinaryUidType +{ + public function getName(): string + { + return 'ulid_binary'; + } + + protected function getUidClass(): string + { + return Ulid::class; + } +} diff --git a/src/Symfony/Bridge/Doctrine/Types/UlidType.php b/src/Symfony/Bridge/Doctrine/Types/UlidType.php new file mode 100644 index 0000000000000..809317b222005 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Types/UlidType.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Types; + +use Symfony\Component\Uid\Ulid; + +final class UlidType extends AbstractUidType +{ + public function getName(): string + { + return 'ulid'; + } + + protected function getUidClass(): string + { + return Ulid::class; + } +} diff --git a/src/Symfony/Bridge/Doctrine/Types/UuidBinaryType.php b/src/Symfony/Bridge/Doctrine/Types/UuidBinaryType.php new file mode 100644 index 0000000000000..9e161a8ccba76 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Types/UuidBinaryType.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Types; + +use Symfony\Component\Uid\Uuid; + +final class UuidBinaryType extends AbstractBinaryUidType +{ + public function getName(): string + { + return 'uuid_binary'; + } + + protected function getUidClass(): string + { + return Uuid::class; + } +} diff --git a/src/Symfony/Bridge/Doctrine/Types/UuidType.php b/src/Symfony/Bridge/Doctrine/Types/UuidType.php new file mode 100644 index 0000000000000..bbf0394034a06 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Types/UuidType.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Types; + +use Symfony\Component\Uid\Uuid; + +final class UuidType extends AbstractUidType +{ + public function getName(): string + { + return 'uuid'; + } + + protected function getUidClass(): string + { + return Uuid::class; + } +} diff --git a/src/Symfony/Bridge/Doctrine/composer.json b/src/Symfony/Bridge/Doctrine/composer.json index 1e05d0a559668..c57bd28de5058 100644 --- a/src/Symfony/Bridge/Doctrine/composer.json +++ b/src/Symfony/Bridge/Doctrine/composer.json @@ -39,6 +39,7 @@ "symfony/proxy-manager-bridge": "^4.4|^5.0", "symfony/security-core": "^5.0", "symfony/expression-language": "^4.4|^5.0", + "symfony/uid": "^5.1", "symfony/validator": "^5.0.2", "symfony/translation": "^4.4|^5.0", "symfony/var-dumper": "^4.4|^5.0", @@ -46,11 +47,12 @@ "doctrine/cache": "~1.6", "doctrine/collections": "~1.0", "doctrine/data-fixtures": "^1.1", - "doctrine/dbal": "~2.4|^3.0", + "doctrine/dbal": "^2.10|^3.0", "doctrine/orm": "^2.6.3", "doctrine/reflection": "~1.0" }, "conflict": { + "doctrine/dbal": "<2.10", "phpunit/phpunit": "<5.4.3", "symfony/dependency-injection": "<4.4", "symfony/form": "<5.1", @@ -78,7 +80,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "5.1-dev" + "dev-master": "5.2-dev" } } } diff --git a/src/Symfony/Bridge/Monolog/CHANGELOG.md b/src/Symfony/Bridge/Monolog/CHANGELOG.md index 2a4d31a2ab340..12cc86541d8c8 100644 --- a/src/Symfony/Bridge/Monolog/CHANGELOG.md +++ b/src/Symfony/Bridge/Monolog/CHANGELOG.md @@ -1,8 +1,15 @@ CHANGELOG ========= +5.2.0 +----- + + * The `$actionLevel` constructor argument of `Symfony\Bridge\Monolog\Handler\FingersCrossed\NotFoundActivationStrategy` has been deprecated and replaced by the `$inner` one which expects an ActivationStrategyInterface to decorate instead. `Symfony\Bridge\Monolog\Handler\FingersCrossed\NotFoundActivationStrategy` will become final in 6.0. + * The `$actionLevel` constructor argument of `Symfony\Bridge\Monolog\Handler\FingersCrossed\HttpCodeActivationStrategy` has been deprecated and replaced by the `$inner` one which expects an ActivationStrategyInterface to decorate instead. `Symfony\Bridge\Monolog\Handler\FingersCrossed\HttpCodeActivationStrategy` will become final in 6.0 + 5.1.0 ----- + * Added `MailerHandler` 5.0.0 diff --git a/src/Symfony/Bridge/Monolog/Handler/FingersCrossed/HttpCodeActivationStrategy.php b/src/Symfony/Bridge/Monolog/Handler/FingersCrossed/HttpCodeActivationStrategy.php index 410c99e53b030..5214717a09cca 100644 --- a/src/Symfony/Bridge/Monolog/Handler/FingersCrossed/HttpCodeActivationStrategy.php +++ b/src/Symfony/Bridge/Monolog/Handler/FingersCrossed/HttpCodeActivationStrategy.php @@ -11,6 +11,7 @@ namespace Symfony\Bridge\Monolog\Handler\FingersCrossed; +use Monolog\Handler\FingersCrossed\ActivationStrategyInterface; use Monolog\Handler\FingersCrossed\ErrorLevelActivationStrategy; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpKernel\Exception\HttpException; @@ -19,17 +20,29 @@ * Activation strategy that ignores certain HTTP codes. * * @author Shaun Simmons + * @author Pierrick Vignand + * + * @final */ -class HttpCodeActivationStrategy extends ErrorLevelActivationStrategy +class HttpCodeActivationStrategy extends ErrorLevelActivationStrategy implements ActivationStrategyInterface { + private $inner; private $exclusions; private $requestStack; /** - * @param array $exclusions each exclusion must have a "code" and "urls" keys + * @param array $exclusions each exclusion must have a "code" and "urls" keys + * @param ActivationStrategyInterface|int|string $inner an ActivationStrategyInterface to decorate */ - public function __construct(RequestStack $requestStack, array $exclusions, $actionLevel) + public function __construct(RequestStack $requestStack, array $exclusions, $inner) { + if (!$inner instanceof ActivationStrategyInterface) { + trigger_deprecation('symfony/monolog-bridge', '5.2', 'Passing an actionLevel (int|string) as constructor\'s 3rd argument of "%s" is deprecated, "%s" expected.', __CLASS__, ActivationStrategyInterface::class); + + $actionLevel = $inner; + $inner = new ErrorLevelActivationStrategy($actionLevel); + } + foreach ($exclusions as $exclusion) { if (!\array_key_exists('code', $exclusion)) { throw new \LogicException(sprintf('An exclusion must have a "code" key.')); @@ -39,15 +52,14 @@ public function __construct(RequestStack $requestStack, array $exclusions, $acti } } - parent::__construct($actionLevel); - + $this->inner = $inner; $this->requestStack = $requestStack; $this->exclusions = $exclusions; } public function isHandlerActivated(array $record): bool { - $isActivated = parent::isHandlerActivated($record); + $isActivated = $this->inner->isHandlerActivated($record); if ( $isActivated diff --git a/src/Symfony/Bridge/Monolog/Handler/FingersCrossed/NotFoundActivationStrategy.php b/src/Symfony/Bridge/Monolog/Handler/FingersCrossed/NotFoundActivationStrategy.php index f73cd2f41b753..a9806d7e92ea6 100644 --- a/src/Symfony/Bridge/Monolog/Handler/FingersCrossed/NotFoundActivationStrategy.php +++ b/src/Symfony/Bridge/Monolog/Handler/FingersCrossed/NotFoundActivationStrategy.php @@ -11,6 +11,7 @@ namespace Symfony\Bridge\Monolog\Handler\FingersCrossed; +use Monolog\Handler\FingersCrossed\ActivationStrategyInterface; use Monolog\Handler\FingersCrossed\ErrorLevelActivationStrategy; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpKernel\Exception\HttpException; @@ -20,23 +21,36 @@ * * @author Jordi Boggiano * @author Fabien Potencier + * @author Pierrick Vignand + * + * @final */ -class NotFoundActivationStrategy extends ErrorLevelActivationStrategy +class NotFoundActivationStrategy extends ErrorLevelActivationStrategy implements ActivationStrategyInterface { + private $inner; private $exclude; private $requestStack; - public function __construct(RequestStack $requestStack, array $excludedUrls, $actionLevel) + /** + * @param ActivationStrategyInterface|int|string $inner an ActivationStrategyInterface to decorate + */ + public function __construct(RequestStack $requestStack, array $excludedUrls, $inner) { - parent::__construct($actionLevel); + if (!$inner instanceof ActivationStrategyInterface) { + trigger_deprecation('symfony/monolog-bridge', '5.2', 'Passing an actionLevel (int|string) as constructor\'s 3rd argument of "%s" is deprecated, "%s" expected.', __CLASS__, ActivationStrategyInterface::class); + + $actionLevel = $inner; + $inner = new ErrorLevelActivationStrategy($actionLevel); + } + $this->inner = $inner; $this->requestStack = $requestStack; $this->exclude = '{('.implode('|', $excludedUrls).')}i'; } public function isHandlerActivated(array $record): bool { - $isActivated = parent::isHandlerActivated($record); + $isActivated = $this->inner->isHandlerActivated($record); if ( $isActivated diff --git a/src/Symfony/Bridge/Monolog/Processor/AbstractTokenProcessor.php b/src/Symfony/Bridge/Monolog/Processor/AbstractTokenProcessor.php new file mode 100644 index 0000000000000..ed37c94b81c00 --- /dev/null +++ b/src/Symfony/Bridge/Monolog/Processor/AbstractTokenProcessor.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Monolog\Processor; + +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + +/** + * The base class for security token processors. + * + * @author Dany Maillard + * @author Igor Timoshenko + */ +abstract class AbstractTokenProcessor +{ + /** + * @var TokenStorageInterface + */ + protected $tokenStorage; + + public function __construct(TokenStorageInterface $tokenStorage) + { + $this->tokenStorage = $tokenStorage; + } + + abstract protected function getKey(): string; + + abstract protected function getToken(): ?TokenInterface; + + public function __invoke(array $record): array + { + $record['extra'][$this->getKey()] = null; + + if (null !== $token = $this->getToken()) { + $record['extra'][$this->getKey()] = [ + 'username' => $token->getUsername(), + 'authenticated' => $token->isAuthenticated(), + 'roles' => $token->getRoleNames(), + ]; + } + + return $record; + } +} diff --git a/src/Symfony/Bridge/Monolog/Processor/SwitchUserTokenProcessor.php b/src/Symfony/Bridge/Monolog/Processor/SwitchUserTokenProcessor.php new file mode 100644 index 0000000000000..76aa7e479d0e5 --- /dev/null +++ b/src/Symfony/Bridge/Monolog/Processor/SwitchUserTokenProcessor.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Monolog\Processor; + +use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + +/** + * Adds the original security token to the log entry. + * + * @author Igor Timoshenko + */ +class SwitchUserTokenProcessor extends AbstractTokenProcessor +{ + /** + * {@inheritdoc} + */ + protected function getKey(): string + { + return 'impersonator_token'; + } + + /** + * {@inheritdoc} + */ + protected function getToken(): ?TokenInterface + { + $token = $this->tokenStorage->getToken(); + + if ($token instanceof SwitchUserToken) { + return $token->getOriginalToken(); + } + + return null; + } +} diff --git a/src/Symfony/Bridge/Monolog/Processor/TokenProcessor.php b/src/Symfony/Bridge/Monolog/Processor/TokenProcessor.php index 78d8dd3249c6d..7ca212eb29770 100644 --- a/src/Symfony/Bridge/Monolog/Processor/TokenProcessor.php +++ b/src/Symfony/Bridge/Monolog/Processor/TokenProcessor.php @@ -11,35 +11,29 @@ namespace Symfony\Bridge\Monolog\Processor; -use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; /** * Adds the current security token to the log entry. * * @author Dany Maillard + * @author Igor Timoshenko */ -class TokenProcessor +class TokenProcessor extends AbstractTokenProcessor { - private $tokenStorage; - - public function __construct(TokenStorageInterface $tokenStorage) + /** + * {@inheritdoc} + */ + protected function getKey(): string { - $this->tokenStorage = $tokenStorage; + return 'token'; } - public function __invoke(array $records) + /** + * {@inheritdoc} + */ + protected function getToken(): ?TokenInterface { - $records['extra']['token'] = null; - if (null !== $token = $this->tokenStorage->getToken()) { - $roles = $token->getRoleNames(); - - $records['extra']['token'] = [ - 'username' => $token->getUsername(), - 'authenticated' => $token->isAuthenticated(), - 'roles' => $roles, - ]; - } - - return $records; + return $this->tokenStorage->getToken(); } } diff --git a/src/Symfony/Bridge/Monolog/Tests/Handler/FingersCrossed/HttpCodeActivationStrategyTest.php b/src/Symfony/Bridge/Monolog/Tests/Handler/FingersCrossed/HttpCodeActivationStrategyTest.php index 75bbe16c146e1..fdf2811876877 100644 --- a/src/Symfony/Bridge/Monolog/Tests/Handler/FingersCrossed/HttpCodeActivationStrategyTest.php +++ b/src/Symfony/Bridge/Monolog/Tests/Handler/FingersCrossed/HttpCodeActivationStrategyTest.php @@ -11,6 +11,7 @@ namespace Symfony\Bridge\Monolog\Tests\Handler\FingersCrossed; +use Monolog\Handler\FingersCrossed\ErrorLevelActivationStrategy; use Monolog\Logger; use PHPUnit\Framework\TestCase; use Symfony\Bridge\Monolog\Handler\FingersCrossed\HttpCodeActivationStrategy; @@ -20,13 +21,19 @@ class HttpCodeActivationStrategyTest extends TestCase { - public function testExclusionsWithoutCode() + /** + * @group legacy + */ + public function testExclusionsWithoutCodeLegacy(): void { $this->expectException('LogicException'); new HttpCodeActivationStrategy(new RequestStack(), [['urls' => []]], Logger::WARNING); } - public function testExclusionsWithoutUrls() + /** + * @group legacy + */ + public function testExclusionsWithoutUrlsLegacy(): void { $this->expectException('LogicException'); new HttpCodeActivationStrategy(new RequestStack(), [['code' => 404]], Logger::WARNING); @@ -34,8 +41,10 @@ public function testExclusionsWithoutUrls() /** * @dataProvider isActivatedProvider + * + * @group legacy */ - public function testIsActivated($url, $record, $expected) + public function testIsActivatedLegacy($url, $record, $expected): void { $requestStack = new RequestStack(); $requestStack->push(Request::create($url)); @@ -51,10 +60,44 @@ public function testIsActivated($url, $record, $expected) Logger::WARNING ); - $this->assertEquals($expected, $strategy->isHandlerActivated($record)); + self::assertEquals($expected, $strategy->isHandlerActivated($record)); + } + + public function testExclusionsWithoutCode(): void + { + $this->expectException('LogicException'); + new HttpCodeActivationStrategy(new RequestStack(), [['urls' => []]], new ErrorLevelActivationStrategy(Logger::WARNING)); + } + + public function testExclusionsWithoutUrls(): void + { + $this->expectException('LogicException'); + new HttpCodeActivationStrategy(new RequestStack(), [['code' => 404]], new ErrorLevelActivationStrategy(Logger::WARNING)); + } + + /** + * @dataProvider isActivatedProvider + */ + public function testIsActivated($url, $record, $expected) + { + $requestStack = new RequestStack(); + $requestStack->push(Request::create($url)); + + $strategy = new HttpCodeActivationStrategy( + $requestStack, + [ + ['code' => 403, 'urls' => []], + ['code' => 404, 'urls' => []], + ['code' => 405, 'urls' => []], + ['code' => 400, 'urls' => ['^/400/a', '^/400/b']], + ], + new ErrorLevelActivationStrategy(Logger::WARNING) + ); + + self::assertEquals($expected, $strategy->isHandlerActivated($record)); } - public function isActivatedProvider() + public function isActivatedProvider(): array { return [ ['/test', ['level' => Logger::ERROR], true], @@ -70,7 +113,7 @@ public function isActivatedProvider() ]; } - protected function getContextException($code) + private function getContextException(int $code): array { return ['exception' => new HttpException($code)]; } diff --git a/src/Symfony/Bridge/Monolog/Tests/Handler/FingersCrossed/NotFoundActivationStrategyTest.php b/src/Symfony/Bridge/Monolog/Tests/Handler/FingersCrossed/NotFoundActivationStrategyTest.php index b04678106c96e..3d8445df3b915 100644 --- a/src/Symfony/Bridge/Monolog/Tests/Handler/FingersCrossed/NotFoundActivationStrategyTest.php +++ b/src/Symfony/Bridge/Monolog/Tests/Handler/FingersCrossed/NotFoundActivationStrategyTest.php @@ -11,6 +11,7 @@ namespace Symfony\Bridge\Monolog\Tests\Handler\FingersCrossed; +use Monolog\Handler\FingersCrossed\ErrorLevelActivationStrategy; use Monolog\Logger; use PHPUnit\Framework\TestCase; use Symfony\Bridge\Monolog\Handler\FingersCrossed\NotFoundActivationStrategy; @@ -22,18 +23,33 @@ class NotFoundActivationStrategyTest extends TestCase { /** * @dataProvider isActivatedProvider + * + * @group legacy */ - public function testIsActivated($url, $record, $expected) + public function testIsActivatedLegacy(string $url, array $record, bool $expected): void { $requestStack = new RequestStack(); $requestStack->push(Request::create($url)); $strategy = new NotFoundActivationStrategy($requestStack, ['^/foo', 'bar'], Logger::WARNING); - $this->assertEquals($expected, $strategy->isHandlerActivated($record)); + self::assertEquals($expected, $strategy->isHandlerActivated($record)); } - public function isActivatedProvider() + /** + * @dataProvider isActivatedProvider + */ + public function testIsActivated(string $url, array $record, bool $expected): void + { + $requestStack = new RequestStack(); + $requestStack->push(Request::create($url)); + + $strategy = new NotFoundActivationStrategy($requestStack, ['^/foo', 'bar'], new ErrorLevelActivationStrategy(Logger::WARNING)); + + self::assertEquals($expected, $strategy->isHandlerActivated($record)); + } + + public function isActivatedProvider(): array { return [ ['/test', ['level' => Logger::DEBUG], false], @@ -48,7 +64,7 @@ public function isActivatedProvider() ]; } - protected function getContextException($code) + protected function getContextException(int $code): array { return ['exception' => new HttpException($code)]; } diff --git a/src/Symfony/Bridge/Monolog/Tests/Processor/SwitchUserTokenProcessorTest.php b/src/Symfony/Bridge/Monolog/Tests/Processor/SwitchUserTokenProcessorTest.php new file mode 100644 index 0000000000000..8a71157cca2c4 --- /dev/null +++ b/src/Symfony/Bridge/Monolog/Tests/Processor/SwitchUserTokenProcessorTest.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Monolog\Tests\Processor; + +use PHPUnit\Framework\TestCase; +use Symfony\Bridge\Monolog\Processor\SwitchUserTokenProcessor; +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; + +/** + * Tests the SwitchUserTokenProcessor. + * + * @author Igor Timoshenko + */ +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); + $tokenStorage = $this->getMockBuilder(TokenStorageInterface::class)->getMock(); + $tokenStorage->method('getToken')->willReturn($switchUserToken); + + $processor = new SwitchUserTokenProcessor($tokenStorage); + $record = ['extra' => []]; + $record = $processor($record); + + $expected = [ + 'impersonator_token' => [ + 'username' => 'original_user', + 'authenticated' => true, + 'roles' => ['ROLE_SUPER_ADMIN'], + ], + ]; + $this->assertSame($expected, $record['extra']); + } +} diff --git a/src/Symfony/Bridge/Monolog/composer.json b/src/Symfony/Bridge/Monolog/composer.json index fb9566af1260e..b261b3de75016 100644 --- a/src/Symfony/Bridge/Monolog/composer.json +++ b/src/Symfony/Bridge/Monolog/composer.json @@ -19,7 +19,8 @@ "php": ">=7.2.5", "monolog/monolog": "^1.25.1|^2", "symfony/service-contracts": "^1.1|^2", - "symfony/http-kernel": "^4.4|^5.0" + "symfony/http-kernel": "^4.4|^5.0", + "symfony/deprecation-contracts": "^2.1" }, "require-dev": { "symfony/console": "^4.4|^5.0", @@ -47,7 +48,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "5.1-dev" + "dev-master": "5.2-dev" } } } diff --git a/src/Symfony/Bridge/PhpUnit/CHANGELOG.md b/src/Symfony/Bridge/PhpUnit/CHANGELOG.md index 2808ad0c50903..c343171feb6a0 100644 --- a/src/Symfony/Bridge/PhpUnit/CHANGELOG.md +++ b/src/Symfony/Bridge/PhpUnit/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +5.2.0 +----- + + * polyfill new phpunit 9.1 assertions + 5.1.0 ----- diff --git a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php index b17c4bfc88b39..c6f9a2170e90a 100644 --- a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php +++ b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php @@ -49,8 +49,9 @@ public function __construct() * Registers and configures the deprecation handler. * * The mode is a query string with options: - * - "disabled" to disable the deprecation handler + * - "disabled" to enable/disable the deprecation handler * - "verbose" to enable/disable displaying the deprecation report + * - "quiet" to disable displaying the deprecation report only for some groups (i.e. quiet[]=other) * - "max" to configure the number of deprecations to allow before exiting with a non-zero * status code; it's an array with keys "total", "self", "direct" and "indirect" * diff --git a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/Configuration.php b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/Configuration.php index bc0fe98499d41..a4892c3d5da11 100644 --- a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/Configuration.php +++ b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/Configuration.php @@ -166,30 +166,29 @@ public static function fromUrlEncodedString($serializedConfiguration) } } - if (isset($normalizedConfiguration['disabled'])) { + $normalizedConfiguration += [ + 'max' => [], + 'disabled' => false, + 'verbose' => true, + 'quiet' => [], + ]; + + if ('' === $normalizedConfiguration['disabled'] || filter_var($normalizedConfiguration['disabled'], \FILTER_VALIDATE_BOOLEAN)) { return self::inDisabledMode(); } $verboseOutput = []; - if (!isset($normalizedConfiguration['verbose'])) { - $normalizedConfiguration['verbose'] = true; - } - foreach (['unsilenced', 'direct', 'indirect', 'self', 'other'] as $group) { - $verboseOutput[$group] = (bool) $normalizedConfiguration['verbose']; + $verboseOutput[$group] = filter_var($normalizedConfiguration['verbose'], \FILTER_VALIDATE_BOOLEAN); } - if (isset($normalizedConfiguration['quiet']) && \is_array($normalizedConfiguration['quiet'])) { + if (\is_array($normalizedConfiguration['quiet'])) { foreach ($normalizedConfiguration['quiet'] as $shushedGroup) { $verboseOutput[$shushedGroup] = false; } } - return new self( - isset($normalizedConfiguration['max']) ? $normalizedConfiguration['max'] : [], - '', - $verboseOutput - ); + return new self($normalizedConfiguration['max'], '', $verboseOutput); } /** diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/ConfigurationTest.php b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/ConfigurationTest.php index bb5b3a72d4932..86fe88cbbe445 100644 --- a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/ConfigurationTest.php +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/ConfigurationTest.php @@ -176,10 +176,22 @@ public function testItCanTellWhetherToDisplayAStackTrace() $this->assertTrue($configuration->shouldDisplayStackTrace('interesting')); } - public function testItCanBeDisabled() + public function provideItCanBeDisabled(): array { - $configuration = Configuration::fromUrlEncodedString('disabled'); - $this->assertFalse($configuration->isEnabled()); + return [ + ['disabled', false], + ['disabled=1', false], + ['disabled=0', true] + ]; + } + + /** + * @dataProvider provideItCanBeDisabled + */ + public function testItCanBeDisabled(string $encodedString, bool $expectedEnabled) + { + $configuration = Configuration::fromUrlEncodedString($encodedString); + $this->assertSame($expectedEnabled, $configuration->isEnabled()); } public function testItCanBeShushed() diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/disabled_1.phpt b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/disabled_1.phpt new file mode 100644 index 0000000000000..acb5f096306d0 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/disabled_1.phpt @@ -0,0 +1,37 @@ +--TEST-- +Test DeprecationErrorHandler in default mode +--FILE-- + +--EXPECTREGEX-- +.{0} diff --git a/src/Symfony/Bridge/PhpUnit/Tests/expectdeprecationfail.phpt b/src/Symfony/Bridge/PhpUnit/Tests/expectdeprecationfail.phpt index 0882f2a15176c..9f9bf8c17508e 100644 --- a/src/Symfony/Bridge/PhpUnit/Tests/expectdeprecationfail.phpt +++ b/src/Symfony/Bridge/PhpUnit/Tests/expectdeprecationfail.phpt @@ -8,7 +8,7 @@ passthru('php '.getenv('SYMFONY_SIMPLE_PHPUNIT_BIN_DIR').'/simple-phpunit.php -- --EXPECTF-- PHPUnit %s by Sebastian Bergmann and contributors. -Testing Symfony\Bridge\PhpUnit\Tests\FailTests\ExpectDeprecationTraitTestFail +%ATesting Symfony\Bridge\PhpUnit\Tests\FailTests\ExpectDeprecationTraitTestFail FF 2 / 2 (100%) Time: %s, Memory: %s diff --git a/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php b/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php index c03a4c2962d02..a4fe9f23a08e8 100644 --- a/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php +++ b/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php @@ -113,6 +113,12 @@ $PHPUNIT_VERSION = '4.8'; } +$MAX_PHPUNIT_VERSION = $getEnvVar('SYMFONY_MAX_PHPUNIT_VERSION', false); + +if ($MAX_PHPUNIT_VERSION && version_compare($MAX_PHPUNIT_VERSION, $PHPUNIT_VERSION, '<')) { + $PHPUNIT_VERSION = $MAX_PHPUNIT_VERSION; +} + $PHPUNIT_REMOVE_RETURN_TYPEHINT = filter_var($getEnvVar('SYMFONY_PHPUNIT_REMOVE_RETURN_TYPEHINT', '0'), \FILTER_VALIDATE_BOOLEAN); $COMPOSER_JSON = getenv('COMPOSER') ?: 'composer.json'; @@ -197,7 +203,7 @@ 'requires' => ['php' => '*'], ]; - $stableVersions = array_filter($info['versions'], function($v) { + $stableVersions = array_filter($info['versions'], function ($v) { return !preg_match('/-dev$|^dev-/', $v); }); diff --git a/src/Symfony/Bridge/PhpUnit/composer.json b/src/Symfony/Bridge/PhpUnit/composer.json index 60c13a0aabc70..42240b1c21482 100644 --- a/src/Symfony/Bridge/PhpUnit/composer.json +++ b/src/Symfony/Bridge/PhpUnit/composer.json @@ -39,7 +39,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "5.1-dev" + "dev-master": "5.2-dev" }, "thanks": { "name": "phpunit/phpunit", diff --git a/src/Symfony/Bridge/ProxyManager/composer.json b/src/Symfony/Bridge/ProxyManager/composer.json index 5c04e00da8407..429106f3edd99 100644 --- a/src/Symfony/Bridge/ProxyManager/composer.json +++ b/src/Symfony/Bridge/ProxyManager/composer.json @@ -35,7 +35,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "5.1-dev" + "dev-master": "5.2-dev" } } } diff --git a/src/Symfony/Bridge/Twig/CHANGELOG.md b/src/Symfony/Bridge/Twig/CHANGELOG.md index 70ca5e7481691..1bc5d3bdefa3c 100644 --- a/src/Symfony/Bridge/Twig/CHANGELOG.md +++ b/src/Symfony/Bridge/Twig/CHANGELOG.md @@ -1,6 +1,16 @@ CHANGELOG ========= +5.2.0 +----- + + * added the `impersonation_exit_url()` and `impersonation_exit_path()` functions. They return a URL that allows to switch back to the original user. + * added the `workflow_transition()` function to easily retrieve a specific transition object + * added support for translating `TranslatableInterface` objects + * added the `t()` function to easily create `Translatable` objects + * Added support for extracting messages from the `t()` function + * Added `field_*` Twig functions to access string values from Form fields + 5.0.0 ----- @@ -16,7 +26,7 @@ CHANGELOG * added a new `TwigErrorRenderer` for `html` format, integrated with the `ErrorHandler` component * marked all classes extending twig as `@final` - * deprecated to pass `$rootDir` and `$fileLinkFormatter` as 5th and 6th argument respectively to the + * deprecated to pass `$rootDir` and `$fileLinkFormatter` as 5th and 6th argument respectively to the `DebugCommand::__construct()` method, swap the variables position. * the `LintCommand` lints all the templates stored in all configured Twig paths if none argument is provided * deprecated accepting STDIN implicitly when using the `lint:twig` command, use `lint:twig -` (append a dash) instead to make it explicit. @@ -29,7 +39,7 @@ CHANGELOG * added the `form_parent()` function that allows to reliably retrieve the parent form in Twig templates * added the `workflow_transition_blockers()` function - * deprecated the `$requestStack` and `$requestContext` arguments of the + * deprecated the `$requestStack` and `$requestContext` arguments of the `HttpFoundationExtension`, pass a `Symfony\Component\HttpFoundation\UrlHelper` instance as the only argument instead diff --git a/src/Symfony/Bridge/Twig/Extension/FormExtension.php b/src/Symfony/Bridge/Twig/Extension/FormExtension.php index 0f4076db53275..080a5eb06e57b 100644 --- a/src/Symfony/Bridge/Twig/Extension/FormExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/FormExtension.php @@ -12,8 +12,11 @@ namespace Symfony\Bridge\Twig\Extension; use Symfony\Bridge\Twig\TokenParser\FormThemeTokenParser; +use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView; use Symfony\Component\Form\ChoiceList\View\ChoiceView; +use Symfony\Component\Form\FormError; use Symfony\Component\Form\FormView; +use Symfony\Contracts\Translation\TranslatorInterface; use Twig\Extension\AbstractExtension; use Twig\TwigFilter; use Twig\TwigFunction; @@ -27,6 +30,13 @@ */ final class FormExtension extends AbstractExtension { + private $translator; + + public function __construct(TranslatorInterface $translator = null) + { + $this->translator = $translator; + } + /** * {@inheritdoc} */ @@ -55,6 +65,12 @@ public function getFunctions(): array new TwigFunction('form_end', null, ['node_class' => 'Symfony\Bridge\Twig\Node\RenderBlockNode', 'is_safe' => ['html']]), new TwigFunction('csrf_token', ['Symfony\Component\Form\FormRenderer', 'renderCsrfToken']), new TwigFunction('form_parent', 'Symfony\Bridge\Twig\Extension\twig_get_form_parent'), + new TwigFunction('field_name', [$this, 'getFieldName']), + new TwigFunction('field_value', [$this, 'getFieldValue']), + new TwigFunction('field_label', [$this, 'getFieldLabel']), + new TwigFunction('field_help', [$this, 'getFieldHelp']), + new TwigFunction('field_errors', [$this, 'getFieldErrors']), + new TwigFunction('field_choices', [$this, 'getFieldChoices']), ]; } @@ -79,6 +95,80 @@ public function getTests(): array new TwigTest('rootform', 'Symfony\Bridge\Twig\Extension\twig_is_root_form'), ]; } + + public function getFieldName(FormView $view): string + { + $view->setRendered(); + + return $view->vars['full_name']; + } + + public function getFieldValue(FormView $view): string + { + return $view->vars['value']; + } + + public function getFieldLabel(FormView $view): string + { + return $this->createFieldTranslation( + $view->vars['label'], + $view->vars['label_translation_parameters'] ?: [], + $view->vars['translation_domain'] + ); + } + + public function getFieldHelp(FormView $view): string + { + return $this->createFieldTranslation( + $view->vars['help'], + $view->vars['help_translation_parameters'] ?: [], + $view->vars['translation_domain'] + ); + } + + /** + * @return string[] + */ + public function getFieldErrors(FormView $view): iterable + { + /** @var FormError $error */ + foreach ($view->vars['errors'] as $error) { + yield $error->getMessage(); + } + } + + /** + * @return string[]|string[][] + */ + public function getFieldChoices(FormView $view): iterable + { + yield from $this->createFieldChoicesList($view->vars['choices'], $view->vars['choice_translation_domain']); + } + + private function createFieldChoicesList(iterable $choices, $translationDomain): iterable + { + foreach ($choices as $choice) { + $translatableLabel = $this->createFieldTranslation($choice->label, [], $translationDomain); + + if ($choice instanceof ChoiceGroupView) { + yield $translatableLabel => $this->createFieldChoicesList($choice, $translationDomain); + + continue; + } + + /* @var ChoiceView $choice */ + yield $translatableLabel => $choice->value; + } + } + + private function createFieldTranslation(?string $value, array $parameters, $domain): string + { + if (!$this->translator || !$value || false === $domain) { + return $value; + } + + return $this->translator->trans($value, $parameters, $domain); + } } /** diff --git a/src/Symfony/Bridge/Twig/Extension/SecurityExtension.php b/src/Symfony/Bridge/Twig/Extension/SecurityExtension.php index bd82ef20b4eff..0e58fc0ec66e4 100644 --- a/src/Symfony/Bridge/Twig/Extension/SecurityExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/SecurityExtension.php @@ -14,6 +14,7 @@ use Symfony\Component\Security\Acl\Voter\FieldVote; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException; +use Symfony\Component\Security\Http\Impersonate\ImpersonateUrlGenerator; use Twig\Extension\AbstractExtension; use Twig\TwigFunction; @@ -26,9 +27,12 @@ final class SecurityExtension extends AbstractExtension { private $securityChecker; - public function __construct(AuthorizationCheckerInterface $securityChecker = null) + private $impersonateUrlGenerator; + + public function __construct(AuthorizationCheckerInterface $securityChecker = null, ImpersonateUrlGenerator $impersonateUrlGenerator = null) { $this->securityChecker = $securityChecker; + $this->impersonateUrlGenerator = $impersonateUrlGenerator; } /** @@ -51,6 +55,24 @@ public function isGranted($role, $object = null, string $field = null): bool } } + public function getImpersonateExitUrl(string $exitTo = null): string + { + if (null === $this->impersonateUrlGenerator) { + return ''; + } + + return $this->impersonateUrlGenerator->generateExitUrl($exitTo); + } + + public function getImpersonateExitPath(string $exitTo = null): string + { + if (null === $this->impersonateUrlGenerator) { + return ''; + } + + return $this->impersonateUrlGenerator->generateExitPath($exitTo); + } + /** * {@inheritdoc} */ @@ -58,6 +80,8 @@ public function getFunctions(): array { return [ new TwigFunction('is_granted', [$this, 'isGranted']), + new TwigFunction('impersonation_exit_url', [$this, 'getImpersonateExitUrl']), + new TwigFunction('impersonation_exit_path', [$this, 'getImpersonateExitPath']), ]; } } diff --git a/src/Symfony/Bridge/Twig/Extension/TranslationExtension.php b/src/Symfony/Bridge/Twig/Extension/TranslationExtension.php index d65578608d1aa..66486fa19d307 100644 --- a/src/Symfony/Bridge/Twig/Extension/TranslationExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/TranslationExtension.php @@ -15,11 +15,13 @@ use Symfony\Bridge\Twig\NodeVisitor\TranslationNodeVisitor; use Symfony\Bridge\Twig\TokenParser\TransDefaultDomainTokenParser; use Symfony\Bridge\Twig\TokenParser\TransTokenParser; +use Symfony\Component\Translation\Translatable; +use Symfony\Contracts\Translation\TranslatableInterface; use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorTrait; use Twig\Extension\AbstractExtension; -use Twig\NodeVisitor\NodeVisitorInterface; use Twig\TwigFilter; +use Twig\TwigFunction; // Help opcache.preload discover always-needed symbols class_exists(TranslatorInterface::class); @@ -34,7 +36,7 @@ final class TranslationExtension extends AbstractExtension private $translator; private $translationNodeVisitor; - public function __construct(TranslatorInterface $translator = null, NodeVisitorInterface $translationNodeVisitor = null) + public function __construct(TranslatorInterface $translator = null, TranslationNodeVisitor $translationNodeVisitor = null) { $this->translator = $translator; $this->translationNodeVisitor = $translationNodeVisitor; @@ -55,6 +57,16 @@ public function getTranslator(): TranslatorInterface return $this->translator; } + /** + * {@inheritdoc} + */ + public function getFunctions(): array + { + return [ + new TwigFunction('t', [$this, 'createTranslatable']), + ]; + } + /** * {@inheritdoc} */ @@ -92,9 +104,25 @@ public function getTranslationNodeVisitor(): TranslationNodeVisitor return $this->translationNodeVisitor ?: $this->translationNodeVisitor = new TranslationNodeVisitor(); } - public function trans(?string $message, array $arguments = [], string $domain = null, string $locale = null, int $count = null): string + /** + * @param string|\Stringable|TranslatableInterface|null $message + * @param array|string $arguments Can be the locale as a string when $message is a TranslatableInterface + */ + public function trans($message, $arguments = [], string $domain = null, string $locale = null, int $count = null): string { - if (null === $message || '' === $message) { + if ($message instanceof TranslatableInterface) { + if ([] !== $arguments && !\is_string($arguments)) { + throw new \TypeError(sprintf('Argument 2 passed to "%s()" must be a locale passed as a string when the message is a "%s", "%s" given.', __METHOD__, TranslatableInterface::class, get_debug_type($arguments))); + } + + return $message->trans($this->getTranslator(), $locale ?? (\is_string($arguments) ? $arguments : null)); + } + + if (!\is_array($arguments)) { + throw new \TypeError(sprintf('Unless the message is a "%s", argument 2 passed to "%s()" must be an array of parameters, "%s" given.', TranslatableInterface::class, __METHOD__, get_debug_type($arguments))); + } + + if ('' === $message = (string) $message) { return ''; } @@ -104,4 +132,13 @@ public function trans(?string $message, array $arguments = [], string $domain = return $this->getTranslator()->trans($message, $arguments, $domain, $locale); } + + public function createTranslatable(string $message, array $parameters = [], string $domain = null): Translatable + { + if (!class_exists(Translatable::class)) { + throw new \LogicException(sprintf('You cannot use the "%s" as the Translation Component is not installed. Try running "composer require symfony/translation".', __CLASS__)); + } + + return new Translatable($message, $parameters, $domain); + } } diff --git a/src/Symfony/Bridge/Twig/Extension/WorkflowExtension.php b/src/Symfony/Bridge/Twig/Extension/WorkflowExtension.php index 04aae60427999..ea7cd17a8fc10 100644 --- a/src/Symfony/Bridge/Twig/Extension/WorkflowExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/WorkflowExtension.php @@ -21,6 +21,7 @@ * WorkflowExtension. * * @author Grégoire Pineau + * @author Carlos Pereira De Amorim */ final class WorkflowExtension extends AbstractExtension { @@ -39,6 +40,7 @@ public function getFunctions(): array return [ new TwigFunction('workflow_can', [$this, 'canTransition']), new TwigFunction('workflow_transitions', [$this, 'getEnabledTransitions']), + new TwigFunction('workflow_transition', [$this, 'getEnabledTransition']), new TwigFunction('workflow_has_marked_place', [$this, 'hasMarkedPlace']), new TwigFunction('workflow_marked_places', [$this, 'getMarkedPlaces']), new TwigFunction('workflow_metadata', [$this, 'getMetadata']), @@ -64,6 +66,11 @@ public function getEnabledTransitions(object $subject, string $name = null): arr return $this->workflowRegistry->get($subject, $name)->getEnabledTransitions($subject); } + public function getEnabledTransition(object $subject, string $transition, string $name = null): ?Transition + { + return $this->workflowRegistry->get($subject, $name)->getEnabledTransition($subject, $transition); + } + /** * Returns true if the place is marked. */ diff --git a/src/Symfony/Bridge/Twig/NodeVisitor/TranslationNodeVisitor.php b/src/Symfony/Bridge/Twig/NodeVisitor/TranslationNodeVisitor.php index 89a15cd622c5d..4de4154b9b46f 100644 --- a/src/Symfony/Bridge/Twig/NodeVisitor/TranslationNodeVisitor.php +++ b/src/Symfony/Bridge/Twig/NodeVisitor/TranslationNodeVisitor.php @@ -15,6 +15,7 @@ use Twig\Environment; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\FilterExpression; +use Twig\Node\Expression\FunctionExpression; use Twig\Node\Node; use Twig\NodeVisitor\AbstractNodeVisitor; @@ -66,6 +67,20 @@ protected function doEnterNode(Node $node, Environment $env): Node $node->getNode('node')->getAttribute('value'), $this->getReadDomainFromArguments($node->getNode('arguments'), 1), ]; + } elseif ( + $node instanceof FilterExpression && + 'trans' === $node->getNode('filter')->getAttribute('value') && + $node->getNode('node') instanceof FunctionExpression && + 't' === $node->getNode('node')->getAttribute('name') + ) { + $nodeArguments = $node->getNode('node')->getNode('arguments'); + + if ($nodeArguments->getIterator()->current() instanceof ConstantExpression) { + $this->messages[] = [ + $this->getReadMessageFromArguments($nodeArguments, 0), + $this->getReadDomainFromArguments($nodeArguments, 2), + ]; + } } elseif ( $node instanceof FilterExpression && 'transchoice' === $node->getNode('filter')->getAttribute('value') && @@ -103,6 +118,28 @@ public function getPriority(): int return 0; } + private function getReadMessageFromArguments(Node $arguments, int $index): ?string + { + if ($arguments->hasNode('message')) { + $argument = $arguments->getNode('message'); + } elseif ($arguments->hasNode($index)) { + $argument = $arguments->getNode($index); + } else { + return null; + } + + return $this->getReadMessageFromNode($argument); + } + + private function getReadMessageFromNode(Node $node): ?string + { + if ($node instanceof ConstantExpression) { + return $node->getAttribute('value'); + } + + return null; + } + private function getReadDomainFromArguments(Node $arguments, int $index): ?string { if ($arguments->hasNode('domain')) { diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionFieldHelpersTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionFieldHelpersTest.php new file mode 100644 index 0000000000000..f773c1b430918 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionFieldHelpersTest.php @@ -0,0 +1,237 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Twig\Tests\Extension; + +use Symfony\Bridge\Twig\Extension\FormExtension; +use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubTranslator; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\FormType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\FormError; +use Symfony\Component\Form\FormView; +use Symfony\Component\Form\Test\FormIntegrationTestCase; + +class FormExtensionFieldHelpersTest extends FormIntegrationTestCase +{ + /** + * @var FormExtension + */ + private $rawExtension; + + /** + * @var FormExtension + */ + private $translatorExtension; + + /** + * @var FormView + */ + private $view; + + protected function getTypes() + { + return [new TextType(), new ChoiceType()]; + } + + protected function setUp(): void + { + parent::setUp(); + + $this->rawExtension = new FormExtension(); + $this->translatorExtension = new FormExtension(new StubTranslator()); + + $form = $this->factory->createNamedBuilder('register', FormType::class, ['username' => 'tgalopin']) + ->add('username', TextType::class, [ + 'label' => 'base.username', + 'label_translation_parameters' => ['%label_brand%' => 'Symfony'], + 'help' => 'base.username_help', + 'help_translation_parameters' => ['%help_brand%' => 'Symfony'], + 'translation_domain' => 'forms', + ]) + ->add('choice_flat', ChoiceType::class, [ + 'choices' => [ + 'base.yes' => 'yes', + 'base.no' => 'no', + ], + 'choice_translation_domain' => 'forms', + ]) + ->add('choice_grouped', ChoiceType::class, [ + 'choices' => [ + 'base.europe' => [ + 'base.fr' => 'fr', + 'base.de' => 'de', + ], + 'base.asia' => [ + 'base.cn' => 'cn', + 'base.jp' => 'jp', + ], + ], + 'choice_translation_domain' => 'forms', + ]) + ->getForm() + ; + + $form->get('username')->addError(new FormError('username.max_length')); + + $this->view = $form->createView(); + } + + public function testFieldName() + { + $this->assertFalse($this->view->children['username']->isRendered()); + $this->assertSame('register[username]', $this->rawExtension->getFieldName($this->view->children['username'])); + $this->assertTrue($this->view->children['username']->isRendered()); + } + + public function testFieldValue() + { + $this->assertSame('tgalopin', $this->rawExtension->getFieldValue($this->view->children['username'])); + } + + public function testFieldLabel() + { + $this->assertSame('base.username', $this->rawExtension->getFieldLabel($this->view->children['username'])); + } + + public function testFieldTranslatedLabel() + { + $this->assertSame('[trans]base.username[/trans]', $this->translatorExtension->getFieldLabel($this->view->children['username'])); + } + + public function testFieldHelp() + { + $this->assertSame('base.username_help', $this->rawExtension->getFieldHelp($this->view->children['username'])); + } + + public function testFieldTranslatedHelp() + { + $this->assertSame('[trans]base.username_help[/trans]', $this->translatorExtension->getFieldHelp($this->view->children['username'])); + } + + public function testFieldErrors() + { + $errors = $this->rawExtension->getFieldErrors($this->view->children['username']); + $this->assertSame(['username.max_length'], iterator_to_array($errors)); + } + + public function testFieldTranslatedErrors() + { + $errors = $this->translatorExtension->getFieldErrors($this->view->children['username']); + $this->assertSame(['username.max_length'], iterator_to_array($errors)); + } + + public function testFieldChoicesFlat() + { + $choices = $this->rawExtension->getFieldChoices($this->view->children['choice_flat']); + + $choicesArray = []; + foreach ($choices as $label => $value) { + $choicesArray[] = ['label' => $label, 'value' => $value]; + } + + $this->assertCount(2, $choicesArray); + + $this->assertSame('yes', $choicesArray[0]['value']); + $this->assertSame('base.yes', $choicesArray[0]['label']); + + $this->assertSame('no', $choicesArray[1]['value']); + $this->assertSame('base.no', $choicesArray[1]['label']); + } + + public function testFieldTranslatedChoicesFlat() + { + $choices = $this->translatorExtension->getFieldChoices($this->view->children['choice_flat']); + + $choicesArray = []; + foreach ($choices as $label => $value) { + $choicesArray[] = ['label' => $label, 'value' => $value]; + } + + $this->assertCount(2, $choicesArray); + + $this->assertSame('yes', $choicesArray[0]['value']); + $this->assertSame('[trans]base.yes[/trans]', $choicesArray[0]['label']); + + $this->assertSame('no', $choicesArray[1]['value']); + $this->assertSame('[trans]base.no[/trans]', $choicesArray[1]['label']); + } + + public function testFieldChoicesGrouped() + { + $choices = $this->rawExtension->getFieldChoices($this->view->children['choice_grouped']); + + $choicesArray = []; + foreach ($choices as $groupLabel => $groupChoices) { + $groupChoicesArray = []; + foreach ($groupChoices as $label => $value) { + $groupChoicesArray[] = ['label' => $label, 'value' => $value]; + } + + $choicesArray[] = ['label' => $groupLabel, 'choices' => $groupChoicesArray]; + } + + $this->assertCount(2, $choicesArray); + + $this->assertCount(2, $choicesArray[0]['choices']); + $this->assertSame('base.europe', $choicesArray[0]['label']); + + $this->assertSame('fr', $choicesArray[0]['choices'][0]['value']); + $this->assertSame('base.fr', $choicesArray[0]['choices'][0]['label']); + + $this->assertSame('de', $choicesArray[0]['choices'][1]['value']); + $this->assertSame('base.de', $choicesArray[0]['choices'][1]['label']); + + $this->assertCount(2, $choicesArray[1]['choices']); + $this->assertSame('base.asia', $choicesArray[1]['label']); + + $this->assertSame('cn', $choicesArray[1]['choices'][0]['value']); + $this->assertSame('base.cn', $choicesArray[1]['choices'][0]['label']); + + $this->assertSame('jp', $choicesArray[1]['choices'][1]['value']); + $this->assertSame('base.jp', $choicesArray[1]['choices'][1]['label']); + } + + public function testFieldTranslatedChoicesGrouped() + { + $choices = $this->translatorExtension->getFieldChoices($this->view->children['choice_grouped']); + + $choicesArray = []; + foreach ($choices as $groupLabel => $groupChoices) { + $groupChoicesArray = []; + foreach ($groupChoices as $label => $value) { + $groupChoicesArray[] = ['label' => $label, 'value' => $value]; + } + + $choicesArray[] = ['label' => $groupLabel, 'choices' => $groupChoicesArray]; + } + + $this->assertCount(2, $choicesArray); + + $this->assertCount(2, $choicesArray[0]['choices']); + $this->assertSame('[trans]base.europe[/trans]', $choicesArray[0]['label']); + + $this->assertSame('fr', $choicesArray[0]['choices'][0]['value']); + $this->assertSame('[trans]base.fr[/trans]', $choicesArray[0]['choices'][0]['label']); + + $this->assertSame('de', $choicesArray[0]['choices'][1]['value']); + $this->assertSame('[trans]base.de[/trans]', $choicesArray[0]['choices'][1]['label']); + + $this->assertCount(2, $choicesArray[1]['choices']); + $this->assertSame('[trans]base.asia[/trans]', $choicesArray[1]['label']); + + $this->assertSame('cn', $choicesArray[1]['choices'][0]['value']); + $this->assertSame('[trans]base.cn[/trans]', $choicesArray[1]['choices'][0]['label']); + + $this->assertSame('jp', $choicesArray[1]['choices'][1]['value']); + $this->assertSame('[trans]base.jp[/trans]', $choicesArray[1]['choices'][1]['label']); + } +} diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/TranslationExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/TranslationExtensionTest.php index 28149e1315549..b0e59d72420ab 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/TranslationExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/TranslationExtensionTest.php @@ -122,6 +122,18 @@ public function getTransTests() // trans filter with null message ['{{ null|trans }}', ''], ['{{ foo|trans }}', '', ['foo' => null]], + + // trans object + ['{{ t("Hello")|trans }}', 'Hello'], + ['{{ t(name)|trans }}', 'Symfony', ['name' => 'Symfony']], + ['{{ t(hello, { \'%name%\': \'Symfony\' })|trans }}', 'Hello Symfony', ['hello' => 'Hello %name%']], + ['{% set vars = { \'%name%\': \'Symfony\' } %}{{ t(hello, vars)|trans }}', 'Hello Symfony', ['hello' => 'Hello %name%']], + ['{{ t("Hello")|trans("fr") }}', 'Hello'], + ['{{ t("Hello")|trans(locale="fr") }}', 'Hello'], + ['{{ t("Hello", {}, "messages")|trans(locale="fr") }}', 'Hello'], + + // trans object with count + ['{{ t("{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples", {\'%count%\': count})|trans }}', 'There is 5 apples', ['count' => 5]], ]; } diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/WorkflowExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/WorkflowExtensionTest.php index 57a09b0a7e918..23dcc64b3d418 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/WorkflowExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/WorkflowExtensionTest.php @@ -81,6 +81,16 @@ public function testGetEnabledTransitions() $this->assertSame('t1', $transitions[0]->getName()); } + public function testGetEnabledTransition() + { + $subject = new Subject(); + + $transition = $this->extension->getEnabledTransition($subject, 't1'); + + $this->assertInstanceOf(Transition::class, $transition); + $this->assertSame('t1', $transition->getName()); + } + public function testHasMarkedPlace() { $subject = new Subject(['ordered' => 1, 'waiting_for_payment' => 1]); diff --git a/src/Symfony/Bridge/Twig/Tests/Mime/TemplatedEmailTest.php b/src/Symfony/Bridge/Twig/Tests/Mime/TemplatedEmailTest.php index 999ca4d078d58..2da04921446eb 100644 --- a/src/Symfony/Bridge/Twig/Tests/Mime/TemplatedEmailTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Mime/TemplatedEmailTest.php @@ -4,6 +4,13 @@ use PHPUnit\Framework\TestCase; use Symfony\Bridge\Twig\Mime\TemplatedEmail; +use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; +use Symfony\Component\Serializer\Encoder\JsonEncoder; +use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; +use Symfony\Component\Serializer\Normalizer\MimeMessageNormalizer; +use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; +use Symfony\Component\Serializer\Normalizer\PropertyNormalizer; +use Symfony\Component\Serializer\Serializer; class TemplatedEmailTest extends TestCase { @@ -33,4 +40,77 @@ public function testSerialize() $this->assertEquals('text.html.twig', $email->getHtmlTemplate()); $this->assertEquals($context, $email->getContext()); } + + public function testSymfonySerialize() + { + // we don't add from/sender to check that validation is not triggered to serialize an email + $e = new TemplatedEmail(); + $e->to('you@example.com'); + $e->textTemplate('email.txt.twig'); + $e->htmlTemplate('email.html.twig'); + $e->context(['foo' => 'bar']); + $e->attach('Some Text file', 'test.txt'); + $expected = clone $e; + + $expectedJson = <<serialize($e, 'json'); + $this->assertSame($expectedJson, json_encode(json_decode($serialized), \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES)); + + $n = $serializer->deserialize($serialized, TemplatedEmail::class, 'json'); + $serialized = $serializer->serialize($e, 'json'); + $this->assertSame($expectedJson, json_encode(json_decode($serialized), \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES)); + + $n->from('fabien@symfony.com'); + $expected->from('fabien@symfony.com'); + $this->assertEquals($expected->getHeaders(), $n->getHeaders()); + $this->assertEquals($expected->getBody(), $n->getBody()); + } } diff --git a/src/Symfony/Bridge/Twig/Tests/Translation/TwigExtractorTest.php b/src/Symfony/Bridge/Twig/Tests/Translation/TwigExtractorTest.php index b8fa860f37980..03e2936d42ca2 100644 --- a/src/Symfony/Bridge/Twig/Tests/Translation/TwigExtractorTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Translation/TwigExtractorTest.php @@ -60,6 +60,9 @@ public function getExtractData() ['{% trans from "domain" %}new key{% endtrans %}', ['new key' => 'domain']], ['{% set foo = "new key" | trans %}', ['new key' => 'messages']], ['{{ 1 ? "new key" | trans : "another key" | trans }}', ['new key' => 'messages', 'another key' => 'messages']], + ['{{ t("new key") | trans() }}', ['new key' => 'messages']], + ['{{ t("new key", {}, "domain") | trans() }}', ['new key' => 'domain']], + ['{{ 1 ? t("new key") | trans : t("another key") | trans }}', ['new key' => 'messages', 'another key' => 'messages']], // make sure 'trans_default_domain' tag is supported ['{% trans_default_domain "domain" %}{{ "new key"|trans }}', ['new key' => 'domain']], diff --git a/src/Symfony/Bridge/Twig/composer.json b/src/Symfony/Bridge/Twig/composer.json index 2f89f709f7127..659d4c8ff364d 100644 --- a/src/Symfony/Bridge/Twig/composer.json +++ b/src/Symfony/Bridge/Twig/composer.json @@ -23,26 +23,29 @@ }, "require-dev": { "egulias/email-validator": "^2.1.10", + "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.1", "symfony/http-foundation": "^4.4|^5.0", "symfony/http-kernel": "^4.4|^5.0", - "symfony/mime": "^4.4|^5.0", + "symfony/mime": "^5.2", "symfony/polyfill-intl-icu": "~1.0", + "symfony/property-info": "^4.4|^5.1", "symfony/routing": "^4.4|^5.0", - "symfony/translation": "^5.0", + "symfony/translation": "^5.2", "symfony/yaml": "^4.4|^5.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": "^4.4|^5.0", + "symfony/workflow": "^5.2", "twig/cssinliner-extra": "^2.12", "twig/inky-extra": "^2.12", "twig/markdown-extra": "^2.12" @@ -52,8 +55,8 @@ "symfony/form": "<5.1", "symfony/http-foundation": "<4.4", "symfony/http-kernel": "<4.4", - "symfony/translation": "<5.0", - "symfony/workflow": "<4.4" + "symfony/translation": "<5.2", + "symfony/workflow": "<5.2" }, "suggest": { "symfony/finder": "", @@ -80,7 +83,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "5.1-dev" + "dev-master": "5.2-dev" } } } diff --git a/src/Symfony/Bundle/DebugBundle/DependencyInjection/DebugExtension.php b/src/Symfony/Bundle/DebugBundle/DependencyInjection/DebugExtension.php index 2528cce5b83a7..b607a4314b5ed 100644 --- a/src/Symfony/Bundle/DebugBundle/DependencyInjection/DebugExtension.php +++ b/src/Symfony/Bundle/DebugBundle/DependencyInjection/DebugExtension.php @@ -16,7 +16,7 @@ use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\Extension; -use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; +use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\VarDumper\Caster\ReflectionCaster; use Symfony\Component\VarDumper\Dumper\CliDumper; @@ -37,8 +37,8 @@ public function load(array $configs, ContainerBuilder $container) $configuration = new Configuration(); $config = $this->processConfiguration($configuration, $configs); - $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); - $loader->load('services.xml'); + $loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); + $loader->load('services.php'); $container->getDefinition('var_dumper.cloner') ->addMethodCall('setMaxItems', [$config['max_items']]) diff --git a/src/Symfony/Bundle/DebugBundle/Resources/config/services.php b/src/Symfony/Bundle/DebugBundle/Resources/config/services.php new file mode 100644 index 0000000000000..abde96d0625ec --- /dev/null +++ b/src/Symfony/Bundle/DebugBundle/Resources/config/services.php @@ -0,0 +1,135 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Bridge\Monolog\Command\ServerLogCommand; +use Symfony\Bridge\Twig\Extension\DumpExtension; +use Symfony\Component\HttpKernel\DataCollector\DumpDataCollector; +use Symfony\Component\HttpKernel\EventListener\DumpListener; +use Symfony\Component\VarDumper\Cloner\VarCloner; +use Symfony\Component\VarDumper\Command\Descriptor\CliDescriptor; +use Symfony\Component\VarDumper\Command\Descriptor\HtmlDescriptor; +use Symfony\Component\VarDumper\Command\ServerDumpCommand; +use Symfony\Component\VarDumper\Dumper\CliDumper; +use Symfony\Component\VarDumper\Dumper\ContextProvider\CliContextProvider; +use Symfony\Component\VarDumper\Dumper\ContextProvider\RequestContextProvider; +use Symfony\Component\VarDumper\Dumper\ContextProvider\SourceContextProvider; +use Symfony\Component\VarDumper\Dumper\ContextualizedDumper; +use Symfony\Component\VarDumper\Dumper\HtmlDumper; +use Symfony\Component\VarDumper\Server\Connection; +use Symfony\Component\VarDumper\Server\DumpServer; + +return static function (ContainerConfigurator $container) { + $container->parameters() + ->set('env(VAR_DUMPER_SERVER)', '127.0.0.1:9912') + ; + + $container->services() + + ->set('twig.extension.dump', DumpExtension::class) + ->args([ + service('var_dumper.cloner'), + service('var_dumper.html_dumper'), + ]) + ->tag('twig.extension') + + ->set('data_collector.dump', DumpDataCollector::class) + ->public() + ->args([ + service('debug.stopwatch')->ignoreOnInvalid(), + service('debug.file_link_formatter')->ignoreOnInvalid(), + param('kernel.charset'), + service('request_stack'), + null, // var_dumper.cli_dumper or var_dumper.server_connection when debug.dump_destination is set + ]) + ->tag('data_collector', [ + 'id' => 'dump', + 'template' => '@Debug/Profiler/dump.html.twig', + 'priority' => 240, + ]) + + ->set('debug.dump_listener', DumpListener::class) + ->args([ + service('var_dumper.cloner'), + service('var_dumper.cli_dumper'), + null, + ]) + ->tag('kernel.event_subscriber') + + ->set('var_dumper.cloner', VarCloner::class) + ->public() + + ->set('var_dumper.cli_dumper', CliDumper::class) + ->args([ + null, // debug.dump_destination, + param('kernel.charset'), + 0, // flags + ]) + + ->set('var_dumper.contextualized_cli_dumper', ContextualizedDumper::class) + ->decorate('var_dumper.cli_dumper') + ->args([ + service('var_dumper.contextualized_cli_dumper.inner'), + [ + 'source' => inline_service(SourceContextProvider::class)->args([ + param('kernel.charset'), + param('kernel.project_dir'), + service('debug.file_link_formatter')->nullOnInvalid(), + ]), + ], + ]) + + ->set('var_dumper.html_dumper', HtmlDumper::class) + ->args([ + null, + param('kernel.charset'), + 0, // flags + ]) + ->call('setDisplayOptions', [ + ['fileLinkFormat' => service('debug.file_link_formatter')->ignoreOnInvalid()], + ]) + + ->set('var_dumper.server_connection', Connection::class) + ->args([ + '', // server host + [ + 'source' => inline_service(SourceContextProvider::class)->args([ + param('kernel.charset'), + param('kernel.project_dir'), + service('debug.file_link_formatter')->nullOnInvalid(), + ]), + 'request' => inline_service(RequestContextProvider::class)->args([service('request_stack')]), + 'cli' => inline_service(CliContextProvider::class), + ], + ]) + + ->set('var_dumper.dump_server', DumpServer::class) + ->args([ + '', // server host + service('logger')->nullOnInvalid(), + ]) + ->tag('monolog.logger', ['channel' => 'debug']) + + ->set('var_dumper.command.server_dump', ServerDumpCommand::class) + ->args([ + service('var_dumper.dump_server'), + [ + 'cli' => inline_service(CliDescriptor::class)->args([service('var_dumper.contextualized_cli_dumper.inner')]), + 'html' => inline_service(HtmlDescriptor::class)->args([service('var_dumper.html_dumper')]), + ], + ]) + ->tag('console.command', ['command' => 'server:dump']) + + ->set('monolog.command.server_log', ServerLogCommand::class) + ->tag('console.command', ['command' => 'server:log']) + ; +}; diff --git a/src/Symfony/Bundle/DebugBundle/Resources/config/services.xml b/src/Symfony/Bundle/DebugBundle/Resources/config/services.xml deleted file mode 100644 index c7cc5725cf8f8..0000000000000 --- a/src/Symfony/Bundle/DebugBundle/Resources/config/services.xml +++ /dev/null @@ -1,115 +0,0 @@ - - - - - - 127.0.0.1:9912 - - - - - - - - - - - - - - - - %kernel.charset% - - null - - - - - - - null - - - - - null - %kernel.charset% - 0 - - - - - - - - %kernel.charset% - %kernel.project_dir% - - - - - - - - null - %kernel.charset% - 0 - - - - - - - - - - - - - %kernel.charset% - %kernel.project_dir% - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/DebugBundle/Tests/DependencyInjection/DebugExtensionTest.php b/src/Symfony/Bundle/DebugBundle/Tests/DependencyInjection/DebugExtensionTest.php index 1f85a1a31696e..31afae4d93acb 100644 --- a/src/Symfony/Bundle/DebugBundle/Tests/DependencyInjection/DebugExtensionTest.php +++ b/src/Symfony/Bundle/DebugBundle/Tests/DependencyInjection/DebugExtensionTest.php @@ -15,7 +15,11 @@ use Symfony\Bundle\DebugBundle\DependencyInjection\DebugExtension; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; +use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\VarDumper\Caster\ReflectionCaster; +use Symfony\Component\VarDumper\Dumper\CliDumper; +use Symfony\Component\VarDumper\Server\Connection; +use Symfony\Component\VarDumper\Server\DumpServer; class DebugExtensionTest extends TestCase { @@ -70,12 +74,50 @@ public function testUnsetClosureFileInfoShouldBeRegisteredInVarCloner() $this->assertTrue($called); } + public function provideServicesUsingDumpDestinationCreation(): array + { + return [ + ['tcp://localhost:1234', 'tcp://localhost:1234', null], + [null, '', null], + ['php://stderr', '', 'php://stderr'], + ]; + } + + /** + * @dataProvider provideServicesUsingDumpDestinationCreation + */ + public function testServicesUsingDumpDestinationCreation(?string $dumpDestination, string $expectedHost, ?string $expectedOutput) + { + $container = $this->createContainer(); + $container->registerExtension(new DebugExtension()); + $container->loadFromExtension('debug', ['dump_destination' => $dumpDestination]); + $container->setAlias('dump_server_public', 'var_dumper.dump_server')->setPublic(true); + $container->setAlias('server_conn_public', 'var_dumper.server_connection')->setPublic(true); + $container->setAlias('cli_dumper_public', 'var_dumper.cli_dumper')->setPublic(true); + $container->register('request_stack', RequestStack::class); + $this->compileContainer($container); + + $dumpServer = $container->get('dump_server_public'); + $this->assertInstanceOf(DumpServer::class, $dumpServer); + $this->assertSame($expectedHost, $container->findDefinition('dump_server_public')->getArgument(0)); + + $serverConn = $container->get('server_conn_public'); + $this->assertInstanceOf(Connection::class, $serverConn); + $this->assertSame($expectedHost, $container->findDefinition('server_conn_public')->getArgument(0)); + + $cliDumper = $container->get('cli_dumper_public'); + $this->assertInstanceOf(CliDumper::class, $cliDumper); + $this->assertSame($expectedOutput, $container->findDefinition('cli_dumper_public')->getArgument(0)); + } + private function createContainer() { $container = new ContainerBuilder(new ParameterBag([ 'kernel.cache_dir' => __DIR__, + 'kernel.build_dir' => __DIR__, 'kernel.charset' => 'UTF-8', 'kernel.debug' => true, + 'kernel.project_dir' => __DIR__, 'kernel.bundles' => ['DebugBundle' => 'Symfony\\Bundle\\DebugBundle\\DebugBundle'], ])); diff --git a/src/Symfony/Bundle/DebugBundle/composer.json b/src/Symfony/Bundle/DebugBundle/composer.json index 34d5a39c01662..b429b797ab735 100644 --- a/src/Symfony/Bundle/DebugBundle/composer.json +++ b/src/Symfony/Bundle/DebugBundle/composer.json @@ -29,7 +29,7 @@ }, "conflict": { "symfony/config": "<4.4", - "symfony/dependency-injection": "<4.4" + "symfony/dependency-injection": "<5.2" }, "suggest": { "symfony/config": "For service container configuration", @@ -44,7 +44,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "5.1-dev" + "dev-master": "5.2-dev" } } } diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index 9815c8fd12616..a734ef682a686 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -1,6 +1,19 @@ CHANGELOG ========= +5.2.0 +----- + + * Added `framework.http_cache` configuration tree + * Added `framework.trusted_proxies` and `framework.trusted_headers` configuration options + * Deprecated the public `form.factory`, `form.type.file`, `translator`, `security.csrf.token_manager`, `serializer`, + `cache_clearer`, `filesystem` and `validator` services to private. + * Added `TemplateAwareDataCollectorInterface` and `AbstractDataCollector` to simplify custom data collector creation and leverage autoconfiguration + * Add `cache.adapter.redis_tag_aware` tag to use `RedisCacheAwareAdapter` + * added `framework.http_client.retry_failing` configuration tree + * added `assertCheckboxChecked()` and `assertCheckboxNotChecked()` in `WebTestCase` + * added `assertFormValue()` and `assertNoFormValue()` in `WebTestCase` + 5.1.0 ----- diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/AboutCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/AboutCommand.php index a776e18d163d8..e39772edf4db1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/AboutCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/AboutCommand.php @@ -61,13 +61,19 @@ protected function execute(InputInterface $input, OutputInterface $output): int /** @var KernelInterface $kernel */ $kernel = $this->getApplication()->getKernel(); + if (method_exists($kernel, 'getBuildDir')) { + $buildDir = $kernel->getBuildDir(); + } else { + $buildDir = $kernel->getCacheDir(); + } + $rows = [ ['Symfony'], new TableSeparator(), ['Version', Kernel::VERSION], ['Long-Term Support', 4 === Kernel::MINOR_VERSION ? 'Yes' : 'No'], - ['End of maintenance', Kernel::END_OF_MAINTENANCE.(self::isExpired(Kernel::END_OF_MAINTENANCE) ? ' Expired' : '')], - ['End of life', Kernel::END_OF_LIFE.(self::isExpired(Kernel::END_OF_LIFE) ? ' Expired' : '')], + ['End of maintenance', Kernel::END_OF_MAINTENANCE.(self::isExpired(Kernel::END_OF_MAINTENANCE) ? ' Expired' : ' ('.self::daysBeforeExpiration(Kernel::END_OF_MAINTENANCE).')')], + ['End of life', Kernel::END_OF_LIFE.(self::isExpired(Kernel::END_OF_LIFE) ? ' Expired' : ' ('.self::daysBeforeExpiration(Kernel::END_OF_LIFE).')')], new TableSeparator(), ['Kernel'], new TableSeparator(), @@ -76,6 +82,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int ['Debug', $kernel->isDebug() ? 'true' : 'false'], ['Charset', $kernel->getCharset()], ['Cache directory', self::formatPath($kernel->getCacheDir(), $kernel->getProjectDir()).' ('.self::formatFileSize($kernel->getCacheDir()).')'], + ['Build directory', self::formatPath($buildDir, $kernel->getProjectDir()).' ('.self::formatFileSize($buildDir).')'], ['Log directory', self::formatPath($kernel->getLogDir(), $kernel->getProjectDir()).' ('.self::formatFileSize($kernel->getLogDir()).')'], new TableSeparator(), ['PHP'], @@ -119,4 +126,11 @@ private static function isExpired(string $date): bool return false !== $date && new \DateTime() > $date->modify('last day of this month 23:59:59'); } + + private static function daysBeforeExpiration(string $date): string + { + $date = \DateTime::createFromFormat('d/m/Y', '01/'.$date); + + return (new \DateTime())->diff($date->modify('last day of this month 23:59:59'))->format('in %R%a days'); + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php index 29791ab119c31..0dada3d7d172f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php @@ -79,17 +79,23 @@ protected function execute(InputInterface $input, OutputInterface $output): int $io = new SymfonyStyle($input, $output); $kernel = $this->getApplication()->getKernel(); + $realBuildDir = $kernel->getContainer()->getParameter('kernel.build_dir'); $realCacheDir = $kernel->getContainer()->getParameter('kernel.cache_dir'); // the old cache dir name must not be longer than the real one to avoid exceeding // the maximum length of a directory or file path within it (esp. Windows MAX_PATH) + $oldBuildDir = substr($realBuildDir, 0, -1).('~' === substr($realBuildDir, -1) ? '+' : '~'); $oldCacheDir = substr($realCacheDir, 0, -1).('~' === substr($realCacheDir, -1) ? '+' : '~'); - $fs->remove($oldCacheDir); + $fs->remove([$oldBuildDir, $oldCacheDir]); + if (!is_writable($realBuildDir)) { + throw new RuntimeException(sprintf('Unable to write in the "%s" directory.', $realBuildDir)); + } if (!is_writable($realCacheDir)) { throw new RuntimeException(sprintf('Unable to write in the "%s" directory.', $realCacheDir)); } $io->comment(sprintf('Clearing the cache for the %s environment with debug %s', $kernel->getEnvironment(), var_export($kernel->isDebug(), true))); + $this->cacheClearer->clear($realBuildDir); $this->cacheClearer->clear($realCacheDir); // The current event dispatcher is stale, let's not use it anymore @@ -155,17 +161,31 @@ protected function execute(InputInterface $input, OutputInterface $output): int } } + if ($oldBuildDir) { + $fs->rename($realBuildDir, $oldBuildDir); + } else { + $fs->remove($realBuildDir); + } if ($oldCacheDir) { $fs->rename($realCacheDir, $oldCacheDir); } else { $fs->remove($realCacheDir); } $fs->rename($warmupDir, $realCacheDir); + // Copy the content of the warmed cache in the build dir + $fs->copy($realCacheDir, $realBuildDir); if ($output->isVerbose()) { - $io->comment('Removing old cache directory...'); + $io->comment('Removing old build and cache directory...'); } + try { + $fs->remove($oldBuildDir); + } catch (IOException $e) { + if ($output->isVerbose()) { + $io->warning($e->getMessage()); + } + } try { $fs->remove($oldCacheDir); } catch (IOException $e) { @@ -184,7 +204,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 0; } - private function warmup(string $warmupDir, string $realCacheDir, bool $enableOptionalWarmers = true) + private function warmup(string $warmupDir, string $realBuildDir, bool $enableOptionalWarmers = true) { // create a temporary kernel $kernel = $this->getApplication()->getKernel(); @@ -207,7 +227,7 @@ private function warmup(string $warmupDir, string $realCacheDir, bool $enableOpt // fix references to cached files with the real cache directory name $search = [$warmupDir, str_replace('\\', '\\\\', $warmupDir)]; - $replace = str_replace('\\', '/', $realCacheDir); + $replace = str_replace('\\', '/', $realBuildDir); foreach (Finder::create()->files()->in($warmupDir) as $file) { $content = str_replace($search, $replace, file_get_contents($file), $count); if ($count) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/RedirectController.php b/src/Symfony/Bundle/FrameworkBundle/Controller/RedirectController.php index 1be9f99f238c7..7ab276128fbb1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/RedirectController.php +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/RedirectController.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Controller; +use Symfony\Component\HttpFoundation\HeaderUtils; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -65,7 +66,7 @@ public function redirectAction(Request $request, string $route, bool $permanent if ($keepQueryParams) { if ($query = $request->server->get('QUERY_STRING')) { - $query = self::parseQuery($query); + $query = HeaderUtils::parseQuery($query); } else { $query = $request->query->all(); } @@ -185,49 +186,4 @@ public function __invoke(Request $request): Response throw new \RuntimeException(sprintf('The parameter "path" or "route" is required to configure the redirect action in "%s" routing configuration.', $request->attributes->get('_route'))); } - - private static function parseQuery(string $query) - { - $q = []; - - foreach (explode('&', $query) as $v) { - if (false !== $i = strpos($v, "\0")) { - $v = substr($v, 0, $i); - } - - if (false === $i = strpos($v, '=')) { - $k = urldecode($v); - $v = ''; - } else { - $k = urldecode(substr($v, 0, $i)); - $v = substr($v, $i); - } - - if (false !== $i = strpos($k, "\0")) { - $k = substr($k, 0, $i); - } - - $k = ltrim($k, ' '); - - if (false === $i = strpos($k, '[')) { - $q[] = bin2hex($k).$v; - } else { - $q[] = substr_replace($k, bin2hex(substr($k, 0, $i)), 0, $i).$v; - } - } - - parse_str(implode('&', $q), $q); - - $query = []; - - foreach ($q as $k => $v) { - if (false !== $i = strpos($k, '_')) { - $query[substr_replace($k, hex2bin(substr($k, 0, $i)).'[', 0, 1 + $i)] = $v; - } else { - $query[hex2bin($k)] = $v; - } - } - - return $query; - } } diff --git a/src/Symfony/Bundle/FrameworkBundle/DataCollector/AbstractDataCollector.php b/src/Symfony/Bundle/FrameworkBundle/DataCollector/AbstractDataCollector.php new file mode 100644 index 0000000000000..428919b963bbf --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/DataCollector/AbstractDataCollector.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\DataCollector; + +/** + * @author Laurent VOULLEMIER + */ +abstract class AbstractDataCollector implements TemplateAwareDataCollectorInterface +{ + /** + * @var array + */ + protected $data = []; + + public function getName(): string + { + return static::class; + } + + public function reset(): void + { + $this->data = []; + } + + public static function getTemplate(): ?string + { + return null; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/DataCollector/TemplateAwareDataCollectorInterface.php b/src/Symfony/Bundle/FrameworkBundle/DataCollector/TemplateAwareDataCollectorInterface.php new file mode 100644 index 0000000000000..5ef17abc54983 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/DataCollector/TemplateAwareDataCollectorInterface.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\DataCollector; + +use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface; + +/** + * @author Laurent VOULLEMIER + */ +interface TemplateAwareDataCollectorInterface extends DataCollectorInterface +{ + public static function getTemplate(): ?string; +} diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/ProfilerPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/ProfilerPass.php index 4318081db32b6..a3339ba355576 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/ProfilerPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/ProfilerPass.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler; +use Symfony\Bundle\FrameworkBundle\DataCollector\TemplateAwareDataCollectorInterface; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; @@ -37,11 +38,14 @@ public function process(ContainerBuilder $container) $priority = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0; $template = null; - if (isset($attributes[0]['template'])) { - if (!isset($attributes[0]['id'])) { + $collectorClass = $container->findDefinition($id)->getClass(); + $isTemplateAware = is_subclass_of($collectorClass, TemplateAwareDataCollectorInterface::class); + if (isset($attributes[0]['template']) || $isTemplateAware) { + $idForTemplate = $attributes[0]['id'] ?? $collectorClass; + if (!$idForTemplate) { throw new InvalidArgumentException(sprintf('Data collector service "%s" must have an id attribute in order to specify a template.', $id)); } - $template = [$attributes[0]['id'], $attributes[0]['template']]; + $template = [$idForTemplate, $attributes[0]['template'] ?? $collectorClass::getTemplate()]; } $collectors->insert([$id, $template], [$priority, --$order]); diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php index 7c2e89790d3d6..37f545468d813 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php @@ -74,6 +74,7 @@ class UnusedTagsPass implements CompilerPassInterface 'routing.route_loader', 'security.expression_language_provider', 'security.remember_me_aware', + 'security.authenticator.login_linker', 'security.voter', 'serializer.encoder', 'serializer.normalizer', diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 0070353a71d6c..b58b2ec220bbc 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -17,6 +17,7 @@ use Symfony\Bundle\FullStack; use Symfony\Component\Asset\Package; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\Config\Definition\Builder\NodeBuilder; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; @@ -30,10 +31,12 @@ use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Notifier\Notifier; use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface; +use Symfony\Component\RateLimiter\TokenBucketLimiter; use Symfony\Component\Serializer\Serializer; use Symfony\Component\Translation\Translator; use Symfony\Component\Validator\Validation; use Symfony\Component\WebLink\HttpHeaderSerializer; +use Symfony\Component\Workflow\WorkflowEvents; /** * FrameworkExtension configuration structure. @@ -85,6 +88,20 @@ public function getConfigTreeBuilder() ->beforeNormalization()->ifString()->then(function ($v) { return [$v]; })->end() ->prototype('scalar')->end() ->end() + ->scalarNode('trusted_proxies')->end() + ->arrayNode('trusted_headers') + ->fixXmlConfig('trusted_header') + ->performNoDeepMerging() + ->defaultValue(['x-forwarded-all', '!x-forwarded-host', '!x-forwarded-prefix']) + ->beforeNormalization()->ifString()->then(function ($v) { return $v ? array_map('trim', explode(',', $v)) : []; })->end() + ->enumPrototype() + ->values([ + 'forwarded', + 'x-forwarded-for', 'x-forwarded-host', 'x-forwarded-proto', 'x-forwarded-port', + 'x-forwarded-all', '!x-forwarded-host', '!x-forwarded-prefix', + ]) + ->end() + ->end() ->scalarNode('error_controller') ->defaultValue('error_controller') ->end() @@ -93,6 +110,7 @@ public function getConfigTreeBuilder() $this->addCsrfSection($rootNode); $this->addFormSection($rootNode); + $this->addHttpCacheSection($rootNode); $this->addEsiSection($rootNode); $this->addSsiSection($rootNode); $this->addFragmentsSection($rootNode); @@ -118,6 +136,7 @@ public function getConfigTreeBuilder() $this->addMailerSection($rootNode); $this->addSecretsSection($rootNode); $this->addNotifierSection($rootNode); + $this->addRateLimiterSection($rootNode); return $treeBuilder; } @@ -174,6 +193,48 @@ private function addFormSection(ArrayNodeDefinition $rootNode) ->scalarNode('field_name')->defaultValue('_token')->end() ->end() ->end() + // to be set to false in Symfony 6.0 + ->booleanNode('legacy_error_messages') + ->defaultTrue() + ->validate() + ->ifTrue() + ->then(function ($v) { + @trigger_error('Since symfony/framework-bundle 5.2: Setting the "framework.form.legacy_error_messages" option to "true" is deprecated. It will have no effect as of Symfony 6.0.', \E_USER_DEPRECATED); + + return $v; + }) + ->end() + ->end() + ->end() + ->end() + ->end() + ; + } + + private function addHttpCacheSection(ArrayNodeDefinition $rootNode) + { + $rootNode + ->children() + ->arrayNode('http_cache') + ->info('HTTP cache configuration') + ->canBeEnabled() + ->children() + ->booleanNode('debug')->defaultValue('%kernel.debug%')->end() + ->enumNode('trace_level') + ->values(['none', 'short', 'full']) + ->end() + ->scalarNode('trace_header')->end() + ->integerNode('default_ttl')->end() + ->arrayNode('private_headers') + ->performNoDeepMerging() + ->requiresAtLeastOneElement() + ->fixXmlConfig('private_header') + ->scalarPrototype()->end() + ->end() + ->booleanNode('allow_reload')->end() + ->booleanNode('allow_revalidate')->end() + ->integerNode('stale_while_revalidate')->end() + ->integerNode('stale_if_error')->end() ->end() ->end() ->end() @@ -282,6 +343,7 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode) ->fixXmlConfig('support') ->fixXmlConfig('place') ->fixXmlConfig('transition') + ->fixXmlConfig('event_to_dispatch', 'events_to_dispatch') ->children() ->arrayNode('audit_trail') ->canBeEnabled() @@ -324,6 +386,33 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode) ->defaultValue([]) ->prototype('scalar')->end() ->end() + ->variableNode('events_to_dispatch') + ->defaultValue(null) + ->validate() + ->ifTrue(function ($v) { + if (null === $v) { + return false; + } + if (!\is_array($v)) { + return true; + } + + foreach ($v as $value) { + if (!\is_string($value)) { + return true; + } + if (class_exists(WorkflowEvents::class) && !\in_array($value, WorkflowEvents::ALIASES)) { + return true; + } + } + + return false; + }) + ->thenInvalid('The value must be "null" or an array of workflow events (like ["workflow.enter"]).') + ->end() + ->info('Select which Transition events should be dispatched for this Workflow') + ->example(['workflow.enter', 'workflow.transition']) + ->end() ->arrayNode('places') ->beforeNormalization() ->always() @@ -452,6 +541,18 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode) }) ->thenInvalid('"supports" or "support_strategy" should be configured.') ->end() + ->beforeNormalization() + ->always() + ->then(function ($values) { + // Special case to deal with XML when the user wants an empty array + if (\array_key_exists('event_to_dispatch', $values) && null === $values['event_to_dispatch']) { + $values['events_to_dispatch'] = []; + unset($values['event_to_dispatch']); + } + + return $values; + }) + ->end() ->end() ->end() ->end() @@ -686,6 +787,22 @@ private function addTranslatorSection(ArrayNodeDefinition $rootNode) ->prototype('scalar')->end() ->defaultValue([]) ->end() + ->arrayNode('pseudo_localization') + ->canBeEnabled() + ->fixXmlConfig('localizable_html_attribute') + ->children() + ->booleanNode('accents')->defaultTrue()->end() + ->floatNode('expansion_factor') + ->min(1.0) + ->defaultValue(1.0) + ->end() + ->booleanNode('brackets')->defaultTrue()->end() + ->booleanNode('parse_html')->defaultFalse()->end() + ->arrayNode('localizable_html_attributes') + ->prototype('scalar')->end() + ->end() + ->end() + ->end() ->end() ->end() ->end() @@ -835,6 +952,8 @@ private function addPropertyAccessSection(ArrayNodeDefinition $rootNode) ->info('Property access configuration') ->children() ->booleanNode('magic_call')->defaultFalse()->end() + ->booleanNode('magic_get')->defaultTrue()->end() + ->booleanNode('magic_set')->defaultTrue()->end() ->booleanNode('throw_exception_on_invalid_index')->defaultFalse()->end() ->booleanNode('throw_exception_on_invalid_property_path')->defaultTrue()->end() ->end() @@ -866,7 +985,8 @@ private function addCacheSection(ArrayNodeDefinition $rootNode) ->children() ->scalarNode('prefix_seed') ->info('Used to namespace cache keys when using several apps with the same shared backend') - ->example('my-application-name') + ->defaultValue('_%kernel.project_dir%.%kernel.container_class%') + ->example('my-application-name/%kernel.environment%') ->end() ->scalarNode('app') ->info('App related cache pools configuration') @@ -924,6 +1044,9 @@ private function addCacheSection(ArrayNodeDefinition $rootNode) ->scalarNode('provider') ->info('Overwrite the setting from the default provider for this adapter.') ->end() + ->scalarNode('early_expiration_message_bus') + ->example('"messenger.default_bus" to send early expiration events to the default Messenger bus.') + ->end() ->scalarNode('clearer')->end() ->end() ->end() @@ -1247,6 +1370,25 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode) ->info('HTTP Client configuration') ->{!class_exists(FullStack::class) && class_exists(HttpClient::class) ? 'canBeDisabled' : 'canBeEnabled'}() ->fixXmlConfig('scoped_client') + ->beforeNormalization() + ->always(function ($config) { + if (empty($config['scoped_clients']) || !\is_array($config['default_options']['retry_failed'] ?? null)) { + return $config; + } + + foreach ($config['scoped_clients'] as &$scopedConfig) { + if (!isset($scopedConfig['retry_failed']) || true === $scopedConfig['retry_failed']) { + $scopedConfig['retry_failed'] = $config['default_options']['retry_failed']; + continue; + } + if (\is_array($scopedConfig['retry_failed'])) { + $scopedConfig['retry_failed'] = $scopedConfig['retry_failed'] + $config['default_options']['retry_failed']; + } + } + + return $config; + }) + ->end() ->children() ->integerNode('max_host_connections') ->info('The maximum number of connections to a single host.') @@ -1332,8 +1474,12 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode) ->variableNode('md5')->end() ->end() ->end() + ->append($this->addHttpClientRetrySection()) ->end() ->end() + ->scalarNode('mock_response_factory') + ->info('The id of the service that should generate mock responses. It should be either an invokable or an iterable.') + ->end() ->arrayNode('scoped_clients') ->useAttributeAsKey('name') ->normalizeKeys(false) @@ -1471,6 +1617,7 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode) ->variableNode('md5')->end() ->end() ->end() + ->append($this->addHttpClientRetrySection()) ->end() ->end() ->end() @@ -1480,6 +1627,50 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode) ; } + private function addHttpClientRetrySection() + { + $root = new NodeBuilder(); + + return $root + ->arrayNode('retry_failed') + ->fixXmlConfig('http_code') + ->canBeEnabled() + ->addDefaultsIfNotSet() + ->beforeNormalization() + ->always(function ($v) { + if (isset($v['backoff_service']) && (isset($v['delay']) || isset($v['multiplier']) || isset($v['max_delay']))) { + throw new \InvalidArgumentException('The "backoff_service" option cannot be used along with the "delay", "multiplier" or "max_delay" options.'); + } + if (isset($v['decider_service']) && (isset($v['http_codes']))) { + throw new \InvalidArgumentException('The "decider_service" option cannot be used along with the "http_codes" options.'); + } + + return $v; + }) + ->end() + ->children() + ->scalarNode('backoff_service')->defaultNull()->info('service id to override the retry backoff')->end() + ->scalarNode('decider_service')->defaultNull()->info('service id to override the retry decider')->end() + ->arrayNode('http_codes') + ->performNoDeepMerging() + ->beforeNormalization() + ->ifArray() + ->then(function ($v) { + return array_filter(array_values($v)); + }) + ->end() + ->prototype('integer')->end() + ->info('A list of HTTP status code that triggers a retry') + ->defaultValue([423, 425, 429, 500, 502, 503, 504, 507, 510]) + ->end() + ->integerNode('max_retries')->defaultValue(3)->min(0)->end() + ->integerNode('delay')->defaultValue(1000)->min(0)->info('Time in ms to delay (or the initial value when multiplier is used)')->end() + ->floatNode('multiplier')->defaultValue(2)->min(1)->info('If greater than 1, delay will grow exponentially for each retry: (delay * (multiple ^ retries))')->end() + ->integerNode('max_delay')->defaultValue(0)->min(0)->info('Max time in ms that a retry should ever be delayed (0 = infinite)')->end() + ->end() + ; + } + private function addMailerSection(ArrayNodeDefinition $rootNode) { $rootNode @@ -1492,6 +1683,7 @@ private function addMailerSection(ArrayNodeDefinition $rootNode) ->thenInvalid('"dsn" and "transports" cannot be used together.') ->end() ->fixXmlConfig('transport') + ->fixXmlConfig('header') ->children() ->scalarNode('message_bus')->defaultNull()->info('The message bus to use. Defaults to the default bus if the Messenger component is installed.')->end() ->scalarNode('dsn')->defaultNull()->end() @@ -1515,6 +1707,20 @@ private function addMailerSection(ArrayNodeDefinition $rootNode) ->end() ->end() ->end() + ->arrayNode('headers') + ->normalizeKeys(false) + ->useAttributeAsKey('name') + ->prototype('array') + ->normalizeKeys(false) + ->beforeNormalization() + ->ifTrue(function ($v) { return !\is_array($v) || array_keys($v) !== ['value']; }) + ->then(function ($v) { return ['value' => $v]; }) + ->end() + ->children() + ->variableNode('value')->end() + ->end() + ->end() + ->end() ->end() ->end() ->end() @@ -1569,4 +1775,72 @@ private function addNotifierSection(ArrayNodeDefinition $rootNode) ->end() ; } + + private function addRateLimiterSection(ArrayNodeDefinition $rootNode) + { + $rootNode + ->children() + ->arrayNode('rate_limiter') + ->info('Rate limiter configuration') + ->{!class_exists(FullStack::class) && class_exists(TokenBucketLimiter::class) ? 'canBeDisabled' : 'canBeEnabled'}() + ->fixXmlConfig('limiter') + ->beforeNormalization() + ->ifTrue(function ($v) { return \is_array($v) && !isset($v['limiters']) && !isset($v['limiter']); }) + ->then(function (array $v) { + $newV = [ + 'enabled' => $v['enabled'], + ]; + unset($v['enabled']); + + $newV['limiters'] = $v; + + return $newV; + }) + ->end() + ->children() + ->arrayNode('limiters') + ->useAttributeAsKey('name') + ->arrayPrototype() + ->children() + ->scalarNode('lock_factory') + ->info('The service ID of the lock factory used by this limiter') + ->defaultValue('lock.factory') + ->end() + ->scalarNode('cache_pool') + ->info('The cache pool to use for storing the current limiter state') + ->defaultValue('cache.rate_limiter') + ->end() + ->scalarNode('storage_service') + ->info('The service ID of a custom storage implementation, this precedes any configured "cache_pool"') + ->defaultNull() + ->end() + ->enumNode('strategy') + ->info('The rate limiting algorithm to use for this rate') + ->isRequired() + ->values(['fixed_window', 'token_bucket']) + ->end() + ->integerNode('limit') + ->info('The maximum allowed hits in a fixed interval or burst') + ->isRequired() + ->end() + ->scalarNode('interval') + ->info('Configures the fixed interval if "strategy" is set to "fixed_window". The value must be a number followed by "second", "minute", "hour", "day", "week" or "month" (or their plural equivalent).') + ->end() + ->arrayNode('rate') + ->info('Configures the fill rate if "strategy" is set to "token_bucket"') + ->children() + ->scalarNode('interval') + ->info('Configures the rate interval. The value must be a number followed by "second", "minute", "hour", "day", "week" or "month" (or their plural equivalent).') + ->end() + ->integerNode('amount')->info('Amount of tokens to add each interval')->defaultValue(1)->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ; + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index cfb5d0728a4dc..a862351bf6540 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -52,7 +52,7 @@ use Symfony\Component\DependencyInjection\EnvVarProcessorInterface; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Exception\LogicException; -use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; +use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; use Symfony\Component\DependencyInjection\Parameter; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\ServiceLocator; @@ -63,7 +63,10 @@ use Symfony\Component\Form\FormTypeExtensionInterface; use Symfony\Component\Form\FormTypeGuesserInterface; use Symfony\Component\Form\FormTypeInterface; +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\HttpClient\RetryableHttpClient; use Symfony\Component\HttpClient\ScopingHttpClient; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\CacheClearer\CacheClearerInterface; use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface; use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; @@ -81,29 +84,40 @@ use Symfony\Component\Mailer\Bridge\Mailjet\Transport\MailjetTransportFactory; 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\Messenger\Bridge\AmazonSqs\Transport\AmazonSqsTransportFactory; use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpTransportFactory; +use Symfony\Component\Messenger\Bridge\Beanstalkd\Transport\BeanstalkdTransportFactory; use Symfony\Component\Messenger\Bridge\Redis\Transport\RedisTransportFactory; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; use Symfony\Component\Messenger\MessageBus; use Symfony\Component\Messenger\MessageBusInterface; +use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface; use Symfony\Component\Messenger\Transport\TransportFactoryInterface; use Symfony\Component\Messenger\Transport\TransportInterface; +use Symfony\Component\Mime\Header\Headers; use Symfony\Component\Mime\MimeTypeGuesserInterface; use Symfony\Component\Mime\MimeTypes; +use Symfony\Component\Notifier\Bridge\Esendex\EsendexTransportFactory; use Symfony\Component\Notifier\Bridge\Firebase\FirebaseTransportFactory; use Symfony\Component\Notifier\Bridge\FreeMobile\FreeMobileTransportFactory; +use Symfony\Component\Notifier\Bridge\GoogleChat\GoogleChatTransportFactory; +use Symfony\Component\Notifier\Bridge\Infobip\InfobipTransportFactory; use Symfony\Component\Notifier\Bridge\Mattermost\MattermostTransportFactory; +use Symfony\Component\Notifier\Bridge\Mobyt\MobytTransportFactory; use Symfony\Component\Notifier\Bridge\Nexmo\NexmoTransportFactory; 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\Smsapi\SmsapiTransportFactory; use Symfony\Component\Notifier\Bridge\Telegram\TelegramTransportFactory; use Symfony\Component\Notifier\Bridge\Twilio\TwilioTransportFactory; +use Symfony\Component\Notifier\Bridge\Zulip\ZulipTransportFactory; use Symfony\Component\Notifier\Notifier; -use Symfony\Component\Notifier\Recipient\AdminRecipient; +use Symfony\Component\Notifier\Recipient\Recipient; use Symfony\Component\PropertyAccess\PropertyAccessor; use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface; use Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface; @@ -113,6 +127,9 @@ use Symfony\Component\PropertyInfo\PropertyReadInfoExtractorInterface; use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; use Symfony\Component\PropertyInfo\PropertyWriteInfoExtractorInterface; +use Symfony\Component\RateLimiter\Limiter; +use Symfony\Component\RateLimiter\LimiterInterface; +use Symfony\Component\RateLimiter\Storage\CacheStorage; use Symfony\Component\Routing\Loader\AnnotationDirectoryLoader; use Symfony\Component\Routing\Loader\AnnotationFileLoader; use Symfony\Component\Security\Core\Security; @@ -126,6 +143,7 @@ use Symfony\Component\String\LazyString; use Symfony\Component\String\Slugger\SluggerInterface; use Symfony\Component\Translation\Command\XliffLintCommand as BaseXliffLintCommand; +use Symfony\Component\Translation\PseudoLocalizationTranslator; use Symfony\Component\Translation\Translator; use Symfony\Component\Validator\ConstraintValidatorInterface; use Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader; @@ -136,6 +154,7 @@ use Symfony\Component\Yaml\Command\LintCommand as BaseYamlLintCommand; use Symfony\Component\Yaml\Yaml; use Symfony\Contracts\Cache\CacheInterface; +use Symfony\Contracts\Cache\CallbackInterface; use Symfony\Contracts\Cache\TagAwareCacheInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\Service\ResetInterface; @@ -160,6 +179,8 @@ class FrameworkExtension extends Extension private $messengerConfigEnabled = false; private $mailerConfigEnabled = false; private $httpClientConfigEnabled = false; + private $notifierConfigEnabled = false; + private $lockConfigEnabled = false; /** * Responds to the app.config configuration parameter. @@ -168,12 +189,12 @@ class FrameworkExtension extends Extension */ public function load(array $configs, ContainerBuilder $container) { - $loader = new XmlFileLoader($container, new FileLocator(\dirname(__DIR__).'/Resources/config')); + $loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__).'/Resources/config')); - $loader->load('web.xml'); - $loader->load('services.xml'); - $loader->load('fragment_renderer.xml'); - $loader->load('error_renderer.xml'); + $loader->load('web.php'); + $loader->load('services.php'); + $loader->load('fragment_renderer.php'); + $loader->load('error_renderer.php'); if (interface_exists(PsrEventDispatcherInterface::class)) { $container->setAlias(PsrEventDispatcherInterface::class, 'event_dispatcher'); @@ -182,7 +203,7 @@ public function load(array $configs, ContainerBuilder $container) $container->registerAliasForArgument('parameter_bag', PsrContainerInterface::class); if (class_exists(Application::class)) { - $loader->load('console.xml'); + $loader->load('console.php'); if (!class_exists(BaseXliffLintCommand::class)) { $container->removeDefinition('console.command.xliff_lint'); @@ -193,7 +214,7 @@ public function load(array $configs, ContainerBuilder $container) } // Load Cache configuration first as it is used by other components - $loader->load('cache.xml'); + $loader->load('cache.php'); $configuration = $this->getConfiguration($configs, $container); $config = $this->processConfiguration($configuration, $configs); @@ -210,7 +231,7 @@ public function load(array $configs, ContainerBuilder $container) } if (class_exists(Translator::class)) { - $loader->load('identity_translator.xml'); + $loader->load('identity_translator.php'); } } @@ -238,6 +259,11 @@ public function load(array $configs, ContainerBuilder $container) $container->setParameter('kernel.default_locale', $config['default_locale']); $container->setParameter('kernel.error_controller', $config['error_controller']); + if (($config['trusted_proxies'] ?? false) && ($config['trusted_headers'] ?? false)) { + $container->setParameter('kernel.trusted_proxies', $config['trusted_proxies']); + $container->setParameter('kernel.trusted_headers', $this->resolveTrustedHeaders($config['trusted_headers'])); + } + if (!$container->hasParameter('debug.file_link_format')) { $links = [ 'textmate' => 'txmt://open?url=file://%%f&line=%%l', @@ -256,7 +282,7 @@ public function load(array $configs, ContainerBuilder $container) } if (!empty($config['test'])) { - $loader->load('test.xml'); + $loader->load('test.php'); if (!class_exists(AbstractBrowser::class)) { $container->removeDefinition('test.client'); @@ -356,12 +382,13 @@ public function load(array $configs, ContainerBuilder $container) $this->registerMailerConfiguration($config['mailer'], $container, $loader); } - if ($this->isConfigEnabled($container, $config['notifier'])) { + if ($this->notifierConfigEnabled = $this->isConfigEnabled($container, $config['notifier'])) { $this->registerNotifierConfiguration($config['notifier'], $container, $loader); } $propertyInfoEnabled = $this->isConfigEnabled($container, $config['property_info']); $this->registerValidationConfiguration($config['validation'], $container, $loader, $propertyInfoEnabled); + $this->registerHttpCacheConfiguration($config['http_cache'], $container); $this->registerEsiConfiguration($config['esi'], $container, $loader); $this->registerSsiConfiguration($config['ssi'], $container, $loader); $this->registerFragmentsConfiguration($config['fragments'], $container, $loader); @@ -386,16 +413,24 @@ public function load(array $configs, ContainerBuilder $container) $this->registerPropertyInfoConfiguration($container, $loader); } - if ($this->isConfigEnabled($container, $config['lock'])) { + if ($this->lockConfigEnabled = $this->isConfigEnabled($container, $config['lock'])) { $this->registerLockConfiguration($config['lock'], $container, $loader); } + if ($this->isConfigEnabled($container, $config['rate_limiter'])) { + if (!interface_exists(LimiterInterface::class)) { + throw new LogicException('Rate limiter support cannot be enabled as the RateLimiter component is not installed. Try running "composer require symfony/rate-limiter".'); + } + + $this->registerRateLimiterConfiguration($config['rate_limiter'], $container, $loader); + } + if ($this->isConfigEnabled($container, $config['web_link'])) { if (!class_exists(HttpHeaderSerializer::class)) { throw new LogicException('WebLink support cannot be enabled as the WebLink component is not installed. Try running "composer require symfony/weblink".'); } - $loader->load('web_link.xml'); + $loader->load('web_link.php'); } $this->addAnnotatedClassesToCompile([ @@ -407,7 +442,7 @@ public function load(array $configs, ContainerBuilder $container) ]); if (class_exists(MimeTypes::class)) { - $loader->load('mime_type.xml'); + $loader->load('mime_type.php'); } $container->registerForAutoconfiguration(Command::class) @@ -418,6 +453,8 @@ public function load(array $configs, ContainerBuilder $container) ->addTag('container.env_var_loader'); $container->registerForAutoconfiguration(EnvVarProcessorInterface::class) ->addTag('container.env_var_processor'); + $container->registerForAutoconfiguration(CallbackInterface::class) + ->addTag('container.reversible'); $container->registerForAutoconfiguration(ServiceLocator::class) ->addTag('container.service_locator'); $container->registerForAutoconfiguration(ServiceSubscriberInterface::class) @@ -502,16 +539,18 @@ public function getConfiguration(array $config, ContainerBuilder $container) return new Configuration($container->getParameter('kernel.debug')); } - private function registerFormConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) + private function registerFormConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader) { - $loader->load('form.xml'); + $loader->load('form.php'); + + $container->getDefinition('form.type_extension.form.validator')->setArgument(1, $config['form']['legacy_error_messages']); if (null === $config['form']['csrf_protection']['enabled']) { $config['form']['csrf_protection']['enabled'] = $config['csrf_protection']['enabled']; } if ($this->isConfigEnabled($container, $config['form']['csrf_protection'])) { - $loader->load('form_csrf.xml'); + $loader->load('form_csrf.php'); $container->setParameter('form.type_extension.csrf.enabled', true); $container->setParameter('form.type_extension.csrf.field_name', $config['form']['csrf_protection']['field_name']); @@ -529,7 +568,21 @@ private function registerFormConfiguration(array $config, ContainerBuilder $cont } } - private function registerEsiConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) + private function registerHttpCacheConfiguration(array $config, ContainerBuilder $container) + { + $options = $config; + unset($options['enabled']); + + if (!$options['private_headers']) { + unset($options['private_headers']); + } + + $container->getDefinition('http_cache') + ->setPublic($config['enabled']) + ->replaceArgument(3, $options); + } + + private function registerEsiConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader) { if (!$this->isConfigEnabled($container, $config)) { $container->removeDefinition('fragment.renderer.esi'); @@ -537,10 +590,10 @@ private function registerEsiConfiguration(array $config, ContainerBuilder $conta return; } - $loader->load('esi.xml'); + $loader->load('esi.php'); } - private function registerSsiConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) + private function registerSsiConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader) { if (!$this->isConfigEnabled($container, $config)) { $container->removeDefinition('fragment.renderer.ssi'); @@ -548,10 +601,10 @@ private function registerSsiConfiguration(array $config, ContainerBuilder $conta return; } - $loader->load('ssi.xml'); + $loader->load('ssi.php'); } - private function registerFragmentsConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) + private function registerFragmentsConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader) { if (!$this->isConfigEnabled($container, $config)) { $container->removeDefinition('fragment.renderer.hinclude'); @@ -561,11 +614,11 @@ private function registerFragmentsConfiguration(array $config, ContainerBuilder $container->setParameter('fragment.renderer.hinclude.global_template', $config['hinclude_default_template']); - $loader->load('fragment_listener.xml'); + $loader->load('fragment_listener.php'); $container->setParameter('fragment.path', $config['path']); } - private function registerProfilerConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) + private function registerProfilerConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader) { if (!$this->isConfigEnabled($container, $config)) { // this is needed for the WebProfiler to work even if the profiler is disabled @@ -574,34 +627,38 @@ private function registerProfilerConfiguration(array $config, ContainerBuilder $ return; } - $loader->load('profiling.xml'); - $loader->load('collectors.xml'); - $loader->load('cache_debug.xml'); + $loader->load('profiling.php'); + $loader->load('collectors.php'); + $loader->load('cache_debug.php'); if ($this->formConfigEnabled) { - $loader->load('form_debug.xml'); + $loader->load('form_debug.php'); } if ($this->validatorConfigEnabled) { - $loader->load('validator_debug.xml'); + $loader->load('validator_debug.php'); } if ($this->translationConfigEnabled) { - $loader->load('translation_debug.xml'); + $loader->load('translation_debug.php'); $container->getDefinition('translator.data_collector')->setDecoratedService('translator'); } if ($this->messengerConfigEnabled) { - $loader->load('messenger_debug.xml'); + $loader->load('messenger_debug.php'); } if ($this->mailerConfigEnabled) { - $loader->load('mailer_debug.xml'); + $loader->load('mailer_debug.php'); } if ($this->httpClientConfigEnabled) { - $loader->load('http_client_debug.xml'); + $loader->load('http_client_debug.php'); + } + + if ($this->notifierConfigEnabled) { + $loader->load('notifier_debug.php'); } $container->setParameter('profiler_listener.only_exceptions', $config['only_exceptions']); @@ -620,7 +677,7 @@ private function registerProfilerConfiguration(array $config, ContainerBuilder $ ->addTag('kernel.reset', ['method' => 'reset']); } - private function registerWorkflowConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) + private function registerWorkflowConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader) { if (!$config['enabled']) { $container->removeDefinition('console.command.workflow_dump'); @@ -632,7 +689,7 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $ throw new LogicException('Workflow support cannot be enabled as the Workflow component is not installed. Try running "composer require symfony/workflow".'); } - $loader->load('workflow.xml'); + $loader->load('workflow.php'); $registryDefinition = $container->getDefinition('workflow.registry'); @@ -709,6 +766,7 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $ } } $metadataStoreDefinition->replaceArgument(2, $transitionsMetadataDefinition); + $container->setDefinition(sprintf('%s.metadata_store', $workflowId), $metadataStoreDefinition); // Create places $places = array_column($workflow['places'], 'name'); @@ -720,7 +778,7 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $ $definitionDefinition->addArgument($places); $definitionDefinition->addArgument($transitions); $definitionDefinition->addArgument($initialMarking); - $definitionDefinition->addArgument($metadataStoreDefinition); + $definitionDefinition->addArgument(new Reference(sprintf('%s.metadata_store', $workflowId))); $definitionDefinition->addTag('workflow.definition', [ 'name' => $name, 'type' => $type, @@ -744,6 +802,7 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $ $workflowDefinition->replaceArgument(1, $markingStoreDefinition); } $workflowDefinition->replaceArgument(3, $name); + $workflowDefinition->replaceArgument(4, $workflow['events_to_dispatch']); // Store to container $container->setDefinition($workflowId, $workflowDefinition); @@ -777,7 +836,6 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $ // Enable the AuditTrail if ($workflow['audit_trail']['enabled']) { $listener = new Definition(Workflow\EventListener\AuditTrailListener::class); - $listener->setPrivate(true); $listener->addTag('monolog.logger', ['channel' => 'workflow']); $listener->addTag('kernel.event_listener', ['event' => sprintf('workflow.%s.leave', $name), 'method' => 'onLeave']); $listener->addTag('kernel.event_listener', ['event' => sprintf('workflow.%s.transition', $name), 'method' => 'onTransition']); @@ -797,7 +855,6 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $ } $guard = new Definition(Workflow\EventListener\GuardListener::class); - $guard->setPrivate(true); $guard->setArguments([ $guardsConfiguration, @@ -818,14 +875,13 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $ } } - private function registerDebugConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) + private function registerDebugConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader) { - $loader->load('debug_prod.xml'); + $loader->load('debug_prod.php'); if (class_exists(Stopwatch::class)) { $container->register('debug.stopwatch', Stopwatch::class) ->addArgument(true) - ->setPrivate(true) ->addTag('kernel.reset', ['method' => 'reset']); $container->setAlias(Stopwatch::class, new Alias('debug.stopwatch', false)); } @@ -837,7 +893,7 @@ private function registerDebugConfiguration(array $config, ContainerBuilder $con } if ($debug && class_exists(Stopwatch::class)) { - $loader->load('debug.xml'); + $loader->load('debug.php'); } $definition = $container->findDefinition('debug.debug_handlers_listener'); @@ -860,7 +916,7 @@ private function registerDebugConfiguration(array $config, ContainerBuilder $con } } - private function registerRouterConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader, array $enabledLocales = []) + private function registerRouterConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader, array $enabledLocales = []) { if (!$this->isConfigEnabled($container, $config)) { $container->removeDefinition('console.command.router_debug'); @@ -869,7 +925,7 @@ private function registerRouterConfiguration(array $config, ContainerBuilder $co return; } - $loader->load('routing.xml'); + $loader->load('routing.php'); if (null === $config['utf8']) { trigger_deprecation('symfony/framework-bundle', '5.1', 'Not setting the "framework.router.utf8" configuration option is deprecated, it will default to "true" in version 6.0.'); @@ -929,12 +985,12 @@ private function registerRouterConfiguration(array $config, ContainerBuilder $co } } - private function registerSessionConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) + private function registerSessionConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader) { - $loader->load('session.xml'); + $loader->load('session.php'); // session storage - $container->setAlias('session.storage', $config['storage_id'])->setPrivate(true); + $container->setAlias('session.storage', $config['storage_id']); $options = ['cache_limiter' => '0']; foreach (['name', 'cookie_lifetime', 'cookie_path', 'cookie_domain', 'cookie_secure', 'cookie_httponly', 'cookie_samesite', 'use_cookies', 'gc_maxlifetime', 'gc_probability', 'gc_divisor', 'sid_length', 'sid_bits_per_character'] as $key) { if (isset($config[$key])) { @@ -957,7 +1013,7 @@ private function registerSessionConfiguration(array $config, ContainerBuilder $c // Set the handler class to be null $container->getDefinition('session.storage.native')->replaceArgument(1, null); $container->getDefinition('session.storage.php_bridge')->replaceArgument(0, null); - $container->setAlias('session.handler', 'session.handler.native_file')->setPrivate(true); + $container->setAlias('session.handler', 'session.handler.native_file'); } else { $container->resolveEnvPlaceholders($config['handler_id'], null, $usedEnvs); @@ -967,9 +1023,9 @@ private function registerSessionConfiguration(array $config, ContainerBuilder $c $container->getDefinition('session.abstract_handler') ->replaceArgument(0, $container->hasDefinition($id) ? new Reference($id) : $config['handler_id']); - $container->setAlias('session.handler', 'session.abstract_handler')->setPrivate(true); + $container->setAlias('session.handler', 'session.abstract_handler'); } else { - $container->setAlias('session.handler', $config['handler_id'])->setPrivate(true); + $container->setAlias('session.handler', $config['handler_id']); } } @@ -978,19 +1034,19 @@ private function registerSessionConfiguration(array $config, ContainerBuilder $c $container->setParameter('session.metadata.update_threshold', $config['metadata_update_threshold']); } - private function registerRequestConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) + private function registerRequestConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader) { if ($config['formats']) { - $loader->load('request.xml'); + $loader->load('request.php'); $listener = $container->getDefinition('request.add_request_formats_listener'); $listener->replaceArgument(0, $config['formats']); } } - private function registerAssetsConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) + private function registerAssetsConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader) { - $loader->load('assets.xml'); + $loader->load('assets.php'); if ($config['version_strategy']) { $defaultVersion = new Reference($config['version_strategy']); @@ -1084,7 +1140,7 @@ private function registerTranslatorConfiguration(array $config, ContainerBuilder return; } - $loader->load('translation.xml'); + $loader->load('translation.php'); // Use the "real" translator instead of the identity default $container->setAlias('translator', 'translator.default')->setPublic(true); @@ -1191,11 +1247,26 @@ private function registerTranslatorConfiguration(array $config, ContainerBuilder $translator->replaceArgument(4, $options); } + + if ($config['pseudo_localization']['enabled']) { + $options = $config['pseudo_localization']; + unset($options['enabled']); + + $container + ->register('translator.pseudo', PseudoLocalizationTranslator::class) + ->setDecoratedService('translator', null, -1) // Lower priority than "translator.data_collector" + ->setArguments([ + new Reference('translator.pseudo.inner'), + $options, + ]); + } } - private function registerValidationConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader, bool $propertyInfoEnabled) + private function registerValidationConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader, bool $propertyInfoEnabled) { if (!$this->validatorConfigEnabled = $this->isConfigEnabled($container, $config)) { + $container->removeDefinition('console.command.validator_debug'); + return; } @@ -1207,7 +1278,7 @@ private function registerValidationConfiguration(array $config, ContainerBuilder $config['email_validation_mode'] = 'loose'; } - $loader->load('validator.xml'); + $loader->load('validator.php'); $validatorBuilder = $container->getDefinition('validator.builder'); @@ -1329,7 +1400,7 @@ private function registerAnnotationsConfiguration(array $config, ContainerBuilde throw new LogicException('Annotations cannot be enabled as the Doctrine Annotation library is not installed.'); } - $loader->load('annotations.xml'); + $loader->load('annotations.php'); if (!method_exists(AnnotationRegistry::class, 'registerUniqueLoader')) { $container->getDefinition('annotations.dummy_registry') @@ -1372,24 +1443,29 @@ private function registerAnnotationsConfiguration(array $config, ContainerBuilde ->addTag('annotations.cached_reader') ; - $container->setAlias('annotation_reader', 'annotations.cached_reader')->setPrivate(true); + $container->setAlias('annotation_reader', 'annotations.cached_reader'); $container->setAlias(Reader::class, new Alias('annotations.cached_reader', false)); } else { $container->removeDefinition('annotations.cached_reader'); } } - private function registerPropertyAccessConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) + private function registerPropertyAccessConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader) { if (!class_exists(PropertyAccessor::class)) { return; } - $loader->load('property_access.xml'); + $loader->load('property_access.php'); + + $magicMethods = PropertyAccessor::DISALLOW_MAGIC_METHODS; + $magicMethods |= $config['magic_call'] ? PropertyAccessor::MAGIC_CALL : 0; + $magicMethods |= $config['magic_get'] ? PropertyAccessor::MAGIC_GET : 0; + $magicMethods |= $config['magic_set'] ? PropertyAccessor::MAGIC_SET : 0; $container ->getDefinition('property_accessor') - ->replaceArgument(0, $config['magic_call']) + ->replaceArgument(0, $magicMethods) ->replaceArgument(1, $config['throw_exception_on_invalid_index']) ->replaceArgument(3, $config['throw_exception_on_invalid_property_path']) ->replaceArgument(4, new Reference(PropertyReadInfoExtractorInterface::class, ContainerInterface::NULL_ON_INVALID_REFERENCE)) @@ -1397,7 +1473,7 @@ private function registerPropertyAccessConfiguration(array $config, ContainerBui ; } - private function registerSecretsConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) + private function registerSecretsConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader) { if (!$this->isConfigEnabled($container, $config)) { $container->removeDefinition('console.command.secrets_set'); @@ -1410,7 +1486,7 @@ private function registerSecretsConfiguration(array $config, ContainerBuilder $c return; } - $loader->load('secrets.xml'); + $loader->load('secrets.php'); $container->getDefinition('secrets.vault')->replaceArgument(0, $config['vault_directory']); @@ -1437,7 +1513,7 @@ private function registerSecretsConfiguration(array $config, ContainerBuilder $c } } - private function registerSecurityCsrfConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) + private function registerSecurityCsrfConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader) { if (!$this->isConfigEnabled($container, $config)) { return; @@ -1452,16 +1528,16 @@ private function registerSecurityCsrfConfiguration(array $config, ContainerBuild } // Enable services for CSRF protection (even without forms) - $loader->load('security_csrf.xml'); + $loader->load('security_csrf.php'); if (!class_exists(CsrfExtension::class)) { $container->removeDefinition('twig.extension.security_csrf'); } } - private function registerSerializerConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) + private function registerSerializerConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader) { - $loader->load('serializer.xml'); + $loader->load('serializer.php'); $chainLoader = $container->getDefinition('serializer.mapping.chain_loader'); @@ -1478,6 +1554,10 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder $container->removeDefinition('serializer.denormalizer.unwrapping'); } + if (!class_exists(Headers::class)) { + $container->removeDefinition('serializer.normalizer.mime_message'); + } + $serializerLoaders = []; if (isset($config['enable_annotations']) && $config['enable_annotations']) { if (!$this->annotationsConfigEnabled) { @@ -1546,17 +1626,16 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder } } - private function registerPropertyInfoConfiguration(ContainerBuilder $container, XmlFileLoader $loader) + private function registerPropertyInfoConfiguration(ContainerBuilder $container, PhpFileLoader $loader) { if (!interface_exists(PropertyInfoExtractorInterface::class)) { throw new LogicException('PropertyInfo support cannot be enabled as the PropertyInfo component is not installed. Try running "composer require symfony/property-info".'); } - $loader->load('property_info.xml'); + $loader->load('property_info.php'); if (interface_exists('phpDocumentor\Reflection\DocBlockFactoryInterface')) { $definition = $container->register('property_info.php_doc_extractor', 'Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor'); - $definition->setPrivate(true); $definition->addTag('property_info.description_extractor', ['priority' => -1000]); $definition->addTag('property_info.type_extractor', ['priority' => -1001]); } @@ -1566,9 +1645,9 @@ private function registerPropertyInfoConfiguration(ContainerBuilder $container, } } - private function registerLockConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) + private function registerLockConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader) { - $loader->load('lock.xml'); + $loader->load('lock.php'); foreach ($config['resources'] as $resourceName => $resourceStores) { if (0 === \count($resourceStores)) { @@ -1627,13 +1706,13 @@ private function registerLockConfiguration(array $config, ContainerBuilder $cont } } - private function registerMessengerConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader, array $validationConfig) + private function registerMessengerConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader, array $validationConfig) { if (!interface_exists(MessageBusInterface::class)) { throw new LogicException('Messenger support cannot be enabled as the Messenger component is not installed. Try running "composer require symfony/messenger".'); } - $loader->load('messenger.xml'); + $loader->load('messenger.php'); if (class_exists(AmqpTransportFactory::class)) { $container->getDefinition('messenger.transport.amqp.factory')->addTag('messenger.transport_factory'); @@ -1647,6 +1726,10 @@ private function registerMessengerConfiguration(array $config, ContainerBuilder $container->getDefinition('messenger.transport.sqs.factory')->addTag('messenger.transport_factory'); } + if (class_exists(BeanstalkdTransportFactory::class)) { + $container->getDefinition('messenger.transport.beanstalkd.factory')->addTag('messenger.transport_factory'); + } + if (null === $config['default_bus'] && 1 === \count($config['buses'])) { $config['default_bus'] = key($config['buses']); } @@ -1705,6 +1788,8 @@ private function registerMessengerConfiguration(array $config, ContainerBuilder $container->removeDefinition('messenger.transport.amqp.factory'); $container->removeDefinition('messenger.transport.redis.factory'); $container->removeDefinition('messenger.transport.sqs.factory'); + $container->removeDefinition('messenger.transport.beanstalkd.factory'); + $container->removeAlias(SerializerInterface::class); } else { $container->getDefinition('messenger.transport.symfony_serializer') ->replaceArgument(1, $config['serializer']['symfony_serializer']['format']) @@ -1835,8 +1920,11 @@ private function registerCacheConfiguration(array $config, ContainerBuilder $con foreach ($config['pools'] as $name => $pool) { $pool['adapters'] = $pool['adapters'] ?: ['cache.app']; + $isRedisTagAware = ['cache.adapter.redis_tag_aware'] === $pool['adapters']; foreach ($pool['adapters'] as $provider => $adapter) { - if ($config['pools'][$adapter]['tags'] ?? false) { + if (($config['pools'][$adapter]['adapters'] ?? null) === ['cache.adapter.redis_tag_aware']) { + $isRedisTagAware = true; + } elseif ($config['pools'][$adapter]['tags'] ?? false) { $pool['adapters'][$provider] = $adapter = '.'.$adapter.'.inner'; } } @@ -1851,7 +1939,10 @@ private function registerCacheConfiguration(array $config, ContainerBuilder $con $pool['reset'] = 'reset'; } - if ($pool['tags']) { + if ($isRedisTagAware) { + $tagAwareId = $name; + $container->setAlias('.'.$name.'.inner', $name); + } elseif ($pool['tags']) { if (true !== $pool['tags'] && ($config['pools'][$pool['tags']]['tags'] ?? false)) { $pool['tags'] = '.'.$pool['tags'].'.inner'; } @@ -1861,22 +1952,20 @@ private function registerCacheConfiguration(array $config, ContainerBuilder $con ->setPublic($pool['public']) ; - $pool['name'] = $name; + $pool['name'] = $tagAwareId = $name; $pool['public'] = false; $name = '.'.$name.'.inner'; - - if (!\in_array($pool['name'], ['cache.app', 'cache.system'], true)) { - $container->registerAliasForArgument($pool['name'], TagAwareCacheInterface::class); - $container->registerAliasForArgument($name, CacheInterface::class, $pool['name']); - $container->registerAliasForArgument($name, CacheItemPoolInterface::class, $pool['name']); - } } elseif (!\in_array($name, ['cache.app', 'cache.system'], true)) { - $container->register('.'.$name.'.taggable', TagAwareAdapter::class) + $tagAwareId = '.'.$name.'.taggable'; + $container->register($tagAwareId, TagAwareAdapter::class) ->addArgument(new Reference($name)) ; - $container->registerAliasForArgument('.'.$name.'.taggable', TagAwareCacheInterface::class, $name); - $container->registerAliasForArgument($name, CacheInterface::class); - $container->registerAliasForArgument($name, CacheItemPoolInterface::class); + } + + if (!\in_array($name, ['cache.app', 'cache.system'], true)) { + $container->registerAliasForArgument($tagAwareId, TagAwareCacheInterface::class, $pool['name'] ?? $name); + $container->registerAliasForArgument($name, CacheInterface::class, $pool['name'] ?? $name); + $container->registerAliasForArgument($name, CacheItemPoolInterface::class, $pool['name'] ?? $name); } $definition->setPublic($pool['public']); @@ -1902,11 +1991,14 @@ private function registerCacheConfiguration(array $config, ContainerBuilder $con } } - private function registerHttpClientConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader, array $profilerConfig) + private function registerHttpClientConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader, array $profilerConfig) { - $loader->load('http_client.xml'); + $loader->load('http_client.php'); - $container->getDefinition('http_client')->setArguments([$config['default_options'] ?? [], $config['max_host_connections'] ?? 6]); + $options = $config['default_options'] ?? []; + $retryOptions = $options['retry_failed'] ?? ['enabled' => false]; + unset($options['retry_failed']); + $container->getDefinition('http_client')->setArguments([$options, $config['max_host_connections'] ?? 6]); if (!$hasPsr18 = interface_exists(ClientInterface::class)) { $container->removeDefinition('psr18.http_client'); @@ -1917,8 +2009,11 @@ private function registerHttpClientConfiguration(array $config, ContainerBuilder $container->removeDefinition(HttpClient::class); } - $httpClientId = $this->isConfigEnabled($container, $profilerConfig) ? '.debug.http_client.inner' : 'http_client'; + if ($this->isConfigEnabled($container, $retryOptions)) { + $this->registerHttpClientRetry($retryOptions, 'http_client', $container); + } + $httpClientId = $retryOptions['enabled'] ?? false ? 'http_client.retry.inner' : ($this->isConfigEnabled($container, $profilerConfig) ? '.debug.http_client.inner' : 'http_client'); foreach ($config['scoped_clients'] as $name => $scopeConfig) { if ('http_client' === $name) { throw new InvalidArgumentException(sprintf('Invalid scope name: "%s" is reserved.', $name)); @@ -1926,6 +2021,8 @@ private function registerHttpClientConfiguration(array $config, ContainerBuilder $scope = $scopeConfig['scope'] ?? null; unset($scopeConfig['scope']); + $retryOptions = $scopeConfig['retry_failed'] ?? ['enabled' => false]; + unset($scopeConfig['retry_failed']); if (null === $scope) { $baseUri = $scopeConfig['base_uri']; @@ -1943,6 +2040,10 @@ private function registerHttpClientConfiguration(array $config, ContainerBuilder ; } + if ($this->isConfigEnabled($container, $retryOptions)) { + $this->registerHttpClientRetry($retryOptions, $name, $container); + } + $container->registerAliasForArgument($name, HttpClientInterface::class); if ($hasPsr18) { @@ -1952,16 +2053,60 @@ private function registerHttpClientConfiguration(array $config, ContainerBuilder $container->registerAliasForArgument('psr18.'.$name, ClientInterface::class, $name); } } + + if ($responseFactoryId = $config['mock_response_factory'] ?? null) { + $container->getDefinition($httpClientId) + ->setClass(MockHttpClient::class) + ->setArguments([new Reference($responseFactoryId)]); + } } - private function registerMailerConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) + private function registerHttpClientRetry(array $retryOptions, string $name, ContainerBuilder $container) + { + if (!class_exists(RetryableHttpClient::class)) { + throw new LogicException('Retry failed request support cannot be enabled as version 5.2+ of the HTTP Client component is required.'); + } + + if (null !== $retryOptions['backoff_service']) { + $backoffReference = new Reference($retryOptions['backoff_service']); + } else { + $retryServiceId = $name.'.retry.exponential_backoff'; + $retryDefinition = new ChildDefinition('http_client.retry.abstract_exponential_backoff'); + $retryDefinition + ->replaceArgument(0, $retryOptions['delay']) + ->replaceArgument(1, $retryOptions['multiplier']) + ->replaceArgument(2, $retryOptions['max_delay']); + $container->setDefinition($retryServiceId, $retryDefinition); + + $backoffReference = new Reference($retryServiceId); + } + if (null !== $retryOptions['decider_service']) { + $deciderReference = new Reference($retryOptions['decider_service']); + } else { + $retryServiceId = $name.'.retry.decider'; + $retryDefinition = new ChildDefinition('http_client.retry.abstract_httpstatuscode_decider'); + $retryDefinition + ->replaceArgument(0, $retryOptions['http_codes']); + $container->setDefinition($retryServiceId, $retryDefinition); + + $deciderReference = new Reference($retryServiceId); + } + + $container + ->register($name.'.retry', RetryableHttpClient::class) + ->setDecoratedService($name, null, -10) // lower priority than TraceableHttpClient + ->setArguments([new Reference($name.'.retry.inner'), $deciderReference, $backoffReference, $retryOptions['max_retries'], new Reference('logger')]) + ->addTag('monolog.logger', ['channel' => 'http_client']); + } + + private function registerMailerConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader) { if (!class_exists(Mailer::class)) { throw new LogicException('Mailer support cannot be enabled as the component is not installed. Try running "composer require symfony/mailer".'); } - $loader->load('mailer.xml'); - $loader->load('mailer_transports.xml'); + $loader->load('mailer.php'); + $loader->load('mailer_transports.php'); if (!\count($config['transports']) && null === $config['dsn']) { $config['dsn'] = 'smtp://null'; } @@ -1969,6 +2114,9 @@ private function registerMailerConfiguration(array $config, ContainerBuilder $co $container->getDefinition('mailer.transports')->setArgument(0, $transports); $container->getDefinition('mailer.default_transport')->setArgument(0, current($transports)); + $container->removeDefinition('mailer.logger_message_listener'); + $container->setAlias('mailer.logger_message_listener', (new Alias('mailer.message_logger_listener'))->setDeprecated('symfony/framework-bundle', '5.2', 'The "%alias_id%" alias is deprecated, use "mailer.message_logger_listener" instead.')); + $mailer = $container->getDefinition('mailer.mailer'); if (false === $messageBus = $config['message_bus']) { $mailer->replaceArgument(1, null); @@ -1983,6 +2131,7 @@ private function registerMailerConfiguration(array $config, ContainerBuilder $co MandrillTransportFactory::class => 'mailer.transport_factory.mailchimp', PostmarkTransportFactory::class => 'mailer.transport_factory.postmark', SendgridTransportFactory::class => 'mailer.transport_factory.sendgrid', + SendinblueTransportFactory::class => 'mailer.transport_factory.sendinblue', SesTransportFactory::class => 'mailer.transport_factory.amazon', ]; @@ -1992,22 +2141,34 @@ private function registerMailerConfiguration(array $config, ContainerBuilder $co } } - $recipients = $config['envelope']['recipients'] ?? null; - $sender = $config['envelope']['sender'] ?? null; - $envelopeListener = $container->getDefinition('mailer.envelope_listener'); - $envelopeListener->setArgument(0, $sender); - $envelopeListener->setArgument(1, $recipients); + $envelopeListener->setArgument(0, $config['envelope']['sender'] ?? null); + $envelopeListener->setArgument(1, $config['envelope']['recipients'] ?? null); + + if ($config['headers']) { + $headers = new Definition(Headers::class); + foreach ($config['headers'] as $name => $data) { + $value = $data['value']; + if (\in_array(strtolower($name), ['from', 'to', 'cc', 'bcc', 'reply-to'])) { + $value = (array) $value; + } + $headers->addMethodCall('addHeader', [$name, $value]); + } + $messageListener = $container->getDefinition('mailer.message_listener'); + $messageListener->setArgument(0, $headers); + } else { + $container->removeDefinition('mailer.message_listener'); + } } - private function registerNotifierConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) + private function registerNotifierConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader) { if (!class_exists(Notifier::class)) { throw new LogicException('Notifier support cannot be enabled as the component is not installed. Try running "composer require symfony/notifier".'); } - $loader->load('notifier.xml'); - $loader->load('notifier_transports.xml'); + $loader->load('notifier.php'); + $loader->load('notifier_transports.php'); if ($config['chatter_transports']) { $container->getDefinition('chatter.transports')->setArgument(0, $config['chatter_transports']); @@ -2046,13 +2207,20 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $ SlackTransportFactory::class => 'notifier.transport_factory.slack', TelegramTransportFactory::class => 'notifier.transport_factory.telegram', MattermostTransportFactory::class => 'notifier.transport_factory.mattermost', + GoogleChatTransportFactory::class => 'notifier.transport_factory.googlechat', NexmoTransportFactory::class => 'notifier.transport_factory.nexmo', RocketChatTransportFactory::class => 'notifier.transport_factory.rocketchat', + InfobipTransportFactory::class => 'notifier.transport_factory.infobip', TwilioTransportFactory::class => 'notifier.transport_factory.twilio', FirebaseTransportFactory::class => 'notifier.transport_factory.firebase', FreeMobileTransportFactory::class => 'notifier.transport_factory.freemobile', OvhCloudTransportFactory::class => 'notifier.transport_factory.ovhcloud', SinchTransportFactory::class => 'notifier.transport_factory.sinch', + ZulipTransportFactory::class => 'notifier.transport_factory.zulip', + MobytTransportFactory::class => 'notifier.transport_factory.mobyt', + SmsapiTransportFactory::class => 'notifier.transport_factory.smsapi', + EsendexTransportFactory::class => 'notifier.transport_factory.esendex', + SendinblueNotifierTransportFactory::class => 'notifier.transport_factory.sendinblue', ]; foreach ($classToServices as $class => $service) { @@ -2065,12 +2233,74 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $ $notifier = $container->getDefinition('notifier'); foreach ($config['admin_recipients'] as $i => $recipient) { $id = 'notifier.admin_recipient.'.$i; - $container->setDefinition($id, new Definition(AdminRecipient::class, [$recipient['email'], $recipient['phone']])); + $container->setDefinition($id, new Definition(Recipient::class, [$recipient['email'], $recipient['phone']])); $notifier->addMethodCall('addAdminRecipient', [new Reference($id)]); } } } + private function registerRateLimiterConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader) + { + if (!$this->lockConfigEnabled) { + throw new LogicException('Rate limiter support cannot be enabled without enabling the Lock component.'); + } + + $loader->load('rate_limiter.php'); + + foreach ($config['limiters'] as $name => $limiterConfig) { + self::registerRateLimiter($container, $name, $limiterConfig); + } + } + + public static function registerRateLimiter(ContainerBuilder $container, string $name, array $limiterConfig) + { + // default configuration (when used by other DI extensions) + $limiterConfig += ['lock_factory' => 'lock.factory', 'cache_pool' => 'cache.rate_limiter']; + + $limiter = $container->setDefinition($limiterId = 'limiter.'.$name, new ChildDefinition('limiter')); + + $limiter->addArgument(new Reference($limiterConfig['lock_factory'])); + unset($limiterConfig['lock_factory']); + + $storageId = $limiterConfig['storage_service'] ?? null; + if (null === $storageId) { + $container->register($storageId = 'limiter.storage.'.$name, CacheStorage::class)->addArgument(new Reference($limiterConfig['cache_pool'])); + } + + $limiter->replaceArgument(1, new Reference($storageId)); + unset($limiterConfig['storage']); + unset($limiterConfig['cache_pool']); + + $limiterConfig['id'] = $name; + $limiter->replaceArgument(0, $limiterConfig); + + $container->registerAliasForArgument($limiterId, Limiter::class, $name.'.limiter'); + } + + private function resolveTrustedHeaders(array $headers): int + { + $trustedHeaders = 0; + + foreach ($headers as $h) { + switch ($h) { + case 'forwarded': $trustedHeaders |= Request::HEADER_FORWARDED; break; + case 'x-forwarded-for': $trustedHeaders |= Request::HEADER_X_FORWARDED_FOR; break; + case 'x-forwarded-host': $trustedHeaders |= Request::HEADER_X_FORWARDED_HOST; break; + case 'x-forwarded-proto': $trustedHeaders |= Request::HEADER_X_FORWARDED_PROTO; break; + case 'x-forwarded-port': $trustedHeaders |= Request::HEADER_X_FORWARDED_PORT; break; + case '!x-forwarded-host': $trustedHeaders &= ~Request::HEADER_X_FORWARDED_HOST; break; + case 'x-forwarded-all': + if (!\in_array('!x-forwarded-prefix', $headers)) { + throw new LogicException('When using "x-forwarded-all" in "framework.trusted_headers", "!x-forwarded-prefix" must be explicitly listed until support for X-Forwarded-Prefix is implemented.'); + } + $trustedHeaders |= Request::HEADER_X_FORWARDED_ALL; + break; + } + } + + return $trustedHeaders; + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php index 1244db03470e9..7f439bb572f87 100644 --- a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php @@ -95,10 +95,6 @@ public function boot() if ($this->container->getParameter('kernel.http_method_override')) { Request::enableHttpMethodParameterOverride(); } - - if ($trustedHosts = $this->container->getParameter('kernel.trusted_hosts')) { - Request::setTrustedHosts($trustedHosts); - } } public function build(ContainerBuilder $container) diff --git a/src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php b/src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php index a1660aea71f7a..35ea73c235771 100644 --- a/src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php +++ b/src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php @@ -16,6 +16,8 @@ use Symfony\Component\HttpKernel\HttpCache\Esi; use Symfony\Component\HttpKernel\HttpCache\HttpCache as BaseHttpCache; use Symfony\Component\HttpKernel\HttpCache\Store; +use Symfony\Component\HttpKernel\HttpCache\StoreInterface; +use Symfony\Component\HttpKernel\HttpCache\SurrogateInterface; use Symfony\Component\HttpKernel\KernelInterface; /** @@ -28,22 +30,36 @@ class HttpCache extends BaseHttpCache protected $cacheDir; protected $kernel; + private $store; + private $surrogate; + private $options; + /** - * @param string $cacheDir The cache directory (default used if null) + * @param string|StoreInterface $cache The cache directory (default used if null) or the storage instance */ - public function __construct(KernelInterface $kernel, string $cacheDir = null) + public function __construct(KernelInterface $kernel, $cache = null, SurrogateInterface $surrogate = null, array $options = null) { $this->kernel = $kernel; - $this->cacheDir = $cacheDir; + $this->surrogate = $surrogate; + $this->options = $options ?? []; + + if ($cache instanceof StoreInterface) { + $this->store = $cache; + } elseif (null !== $cache && !\is_string($cache)) { + throw new \TypeError(sprintf('Argument 2 passed to "%s()" must be a string or a SurrogateInterface, "%s" given.', __METHOD__, get_debug_type($cache))); + } else { + $this->cacheDir = $cache; + } - $isDebug = $kernel->isDebug(); - $options = ['debug' => $isDebug]; + if (null === $options && $kernel->isDebug()) { + $this->options = ['debug' => true]; + } - if ($isDebug) { - $options['stale_if_error'] = 0; + if ($this->options['debug'] ?? false) { + $this->options += ['stale_if_error' => 0]; } - parent::__construct($kernel, $this->createStore(), $this->createSurrogate(), array_merge($options, $this->getOptions())); + parent::__construct($kernel, $this->createStore(), $this->createSurrogate(), array_merge($this->options, $this->getOptions())); } /** @@ -69,11 +85,11 @@ protected function getOptions() protected function createSurrogate() { - return new Esi(); + return $this->surrogate ?? new Esi(); } protected function createStore() { - return new Store($this->cacheDir ?: $this->kernel->getCacheDir().'/http_cache'); + 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 8e5247faea7ae..7f3d35d803563 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Kernel/MicroKernelTrait.php +++ b/src/Symfony/Bundle/FrameworkBundle/Kernel/MicroKernelTrait.php @@ -63,6 +63,26 @@ trait MicroKernelTrait */ //abstract protected function configureContainer(ContainerConfigurator $c): void; + /** + * {@inheritdoc} + */ + public function getCacheDir(): string + { + if (isset($_SERVER['APP_CACHE_DIR'])) { + return $_SERVER['APP_CACHE_DIR'].'/'.$this->environment; + } + + return parent::getCacheDir(); + } + + /** + * {@inheritdoc} + */ + public function getLogDir(): string + { + return $_SERVER['APP_LOG_DIR'] ?? parent::getLogDir(); + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/annotations.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/annotations.php new file mode 100644 index 0000000000000..cc66f6f6056bb --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/annotations.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Doctrine\Common\Annotations\AnnotationReader; +use Doctrine\Common\Annotations\AnnotationRegistry; +use Doctrine\Common\Annotations\CachedReader; +use Doctrine\Common\Annotations\Reader; +use Doctrine\Common\Cache\ArrayCache; +use Doctrine\Common\Cache\FilesystemCache; +use Symfony\Bundle\FrameworkBundle\CacheWarmer\AnnotationsCacheWarmer; +use Symfony\Component\Cache\Adapter\PhpArrayAdapter; +use Symfony\Component\Cache\DoctrineProvider; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('annotations.reader', AnnotationReader::class) + ->call('addGlobalIgnoredName', [ + 'required', + service('annotations.dummy_registry'), // dummy arg to register class_exists as annotation loader only when required + ]) + + ->set('annotations.dummy_registry', AnnotationRegistry::class) + ->call('registerUniqueLoader', ['class_exists']) + + ->set('annotations.cached_reader', CachedReader::class) + ->args([ + service('annotations.reader'), + inline_service(ArrayCache::class), + abstract_arg('Debug-Flag'), + ]) + + ->set('annotations.filesystem_cache', FilesystemCache::class) + ->args([ + abstract_arg('Cache-Directory'), + ]) + + ->set('annotations.cache_warmer', AnnotationsCacheWarmer::class) + ->args([ + service('annotations.reader'), + param('kernel.cache_dir').'/annotations.php', + '#^Symfony\\\\(?:Component\\\\HttpKernel\\\\|Bundle\\\\FrameworkBundle\\\\Controller\\\\(?!.*Controller$))#', + param('kernel.debug'), + ]) + + ->set('annotations.cache', DoctrineProvider::class) + ->args([ + inline_service(PhpArrayAdapter::class) + ->factory([PhpArrayAdapter::class, 'create']) + ->args([ + param('kernel.cache_dir').'/annotations.php', + service('cache.annotations'), + ]), + ]) + + ->alias('annotation_reader', 'annotations.reader') + ->alias(Reader::class, 'annotation_reader'); +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/annotations.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/annotations.xml deleted file mode 100644 index 0ce6bf6594e31..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/annotations.xml +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - required - - - - - - - - class_exists - - - - - - - - - - - - - - - - - - %kernel.cache_dir%/annotations.php - #^Symfony\\(?:Component\\HttpKernel\\|Bundle\\FrameworkBundle\\Controller\\(?!.*Controller$))# - %kernel.debug% - - - - - - - %kernel.cache_dir%/annotations.php - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/assets.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/assets.php new file mode 100644 index 0000000000000..28f7bba6a45fb --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/assets.php @@ -0,0 +1,89 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\Asset\Context\RequestStackContext; +use Symfony\Component\Asset\Package; +use Symfony\Component\Asset\Packages; +use Symfony\Component\Asset\PathPackage; +use Symfony\Component\Asset\UrlPackage; +use Symfony\Component\Asset\VersionStrategy\EmptyVersionStrategy; +use Symfony\Component\Asset\VersionStrategy\JsonManifestVersionStrategy; +use Symfony\Component\Asset\VersionStrategy\RemoteJsonManifestVersionStrategy; +use Symfony\Component\Asset\VersionStrategy\StaticVersionStrategy; + +return static function (ContainerConfigurator $container) { + $container->parameters() + ->set('asset.request_context.base_path', null) + ->set('asset.request_context.secure', null) + ; + + $container->services() + ->set('assets.packages', Packages::class) + ->args([ + service('assets.empty_package'), + [], + ]) + + ->alias(Packages::class, 'assets.packages') + + ->set('assets.empty_package', Package::class) + ->args([ + service('assets.empty_version_strategy'), + ]) + + ->set('assets.context', RequestStackContext::class) + ->args([ + service('request_stack'), + param('asset.request_context.base_path'), + param('asset.request_context.secure'), + ]) + + ->set('assets.path_package', PathPackage::class) + ->abstract() + ->args([ + abstract_arg('base path'), + abstract_arg('version strategy'), + service('assets.context'), + ]) + + ->set('assets.url_package', UrlPackage::class) + ->abstract() + ->args([ + abstract_arg('base URLs'), + abstract_arg('version strategy'), + service('assets.context'), + ]) + + ->set('assets.static_version_strategy', StaticVersionStrategy::class) + ->abstract() + ->args([ + abstract_arg('version'), + abstract_arg('format'), + ]) + + ->set('assets.empty_version_strategy', EmptyVersionStrategy::class) + + ->set('assets.json_manifest_version_strategy', JsonManifestVersionStrategy::class) + ->abstract() + ->args([ + abstract_arg('manifest path'), + ]) + + ->set('assets.remote_json_manifest_version_strategy', RemoteJsonManifestVersionStrategy::class) + ->abstract() + ->args([ + abstract_arg('manifest url'), + service('http_client'), + ]) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/assets.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/assets.xml deleted file mode 100644 index 73ec21ab429e0..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/assets.xml +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - null - null - - - - - - - - - - - - - - - - - - %asset.request_context.base_path% - %asset.request_context.secure% - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.php new file mode 100644 index 0000000000000..b44f3b9fb315d --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.php @@ -0,0 +1,246 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\Cache\Adapter\AbstractAdapter; +use Symfony\Component\Cache\Adapter\AdapterInterface; +use Symfony\Component\Cache\Adapter\ApcuAdapter; +use Symfony\Component\Cache\Adapter\ArrayAdapter; +use Symfony\Component\Cache\Adapter\DoctrineAdapter; +use Symfony\Component\Cache\Adapter\FilesystemAdapter; +use Symfony\Component\Cache\Adapter\MemcachedAdapter; +use Symfony\Component\Cache\Adapter\PdoAdapter; +use Symfony\Component\Cache\Adapter\ProxyAdapter; +use Symfony\Component\Cache\Adapter\RedisAdapter; +use Symfony\Component\Cache\Adapter\RedisTagAwareAdapter; +use Symfony\Component\Cache\Adapter\TagAwareAdapter; +use Symfony\Component\Cache\Marshaller\DefaultMarshaller; +use Symfony\Component\Cache\Messenger\EarlyExpirationHandler; +use Symfony\Component\HttpKernel\CacheClearer\Psr6CacheClearer; +use Symfony\Contracts\Cache\CacheInterface; +use Symfony\Contracts\Cache\TagAwareCacheInterface; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('cache.app') + ->parent('cache.adapter.filesystem') + ->public() + ->tag('cache.pool', ['clearer' => 'cache.app_clearer']) + + ->set('cache.app.taggable', TagAwareAdapter::class) + ->args([service('cache.app')]) + + ->set('cache.system') + ->parent('cache.adapter.system') + ->public() + ->tag('cache.pool') + + ->set('cache.validator') + ->parent('cache.system') + ->private() + ->tag('cache.pool') + + ->set('cache.serializer') + ->parent('cache.system') + ->private() + ->tag('cache.pool') + + ->set('cache.annotations') + ->parent('cache.system') + ->private() + ->tag('cache.pool') + + ->set('cache.property_info') + ->parent('cache.system') + ->private() + ->tag('cache.pool') + + ->set('cache.messenger.restart_workers_signal') + ->parent('cache.app') + ->private() + ->tag('cache.pool') + + ->set('cache.adapter.system', AdapterInterface::class) + ->abstract() + ->factory([AbstractAdapter::class, 'createSystemCache']) + ->args([ + '', // namespace + 0, // default lifetime + abstract_arg('version'), + sprintf('%s/pools', param('kernel.cache_dir')), + service('logger')->ignoreOnInvalid(), + ]) + ->tag('cache.pool', ['clearer' => 'cache.system_clearer', 'reset' => 'reset']) + ->tag('monolog.logger', ['channel' => 'cache']) + + ->set('cache.adapter.apcu', ApcuAdapter::class) + ->abstract() + ->args([ + '', // namespace + 0, // default lifetime + abstract_arg('version'), + ]) + ->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) + ->abstract() + ->args([ + abstract_arg('Doctrine provider service'), + '', // namespace + 0, // default lifetime + ]) + ->call('setLogger', [service('logger')->ignoreOnInvalid()]) + ->tag('cache.pool', [ + 'provider' => 'cache.default_doctrine_provider', + 'clearer' => 'cache.default_clearer', + 'reset' => 'reset', + ]) + ->tag('monolog.logger', ['channel' => 'cache']) + + ->set('cache.adapter.filesystem', FilesystemAdapter::class) + ->abstract() + ->args([ + '', // namespace + 0, // default lifetime + sprintf('%s/pools', param('kernel.cache_dir')), + service('cache.default_marshaller')->ignoreOnInvalid(), + ]) + ->call('setLogger', [service('logger')->ignoreOnInvalid()]) + ->tag('cache.pool', ['clearer' => 'cache.default_clearer', 'reset' => 'reset']) + ->tag('monolog.logger', ['channel' => 'cache']) + + ->set('cache.adapter.psr6', ProxyAdapter::class) + ->abstract() + ->args([ + abstract_arg('PSR-6 provider service'), + '', // namespace + 0, // default lifetime + ]) + ->tag('cache.pool', [ + 'provider' => 'cache.default_psr6_provider', + 'clearer' => 'cache.default_clearer', + 'reset' => 'reset', + ]) + + ->set('cache.adapter.redis', RedisAdapter::class) + ->abstract() + ->args([ + abstract_arg('Redis connection service'), + '', // namespace + 0, // default lifetime + service('cache.default_marshaller')->ignoreOnInvalid(), + ]) + ->call('setLogger', [service('logger')->ignoreOnInvalid()]) + ->tag('cache.pool', [ + 'provider' => 'cache.default_redis_provider', + 'clearer' => 'cache.default_clearer', + 'reset' => 'reset', + ]) + ->tag('monolog.logger', ['channel' => 'cache']) + + ->set('cache.adapter.redis_tag_aware', RedisTagAwareAdapter::class) + ->abstract() + ->args([ + abstract_arg('Redis connection service'), + '', // namespace + 0, // default lifetime + service('cache.default_marshaller')->ignoreOnInvalid(), + ]) + ->call('setLogger', [service('logger')->ignoreOnInvalid()]) + ->tag('cache.pool', [ + 'provider' => 'cache.default_redis_provider', + 'clearer' => 'cache.default_clearer', + 'reset' => 'reset', + ]) + ->tag('monolog.logger', ['channel' => 'cache']) + + ->set('cache.adapter.memcached', MemcachedAdapter::class) + ->abstract() + ->args([ + abstract_arg('Memcached connection service'), + '', // namespace + 0, // default lifetime + service('cache.default_marshaller')->ignoreOnInvalid(), + ]) + ->call('setLogger', [service('logger')->ignoreOnInvalid()]) + ->tag('cache.pool', [ + 'provider' => 'cache.default_memcached_provider', + 'clearer' => 'cache.default_clearer', + 'reset' => 'reset', + ]) + ->tag('monolog.logger', ['channel' => 'cache']) + + ->set('cache.adapter.pdo', PdoAdapter::class) + ->abstract() + ->args([ + abstract_arg('PDO connection service'), + '', // namespace + 0, // default lifetime + [], // table options + service('cache.default_marshaller')->ignoreOnInvalid(), + ]) + ->call('setLogger', [service('logger')->ignoreOnInvalid()]) + ->tag('cache.pool', [ + 'provider' => 'cache.default_pdo_provider', + 'clearer' => 'cache.default_clearer', + 'reset' => 'reset', + ]) + ->tag('monolog.logger', ['channel' => 'cache']) + + ->set('cache.adapter.array', ArrayAdapter::class) + ->abstract() + ->args([ + 0, // default lifetime + ]) + ->call('setLogger', [service('logger')->ignoreOnInvalid()]) + ->tag('cache.pool', ['clearer' => 'cache.default_clearer', 'reset' => 'reset']) + ->tag('monolog.logger', ['channel' => 'cache']) + + ->set('cache.default_marshaller', DefaultMarshaller::class) + ->args([ + null, // use igbinary_serialize() when available + ]) + + ->set('cache.early_expiration_handler', EarlyExpirationHandler::class) + ->args([ + service('reverse_container'), + ]) + ->tag('messenger.message_handler') + + ->set('cache.default_clearer', Psr6CacheClearer::class) + ->args([ + [], + ]) + + ->set('cache.system_clearer') + ->parent('cache.default_clearer') + ->public() + + ->set('cache.global_clearer') + ->parent('cache.default_clearer') + ->public() + + ->alias('cache.app_clearer', 'cache.default_clearer') + ->public() + + ->alias(CacheItemPoolInterface::class, 'cache.app') + + ->alias(AdapterInterface::class, 'cache.app') + + ->alias(CacheInterface::class, 'cache.app') + + ->alias(TagAwareCacheInterface::class, 'cache.app.taggable') + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml deleted file mode 100644 index 9959debfa94a5..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml +++ /dev/null @@ -1,157 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 0 - - %kernel.cache_dir%/pools - - - - - - - - 0 - - - - - - - - - - - - 0 - - - - - - - - - - 0 - %kernel.cache_dir%/pools - - - - - - - - - - - 0 - - - - - - - - 0 - - - - - - - - - - - - 0 - - - - - - - - - - - - 0 - - - - - - - - - - - 0 - - - - - - - null - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache_debug.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache_debug.php new file mode 100644 index 0000000000000..82461d91a69dd --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache_debug.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\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Bundle\FrameworkBundle\CacheWarmer\CachePoolClearerCacheWarmer; +use Symfony\Component\Cache\DataCollector\CacheDataCollector; + +return static function (ContainerConfigurator $container) { + $container->services() + // DataCollector (public to prevent inlining, made private in CacheCollectorPass) + ->set('data_collector.cache', CacheDataCollector::class) + ->public() + ->tag('data_collector', [ + 'template' => '@WebProfiler/Collector/cache.html.twig', + 'id' => 'cache', + 'priority' => 275, + ]) + + // CacheWarmer used in dev to clear cache pool + ->set('cache_pool_clearer.cache_warmer', CachePoolClearerCacheWarmer::class) + ->args([ + service('cache.system_clearer'), + [ + 'cache.validator', + 'cache.serializer', + ], + ]) + ->tag('kernel.cache_warmer', ['priority' => 64]) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache_debug.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache_debug.xml deleted file mode 100644 index d4a7396c60d67..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache_debug.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - cache.validator - cache.serializer - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.php new file mode 100644 index 0000000000000..010b5bf8fccc6 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.php @@ -0,0 +1,78 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Bundle\FrameworkBundle\DataCollector\RouterDataCollector; +use Symfony\Component\HttpKernel\DataCollector\AjaxDataCollector; +use Symfony\Component\HttpKernel\DataCollector\ConfigDataCollector; +use Symfony\Component\HttpKernel\DataCollector\EventDataCollector; +use Symfony\Component\HttpKernel\DataCollector\ExceptionDataCollector; +use Symfony\Component\HttpKernel\DataCollector\LoggerDataCollector; +use Symfony\Component\HttpKernel\DataCollector\MemoryDataCollector; +use Symfony\Component\HttpKernel\DataCollector\RequestDataCollector; +use Symfony\Component\HttpKernel\DataCollector\TimeDataCollector; +use Symfony\Component\HttpKernel\KernelEvents; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('data_collector.config', ConfigDataCollector::class) + ->call('setKernel', [service('kernel')->ignoreOnInvalid()]) + ->tag('data_collector', ['template' => '@WebProfiler/Collector/config.html.twig', 'id' => 'config', 'priority' => -255]) + + ->set('data_collector.request', RequestDataCollector::class) + ->args([ + service('request_stack')->ignoreOnInvalid(), + ]) + ->tag('kernel.event_subscriber') + ->tag('data_collector', ['template' => '@WebProfiler/Collector/request.html.twig', 'id' => 'request', 'priority' => 335]) + + ->set('data_collector.request.session_collector', \Closure::class) + ->factory([\Closure::class, 'fromCallable']) + ->args([[service('data_collector.request'), 'collectSessionUsage']]) + + ->set('data_collector.ajax', AjaxDataCollector::class) + ->tag('data_collector', ['template' => '@WebProfiler/Collector/ajax.html.twig', 'id' => 'ajax', 'priority' => 315]) + + ->set('data_collector.exception', ExceptionDataCollector::class) + ->tag('data_collector', ['template' => '@WebProfiler/Collector/exception.html.twig', 'id' => 'exception', 'priority' => 305]) + + ->set('data_collector.events', EventDataCollector::class) + ->args([ + service('debug.event_dispatcher')->ignoreOnInvalid(), + service('request_stack')->ignoreOnInvalid(), + ]) + ->tag('data_collector', ['template' => '@WebProfiler/Collector/events.html.twig', 'id' => 'events', 'priority' => 290]) + + ->set('data_collector.logger', LoggerDataCollector::class) + ->args([ + service('logger')->ignoreOnInvalid(), + sprintf('%s/%s', param('kernel.cache_dir'), param('kernel.container_class')), + service('request_stack')->ignoreOnInvalid(), + ]) + ->tag('monolog.logger', ['channel' => 'profiler']) + ->tag('data_collector', ['template' => '@WebProfiler/Collector/logger.html.twig', 'id' => 'logger', 'priority' => 300]) + + ->set('data_collector.time', TimeDataCollector::class) + ->args([ + service('kernel')->ignoreOnInvalid(), + service('debug.stopwatch')->ignoreOnInvalid(), + ]) + ->tag('data_collector', ['template' => '@WebProfiler/Collector/time.html.twig', 'id' => 'time', 'priority' => 330]) + + ->set('data_collector.memory', MemoryDataCollector::class) + ->tag('data_collector', ['template' => '@WebProfiler/Collector/memory.html.twig', 'id' => 'memory', 'priority' => 325]) + + ->set('data_collector.router', RouterDataCollector::class) + ->tag('kernel.event_listener', ['event' => KernelEvents::CONTROLLER, 'method' => 'onKernelController']) + ->tag('data_collector', ['template' => '@WebProfiler/Collector/router.html.twig', 'id' => 'router', 'priority' => 285]) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.xml deleted file mode 100644 index 17df61db1c13a..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.xml +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - %kernel.cache_dir%/%kernel.container_class% - - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php new file mode 100644 index 0000000000000..e9b3d2e36a855 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php @@ -0,0 +1,297 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Bundle\FrameworkBundle\Command\AboutCommand; +use Symfony\Bundle\FrameworkBundle\Command\AssetsInstallCommand; +use Symfony\Bundle\FrameworkBundle\Command\CacheClearCommand; +use Symfony\Bundle\FrameworkBundle\Command\CachePoolClearCommand; +use Symfony\Bundle\FrameworkBundle\Command\CachePoolDeleteCommand; +use Symfony\Bundle\FrameworkBundle\Command\CachePoolListCommand; +use Symfony\Bundle\FrameworkBundle\Command\CachePoolPruneCommand; +use Symfony\Bundle\FrameworkBundle\Command\CacheWarmupCommand; +use Symfony\Bundle\FrameworkBundle\Command\ConfigDebugCommand; +use Symfony\Bundle\FrameworkBundle\Command\ConfigDumpReferenceCommand; +use Symfony\Bundle\FrameworkBundle\Command\ContainerDebugCommand; +use Symfony\Bundle\FrameworkBundle\Command\ContainerLintCommand; +use Symfony\Bundle\FrameworkBundle\Command\DebugAutowiringCommand; +use Symfony\Bundle\FrameworkBundle\Command\EventDispatcherDebugCommand; +use Symfony\Bundle\FrameworkBundle\Command\RouterDebugCommand; +use Symfony\Bundle\FrameworkBundle\Command\RouterMatchCommand; +use Symfony\Bundle\FrameworkBundle\Command\SecretsDecryptToLocalCommand; +use Symfony\Bundle\FrameworkBundle\Command\SecretsEncryptFromLocalCommand; +use Symfony\Bundle\FrameworkBundle\Command\SecretsGenerateKeysCommand; +use Symfony\Bundle\FrameworkBundle\Command\SecretsListCommand; +use Symfony\Bundle\FrameworkBundle\Command\SecretsRemoveCommand; +use Symfony\Bundle\FrameworkBundle\Command\SecretsSetCommand; +use Symfony\Bundle\FrameworkBundle\Command\TranslationDebugCommand; +use Symfony\Bundle\FrameworkBundle\Command\TranslationUpdateCommand; +use Symfony\Bundle\FrameworkBundle\Command\WorkflowDumpCommand; +use Symfony\Bundle\FrameworkBundle\Command\YamlLintCommand; +use Symfony\Bundle\FrameworkBundle\EventListener\SuggestMissingPackageSubscriber; +use Symfony\Component\Console\EventListener\ErrorListener; +use Symfony\Component\Messenger\Command\ConsumeMessagesCommand; +use Symfony\Component\Messenger\Command\DebugCommand; +use Symfony\Component\Messenger\Command\FailedMessagesRemoveCommand; +use Symfony\Component\Messenger\Command\FailedMessagesRetryCommand; +use Symfony\Component\Messenger\Command\FailedMessagesShowCommand; +use Symfony\Component\Messenger\Command\SetupTransportsCommand; +use Symfony\Component\Messenger\Command\StopWorkersCommand; +use Symfony\Component\Translation\Command\XliffLintCommand; +use Symfony\Component\Validator\Command\DebugCommand as ValidatorDebugCommand; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('console.error_listener', ErrorListener::class) + ->args([ + service('logger')->nullOnInvalid(), + ]) + ->tag('kernel.event_subscriber') + ->tag('monolog.logger', ['channel' => 'console']) + + ->set('console.suggest_missing_package_subscriber', SuggestMissingPackageSubscriber::class) + ->tag('kernel.event_subscriber') + + ->set('console.command.about', AboutCommand::class) + ->tag('console.command', ['command' => 'about']) + + ->set('console.command.assets_install', AssetsInstallCommand::class) + ->args([ + service('filesystem'), + param('kernel.project_dir'), + ]) + ->tag('console.command', ['command' => 'assets:install']) + + ->set('console.command.cache_clear', CacheClearCommand::class) + ->args([ + service('cache_clearer'), + service('filesystem'), + ]) + ->tag('console.command', ['command' => 'cache:clear']) + + ->set('console.command.cache_pool_clear', CachePoolClearCommand::class) + ->args([ + service('cache.global_clearer'), + ]) + ->tag('console.command', ['command' => 'cache:pool:clear']) + + ->set('console.command.cache_pool_prune', CachePoolPruneCommand::class) + ->args([ + [], + ]) + ->tag('console.command', ['command' => 'cache:pool:prune']) + + ->set('console.command.cache_pool_delete', CachePoolDeleteCommand::class) + ->args([ + service('cache.global_clearer'), + ]) + ->tag('console.command', ['command' => 'cache:pool:delete']) + + ->set('console.command.cache_pool_list', CachePoolListCommand::class) + ->args([ + null, + ]) + ->tag('console.command', ['command' => 'cache:pool:list']) + + ->set('console.command.cache_warmup', CacheWarmupCommand::class) + ->args([ + service('cache_warmer'), + ]) + ->tag('console.command', ['command' => 'cache:warmup']) + + ->set('console.command.config_debug', ConfigDebugCommand::class) + ->tag('console.command', ['command' => 'debug:config']) + + ->set('console.command.config_dump_reference', ConfigDumpReferenceCommand::class) + ->tag('console.command', ['command' => 'config:dump-reference']) + + ->set('console.command.container_debug', ContainerDebugCommand::class) + ->tag('console.command', ['command' => 'debug:container']) + + ->set('console.command.container_lint', ContainerLintCommand::class) + ->tag('console.command', ['command' => 'lint:container']) + + ->set('console.command.debug_autowiring', DebugAutowiringCommand::class) + ->args([ + null, + service('debug.file_link_formatter')->nullOnInvalid(), + ]) + ->tag('console.command', ['command' => 'debug:autowiring']) + + ->set('console.command.event_dispatcher_debug', EventDispatcherDebugCommand::class) + ->args([ + service('event_dispatcher'), + ]) + ->tag('console.command', ['command' => 'debug:event-dispatcher']) + + ->set('console.command.messenger_consume_messages', ConsumeMessagesCommand::class) + ->args([ + abstract_arg('Routable message bus'), + service('messenger.receiver_locator'), + service('event_dispatcher'), + service('logger')->nullOnInvalid(), + [], // Receiver names + ]) + ->tag('console.command', ['command' => 'messenger:consume']) + ->tag('monolog.logger', ['channel' => 'messenger']) + + ->set('console.command.messenger_setup_transports', SetupTransportsCommand::class) + ->args([ + service('messenger.receiver_locator'), + [], // Receiver names + ]) + ->tag('console.command', ['command' => 'messenger:setup-transports']) + + ->set('console.command.messenger_debug', DebugCommand::class) + ->args([ + [], // Message to handlers mapping + ]) + ->tag('console.command', ['command' => 'debug:messenger']) + + ->set('console.command.messenger_stop_workers', StopWorkersCommand::class) + ->args([ + service('cache.messenger.restart_workers_signal'), + ]) + ->tag('console.command', ['command' => 'messenger:stop-workers']) + + ->set('console.command.messenger_failed_messages_retry', FailedMessagesRetryCommand::class) + ->args([ + abstract_arg('Receiver name'), + abstract_arg('Receiver'), + service('messenger.routable_message_bus'), + service('event_dispatcher'), + service('logger'), + ]) + ->tag('console.command', ['command' => 'messenger:failed:retry']) + + ->set('console.command.messenger_failed_messages_show', FailedMessagesShowCommand::class) + ->args([ + abstract_arg('Receiver name'), + abstract_arg('Receiver'), + ]) + ->tag('console.command', ['command' => 'messenger:failed:show']) + + ->set('console.command.messenger_failed_messages_remove', FailedMessagesRemoveCommand::class) + ->args([ + abstract_arg('Receiver name'), + abstract_arg('Receiver'), + ]) + ->tag('console.command', ['command' => 'messenger:failed:remove']) + + ->set('console.command.router_debug', RouterDebugCommand::class) + ->args([ + service('router'), + service('debug.file_link_formatter')->nullOnInvalid(), + ]) + ->tag('console.command', ['command' => 'debug:router']) + + ->set('console.command.router_match', RouterMatchCommand::class) + ->args([ + service('router'), + tagged_iterator('routing.expression_language_provider'), + ]) + ->tag('console.command', ['command' => 'router:match']) + + ->set('console.command.translation_debug', TranslationDebugCommand::class) + ->args([ + service('translator'), + service('translation.reader'), + service('translation.extractor'), + param('translator.default_path'), + null, // twig.default_path + [], // Translator paths + [], // Twig paths + ]) + ->tag('console.command', ['command' => 'debug:translation']) + + ->set('console.command.translation_update', TranslationUpdateCommand::class) + ->args([ + service('translation.writer'), + service('translation.reader'), + service('translation.extractor'), + param('kernel.default_locale'), + param('translator.default_path'), + null, // twig.default_path + [], // Translator paths + [], // Twig paths + ]) + ->tag('console.command', ['command' => 'translation:update']) + + ->set('console.command.validator_debug', ValidatorDebugCommand::class) + ->args([ + service('validator'), + ]) + ->tag('console.command', ['command' => 'debug:validator']) + + ->set('console.command.workflow_dump', WorkflowDumpCommand::class) + ->tag('console.command', ['command' => 'workflow:dump']) + + ->set('console.command.xliff_lint', XliffLintCommand::class) + ->tag('console.command', ['command' => 'lint:xliff']) + + ->set('console.command.yaml_lint', YamlLintCommand::class) + ->tag('console.command', ['command' => 'lint:yaml']) + + ->set('console.command.form_debug', \Symfony\Component\Form\Command\DebugCommand::class) + ->args([ + service('form.registry'), + [], // All form types namespaces are stored here by FormPass + [], // All services form types are stored here by FormPass + [], // All type extensions are stored here by FormPass + [], // All type guessers are stored here by FormPass + service('debug.file_link_formatter')->nullOnInvalid(), + ]) + ->tag('console.command', ['command' => 'debug:form']) + + ->set('console.command.secrets_set', SecretsSetCommand::class) + ->args([ + service('secrets.vault'), + service('secrets.local_vault')->nullOnInvalid(), + ]) + ->tag('console.command', ['command' => 'secrets:set']) + + ->set('console.command.secrets_remove', SecretsRemoveCommand::class) + ->args([ + service('secrets.vault'), + service('secrets.local_vault')->nullOnInvalid(), + ]) + ->tag('console.command', ['command' => 'secrets:remove']) + + ->set('console.command.secrets_generate_key', SecretsGenerateKeysCommand::class) + ->args([ + service('secrets.vault'), + service('secrets.local_vault')->ignoreOnInvalid(), + ]) + ->tag('console.command', ['command' => 'secrets:generate-keys']) + + ->set('console.command.secrets_list', SecretsListCommand::class) + ->args([ + service('secrets.vault'), + service('secrets.local_vault'), + ]) + ->tag('console.command', ['command' => 'secrets:list']) + + ->set('console.command.secrets_decrypt_to_local', SecretsDecryptToLocalCommand::class) + ->args([ + service('secrets.vault'), + service('secrets.local_vault')->ignoreOnInvalid(), + ]) + ->tag('console.command', ['command' => 'secrets:decrypt-to-local']) + + ->set('console.command.secrets_encrypt_from_local', SecretsEncryptFromLocalCommand::class) + ->args([ + service('secrets.vault'), + service('secrets.local_vault'), + ]) + ->tag('console.command', ['command' => 'secrets:encrypt-from-local']) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml deleted file mode 100644 index cbd43ac7a6a93..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml +++ /dev/null @@ -1,233 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - %kernel.project_dir% - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - null - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - %translator.default_path% - - - - - - - - - - - %kernel.default_locale% - %translator.default_path% - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.php new file mode 100644 index 0000000000000..cfaad8c1de241 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\NotTaggedControllerValueResolver; +use Symfony\Component\HttpKernel\Controller\TraceableArgumentResolver; +use Symfony\Component\HttpKernel\Controller\TraceableControllerResolver; +use Symfony\Component\HttpKernel\Debug\TraceableEventDispatcher; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('debug.event_dispatcher', TraceableEventDispatcher::class) + ->decorate('event_dispatcher') + ->args([ + service('debug.event_dispatcher.inner'), + service('debug.stopwatch'), + service('logger')->nullOnInvalid(), + service('request_stack')->nullOnInvalid(), + ]) + ->tag('monolog.logger', ['channel' => 'event']) + ->tag('kernel.reset', ['method' => 'reset']) + + ->set('debug.controller_resolver', TraceableControllerResolver::class) + ->decorate('controller_resolver') + ->args([ + service('debug.controller_resolver.inner'), + service('debug.stopwatch'), + ]) + + ->set('debug.argument_resolver', TraceableArgumentResolver::class) + ->decorate('argument_resolver') + ->args([ + service('debug.argument_resolver.inner'), + service('debug.stopwatch'), + ]) + + ->set('argument_resolver.not_tagged_controller', NotTaggedControllerValueResolver::class) + ->args([abstract_arg('Controller argument, set in FrameworkExtension')]) + ->tag('controller.argument_value_resolver', ['priority' => -200]) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.xml deleted file mode 100644 index 63a61efe4bb51..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug_prod.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug_prod.php new file mode 100644 index 0000000000000..51492cfe1823f --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug_prod.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\HttpKernel\Debug\FileLinkFormatter; +use Symfony\Component\HttpKernel\EventListener\DebugHandlersListener; + +return static function (ContainerConfigurator $container) { + $container->parameters()->set('debug.error_handler.throw_at', -1); + + $container->services() + ->set('debug.debug_handlers_listener', DebugHandlersListener::class) + ->args([ + null, // Exception handler + service('monolog.logger.php')->nullOnInvalid(), + 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(), + ]) + ->tag('kernel.event_subscriber') + ->tag('monolog.logger', ['channel' => 'php']) + + ->set('debug.file_link_formatter', FileLinkFormatter::class) + ->args([param('debug.file_link_format')]) + + ->alias(FileLinkFormatter::class, 'debug.file_link_formatter') + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug_prod.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug_prod.xml deleted file mode 100644 index fb0b99255ade2..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug_prod.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - -1 - - - - - - - - - null - - null - %debug.error_handler.throw_at% - %kernel.debug% - - %kernel.debug% - - - - - %debug.file_link_format% - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/error_renderer.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/error_renderer.php new file mode 100644 index 0000000000000..67f28ce44d838 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/error_renderer.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\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('error_handler.error_renderer.html', HtmlErrorRenderer::class) + ->args([ + inline_service() + ->factory([HtmlErrorRenderer::class, 'isDebug']) + ->args([ + service('request_stack'), + param('kernel.debug'), + ]), + param('kernel.charset'), + service('debug.file_link_formatter')->nullOnInvalid(), + param('kernel.project_dir'), + inline_service() + ->factory([HtmlErrorRenderer::class, 'getAndCleanOutputBuffer']) + ->args([service('request_stack')]), + service('logger')->nullOnInvalid(), + ]) + + ->alias('error_renderer.html', 'error_handler.error_renderer.html') + ->alias('error_renderer', 'error_renderer.html') + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/error_renderer.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/error_renderer.xml deleted file mode 100644 index 4d2423feeeede..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/error_renderer.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - %kernel.debug% - - - %kernel.charset% - - %kernel.project_dir% - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/esi.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/esi.php new file mode 100644 index 0000000000000..ca0362a3e0965 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/esi.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\HttpKernel\EventListener\SurrogateListener; +use Symfony\Component\HttpKernel\HttpCache\Esi; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('esi', Esi::class) + + ->set('esi_listener', SurrogateListener::class) + ->args([service('esi')->ignoreOnInvalid()]) + ->tag('kernel.event_subscriber') + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/esi.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/esi.xml deleted file mode 100644 index 65e26d81e25c3..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/esi.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.php new file mode 100644 index 0000000000000..51c6ae38f2dd1 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.php @@ -0,0 +1,149 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator; +use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; +use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\ColorType; +use Symfony\Component\Form\Extension\Core\Type\FileType; +use Symfony\Component\Form\Extension\Core\Type\FormType; +use Symfony\Component\Form\Extension\Core\Type\SubmitType; +use Symfony\Component\Form\Extension\Core\Type\TransformationFailureExtension; +use Symfony\Component\Form\Extension\DependencyInjection\DependencyInjectionExtension; +use Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationRequestHandler; +use Symfony\Component\Form\Extension\HttpFoundation\Type\FormTypeHttpFoundationExtension; +use Symfony\Component\Form\Extension\Validator\Type\FormTypeValidatorExtension; +use Symfony\Component\Form\Extension\Validator\Type\RepeatedTypeValidatorExtension; +use Symfony\Component\Form\Extension\Validator\Type\SubmitTypeValidatorExtension; +use Symfony\Component\Form\Extension\Validator\Type\UploadValidatorExtension; +use Symfony\Component\Form\Extension\Validator\ValidatorTypeGuesser; +use Symfony\Component\Form\FormFactory; +use Symfony\Component\Form\FormFactoryInterface; +use Symfony\Component\Form\FormRegistry; +use Symfony\Component\Form\FormRegistryInterface; +use Symfony\Component\Form\ResolvedFormTypeFactory; +use Symfony\Component\Form\ResolvedFormTypeFactoryInterface; +use Symfony\Component\Form\Util\ServerParams; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('form.resolved_type_factory', ResolvedFormTypeFactory::class) + + ->alias(ResolvedFormTypeFactoryInterface::class, 'form.resolved_type_factory') + + ->set('form.registry', FormRegistry::class) + ->args([ + [ + /* + * We don't need to be able to add more extensions. + * more types can be registered with the form.type tag + * more type extensions can be registered with the form.type_extension tag + * more type_guessers can be registered with the form.type_guesser tag + */ + service('form.extension'), + ], + service('form.resolved_type_factory'), + ]) + + ->alias(FormRegistryInterface::class, 'form.registry') + + ->set('form.factory', FormFactory::class) + ->public() + ->args([service('form.registry')]) + ->tag('container.private', ['package' => 'symfony/framework-bundle', 'version' => '5.2']) + + ->alias(FormFactoryInterface::class, 'form.factory') + + ->set('form.extension', DependencyInjectionExtension::class) + ->args([ + abstract_arg('All services with tag "form.type" are stored in a service locator by FormPass'), + abstract_arg('All services with tag "form.type_extension" are stored here by FormPass'), + abstract_arg('All services with tag "form.type_guesser" are stored here by FormPass'), + ]) + + ->set('form.type_guesser.validator', ValidatorTypeGuesser::class) + ->args([service('validator.mapping.class_metadata_factory')]) + ->tag('form.type_guesser') + + ->alias('form.property_accessor', 'property_accessor') + + ->set('form.choice_list_factory.default', DefaultChoiceListFactory::class) + + ->set('form.choice_list_factory.property_access', PropertyAccessDecorator::class) + ->args([ + service('form.choice_list_factory.default'), + service('form.property_accessor'), + ]) + + ->set('form.choice_list_factory.cached', CachingFactoryDecorator::class) + ->args([service('form.choice_list_factory.property_access')]) + ->tag('kernel.reset', ['method' => 'reset']) + + ->alias('form.choice_list_factory', 'form.choice_list_factory.cached') + + ->set('form.type.form', FormType::class) + ->args([service('form.property_accessor')]) + ->tag('form.type') + + ->set('form.type.choice', ChoiceType::class) + ->args([service('form.choice_list_factory')]) + ->tag('form.type') + + ->set('form.type.file', FileType::class) + ->public() + ->args([service('translator')->ignoreOnInvalid()]) + ->tag('form.type') + ->tag('container.private', ['package' => 'symfony/framework-bundle', 'version' => '5.2']) + + ->set('form.type.color', ColorType::class) + ->args([service('translator')->ignoreOnInvalid()]) + ->tag('form.type') + + ->set('form.type_extension.form.transformation_failure_handling', TransformationFailureExtension::class) + ->args([service('translator')->ignoreOnInvalid()]) + ->tag('form.type_extension', ['extended-type' => FormType::class]) + + ->set('form.type_extension.form.http_foundation', FormTypeHttpFoundationExtension::class) + ->args([service('form.type_extension.form.request_handler')]) + ->tag('form.type_extension') + + ->set('form.type_extension.form.request_handler', HttpFoundationRequestHandler::class) + ->args([service('form.server_params')]) + + ->set('form.server_params', ServerParams::class) + ->args([service('request_stack')]) + + ->set('form.type_extension.form.validator', FormTypeValidatorExtension::class) + ->args([ + service('validator'), + true, + service('twig.form.renderer')->ignoreOnInvalid(), + service('translator')->ignoreOnInvalid(), + ]) + ->tag('form.type_extension', ['extended-type' => FormType::class]) + + ->set('form.type_extension.repeated.validator', RepeatedTypeValidatorExtension::class) + ->tag('form.type_extension') + + ->set('form.type_extension.submit.validator', SubmitTypeValidatorExtension::class) + ->tag('form.type_extension', ['extended-type' => SubmitType::class]) + + ->set('form.type_extension.upload.validator', UploadValidatorExtension::class) + ->args([ + service('translator'), + param('validator.translation_domain'), + ]) + ->tag('form.type_extension') + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml deleted file mode 100644 index bd239ff0d5693..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml +++ /dev/null @@ -1,119 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - %validator.translation_domain% - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_csrf.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_csrf.php new file mode 100644 index 0000000000000..c8e5e973e40f9 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_csrf.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\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\Form\Extension\Csrf\Type\FormTypeCsrfExtension; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('form.type_extension.csrf', FormTypeCsrfExtension::class) + ->args([ + service('security.csrf.token_manager'), + param('form.type_extension.csrf.enabled'), + param('form.type_extension.csrf.field_name'), + service('translator')->nullOnInvalid(), + param('validator.translation_domain'), + service('form.server_params'), + ]) + ->tag('form.type_extension') + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_csrf.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_csrf.xml deleted file mode 100644 index 5e897bea8a30b..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_csrf.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - %form.type_extension.csrf.enabled% - %form.type_extension.csrf.field_name% - - %validator.translation_domain% - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_debug.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_debug.php new file mode 100644 index 0000000000000..f5e2c3ecdd57e --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_debug.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\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\Form\Extension\DataCollector\FormDataCollector; +use Symfony\Component\Form\Extension\DataCollector\FormDataExtractor; +use Symfony\Component\Form\Extension\DataCollector\Proxy\ResolvedTypeFactoryDataCollectorProxy; +use Symfony\Component\Form\Extension\DataCollector\Type\DataCollectorTypeExtension; +use Symfony\Component\Form\ResolvedFormTypeFactory; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('form.resolved_type_factory', ResolvedTypeFactoryDataCollectorProxy::class) + ->args([ + inline_service(ResolvedFormTypeFactory::class), + service('data_collector.form'), + ]) + + ->set('form.type_extension.form.data_collector', DataCollectorTypeExtension::class) + ->args([service('data_collector.form')]) + ->tag('form.type_extension') + + ->set('data_collector.form.extractor', FormDataExtractor::class) + + ->set('data_collector.form', FormDataCollector::class) + ->args([service('data_collector.form.extractor')]) + ->tag('data_collector', ['template' => '@WebProfiler/Collector/form.html.twig', 'id' => 'form', 'priority' => 310]) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_debug.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_debug.xml deleted file mode 100644 index 5e3e97aad5242..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_debug.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_listener.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_listener.php new file mode 100644 index 0000000000000..465c304263dac --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_listener.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\HttpKernel\EventListener\FragmentListener; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('fragment.listener', FragmentListener::class) + ->args([service('uri_signer'), param('fragment.path')]) + ->tag('kernel.event_subscriber') + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_listener.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_listener.xml deleted file mode 100644 index b7c64119f88e6..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_listener.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - %fragment.path% - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_renderer.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_renderer.php new file mode 100644 index 0000000000000..2d42a10026f05 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_renderer.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\HttpKernel\DependencyInjection\LazyLoadingFragmentHandler; +use Symfony\Component\HttpKernel\Fragment\EsiFragmentRenderer; +use Symfony\Component\HttpKernel\Fragment\HIncludeFragmentRenderer; +use Symfony\Component\HttpKernel\Fragment\InlineFragmentRenderer; +use Symfony\Component\HttpKernel\Fragment\SsiFragmentRenderer; + +return static function (ContainerConfigurator $container) { + $container->parameters() + ->set('fragment.renderer.hinclude.global_template', null) + ->set('fragment.path', '/_fragment') + ; + + $container->services() + ->set('fragment.handler', LazyLoadingFragmentHandler::class) + ->args([ + abstract_arg('fragment renderer locator'), + service('request_stack'), + param('kernel.debug'), + ]) + + ->set('fragment.renderer.inline', InlineFragmentRenderer::class) + ->args([service('http_kernel'), service('event_dispatcher')]) + ->call('setFragmentPath', [param('fragment.path')]) + ->tag('kernel.fragment_renderer', ['alias' => 'inline']) + + ->set('fragment.renderer.hinclude', HIncludeFragmentRenderer::class) + ->args([ + service('twig')->nullOnInvalid(), + service('uri_signer'), + param('fragment.renderer.hinclude.global_template'), + ]) + ->call('setFragmentPath', [param('fragment.path')]) + + ->set('fragment.renderer.esi', EsiFragmentRenderer::class) + ->args([ + service('esi')->nullOnInvalid(), + service('fragment.renderer.inline'), + service('uri_signer'), + ]) + ->call('setFragmentPath', [param('fragment.path')]) + ->tag('kernel.fragment_renderer', ['alias' => 'esi']) + + ->set('fragment.renderer.ssi', SsiFragmentRenderer::class) + ->args([ + service('ssi')->nullOnInvalid(), + service('fragment.renderer.inline'), + service('uri_signer'), + ]) + ->call('setFragmentPath', [param('fragment.path')]) + ->tag('kernel.fragment_renderer', ['alias' => 'ssi']) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_renderer.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_renderer.xml deleted file mode 100644 index 827a22f9a4668..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_renderer.xml +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - /_fragment - - - - - - - - - %kernel.debug% - - - - - - - %fragment.path% - - - - - - %fragment.renderer.hinclude.global_template% - %fragment.path% - - - - - - - - %fragment.path% - - - - - - - - %fragment.path% - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client.php new file mode 100644 index 0000000000000..447d07a4a1ad9 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Psr\Http\Client\ClientInterface; +use Psr\Http\Message\ResponseFactoryInterface; +use Psr\Http\Message\StreamFactoryInterface; +use Symfony\Component\HttpClient\HttpClient; +use Symfony\Component\HttpClient\HttplugClient; +use Symfony\Component\HttpClient\Psr18Client; +use Symfony\Component\HttpClient\Retry\ExponentialBackOff; +use Symfony\Component\HttpClient\Retry\HttpStatusCodeDecider; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('http_client', HttpClientInterface::class) + ->factory([HttpClient::class, 'create']) + ->args([ + [], // default options + abstract_arg('max host connections'), + ]) + ->call('setLogger', [service('logger')->ignoreOnInvalid()]) + ->tag('monolog.logger', ['channel' => 'http_client']) + ->tag('http_client.client') + + ->alias(HttpClientInterface::class, 'http_client') + + ->set('psr18.http_client', Psr18Client::class) + ->args([ + service('http_client'), + service(ResponseFactoryInterface::class)->ignoreOnInvalid(), + service(StreamFactoryInterface::class)->ignoreOnInvalid(), + ]) + + ->alias(ClientInterface::class, 'psr18.http_client') + + ->set(\Http\Client\HttpClient::class, HttplugClient::class) + ->args([ + service('http_client'), + service(ResponseFactoryInterface::class)->ignoreOnInvalid(), + service(StreamFactoryInterface::class)->ignoreOnInvalid(), + ]) + + // retry + ->set('http_client.retry.abstract_exponential_backoff', ExponentialBackOff::class) + ->abstract() + ->args([ + abstract_arg('delay ms'), + abstract_arg('multiplier'), + abstract_arg('max delay ms'), + ]) + ->set('http_client.retry.abstract_httpstatuscode_decider', HttpStatusCodeDecider::class) + ->abstract() + ->args([ + abstract_arg('http codes'), + ]) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client.xml deleted file mode 100644 index 10256b69d5e96..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client_debug.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client_debug.php new file mode 100644 index 0000000000000..44031eb5f8e52 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client_debug.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\HttpClient\DataCollector\HttpClientDataCollector; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('data_collector.http_client', HttpClientDataCollector::class) + ->tag('data_collector', [ + 'template' => '@WebProfiler/Collector/http_client.html.twig', + 'id' => 'http_client', + 'priority' => 250, + ]) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client_debug.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client_debug.xml deleted file mode 100644 index 6d6ae4b729093..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client_debug.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/identity_translator.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/identity_translator.php new file mode 100644 index 0000000000000..a9066e1f00e80 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/identity_translator.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\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\Translation\IdentityTranslator; +use Symfony\Contracts\Translation\TranslatorInterface; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('translator', IdentityTranslator::class) + ->public() + ->tag('container.private', ['package' => 'symfony/framework-bundle', 'version' => '5.2']) + ->alias(TranslatorInterface::class, 'translator') + + ->set('identity_translator', IdentityTranslator::class) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/identity_translator.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/identity_translator.xml deleted file mode 100644 index 9dccb43ee52e0..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/identity_translator.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/lock.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/lock.php new file mode 100644 index 0000000000000..4e14636211c2d --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/lock.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\Lock\LockFactory; +use Symfony\Component\Lock\Store\CombinedStore; +use Symfony\Component\Lock\Strategy\ConsensusStrategy; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('lock.store.combined.abstract', CombinedStore::class)->abstract() + ->args([abstract_arg('List of stores'), service('lock.strategy.majority')]) + + ->set('lock.strategy.majority', ConsensusStrategy::class) + + ->set('lock.factory.abstract', LockFactory::class)->abstract() + ->args([abstract_arg('Store')]) + ->call('setLogger', [service('logger')->ignoreOnInvalid()]) + ->tag('monolog.logger', ['channel' => 'lock']) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/lock.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/lock.xml deleted file mode 100644 index 86b8571c083e9..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/lock.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.php new file mode 100644 index 0000000000000..350a6b27d7dc6 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\Mailer\EventListener\EnvelopeListener; +use Symfony\Component\Mailer\EventListener\MessageListener; +use Symfony\Component\Mailer\EventListener\MessageLoggerListener; +use Symfony\Component\Mailer\Mailer; +use Symfony\Component\Mailer\MailerInterface; +use Symfony\Component\Mailer\Messenger\MessageHandler; +use Symfony\Component\Mailer\Transport; +use Symfony\Component\Mailer\Transport\TransportInterface; +use Symfony\Component\Mailer\Transport\Transports; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('mailer.mailer', Mailer::class) + ->args([ + service('mailer.transports'), + abstract_arg('message bus'), + service('event_dispatcher')->ignoreOnInvalid(), + ]) + ->alias('mailer', 'mailer.mailer') + ->alias(MailerInterface::class, 'mailer.mailer') + + ->set('mailer.transports', Transports::class) + ->factory([service('mailer.transport_factory'), 'fromStrings']) + ->args([ + abstract_arg('transports'), + ]) + + ->set('mailer.transport_factory', Transport::class) + ->args([ + tagged_iterator('mailer.transport_factory'), + ]) + + ->set('mailer.default_transport', TransportInterface::class) + ->factory([service('mailer.transport_factory'), 'fromString']) + ->args([ + abstract_arg('env(MAILER_DSN)'), + ]) + ->alias(TransportInterface::class, 'mailer.default_transport') + + ->set('mailer.messenger.message_handler', MessageHandler::class) + ->args([ + service('mailer.transports'), + ]) + ->tag('messenger.message_handler') + + ->set('mailer.envelope_listener', EnvelopeListener::class) + ->args([ + abstract_arg('sender'), + abstract_arg('recipients'), + ]) + ->tag('kernel.event_subscriber') + + ->set('mailer.message_listener', MessageListener::class) + ->args([ + abstract_arg('headers'), + ]) + ->tag('kernel.event_subscriber') + + ->set('mailer.logger_message_listener', MessageLoggerListener::class) + ->tag('kernel.event_subscriber') + ->tag('kernel.reset', ['method' => 'reset']) + ->deprecate('symfony/framework-bundle', '5.2', 'The "%service_id%" service is deprecated, use "mailer.message_logger_listener" instead.') + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.xml deleted file mode 100644 index b7a6cbc11897d..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_debug.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_debug.php new file mode 100644 index 0000000000000..5bc56faad5a05 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_debug.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\Mailer\DataCollector\MessageDataCollector; +use Symfony\Component\Mailer\EventListener\MessageLoggerListener; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('mailer.data_collector', MessageDataCollector::class) + ->args([ + service('mailer.message_logger_listener'), + ]) + ->tag('data_collector', [ + 'template' => '@WebProfiler/Collector/mailer.html.twig', + 'id' => 'mailer', + ]) + + ->set('mailer.message_logger_listener', MessageLoggerListener::class) + ->tag('kernel.event_subscriber') + ->tag('kernel.reset', ['method' => 'reset']) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_debug.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_debug.xml deleted file mode 100644 index 17e1a6ed54ad9..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_debug.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.php new file mode 100644 index 0000000000000..ec6dd28aafbc8 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\Mailer\Bridge\Amazon\Transport\SesTransportFactory; +use Symfony\Component\Mailer\Bridge\Google\Transport\GmailTransportFactory; +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\Postmark\Transport\PostmarkTransportFactory; +use Symfony\Component\Mailer\Bridge\Sendgrid\Transport\SendgridTransportFactory; +use Symfony\Component\Mailer\Bridge\Sendinblue\Transport\SendinblueTransportFactory; +use Symfony\Component\Mailer\Transport\AbstractTransportFactory; +use Symfony\Component\Mailer\Transport\NativeTransportFactory; +use Symfony\Component\Mailer\Transport\NullTransportFactory; +use Symfony\Component\Mailer\Transport\SendmailTransportFactory; +use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransportFactory; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('mailer.transport_factory.abstract', AbstractTransportFactory::class) + ->abstract() + ->args([ + service('event_dispatcher'), + service('http_client')->ignoreOnInvalid(), + service('logger')->ignoreOnInvalid(), + ]) + + ->set('mailer.transport_factory.amazon', SesTransportFactory::class) + ->parent('mailer.transport_factory.abstract') + ->tag('mailer.transport_factory') + + ->set('mailer.transport_factory.gmail', GmailTransportFactory::class) + ->parent('mailer.transport_factory.abstract') + ->tag('mailer.transport_factory') + + ->set('mailer.transport_factory.mailchimp', MandrillTransportFactory::class) + ->parent('mailer.transport_factory.abstract') + ->tag('mailer.transport_factory') + + ->set('mailer.transport_factory.mailjet', MailjetTransportFactory::class) + ->parent('mailer.transport_factory.abstract') + ->tag('mailer.transport_factory') + + ->set('mailer.transport_factory.mailgun', MailgunTransportFactory::class) + ->parent('mailer.transport_factory.abstract') + ->tag('mailer.transport_factory') + + ->set('mailer.transport_factory.postmark', PostmarkTransportFactory::class) + ->parent('mailer.transport_factory.abstract') + ->tag('mailer.transport_factory') + + ->set('mailer.transport_factory.sendgrid', SendgridTransportFactory::class) + ->parent('mailer.transport_factory.abstract') + ->tag('mailer.transport_factory') + + ->set('mailer.transport_factory.null', NullTransportFactory::class) + ->parent('mailer.transport_factory.abstract') + ->tag('mailer.transport_factory') + + ->set('mailer.transport_factory.sendmail', SendmailTransportFactory::class) + ->parent('mailer.transport_factory.abstract') + ->tag('mailer.transport_factory') + + ->set('mailer.transport_factory.sendinblue', SendinblueTransportFactory::class) + ->parent('mailer.transport_factory.abstract') + ->tag('mailer.transport_factory') + + ->set('mailer.transport_factory.smtp', EsmtpTransportFactory::class) + ->parent('mailer.transport_factory.abstract') + ->tag('mailer.transport_factory', ['priority' => -100]) + + ->set('mailer.transport_factory.native', NativeTransportFactory::class) + ->parent('mailer.transport_factory.abstract') + ->tag('mailer.transport_factory'); +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.php new file mode 100644 index 0000000000000..120ce6c50b42c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.php @@ -0,0 +1,188 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\Messenger\Bridge\AmazonSqs\Transport\AmazonSqsTransportFactory; +use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpTransportFactory; +use Symfony\Component\Messenger\Bridge\Beanstalkd\Transport\BeanstalkdTransportFactory; +use Symfony\Component\Messenger\Bridge\Redis\Transport\RedisTransportFactory; +use Symfony\Component\Messenger\EventListener\DispatchPcntlSignalListener; +use Symfony\Component\Messenger\EventListener\SendFailedMessageForRetryListener; +use Symfony\Component\Messenger\EventListener\SendFailedMessageToFailureTransportListener; +use Symfony\Component\Messenger\EventListener\StopWorkerOnRestartSignalListener; +use Symfony\Component\Messenger\EventListener\StopWorkerOnSigtermSignalListener; +use Symfony\Component\Messenger\Middleware\AddBusNameStampMiddleware; +use Symfony\Component\Messenger\Middleware\DispatchAfterCurrentBusMiddleware; +use Symfony\Component\Messenger\Middleware\FailedMessageProcessingMiddleware; +use Symfony\Component\Messenger\Middleware\HandleMessageMiddleware; +use Symfony\Component\Messenger\Middleware\RejectRedeliveredMessageMiddleware; +use Symfony\Component\Messenger\Middleware\SendMessageMiddleware; +use Symfony\Component\Messenger\Middleware\TraceableMiddleware; +use Symfony\Component\Messenger\Middleware\ValidationMiddleware; +use Symfony\Component\Messenger\Retry\MultiplierRetryStrategy; +use Symfony\Component\Messenger\RoutableMessageBus; +use Symfony\Component\Messenger\Transport\InMemoryTransportFactory; +use Symfony\Component\Messenger\Transport\Sender\SendersLocator; +use Symfony\Component\Messenger\Transport\Serialization\Normalizer\FlattenExceptionNormalizer; +use Symfony\Component\Messenger\Transport\Serialization\PhpSerializer; +use Symfony\Component\Messenger\Transport\Serialization\Serializer; +use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface; +use Symfony\Component\Messenger\Transport\Sync\SyncTransportFactory; +use Symfony\Component\Messenger\Transport\TransportFactory; + +return static function (ContainerConfigurator $container) { + $container->services() + ->alias(SerializerInterface::class, 'messenger.default_serializer') + + // Asynchronous + ->set('messenger.senders_locator', SendersLocator::class) + ->args([ + abstract_arg('per message senders map'), + abstract_arg('senders service locator'), + ]) + ->set('messenger.middleware.send_message', SendMessageMiddleware::class) + ->args([ + service('messenger.senders_locator'), + service('event_dispatcher'), + ]) + ->call('setLogger', [service('logger')->ignoreOnInvalid()]) + ->tag('monolog.logger', ['channel' => 'messenger']) + + // Message encoding/decoding + ->set('messenger.transport.symfony_serializer', Serializer::class) + ->args([ + service('serializer'), + abstract_arg('format'), + abstract_arg('context'), + ]) + + ->set('serializer.normalizer.flatten_exception', FlattenExceptionNormalizer::class) + ->tag('serializer.normalizer', ['priority' => -880]) + + ->set('messenger.transport.native_php_serializer', PhpSerializer::class) + + // Middleware + ->set('messenger.middleware.handle_message', HandleMessageMiddleware::class) + ->abstract() + ->args([ + abstract_arg('bus handler resolver'), + ]) + ->tag('monolog.logger', ['channel' => 'messenger']) + ->call('setLogger', [service('logger')->ignoreOnInvalid()]) + + ->set('messenger.middleware.add_bus_name_stamp_middleware', AddBusNameStampMiddleware::class) + ->abstract() + + ->set('messenger.middleware.dispatch_after_current_bus', DispatchAfterCurrentBusMiddleware::class) + + ->set('messenger.middleware.validation', ValidationMiddleware::class) + ->args([ + service('validator'), + ]) + + ->set('messenger.middleware.reject_redelivered_message_middleware', RejectRedeliveredMessageMiddleware::class) + + ->set('messenger.middleware.failed_message_processing_middleware', FailedMessageProcessingMiddleware::class) + + ->set('messenger.middleware.traceable', TraceableMiddleware::class) + ->abstract() + ->args([ + service('debug.stopwatch'), + ]) + + // Discovery + ->set('messenger.receiver_locator') + ->args([ + [], + ]) + ->tag('container.service_locator') + + // Transports + ->set('messenger.transport_factory', TransportFactory::class) + ->args([ + tagged_iterator('messenger.transport_factory'), + ]) + + ->set('messenger.transport.amqp.factory', AmqpTransportFactory::class) + + ->set('messenger.transport.redis.factory', RedisTransportFactory::class) + + ->set('messenger.transport.sync.factory', SyncTransportFactory::class) + ->args([ + service('messenger.routable_message_bus'), + ]) + ->tag('messenger.transport_factory') + + ->set('messenger.transport.in_memory.factory', InMemoryTransportFactory::class) + ->tag('messenger.transport_factory') + ->tag('kernel.reset', ['method' => 'reset']) + + ->set('messenger.transport.sqs.factory', AmazonSqsTransportFactory::class) + + ->set('messenger.transport.beanstalkd.factory', BeanstalkdTransportFactory::class) + + // retry + ->set('messenger.retry_strategy_locator') + ->args([ + [], + ]) + ->tag('container.service_locator') + + ->set('messenger.retry.abstract_multiplier_retry_strategy', MultiplierRetryStrategy::class) + ->abstract() + ->args([ + abstract_arg('max retries'), + abstract_arg('delay ms'), + abstract_arg('multiplier'), + abstract_arg('max delay ms'), + ]) + + // worker event listener + ->set('messenger.retry.send_failed_message_for_retry_listener', SendFailedMessageForRetryListener::class) + ->args([ + abstract_arg('senders service locator'), + service('messenger.retry_strategy_locator'), + service('logger')->ignoreOnInvalid(), + service('event_dispatcher'), + ]) + ->tag('kernel.event_subscriber') + ->tag('monolog.logger', ['channel' => 'messenger']) + + ->set('messenger.failure.send_failed_message_to_failure_transport_listener', SendFailedMessageToFailureTransportListener::class) + ->args([ + abstract_arg('failure transport'), + service('logger')->ignoreOnInvalid(), + ]) + ->tag('kernel.event_subscriber') + ->tag('monolog.logger', ['channel' => 'messenger']) + + ->set('messenger.listener.dispatch_pcntl_signal_listener', DispatchPcntlSignalListener::class) + ->tag('kernel.event_subscriber') + + ->set('messenger.listener.stop_worker_on_restart_signal_listener', StopWorkerOnRestartSignalListener::class) + ->args([ + service('cache.messenger.restart_workers_signal'), + service('logger')->ignoreOnInvalid(), + ]) + ->tag('kernel.event_subscriber') + ->tag('monolog.logger', ['channel' => 'messenger']) + + ->set('messenger.listener.stop_worker_on_sigterm_signal_listener', StopWorkerOnSigtermSignalListener::class) + ->tag('kernel.event_subscriber') + + ->set('messenger.routable_message_bus', RoutableMessageBus::class) + ->args([ + abstract_arg('message bus locator'), + service('messenger.default_bus'), + ]) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.xml deleted file mode 100644 index 1cd003170de23..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.xml +++ /dev/null @@ -1,136 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger_debug.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger_debug.php new file mode 100644 index 0000000000000..58f9be1f9e531 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger_debug.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\Messenger\DataCollector\MessengerDataCollector; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('data_collector.messenger', MessengerDataCollector::class) + ->tag('data_collector', [ + 'template' => '@WebProfiler/Collector/messenger.html.twig', + 'id' => 'messenger', + 'priority' => 100, + ]) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger_debug.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger_debug.xml deleted file mode 100644 index 96f43b3b33d79..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger_debug.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mime_type.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mime_type.php new file mode 100644 index 0000000000000..0d874a07ed321 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mime_type.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\Mime\MimeTypes; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('mime_types', MimeTypes::class) + ->call('setDefault', [service('mime_types')]) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mime_type.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mime_type.xml deleted file mode 100644 index d4c1eb15b9088..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mime_type.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier.php new file mode 100644 index 0000000000000..a9c447470b667 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier.php @@ -0,0 +1,109 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Bridge\Monolog\Handler\NotifierHandler; +use Symfony\Component\Notifier\Channel\BrowserChannel; +use Symfony\Component\Notifier\Channel\ChannelPolicy; +use Symfony\Component\Notifier\Channel\ChatChannel; +use Symfony\Component\Notifier\Channel\EmailChannel; +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\SmsMessage; +use Symfony\Component\Notifier\Messenger\MessageHandler; +use Symfony\Component\Notifier\Notifier; +use Symfony\Component\Notifier\NotifierInterface; +use Symfony\Component\Notifier\Texter; +use Symfony\Component\Notifier\TexterInterface; +use Symfony\Component\Notifier\Transport; +use Symfony\Component\Notifier\Transport\Transports; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('notifier', Notifier::class) + ->args([tagged_locator('notifier.channel', 'channel'), service('notifier.channel_policy')->ignoreOnInvalid()]) + + ->alias(NotifierInterface::class, 'notifier') + + ->set('notifier.channel_policy', ChannelPolicy::class) + ->args([[]]) + + ->set('notifier.channel.browser', BrowserChannel::class) + ->args([service('request_stack')]) + ->tag('notifier.channel', ['channel' => 'browser']) + + ->set('notifier.channel.chat', ChatChannel::class) + ->args([service('chatter.transports'), service('messenger.default_bus')->ignoreOnInvalid()]) + ->tag('notifier.channel', ['channel' => 'chat']) + + ->set('notifier.channel.sms', SmsChannel::class) + ->args([service('texter.transports'), service('messenger.default_bus')->ignoreOnInvalid()]) + ->tag('notifier.channel', ['channel' => 'sms']) + + ->set('notifier.channel.email', EmailChannel::class) + ->args([service('mailer.transports'), service('messenger.default_bus')->ignoreOnInvalid()]) + ->tag('notifier.channel', ['channel' => 'email']) + + ->set('notifier.monolog_handler', NotifierHandler::class) + ->args([service('notifier')]) + + ->set('notifier.failed_message_listener', SendFailedMessageToNotifierListener::class) + ->args([service('notifier')]) + + ->set('chatter', Chatter::class) + ->args([ + service('chatter.transports'), + service('messenger.default_bus')->ignoreOnInvalid(), + service('event_dispatcher')->ignoreOnInvalid(), + ]) + + ->alias(ChatterInterface::class, 'chatter') + + ->set('chatter.transports', Transports::class) + ->factory([service('chatter.transport_factory'), 'fromStrings']) + ->args([[]]) + + ->set('chatter.transport_factory', Transport::class) + ->args([tagged_iterator('chatter.transport_factory')]) + + ->set('chatter.messenger.chat_handler', MessageHandler::class) + ->args([service('chatter.transports')]) + ->tag('messenger.message_handler', ['handles' => ChatMessage::class]) + + ->set('texter', Texter::class) + ->args([ + service('texter.transports'), + service('messenger.default_bus')->ignoreOnInvalid(), + service('event_dispatcher')->ignoreOnInvalid(), + ]) + + ->alias(TexterInterface::class, 'texter') + + ->set('texter.transports', Transports::class) + ->factory([service('texter.transport_factory'), 'fromStrings']) + ->args([[]]) + + ->set('texter.transport_factory', Transport::class) + ->args([tagged_iterator('texter.transport_factory')]) + + ->set('texter.messenger.sms_handler', MessageHandler::class) + ->args([service('texter.transports')]) + ->tag('messenger.message_handler', ['handles' => SmsMessage::class]) + + ->set('notifier.logger_notification_listener', NotificationLoggerListener::class) + ->tag('kernel.event_subscriber') + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier.xml deleted file mode 100644 index dfc6cdccd34c7..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier.xml +++ /dev/null @@ -1,93 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_debug.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_debug.php new file mode 100644 index 0000000000000..6147d34e4e7eb --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_debug.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\Notifier\DataCollector\NotificationDataCollector; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('notifier.data_collector', NotificationDataCollector::class) + ->args([service('notifier.logger_notification_listener')]) + ->tag('data_collector', ['template' => '@WebProfiler/Collector/notifier.html.twig', 'id' => 'notifier']) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php new file mode 100644 index 0000000000000..37e339734ea52 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php @@ -0,0 +1,118 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\Notifier\Bridge\Esendex\EsendexTransportFactory; +use Symfony\Component\Notifier\Bridge\Firebase\FirebaseTransportFactory; +use Symfony\Component\Notifier\Bridge\FreeMobile\FreeMobileTransportFactory; +use Symfony\Component\Notifier\Bridge\GoogleChat\GoogleChatTransportFactory; +use Symfony\Component\Notifier\Bridge\Infobip\InfobipTransportFactory; +use Symfony\Component\Notifier\Bridge\LinkedIn\LinkedInTransportFactory; +use Symfony\Component\Notifier\Bridge\Mattermost\MattermostTransportFactory; +use Symfony\Component\Notifier\Bridge\Mobyt\MobytTransportFactory; +use Symfony\Component\Notifier\Bridge\Nexmo\NexmoTransportFactory; +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\Smsapi\SmsapiTransportFactory; +use Symfony\Component\Notifier\Bridge\Telegram\TelegramTransportFactory; +use Symfony\Component\Notifier\Bridge\Twilio\TwilioTransportFactory; +use Symfony\Component\Notifier\Bridge\Zulip\ZulipTransportFactory; +use Symfony\Component\Notifier\Transport\AbstractTransportFactory; +use Symfony\Component\Notifier\Transport\NullTransportFactory; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('notifier.transport_factory.abstract', AbstractTransportFactory::class) + ->abstract() + ->args([service('event_dispatcher'), service('http_client')->ignoreOnInvalid()]) + + ->set('notifier.transport_factory.slack', SlackTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('chatter.transport_factory') + + ->set('notifier.transport_factory.linkedin', LinkedInTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('chatter.transport_factory') + + ->set('notifier.transport_factory.telegram', TelegramTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('chatter.transport_factory') + + ->set('notifier.transport_factory.mattermost', MattermostTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('chatter.transport_factory') + + ->set('notifier.transport_factory.nexmo', NexmoTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.rocketchat', RocketChatTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('chatter.transport_factory') + + ->set('notifier.transport_factory.googlechat', GoogleChatTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('chatter.transport_factory') + + ->set('notifier.transport_factory.twilio', TwilioTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.firebase', FirebaseTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.freemobile', FreeMobileTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.ovhcloud', OvhCloudTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.sinch', SinchTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.zulip', ZulipTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.infobip', InfobipTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.mobyt', MobytTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.smsapi', SmsapiTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.esendex', EsendexTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.sendinblue', SendinblueTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.null', NullTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('chatter.transport_factory') + ->tag('texter.transport_factory') + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.xml deleted file mode 100644 index 045eb52a1b96e..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.xml +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/profiling.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/profiling.php new file mode 100644 index 0000000000000..68a09c9f5fe17 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/profiling.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\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\HttpKernel\EventListener\ProfilerListener; +use Symfony\Component\HttpKernel\Profiler\FileProfilerStorage; +use Symfony\Component\HttpKernel\Profiler\Profiler; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('profiler', Profiler::class) + ->public() + ->args([service('profiler.storage'), service('logger')->nullOnInvalid()]) + ->tag('monolog.logger', ['channel' => 'profiler']) + + ->set('profiler.storage', FileProfilerStorage::class) + ->args([param('profiler.storage.dsn')]) + + ->set('profiler_listener', ProfilerListener::class) + ->args([ + service('profiler'), + service('request_stack'), + null, + param('profiler_listener.only_exceptions'), + param('profiler_listener.only_master_requests'), + ]) + ->tag('kernel.event_subscriber') + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/profiling.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/profiling.xml deleted file mode 100644 index 166be86b2e203..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/profiling.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - %profiler.storage.dsn% - - - - - - - null - %profiler_listener.only_exceptions% - %profiler_listener.only_master_requests% - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_access.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_access.php new file mode 100644 index 0000000000000..00d8f66b5afa6 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_access.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\PropertyAccess\PropertyAccessor; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('property_accessor', PropertyAccessor::class) + ->args([ + abstract_arg('magic methods allowed, set by the extension'), + abstract_arg('throwExceptionOnInvalidIndex, set by the extension'), + service('cache.property_access')->ignoreOnInvalid(), + abstract_arg('throwExceptionOnInvalidPropertyPath, set by the extension'), + abstract_arg('propertyReadInfoExtractor, set by the extension'), + abstract_arg('propertyWriteInfoExtractor, set by the extension'), + ]) + + ->alias(PropertyAccessorInterface::class, 'property_accessor') + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_access.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_access.xml deleted file mode 100644 index 4dfe97e0de6da..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_access.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_info.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_info.php new file mode 100644 index 0000000000000..90587839d54c4 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_info.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; +use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyInfoCacheExtractor; +use Symfony\Component\PropertyInfo\PropertyInfoExtractor; +use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyInitializableExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyReadInfoExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyWriteInfoExtractorInterface; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('property_info', PropertyInfoExtractor::class) + ->args([[], [], [], [], []]) + + ->alias(PropertyAccessExtractorInterface::class, 'property_info') + ->alias(PropertyDescriptionExtractorInterface::class, 'property_info') + ->alias(PropertyInfoExtractorInterface::class, 'property_info') + ->alias(PropertyTypeExtractorInterface::class, 'property_info') + ->alias(PropertyListExtractorInterface::class, 'property_info') + ->alias(PropertyInitializableExtractorInterface::class, 'property_info') + + ->set('property_info.cache', PropertyInfoCacheExtractor::class) + ->decorate('property_info') + ->args([service('property_info.cache.inner'), service('cache.property_info')]) + + // Extractor + ->set('property_info.reflection_extractor', ReflectionExtractor::class) + ->tag('property_info.list_extractor', ['priority' => -1000]) + ->tag('property_info.type_extractor', ['priority' => -1002]) + ->tag('property_info.access_extractor', ['priority' => -1000]) + ->tag('property_info.initializable_extractor', ['priority' => -1000]) + + ->alias(PropertyReadInfoExtractorInterface::class, 'property_info.reflection_extractor') + ->alias(PropertyWriteInfoExtractorInterface::class, 'property_info.reflection_extractor') + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_info.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_info.xml deleted file mode 100644 index 103baa2b8884c..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_info.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/rate_limiter.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/rate_limiter.php new file mode 100644 index 0000000000000..104b8023790bd --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/rate_limiter.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\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\RateLimiter\Limiter; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('cache.rate_limiter') + ->parent('cache.app') + ->tag('cache.pool') + + ->set('limiter', Limiter::class) + ->abstract() + ->args([ + abstract_arg('config'), + abstract_arg('storage'), + ]) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/request.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/request.php new file mode 100644 index 0000000000000..ef8fc9a5e7d8c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/request.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\HttpKernel\EventListener\AddRequestFormatsListener; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('request.add_request_formats_listener', AddRequestFormatsListener::class) + ->args([abstract_arg('formats')]) + ->tag('kernel.event_subscriber') + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/request.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/request.xml deleted file mode 100644 index 048b61ec466f0..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/request.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.php new file mode 100644 index 0000000000000..bd44427bf65a1 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.php @@ -0,0 +1,176 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Psr\Container\ContainerInterface; +use Symfony\Bundle\FrameworkBundle\CacheWarmer\RouterCacheWarmer; +use Symfony\Bundle\FrameworkBundle\Controller\RedirectController; +use Symfony\Bundle\FrameworkBundle\Controller\TemplateController; +use Symfony\Bundle\FrameworkBundle\Routing\DelegatingLoader; +use Symfony\Bundle\FrameworkBundle\Routing\RedirectableCompiledUrlMatcher; +use Symfony\Bundle\FrameworkBundle\Routing\Router; +use Symfony\Component\Config\Loader\LoaderResolver; +use Symfony\Component\HttpKernel\EventListener\RouterListener; +use Symfony\Component\Routing\Generator\CompiledUrlGenerator; +use Symfony\Component\Routing\Generator\Dumper\CompiledUrlGeneratorDumper; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Component\Routing\Loader\ContainerLoader; +use Symfony\Component\Routing\Loader\DirectoryLoader; +use Symfony\Component\Routing\Loader\GlobFileLoader; +use Symfony\Component\Routing\Loader\PhpFileLoader; +use Symfony\Component\Routing\Loader\XmlFileLoader; +use Symfony\Component\Routing\Loader\YamlFileLoader; +use Symfony\Component\Routing\Matcher\Dumper\CompiledUrlMatcherDumper; +use Symfony\Component\Routing\Matcher\ExpressionLanguageProvider; +use Symfony\Component\Routing\Matcher\UrlMatcherInterface; +use Symfony\Component\Routing\RequestContext; +use Symfony\Component\Routing\RequestContextAwareInterface; +use Symfony\Component\Routing\RouterInterface; + +return static function (ContainerConfigurator $container) { + $container->parameters() + ->set('router.request_context.host', 'localhost') + ->set('router.request_context.scheme', 'http') + ->set('router.request_context.base_url', '') + ; + + $container->services() + ->set('routing.resolver', LoaderResolver::class) + + ->set('routing.loader.xml', XmlFileLoader::class) + ->args([ + service('file_locator'), + ]) + ->tag('routing.loader') + + ->set('routing.loader.yml', YamlFileLoader::class) + ->args([ + service('file_locator'), + ]) + ->tag('routing.loader') + + ->set('routing.loader.php', PhpFileLoader::class) + ->args([ + service('file_locator'), + ]) + ->tag('routing.loader') + + ->set('routing.loader.glob', GlobFileLoader::class) + ->args([ + service('file_locator'), + ]) + ->tag('routing.loader') + + ->set('routing.loader.directory', DirectoryLoader::class) + ->args([ + service('file_locator'), + ]) + ->tag('routing.loader') + + ->set('routing.loader.container', ContainerLoader::class) + ->args([ + tagged_locator('routing.route_loader'), + ]) + ->tag('routing.loader') + + ->set('routing.loader', DelegatingLoader::class) + ->public() + ->args([ + service('routing.resolver'), + [], // Default options + [], // Default requirements + ]) + + ->set('router.default', Router::class) + ->args([ + service(ContainerInterface::class), + param('router.resource'), + [ + 'cache_dir' => param('kernel.cache_dir'), + 'debug' => param('kernel.debug'), + 'generator_class' => CompiledUrlGenerator::class, + 'generator_dumper_class' => CompiledUrlGeneratorDumper::class, + 'matcher_class' => RedirectableCompiledUrlMatcher::class, + 'matcher_dumper_class' => CompiledUrlMatcherDumper::class, + ], + service('router.request_context')->ignoreOnInvalid(), + service('parameter_bag')->ignoreOnInvalid(), + service('logger')->ignoreOnInvalid(), + param('kernel.default_locale'), + ]) + ->call('setConfigCacheFactory', [ + service('config_cache_factory'), + ]) + ->tag('monolog.logger', ['channel' => 'router']) + ->tag('container.service_subscriber', ['id' => 'routing.loader']) + ->alias('router', 'router.default') + ->public() + ->alias(RouterInterface::class, 'router') + ->alias(UrlGeneratorInterface::class, 'router') + ->alias(UrlMatcherInterface::class, 'router') + ->alias(RequestContextAwareInterface::class, 'router') + + ->set('router.request_context', RequestContext::class) + ->factory([RequestContext::class, 'fromUri']) + ->args([ + param('router.request_context.base_url'), + param('router.request_context.host'), + param('router.request_context.scheme'), + param('request_listener.http_port'), + param('request_listener.https_port'), + ]) + ->call('setParameter', [ + '_functions', + service('router.expression_language_provider')->ignoreOnInvalid(), + ]) + ->alias(RequestContext::class, 'router.request_context') + + ->set('router.expression_language_provider', ExpressionLanguageProvider::class) + ->args([ + tagged_locator('routing.expression_language_function', 'function'), + ]) + ->tag('routing.expression_language_provider') + + ->set('router.cache_warmer', RouterCacheWarmer::class) + ->args([service(ContainerInterface::class)]) + ->tag('container.service_subscriber', ['id' => 'router']) + ->tag('kernel.cache_warmer') + + ->set('router_listener', RouterListener::class) + ->args([ + service('router'), + service('request_stack'), + service('router.request_context')->ignoreOnInvalid(), + service('logger')->ignoreOnInvalid(), + param('kernel.project_dir'), + param('kernel.debug'), + ]) + ->tag('kernel.event_subscriber') + ->tag('monolog.logger', ['channel' => 'request']) + + ->set(RedirectController::class) + ->public() + ->args([ + service('router'), + inline_service('int') + ->factory([service('router.request_context'), 'getHttpPort']), + inline_service('int') + ->factory([service('router.request_context'), 'getHttpsPort']), + ]) + + ->set(TemplateController::class) + ->args([ + service('twig')->ignoreOnInvalid(), + ]) + ->public() + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml deleted file mode 100644 index 669b27d72cbd3..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml +++ /dev/null @@ -1,128 +0,0 @@ - - - - - - localhost - http - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - %router.resource% - - %kernel.cache_dir% - %kernel.debug% - Symfony\Component\Routing\Generator\CompiledUrlGenerator - Symfony\Component\Routing\Generator\Dumper\CompiledUrlGeneratorDumper - Symfony\Bundle\FrameworkBundle\Routing\RedirectableCompiledUrlMatcher - Symfony\Component\Routing\Matcher\Dumper\CompiledUrlMatcherDumper - - - - - %kernel.default_locale% - - - - - - - - - - - - - - %router.request_context.base_url% - %router.request_context.host% - %router.request_context.scheme% - %request_listener.http_port% - %request_listener.https_port% - - _functions - - - - - - - - - - - - - - - - - - - - - - - - %kernel.project_dir% - %kernel.debug% - - - - - - - - - - - - - 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 815efd8870650..49c1eb8b57dba 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 @@ -33,6 +33,8 @@ + + @@ -41,6 +43,9 @@ + + + @@ -48,6 +53,7 @@ + @@ -165,6 +171,7 @@ + @@ -173,6 +180,17 @@ + + + + + + + + + + + @@ -219,6 +237,8 @@ + + @@ -265,6 +285,7 @@ + @@ -278,6 +299,7 @@ + @@ -335,6 +357,18 @@ + + + + + + + + + + + + @@ -478,6 +512,7 @@ + @@ -485,6 +520,7 @@ + @@ -501,7 +537,6 @@ - @@ -510,6 +545,7 @@ + @@ -540,6 +576,20 @@ + + + + + + + + + + + + + + @@ -556,11 +606,16 @@ + + + + + @@ -571,4 +626,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/secrets.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/secrets.php new file mode 100644 index 0000000000000..a21d282702e13 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/secrets.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Bundle\FrameworkBundle\Secrets\DotenvVault; +use Symfony\Bundle\FrameworkBundle\Secrets\SodiumVault; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('secrets.vault', SodiumVault::class) + ->args([ + abstract_arg('Secret dir, set in FrameworkExtension'), + service('secrets.decryption_key')->ignoreOnInvalid(), + ]) + ->tag('container.env_var_loader') + + ->set('secrets.decryption_key') + ->parent('container.env') + ->args([abstract_arg('Decryption env var, set in FrameworkExtension')]) + + ->set('secrets.local_vault', DotenvVault::class) + ->args([abstract_arg('.env file path, set in FrameworkExtension')]) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/secrets.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/secrets.xml deleted file mode 100644 index 5c514e3461b51..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/secrets.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/security_csrf.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/security_csrf.php new file mode 100644 index 0000000000000..0afc740cd89bf --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/security_csrf.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Bridge\Twig\Extension\CsrfExtension; +use Symfony\Bridge\Twig\Extension\CsrfRuntime; +use Symfony\Component\Security\Csrf\CsrfTokenManager; +use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; +use Symfony\Component\Security\Csrf\TokenGenerator\TokenGeneratorInterface; +use Symfony\Component\Security\Csrf\TokenGenerator\UriSafeTokenGenerator; +use Symfony\Component\Security\Csrf\TokenStorage\SessionTokenStorage; +use Symfony\Component\Security\Csrf\TokenStorage\TokenStorageInterface; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('security.csrf.token_generator', UriSafeTokenGenerator::class) + + ->alias(TokenGeneratorInterface::class, 'security.csrf.token_generator') + + ->set('security.csrf.token_storage', SessionTokenStorage::class) + ->args([service('session')]) + + ->alias(TokenStorageInterface::class, 'security.csrf.token_storage') + + ->set('security.csrf.token_manager', CsrfTokenManager::class) + ->public() + ->args([ + service('security.csrf.token_generator'), + service('security.csrf.token_storage'), + service('request_stack')->ignoreOnInvalid(), + ]) + ->tag('container.private', ['package' => 'symfony/framework-bundle', 'version' => '5.2']) + + ->alias(CsrfTokenManagerInterface::class, 'security.csrf.token_manager') + + ->set('twig.runtime.security_csrf', CsrfRuntime::class) + ->args([service('security.csrf.token_manager')]) + ->tag('twig.runtime') + + ->set('twig.extension.security_csrf', CsrfExtension::class) + ->tag('twig.extension') + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/security_csrf.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/security_csrf.xml deleted file mode 100644 index eefe6ad73601f..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/security_csrf.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php new file mode 100644 index 0000000000000..98f40257883e6 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php @@ -0,0 +1,212 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Bundle\FrameworkBundle\CacheWarmer\SerializerCacheWarmer; +use Symfony\Component\Cache\Adapter\PhpArrayAdapter; +use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer; +use Symfony\Component\ErrorHandler\ErrorRenderer\SerializerErrorRenderer; +use Symfony\Component\PropertyInfo\Extractor\SerializerExtractor; +use Symfony\Component\Serializer\Encoder\CsvEncoder; +use Symfony\Component\Serializer\Encoder\DecoderInterface; +use Symfony\Component\Serializer\Encoder\EncoderInterface; +use Symfony\Component\Serializer\Encoder\JsonEncoder; +use Symfony\Component\Serializer\Encoder\XmlEncoder; +use Symfony\Component\Serializer\Encoder\YamlEncoder; +use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata; +use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface; +use Symfony\Component\Serializer\Mapping\Factory\CacheClassMetadataFactory; +use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; +use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; +use Symfony\Component\Serializer\Mapping\Loader\LoaderChain; +use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; +use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter; +use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; +use Symfony\Component\Serializer\Normalizer\ConstraintViolationListNormalizer; +use Symfony\Component\Serializer\Normalizer\DataUriNormalizer; +use Symfony\Component\Serializer\Normalizer\DateIntervalNormalizer; +use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; +use Symfony\Component\Serializer\Normalizer\DateTimeZoneNormalizer; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\FormErrorNormalizer; +use Symfony\Component\Serializer\Normalizer\JsonSerializableNormalizer; +use Symfony\Component\Serializer\Normalizer\MimeMessageNormalizer; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; +use Symfony\Component\Serializer\Normalizer\ProblemNormalizer; +use Symfony\Component\Serializer\Normalizer\PropertyNormalizer; +use Symfony\Component\Serializer\Normalizer\UidNormalizer; +use Symfony\Component\Serializer\Normalizer\UnwrappingDenormalizer; +use Symfony\Component\Serializer\Serializer; +use Symfony\Component\Serializer\SerializerInterface; + +return static function (ContainerConfigurator $container) { + $container->parameters() + ->set('serializer.mapping.cache.file', '%kernel.cache_dir%/serialization.php') + ; + + $container->services() + ->set('serializer', Serializer::class) + ->public() + ->args([[], []]) + ->tag('container.private', ['package' => 'symfony/framework-bundle', 'version' => '5.2']) + + ->alias(SerializerInterface::class, 'serializer') + ->alias(NormalizerInterface::class, 'serializer') + ->alias(DenormalizerInterface::class, 'serializer') + ->alias(EncoderInterface::class, 'serializer') + ->alias(DecoderInterface::class, 'serializer') + + ->alias('serializer.property_accessor', 'property_accessor') + + // Discriminator Map + ->set('serializer.mapping.class_discriminator_resolver', ClassDiscriminatorFromClassMetadata::class) + ->args([service('serializer.mapping.class_metadata_factory')]) + + ->alias(ClassDiscriminatorResolverInterface::class, 'serializer.mapping.class_discriminator_resolver') + + // Normalizer + ->set('serializer.normalizer.constraint_violation_list', ConstraintViolationListNormalizer::class) + ->args([[], service('serializer.name_converter.metadata_aware')]) + ->tag('serializer.normalizer', ['priority' => -915]) + + ->set('serializer.normalizer.mime_message', MimeMessageNormalizer::class) + ->args([service('serializer.normalizer.property')]) + ->tag('serializer.normalizer', ['priority' => -915]) + + ->set('serializer.normalizer.datetimezone', DateTimeZoneNormalizer::class) + ->tag('serializer.normalizer', ['priority' => -915]) + + ->set('serializer.normalizer.dateinterval', DateIntervalNormalizer::class) + ->tag('serializer.normalizer', ['priority' => -915]) + + ->set('serializer.normalizer.data_uri', DataUriNormalizer::class) + ->args([service('mime_types')->nullOnInvalid()]) + ->tag('serializer.normalizer', ['priority' => -920]) + + ->set('serializer.normalizer.datetime', DateTimeNormalizer::class) + ->tag('serializer.normalizer', ['priority' => -910]) + + ->set('serializer.normalizer.json_serializable', JsonSerializableNormalizer::class) + ->tag('serializer.normalizer', ['priority' => -900]) + + ->set('serializer.normalizer.problem', ProblemNormalizer::class) + ->args([param('kernel.debug')]) + ->tag('serializer.normalizer', ['priority' => -890]) + + ->set('serializer.denormalizer.unwrapping', UnwrappingDenormalizer::class) + ->args([service('serializer.property_accessor')]) + ->tag('serializer.normalizer', ['priority' => 1000]) + + ->set('serializer.normalizer.uid', UidNormalizer::class) + ->tag('serializer.normalizer', ['priority' => -915]) + + ->set('serializer.normalizer.form_error', FormErrorNormalizer::class) + ->tag('serializer.normalizer', ['priority' => -915]) + + ->set('serializer.normalizer.object', ObjectNormalizer::class) + ->args([ + service('serializer.mapping.class_metadata_factory'), + service('serializer.name_converter.metadata_aware'), + service('serializer.property_accessor'), + service('property_info')->ignoreOnInvalid(), + service('serializer.mapping.class_discriminator_resolver')->ignoreOnInvalid(), + null, + [], + ]) + ->tag('serializer.normalizer', ['priority' => -1000]) + + ->alias(ObjectNormalizer::class, 'serializer.normalizer.object') + + ->set('serializer.normalizer.property', PropertyNormalizer::class) + ->args([ + service('serializer.mapping.class_metadata_factory'), + service('serializer.name_converter.metadata_aware'), + service('property_info')->ignoreOnInvalid(), + service('serializer.mapping.class_discriminator_resolver')->ignoreOnInvalid(), + null, + [], + ]) + + ->alias(PropertyNormalizer::class, 'serializer.normalizer.property') + + ->set('serializer.denormalizer.array', ArrayDenormalizer::class) + ->tag('serializer.normalizer', ['priority' => -990]) + + // Loader + ->set('serializer.mapping.chain_loader', LoaderChain::class) + ->args([[]]) + + // Class Metadata Factory + ->set('serializer.mapping.class_metadata_factory', ClassMetadataFactory::class) + ->args([service('serializer.mapping.chain_loader')]) + + ->alias(ClassMetadataFactoryInterface::class, 'serializer.mapping.class_metadata_factory') + + // Cache + ->set('serializer.mapping.cache_warmer', SerializerCacheWarmer::class) + ->args([abstract_arg('The serializer metadata loaders'), param('serializer.mapping.cache.file')]) + ->tag('kernel.cache_warmer') + + ->set('serializer.mapping.cache.symfony', CacheItemPoolInterface::class) + ->factory([PhpArrayAdapter::class, 'create']) + ->args([param('serializer.mapping.cache.file'), service('cache.serializer')]) + + ->set('serializer.mapping.cache_class_metadata_factory', CacheClassMetadataFactory::class) + ->decorate('serializer.mapping.class_metadata_factory') + ->args([ + service('serializer.mapping.cache_class_metadata_factory.inner'), + service('serializer.mapping.cache.symfony'), + ]) + + // Encoders + ->set('serializer.encoder.xml', XmlEncoder::class) + ->tag('serializer.encoder') + + ->set('serializer.encoder.json', JsonEncoder::class) + ->tag('serializer.encoder') + + ->set('serializer.encoder.yaml', YamlEncoder::class) + ->tag('serializer.encoder') + + ->set('serializer.encoder.csv', CsvEncoder::class) + ->tag('serializer.encoder') + + // Name converter + ->set('serializer.name_converter.camel_case_to_snake_case', CamelCaseToSnakeCaseNameConverter::class) + + ->set('serializer.name_converter.metadata_aware', MetadataAwareNameConverter::class) + ->args([service('serializer.mapping.class_metadata_factory')]) + + // PropertyInfo extractor + ->set('property_info.serializer_extractor', SerializerExtractor::class) + ->args([service('serializer.mapping.class_metadata_factory')]) + ->tag('property_info.list_extractor', ['priority' => -999]) + + // ErrorRenderer integration + ->alias('error_renderer', 'error_renderer.serializer') + ->alias('error_renderer.serializer', 'error_handler.error_renderer.serializer') + + ->set('error_handler.error_renderer.serializer', SerializerErrorRenderer::class) + ->args([ + service('serializer'), + inline_service() + ->factory([SerializerErrorRenderer::class, 'getPreferredFormat']) + ->args([service('request_stack')]), + service('error_renderer.html'), + inline_service() + ->factory([HtmlErrorRenderer::class, 'isDebug']) + ->args([service('request_stack'), param('kernel.debug')]), + ]) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml deleted file mode 100644 index bc7dd2028a39a..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml +++ /dev/null @@ -1,179 +0,0 @@ - - - - - - %kernel.cache_dir%/serialization.php - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - %kernel.debug% - - - - - - - - - - - - - - - - - null - - - - - - - - - - - - - - - - - - - - - - - - - - - - %serializer.mapping.cache.file% - - - - - - %serializer.mapping.cache.file% - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - %kernel.debug% - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php new file mode 100644 index 0000000000000..4df97ed19c531 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php @@ -0,0 +1,205 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache; +use Symfony\Component\Config\Resource\SelfCheckingResourceChecker; +use Symfony\Component\Config\ResourceCheckerConfigCacheFactory; +use Symfony\Component\Console\ConsoleEvents; +use Symfony\Component\DependencyInjection\Config\ContainerParametersResourceChecker; +use Symfony\Component\DependencyInjection\EnvVarProcessor; +use Symfony\Component\DependencyInjection\ParameterBag\ContainerBag; +use Symfony\Component\DependencyInjection\ParameterBag\ContainerBagInterface; +use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; +use Symfony\Component\DependencyInjection\ReverseContainer; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\EventDispatcher\EventDispatcherInterface as EventDispatcherInterfaceComponentAlias; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\Form\FormEvents; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpFoundation\UrlHelper; +use Symfony\Component\HttpKernel\CacheClearer\ChainCacheClearer; +use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerAggregate; +use Symfony\Component\HttpKernel\Config\FileLocator; +use Symfony\Component\HttpKernel\DependencyInjection\ServicesResetter; +use Symfony\Component\HttpKernel\EventListener\LocaleAwareListener; +use Symfony\Component\HttpKernel\HttpCache\Store; +use Symfony\Component\HttpKernel\HttpKernel; +use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\HttpKernel\KernelInterface; +use Symfony\Component\HttpKernel\UriSigner; +use Symfony\Component\String\LazyString; +use Symfony\Component\String\Slugger\AsciiSlugger; +use Symfony\Component\String\Slugger\SluggerInterface; +use Symfony\Component\Workflow\WorkflowEvents; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; + +return static function (ContainerConfigurator $container) { + // this parameter is used at compile time in RegisterListenersPass + $container->parameters()->set('event_dispatcher.event_aliases', array_merge( + class_exists(ConsoleEvents::class) ? ConsoleEvents::ALIASES : [], + class_exists(FormEvents::class) ? FormEvents::ALIASES : [], + KernelEvents::ALIASES, + class_exists(WorkflowEvents::class) ? WorkflowEvents::ALIASES : [] + )); + + $container->services() + + ->set('parameter_bag', ContainerBag::class) + ->args([ + service('service_container'), + ]) + ->alias(ContainerBagInterface::class, 'parameter_bag') + ->alias(ParameterBagInterface::class, 'parameter_bag') + + ->set('event_dispatcher', EventDispatcher::class) + ->public() + ->tag('container.hot_path') + ->alias(EventDispatcherInterfaceComponentAlias::class, 'event_dispatcher') + ->alias(EventDispatcherInterface::class, 'event_dispatcher') + + ->set('http_kernel', HttpKernel::class) + ->public() + ->args([ + service('event_dispatcher'), + service('controller_resolver'), + service('request_stack'), + service('argument_resolver'), + ]) + ->tag('container.hot_path') + ->alias(HttpKernelInterface::class, 'http_kernel') + + ->set('request_stack', RequestStack::class) + ->public() + ->alias(RequestStack::class, 'request_stack') + + ->set('http_cache', HttpCache::class) + ->args([ + service('kernel'), + service('http_cache.store'), + service('esi')->nullOnInvalid(), + abstract_arg('options'), + ]) + ->tag('container.hot_path') + + ->set('http_cache.store', Store::class) + ->args([ + param('kernel.cache_dir').'/http_cache', + ]) + + ->set('url_helper', UrlHelper::class) + ->args([ + service('request_stack'), + service('router.request_context')->ignoreOnInvalid(), + ]) + ->alias(UrlHelper::class, 'url_helper') + + ->set('cache_warmer', CacheWarmerAggregate::class) + ->public() + ->args([ + tagged_iterator('kernel.cache_warmer'), + param('kernel.debug'), + sprintf('%s/%sDeprecations.log', param('kernel.cache_dir'), param('kernel.container_class')), + ]) + ->tag('container.no_preload') + + ->set('cache_clearer', ChainCacheClearer::class) + ->public() + ->args([ + tagged_iterator('kernel.cache_clearer'), + ]) + ->tag('container.private', ['package' => 'symfony/framework-bundle', 'version' => '5.2']) + + ->set('kernel') + ->synthetic() + ->public() + ->alias(KernelInterface::class, 'kernel') + + ->set('filesystem', Filesystem::class) + ->public() + ->tag('container.private', ['package' => 'symfony/framework-bundle', 'version' => '5.2']) + ->alias(Filesystem::class, 'filesystem') + + ->set('file_locator', FileLocator::class) + ->args([ + service('kernel'), + ]) + ->alias(FileLocator::class, 'file_locator') + + ->set('uri_signer', UriSigner::class) + ->args([ + param('kernel.secret'), + ]) + ->alias(UriSigner::class, 'uri_signer') + + ->set('config_cache_factory', ResourceCheckerConfigCacheFactory::class) + ->args([ + tagged_iterator('config_cache.resource_checker'), + ]) + + ->set('dependency_injection.config.container_parameters_resource_checker', ContainerParametersResourceChecker::class) + ->args([ + service('service_container'), + ]) + ->tag('config_cache.resource_checker', ['priority' => -980]) + + ->set('config.resource.self_checking_resource_checker', SelfCheckingResourceChecker::class) + ->tag('config_cache.resource_checker', ['priority' => -990]) + + ->set('services_resetter', ServicesResetter::class) + ->public() + + ->set('reverse_container', ReverseContainer::class) + ->args([ + service('service_container'), + service_locator([]), + ]) + ->alias(ReverseContainer::class, 'reverse_container') + + ->set('locale_aware_listener', LocaleAwareListener::class) + ->args([ + [], // locale aware services + service('request_stack'), + ]) + ->tag('kernel.event_subscriber') + + ->set('container.env_var_processor', EnvVarProcessor::class) + ->args([ + service('service_container'), + tagged_iterator('container.env_var_loader'), + ]) + ->tag('container.env_var_processor') + + ->set('slugger', AsciiSlugger::class) + ->args([ + param('kernel.default_locale'), + ]) + ->tag('kernel.locale_aware') + ->alias(SluggerInterface::class, 'slugger') + + ->set('container.getenv', \Closure::class) + ->factory([\Closure::class, 'fromCallable']) + ->args([ + [service('service_container'), 'getEnv'], + ]) + ->tag('routing.expression_language_function', ['function' => 'env']) + + // inherit from this service to lazily access env vars + ->set('container.env', LazyString::class) + ->abstract() + ->factory([LazyString::class, 'fromCallable']) + ->args([ + service('container.getenv'), + ]) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml deleted file mode 100644 index 0c22d637d5a10..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml +++ /dev/null @@ -1,149 +0,0 @@ - - - - - - - - console.command - console.error - console.terminate - form.pre_submit - form.submit - form.post_submit - form.pre_set_data - form.post_set_data - kernel.controller_arguments - kernel.controller - kernel.response - kernel.finish_request - kernel.request - kernel.view - kernel.exception - kernel.terminate - workflow.guard - workflow.leave - workflow.transition - workflow.enter - workflow.entered - workflow.completed - workflow.announce - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - %kernel.debug% - %kernel.cache_dir%/%kernel.container_class%Deprecations.log - - - - - - - - - - - - - - - - - - - %kernel.secret% - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - %kernel.default_locale% - - - - - - - - - getEnv - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/session.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/session.php new file mode 100644 index 0000000000000..812ee50e7ce81 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/session.php @@ -0,0 +1,118 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBag; +use Symfony\Component\HttpFoundation\Session\Flash\FlashBag; +use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface; +use Symfony\Component\HttpFoundation\Session\Session; +use Symfony\Component\HttpFoundation\Session\SessionInterface; +use Symfony\Component\HttpFoundation\Session\Storage\Handler\AbstractSessionHandler; +use Symfony\Component\HttpFoundation\Session\Storage\Handler\IdentityMarshaller; +use Symfony\Component\HttpFoundation\Session\Storage\Handler\MarshallingSessionHandler; +use Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeFileSessionHandler; +use Symfony\Component\HttpFoundation\Session\Storage\Handler\SessionHandlerFactory; +use Symfony\Component\HttpFoundation\Session\Storage\Handler\StrictSessionHandler; +use Symfony\Component\HttpFoundation\Session\Storage\MetadataBag; +use Symfony\Component\HttpFoundation\Session\Storage\MockFileSessionStorage; +use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage; +use Symfony\Component\HttpFoundation\Session\Storage\PhpBridgeSessionStorage; +use Symfony\Component\HttpFoundation\Session\Storage\SessionStorageInterface; +use Symfony\Component\HttpKernel\EventListener\SessionListener; + +return static function (ContainerConfigurator $container) { + $container->parameters()->set('session.metadata.storage_key', '_sf2_meta'); + + $container->services() + ->set('session', Session::class) + ->public() + ->args([ + service('session.storage'), + null, // AttributeBagInterface + null, // FlashBagInterface + [service('session_listener'), 'onSessionUsage'], + ]) + ->alias(SessionInterface::class, 'session') + ->alias(SessionStorageInterface::class, 'session.storage') + ->alias(\SessionHandlerInterface::class, 'session.handler') + + ->set('session.storage.metadata_bag', MetadataBag::class) + ->args([ + param('session.metadata.storage_key'), + param('session.metadata.update_threshold'), + ]) + + ->set('session.storage.native', NativeSessionStorage::class) + ->args([ + param('session.storage.options'), + service('session.handler'), + service('session.storage.metadata_bag'), + ]) + + ->set('session.storage.php_bridge', PhpBridgeSessionStorage::class) + ->args([ + service('session.handler'), + service('session.storage.metadata_bag'), + ]) + + ->set('session.flash_bag', FlashBag::class) + ->factory([service('session'), 'getFlashBag']) + ->deprecate('symfony/framework-bundle', '5.1', 'The "%service_id%" service is deprecated, use "$session->getFlashBag()" instead.') + ->alias(FlashBagInterface::class, 'session.flash_bag') + + ->set('session.attribute_bag', AttributeBag::class) + ->factory([service('session'), 'getBag']) + ->args(['attributes']) + ->deprecate('symfony/framework-bundle', '5.1', 'The "%service_id%" service is deprecated, use "$session->getAttributeBag()" instead.') + + ->set('session.storage.mock_file', MockFileSessionStorage::class) + ->args([ + param('kernel.cache_dir').'/sessions', + 'MOCKSESSID', + service('session.storage.metadata_bag'), + ]) + + ->set('session.handler.native_file', StrictSessionHandler::class) + ->args([ + inline_service(NativeFileSessionHandler::class) + ->args([param('session.save_path')]), + ]) + + ->set('session.abstract_handler', AbstractSessionHandler::class) + ->factory([SessionHandlerFactory::class, 'createHandler']) + ->args([abstract_arg('A string or a connection object')]) + + ->set('session_listener', SessionListener::class) + ->args([ + service_locator([ + 'session' => service('session')->ignoreOnInvalid(), + 'initialized_session' => service('session')->ignoreOnUninitialized(), + 'logger' => service('logger')->ignoreOnInvalid(), + 'session_collector' => service('data_collector.request.session_collector')->ignoreOnInvalid(), + ]), + param('kernel.debug'), + ]) + ->tag('kernel.event_subscriber') + + // for BC + ->alias('session.storage.filesystem', 'session.storage.mock_file') + + ->set('session.marshaller', IdentityMarshaller::class) + + ->set('session.marshalling_handler', MarshallingSessionHandler::class) + ->decorate('session.handler') + ->args([ + service('session.marshalling_handler.inner'), + service('session.marshaller'), + ]) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/session.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/session.xml deleted file mode 100644 index eba617daa46bb..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/session.xml +++ /dev/null @@ -1,95 +0,0 @@ - - - - - - _sf2_meta - - - - - - - - null - null - - - onSessionUsage - - - - - - - - - %session.metadata.storage_key% - %session.metadata.update_threshold% - - - - %session.storage.options% - - - - - - - - - - - - The "%service_id%" service is deprecated, use "$session->getFlashBag()" instead. - - - - - - attributes - The "%service_id%" service is deprecated, use "$session->getAttributeBag()" instead. - - - - %kernel.cache_dir%/sessions - MOCKSESSID - - - - - - - %session.save_path% - - - - - - - - - - - - - - - - - %kernel.debug% - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/ssi.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/ssi.php new file mode 100644 index 0000000000000..d41aa74d1e8dd --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/ssi.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\HttpKernel\EventListener\SurrogateListener; +use Symfony\Component\HttpKernel\HttpCache\Ssi; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('ssi', Ssi::class) + + ->set('ssi_listener', SurrogateListener::class) + ->args([service('ssi')->ignoreOnInvalid()]) + ->tag('kernel.event_subscriber') + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/ssi.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/ssi.xml deleted file mode 100644 index b4e5b3d3df899..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/ssi.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/test.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/test.php new file mode 100644 index 0000000000000..af0df318a0768 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/test.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Bundle\FrameworkBundle\KernelBrowser; +use Symfony\Bundle\FrameworkBundle\Test\TestContainer; +use Symfony\Component\BrowserKit\CookieJar; +use Symfony\Component\BrowserKit\History; +use Symfony\Component\DependencyInjection\ServiceLocator; +use Symfony\Component\HttpKernel\EventListener\TestSessionListener; + +return static function (ContainerConfigurator $container) { + $container->parameters()->set('test.client.parameters', []); + + $container->services() + ->set('test.client', KernelBrowser::class) + ->args([ + service('kernel'), + param('test.client.parameters'), + service('test.client.history'), + service('test.client.cookiejar'), + ]) + ->share(false) + ->public() + + ->set('test.client.history', History::class)->share(false) + ->set('test.client.cookiejar', CookieJar::class)->share(false) + + ->set('test.session.listener', TestSessionListener::class) + ->args([ + service_locator([ + 'session' => service('session')->ignoreOnInvalid(), + ]), + ]) + ->tag('kernel.event_subscriber') + + ->set('test.service_container', TestContainer::class) + ->args([ + service('kernel'), + 'test.private_services_locator', + ]) + ->public() + + ->set('test.private_services_locator', ServiceLocator::class) + ->args([abstract_arg('callable collection')]) + ->public() + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/test.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/test.xml deleted file mode 100644 index ef571fdbc6748..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/test.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - %test.client.parameters% - - - - - - - - - - - - - - - - - - test.private_services_locator - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.php new file mode 100644 index 0000000000000..706e4928ee2e0 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.php @@ -0,0 +1,162 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Psr\Container\ContainerInterface; +use Symfony\Bundle\FrameworkBundle\CacheWarmer\TranslationsCacheWarmer; +use Symfony\Bundle\FrameworkBundle\Translation\Translator; +use Symfony\Component\Translation\Dumper\CsvFileDumper; +use Symfony\Component\Translation\Dumper\IcuResFileDumper; +use Symfony\Component\Translation\Dumper\IniFileDumper; +use Symfony\Component\Translation\Dumper\JsonFileDumper; +use Symfony\Component\Translation\Dumper\MoFileDumper; +use Symfony\Component\Translation\Dumper\PhpFileDumper; +use Symfony\Component\Translation\Dumper\PoFileDumper; +use Symfony\Component\Translation\Dumper\QtFileDumper; +use Symfony\Component\Translation\Dumper\XliffFileDumper; +use Symfony\Component\Translation\Dumper\YamlFileDumper; +use Symfony\Component\Translation\Extractor\ChainExtractor; +use Symfony\Component\Translation\Extractor\ExtractorInterface; +use Symfony\Component\Translation\Extractor\PhpExtractor; +use Symfony\Component\Translation\Formatter\MessageFormatter; +use Symfony\Component\Translation\Loader\CsvFileLoader; +use Symfony\Component\Translation\Loader\IcuDatFileLoader; +use Symfony\Component\Translation\Loader\IcuResFileLoader; +use Symfony\Component\Translation\Loader\IniFileLoader; +use Symfony\Component\Translation\Loader\JsonFileLoader; +use Symfony\Component\Translation\Loader\MoFileLoader; +use Symfony\Component\Translation\Loader\PhpFileLoader; +use Symfony\Component\Translation\Loader\PoFileLoader; +use Symfony\Component\Translation\Loader\QtFileLoader; +use Symfony\Component\Translation\Loader\XliffFileLoader; +use Symfony\Component\Translation\Loader\YamlFileLoader; +use Symfony\Component\Translation\LoggingTranslator; +use Symfony\Component\Translation\Reader\TranslationReader; +use Symfony\Component\Translation\Reader\TranslationReaderInterface; +use Symfony\Component\Translation\Writer\TranslationWriter; +use Symfony\Component\Translation\Writer\TranslationWriterInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('translator.default', Translator::class) + ->args([ + abstract_arg('translation loaders locator'), + service('translator.formatter'), + param('kernel.default_locale'), + abstract_arg('translation loaders ids'), + [ + 'cache_dir' => param('kernel.cache_dir').'/translations', + 'debug' => param('kernel.debug'), + ], + abstract_arg('enabled locales'), + ]) + ->call('setConfigCacheFactory', [service('config_cache_factory')]) + ->tag('kernel.locale_aware') + + ->alias(TranslatorInterface::class, 'translator') + + ->set('translator.logging', LoggingTranslator::class) + ->args([ + service('translator.logging.inner'), + service('logger'), + ]) + ->tag('monolog.logger', ['channel' => 'translation']) + + ->set('translator.formatter.default', MessageFormatter::class) + ->args([service('identity_translator')]) + + ->set('translation.loader.php', PhpFileLoader::class) + ->tag('translation.loader', ['alias' => 'php']) + + ->set('translation.loader.yml', YamlFileLoader::class) + ->tag('translation.loader', ['alias' => 'yaml', 'legacy-alias' => 'yml']) + + ->set('translation.loader.xliff', XliffFileLoader::class) + ->tag('translation.loader', ['alias' => 'xlf', 'legacy-alias' => 'xliff']) + + ->set('translation.loader.po', PoFileLoader::class) + ->tag('translation.loader', ['alias' => 'po']) + + ->set('translation.loader.mo', MoFileLoader::class) + ->tag('translation.loader', ['alias' => 'mo']) + + ->set('translation.loader.qt', QtFileLoader::class) + ->tag('translation.loader', ['alias' => 'ts']) + + ->set('translation.loader.csv', CsvFileLoader::class) + ->tag('translation.loader', ['alias' => 'csv']) + + ->set('translation.loader.res', IcuResFileLoader::class) + ->tag('translation.loader', ['alias' => 'res']) + + ->set('translation.loader.dat', IcuDatFileLoader::class) + ->tag('translation.loader', ['alias' => 'dat']) + + ->set('translation.loader.ini', IniFileLoader::class) + ->tag('translation.loader', ['alias' => 'ini']) + + ->set('translation.loader.json', JsonFileLoader::class) + ->tag('translation.loader', ['alias' => 'json']) + + ->set('translation.dumper.php', PhpFileDumper::class) + ->tag('translation.dumper', ['alias' => 'php']) + + ->set('translation.dumper.xliff', XliffFileDumper::class) + ->tag('translation.dumper', ['alias' => 'xlf']) + + ->set('translation.dumper.po', PoFileDumper::class) + ->tag('translation.dumper', ['alias' => 'po']) + + ->set('translation.dumper.mo', MoFileDumper::class) + ->tag('translation.dumper', ['alias' => 'mo']) + + ->set('translation.dumper.yml', YamlFileDumper::class) + ->tag('translation.dumper', ['alias' => 'yml']) + + ->set('translation.dumper.yaml', YamlFileDumper::class) + ->args(['yaml']) + ->tag('translation.dumper', ['alias' => 'yaml']) + + ->set('translation.dumper.qt', QtFileDumper::class) + ->tag('translation.dumper', ['alias' => 'ts']) + + ->set('translation.dumper.csv', CsvFileDumper::class) + ->tag('translation.dumper', ['alias' => 'csv']) + + ->set('translation.dumper.ini', IniFileDumper::class) + ->tag('translation.dumper', ['alias' => 'ini']) + + ->set('translation.dumper.json', JsonFileDumper::class) + ->tag('translation.dumper', ['alias' => 'json']) + + ->set('translation.dumper.res', IcuResFileDumper::class) + ->tag('translation.dumper', ['alias' => 'res']) + + ->set('translation.extractor.php', PhpExtractor::class) + ->tag('translation.extractor', ['alias' => 'php']) + + ->set('translation.reader', TranslationReader::class) + ->alias(TranslationReaderInterface::class, 'translation.reader') + + ->set('translation.extractor', ChainExtractor::class) + ->alias(ExtractorInterface::class, 'translation.extractor') + + ->set('translation.writer', TranslationWriter::class) + ->alias(TranslationWriterInterface::class, 'translation.writer') + + ->set('translation.warmer', TranslationsCacheWarmer::class) + ->args([service(ContainerInterface::class)]) + ->tag('container.service_subscriber', ['id' => 'translator']) + ->tag('kernel.cache_warmer') + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.xml deleted file mode 100644 index 3c158abb02358..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.xml +++ /dev/null @@ -1,145 +0,0 @@ - - - - - - - - - - - %kernel.default_locale% - - - %kernel.cache_dir%/translations - %kernel.debug% - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - yaml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation_debug.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation_debug.php new file mode 100644 index 0000000000000..7a83301811f28 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation_debug.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\Translation\DataCollector\TranslationDataCollector; +use Symfony\Component\Translation\DataCollectorTranslator; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('translator.data_collector', DataCollectorTranslator::class) + ->args([service('translator.data_collector.inner')]) + + ->set('data_collector.translation', TranslationDataCollector::class) + ->args([service('translator.data_collector')]) + ->tag('data_collector', [ + 'template' => '@WebProfiler/Collector/translation.html.twig', + 'id' => 'translation', + 'priority' => 275, + ]) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation_debug.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation_debug.xml deleted file mode 100644 index c9c5385fbfb76..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation_debug.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.php new file mode 100644 index 0000000000000..5dcb427b565bc --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.php @@ -0,0 +1,99 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Bundle\FrameworkBundle\CacheWarmer\ValidatorCacheWarmer; +use Symfony\Component\Cache\Adapter\PhpArrayAdapter; +use Symfony\Component\Validator\Constraints\EmailValidator; +use Symfony\Component\Validator\Constraints\ExpressionValidator; +use Symfony\Component\Validator\Constraints\NotCompromisedPasswordValidator; +use Symfony\Component\Validator\ContainerConstraintValidatorFactory; +use Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader; +use Symfony\Component\Validator\Validation; +use Symfony\Component\Validator\Validator\ValidatorInterface; +use Symfony\Component\Validator\ValidatorBuilder; + +return static function (ContainerConfigurator $container) { + $container->parameters() + ->set('validator.mapping.cache.file', param('kernel.cache_dir').'/validation.php'); + + $container->services() + ->set('validator', ValidatorInterface::class) + ->public() + ->factory([service('validator.builder'), 'getValidator']) + ->tag('container.private', ['package' => 'symfony/framework-bundle', 'version' => '5.2']) + ->alias(ValidatorInterface::class, 'validator') + + ->set('validator.builder', ValidatorBuilder::class) + ->factory([Validation::class, 'createValidatorBuilder']) + ->call('setConstraintValidatorFactory', [ + service('validator.validator_factory'), + ]) + ->call('setTranslator', [ + service('translator')->ignoreOnInvalid(), + ]) + ->call('setTranslationDomain', [ + param('validator.translation_domain'), + ]) + ->alias('validator.mapping.class_metadata_factory', 'validator') + + ->set('validator.mapping.cache_warmer', ValidatorCacheWarmer::class) + ->args([ + service('validator.builder'), + param('validator.mapping.cache.file'), + ]) + ->tag('kernel.cache_warmer') + + ->set('validator.mapping.cache.adapter', PhpArrayAdapter::class) + ->factory([PhpArrayAdapter::class, 'create']) + ->args([ + param('validator.mapping.cache.file'), + service('cache.validator'), + ]) + + ->set('validator.validator_factory', ContainerConstraintValidatorFactory::class) + ->args([ + abstract_arg('Constraint validators locator'), + ]) + + ->set('validator.expression', ExpressionValidator::class) + ->tag('validator.constraint_validator', [ + 'alias' => 'validator.expression', + ]) + + ->set('validator.email', EmailValidator::class) + ->args([ + abstract_arg('Default mode'), + ]) + ->tag('validator.constraint_validator', [ + 'alias' => EmailValidator::class, + ]) + + ->set('validator.not_compromised_password', NotCompromisedPasswordValidator::class) + ->args([ + service('http_client')->nullOnInvalid(), + param('kernel.charset'), + false, + ]) + ->tag('validator.constraint_validator', [ + 'alias' => NotCompromisedPasswordValidator::class, + ]) + + ->set('validator.property_info_loader', PropertyInfoLoader::class) + ->args([ + service('property_info'), + service('property_info'), + service('property_info'), + ]) + ->tag('validator.auto_mapper') + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.xml deleted file mode 100644 index 7c10470d5196c..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.xml +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - %kernel.cache_dir%/validation.php - - - - - - - - - - - - - - - - - - - - %validator.translation_domain% - - - - - - - - %validator.mapping.cache.file% - - - - - - %validator.mapping.cache.file% - - - - - - - - - - - - - - - - - - - %kernel.charset% - false - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator_debug.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator_debug.php new file mode 100644 index 0000000000000..e9fe441140742 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator_debug.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\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\Validator\DataCollector\ValidatorDataCollector; +use Symfony\Component\Validator\Validator\TraceableValidator; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('debug.validator', TraceableValidator::class) + ->decorate('validator', null, 255) + ->args([ + service('debug.validator.inner'), + ]) + ->tag('kernel.reset', [ + 'method' => 'reset', + ]) + + ->set('data_collector.validator', ValidatorDataCollector::class) + ->args([ + service('debug.validator'), + ]) + ->tag('data_collector', [ + 'template' => '@WebProfiler/Collector/validator.html.twig', + 'id' => 'validator', + 'priority' => 320, + ]) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator_debug.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator_debug.xml deleted file mode 100644 index 939c55553ca38..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator_debug.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php new file mode 100644 index 0000000000000..8d1934e345ed6 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php @@ -0,0 +1,109 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Bundle\FrameworkBundle\Controller\ControllerResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestAttributeValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\ServiceValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\SessionValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\VariadicValueResolver; +use Symfony\Component\HttpKernel\Controller\ErrorController; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactory; +use Symfony\Component\HttpKernel\EventListener\DisallowRobotsIndexingListener; +use Symfony\Component\HttpKernel\EventListener\ErrorListener; +use Symfony\Component\HttpKernel\EventListener\LocaleListener; +use Symfony\Component\HttpKernel\EventListener\ResponseListener; +use Symfony\Component\HttpKernel\EventListener\StreamedResponseListener; +use Symfony\Component\HttpKernel\EventListener\ValidateRequestListener; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('controller_resolver', ControllerResolver::class) + ->args([ + service('service_container'), + service('logger')->ignoreOnInvalid(), + ]) + ->tag('monolog.logger', ['channel' => 'request']) + + ->set('argument_metadata_factory', ArgumentMetadataFactory::class) + + ->set('argument_resolver', ArgumentResolver::class) + ->args([ + service('argument_metadata_factory'), + abstract_arg('argument value resolvers'), + ]) + + ->set('argument_resolver.request_attribute', RequestAttributeValueResolver::class) + ->tag('controller.argument_value_resolver', ['priority' => 100]) + + ->set('argument_resolver.request', RequestValueResolver::class) + ->tag('controller.argument_value_resolver', ['priority' => 50]) + + ->set('argument_resolver.session', SessionValueResolver::class) + ->tag('controller.argument_value_resolver', ['priority' => 50]) + + ->set('argument_resolver.service', ServiceValueResolver::class) + ->args([ + abstract_arg('service locator, set in RegisterControllerArgumentLocatorsPass'), + ]) + ->tag('controller.argument_value_resolver', ['priority' => -50]) + + ->set('argument_resolver.default', DefaultValueResolver::class) + ->tag('controller.argument_value_resolver', ['priority' => -100]) + + ->set('argument_resolver.variadic', VariadicValueResolver::class) + ->tag('controller.argument_value_resolver', ['priority' => -150]) + + ->set('response_listener', ResponseListener::class) + ->args([ + param('kernel.charset'), + ]) + ->tag('kernel.event_subscriber') + + ->set('streamed_response_listener', StreamedResponseListener::class) + ->tag('kernel.event_subscriber') + + ->set('locale_listener', LocaleListener::class) + ->args([ + service('request_stack'), + param('kernel.default_locale'), + service('router')->ignoreOnInvalid(), + ]) + ->tag('kernel.event_subscriber') + + ->set('validate_request_listener', ValidateRequestListener::class) + ->tag('kernel.event_subscriber') + + ->set('disallow_search_engine_index_response_listener', DisallowRobotsIndexingListener::class) + ->tag('kernel.event_subscriber') + + ->set('error_controller', ErrorController::class) + ->public() + ->args([ + service('http_kernel'), + param('kernel.error_controller'), + service('error_renderer'), + ]) + + ->set('exception_listener', ErrorListener::class) + ->args([ + param('kernel.error_controller'), + service('logger')->nullOnInvalid(), + param('kernel.debug'), + ]) + ->tag('kernel.event_subscriber') + ->tag('monolog.logger', ['channel' => 'request']) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml deleted file mode 100644 index cbdc5586a27ad..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml +++ /dev/null @@ -1,86 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - %kernel.charset% - - - - - - - - - - %kernel.default_locale% - - - - - - - - - - - - - - %kernel.error_controller% - - - - - - - %kernel.error_controller% - - %kernel.debug% - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web_link.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web_link.php new file mode 100644 index 0000000000000..0b0e79db8c1bf --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web_link.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\WebLink\EventListener\AddLinkHeaderListener; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('web_link.add_link_header_listener', AddLinkHeaderListener::class) + ->tag('kernel.event_subscriber') + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web_link.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web_link.xml deleted file mode 100644 index bf3e8d7211e00..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web_link.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/workflow.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/workflow.php new file mode 100644 index 0000000000000..6eae2b16c4118 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/workflow.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\Workflow\EventListener\ExpressionLanguage; +use Symfony\Component\Workflow\MarkingStore\MethodMarkingStore; +use Symfony\Component\Workflow\Registry; +use Symfony\Component\Workflow\StateMachine; +use Symfony\Component\Workflow\Workflow; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('workflow.abstract', Workflow::class) + ->args([ + abstract_arg('workflow definition'), + abstract_arg('marking store'), + service('event_dispatcher')->ignoreOnInvalid(), + abstract_arg('workflow name'), + abstract_arg('events to dispatch'), + ]) + ->abstract() + ->public() + ->set('state_machine.abstract', StateMachine::class) + ->args([ + abstract_arg('workflow definition'), + abstract_arg('marking store'), + service('event_dispatcher')->ignoreOnInvalid(), + abstract_arg('workflow name'), + abstract_arg('events to dispatch'), + ]) + ->abstract() + ->public() + ->set('workflow.marking_store.method', MethodMarkingStore::class) + ->abstract() + ->set('workflow.registry', Registry::class) + ->alias(Registry::class, 'workflow.registry') + ->set('workflow.security.expression_language', ExpressionLanguage::class) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/workflow.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/workflow.xml deleted file mode 100644 index 78741deb8ebbf..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/workflow.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - null - - - - - - null - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/DomCrawlerAssertionsTrait.php b/src/Symfony/Bundle/FrameworkBundle/Test/DomCrawlerAssertionsTrait.php index 465c265f6921d..2a692d6f5a367 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Test/DomCrawlerAssertionsTrait.php +++ b/src/Symfony/Bundle/FrameworkBundle/Test/DomCrawlerAssertionsTrait.php @@ -15,6 +15,8 @@ use PHPUnit\Framework\Constraint\LogicalNot; use Symfony\Component\DomCrawler\Crawler; use Symfony\Component\DomCrawler\Test\Constraint as DomCrawlerConstraint; +use Symfony\Component\DomCrawler\Test\Constraint\CrawlerSelectorAttributeValueSame; +use Symfony\Component\DomCrawler\Test\Constraint\CrawlerSelectorExists; /** * Ideas borrowed from Laravel Dusk's assertions. @@ -83,6 +85,39 @@ public static function assertInputValueNotSame(string $fieldName, string $expect ), $message); } + public static function assertCheckboxChecked(string $fieldName, string $message = ''): void + { + self::assertThat(self::getCrawler(), LogicalAnd::fromConstraints( + new CrawlerSelectorExists("input[name=\"$fieldName\"]"), + new CrawlerSelectorAttributeValueSame("input[name=\"$fieldName\"]", 'checked', 'checked') + ), $message); + } + + public static function assertCheckboxNotChecked(string $fieldName, string $message = ''): void + { + self::assertThat(self::getCrawler(), LogicalAnd::fromConstraints( + new CrawlerSelectorExists("input[name=\"$fieldName\"]"), + new LogicalNot(new CrawlerSelectorAttributeValueSame("input[name=\"$fieldName\"]", 'checked', 'checked')) + ), $message); + } + + public static function assertFormValue(string $formSelector, string $fieldName, string $value, string $message = ''): void + { + $node = self::getCrawler()->filter($formSelector); + self::assertNotEmpty($node, sprintf('Form "%s" not found.', $formSelector)); + $values = $node->form()->getValues(); + self::assertArrayHasKey($fieldName, $values, $message ?: sprintf('Field "%s" not found in form "%s".', $fieldName, $formSelector)); + self::assertSame($value, $values[$fieldName]); + } + + public static function assertNoFormValue(string $formSelector, string $fieldName, string $message = ''): void + { + $node = self::getCrawler()->filter($formSelector); + self::assertNotEmpty($node, sprintf('Form "%s" not found.', $formSelector)); + $values = $node->form()->getValues(); + self::assertArrayNotHasKey($fieldName, $values, $message ?: sprintf('Field "%s" has a value in form "%s".', $fieldName, $formSelector)); + } + private static function getCrawler(): Crawler { if (!$crawler = self::getClient()->getCrawler()) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/MailerAssertionsTrait.php b/src/Symfony/Bundle/FrameworkBundle/Test/MailerAssertionsTrait.php index d0ca84e25ae7a..9b61a61ddac7d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Test/MailerAssertionsTrait.php +++ b/src/Symfony/Bundle/FrameworkBundle/Test/MailerAssertionsTrait.php @@ -118,10 +118,14 @@ public static function getMailerMessage(int $index = 0, string $transport = null private static function getMessageMailerEvents(): MessageEvents { - if (!self::$container->has('mailer.logger_message_listener')) { - static::fail('A client must have Mailer enabled to make email assertions. Did you forget to require symfony/mailer?'); + if (self::$container->has('mailer.message_logger_listener')) { + return self::$container->get('mailer.message_logger_listener')->getEvents(); } - return self::$container->get('mailer.logger_message_listener')->getEvents(); + if (self::$container->has('mailer.logger_message_listener')) { + return self::$container->get('mailer.logger_message_listener')->getEvents(); + } + + static::fail('A client must have Mailer enabled to make email assertions. Did you forget to require symfony/mailer?'); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/ProfilerPassTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/ProfilerPassTest.php index 99299282aa06c..c9594b0b0896e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/ProfilerPassTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/ProfilerPassTest.php @@ -12,8 +12,15 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler; use PHPUnit\Framework\TestCase; +use Symfony\Bundle\FrameworkBundle\DataCollector\AbstractDataCollector; +use Symfony\Bundle\FrameworkBundle\DataCollector\TemplateAwareDataCollectorInterface; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ProfilerPass; +use Symfony\Component\DependencyInjection\Compiler\ResolveChildDefinitionsPass; +use Symfony\Component\DependencyInjection\Compiler\ResolveInstanceofConditionalsPass; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface; class ProfilerPassTest extends TestCase { @@ -54,4 +61,64 @@ public function testValidCollector() $this->assertCount(1, $methodCalls); $this->assertEquals('add', $methodCalls[0][0]); // grab the method part of the first call } + + public function provideValidCollectorWithTemplateUsingAutoconfigure(): \Generator + { + yield [new class() implements TemplateAwareDataCollectorInterface { + public function collect(Request $request, Response $response, \Throwable $exception = null) + { + } + + public function getName(): string + { + return static::class; + } + + public function reset() + { + } + + public static function getTemplate(): string + { + return 'foo'; + } + }]; + + yield [new class() extends AbstractDataCollector { + public function collect(Request $request, Response $response, \Throwable $exception = null) + { + } + + public static function getTemplate(): string + { + return 'foo'; + } + }]; + } + + /** + * @dataProvider provideValidCollectorWithTemplateUsingAutoconfigure + */ + public function testValidCollectorWithTemplateUsingAutoconfigure(TemplateAwareDataCollectorInterface $dataCollector) + { + $container = new ContainerBuilder(); + $profilerDefinition = $container->register('profiler', 'ProfilerClass'); + + $container->registerForAutoconfiguration(DataCollectorInterface::class)->addTag('data_collector'); + $container->register('mydatacollector', \get_class($dataCollector))->setAutoconfigured(true); + + (new ResolveInstanceofConditionalsPass())->process($container); + (new ProfilerPass())->process($container); + + $idForTemplate = \get_class($dataCollector); + $this->assertSame(['mydatacollector' => [$idForTemplate, 'foo']], $container->getParameter('data_collector.templates')); + + // grab the method calls off of the "profiler" definition + $methodCalls = $profilerDefinition->getMethodCalls(); + $this->assertCount(1, $methodCalls); + $this->assertEquals('add', $methodCalls[0][0]); // grab the method part of the first call + + (new ResolveChildDefinitionsPass())->process($container); + $this->assertSame($idForTemplate, $container->get('mydatacollector')->getName()); + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/UnusedTagsPassUtils.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/UnusedTagsPassUtils.php index 67c97263ccdfd..5bbd93722e2ad 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/UnusedTagsPassUtils.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/UnusedTagsPassUtils.php @@ -37,6 +37,22 @@ public static function getDefinedTags(): array } } + // get all tags used in PHP configs + $files = Finder::create()->files()->name('*.php')->path('Resources')->notPath('Tests')->in(\dirname(__DIR__, 5)); + foreach ($files as $file) { + $contents = file_get_contents($file); + if (preg_match_all("{->tag\('([^']+)'}", $contents, $matches)) { + foreach ($matches[1] as $match) { + $tags[$match] = true; + } + } + if (preg_match_all("{tagged_(?:locator|iterator)\('([^']+)'}", $contents, $matches)) { + foreach ($matches[1] as $match) { + $tags[$match] = true; + } + } + } + // get all tags used in findTaggedServiceIds calls() $files = Finder::create()->files()->name('*.php')->path('DependencyInjection')->notPath('Tests')->in(\dirname(__DIR__, 5)); foreach ($files as $file) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php index 3ba4c3ecfecf8..00afdfd00055f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -30,10 +30,7 @@ public function testDefaultConfig() $processor = new Processor(); $config = $processor->processConfiguration(new Configuration(true), [['secret' => 's3cr3t']]); - $this->assertEquals( - array_merge(['secret' => 's3cr3t', 'trusted_hosts' => []], self::getBundleDefaultConfig()), - $config - ); + $this->assertEquals(self::getBundleDefaultConfig(), $config); } public function getTestValidSessionName() @@ -341,6 +338,13 @@ protected static function getBundleDefaultConfig() 'http_method_override' => true, 'ide' => null, 'default_locale' => 'en', + 'secret' => 's3cr3t', + 'trusted_hosts' => [], + 'trusted_headers' => [ + 'x-forwarded-all', + '!x-forwarded-host', + '!x-forwarded-prefix', + ], 'csrf_protection' => [ 'enabled' => false, ], @@ -350,6 +354,7 @@ protected static function getBundleDefaultConfig() 'enabled' => null, // defaults to csrf_protection.enabled 'field_name' => '_token', ], + 'legacy_error_messages' => true, ], 'esi' => ['enabled' => false], 'ssi' => ['enabled' => false], @@ -374,6 +379,14 @@ protected static function getBundleDefaultConfig() 'paths' => [], 'default_path' => '%kernel.project_dir%/translations', 'enabled_locales' => [], + 'pseudo_localization' => [ + 'enabled' => false, + 'accents' => true, + 'expansion_factor' => 1.0, + 'brackets' => true, + 'parse_html' => false, + 'localizable_html_attributes' => [], + ], ], 'validation' => [ 'enabled' => !class_exists(FullStack::class), @@ -402,6 +415,8 @@ protected static function getBundleDefaultConfig() ], 'property_access' => [ 'magic_call' => false, + 'magic_get' => true, + 'magic_set' => true, 'throw_exception_on_invalid_index' => false, 'throw_exception_on_invalid_property_path' => true, ], @@ -448,6 +463,7 @@ protected static function getBundleDefaultConfig() 'default_redis_provider' => 'redis://localhost', 'default_memcached_provider' => 'memcached://localhost', 'default_pdo_provider' => class_exists(Connection::class) ? 'database_connection' : null, + 'prefix_seed' => '_%kernel.project_dir%.%kernel.container_class%', ], 'workflows' => [ 'enabled' => false, @@ -493,6 +509,7 @@ class_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphor 'transports' => [], 'enabled' => !class_exists(FullStack::class) && class_exists(Mailer::class), 'message_bus' => null, + 'headers' => [], ], 'notifier' => [ 'enabled' => !class_exists(FullStack::class) && class_exists(Notifier::class), @@ -509,6 +526,15 @@ class_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphor 'local_dotenv_file' => '%kernel.project_dir%/.env.%kernel.environment%.local', 'decryption_env_var' => 'base64:default::SYMFONY_DECRYPTION_SECRET', ], + 'http_cache' => [ + 'enabled' => false, + 'debug' => '%kernel.debug%', + 'private_headers' => [], + ], + 'rate_limiter' => [ + 'enabled' => false, + 'limiters' => [], + ], ]; } } 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 8d92edf766924..040e29bbd3edd 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/cache.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/cache.php @@ -32,6 +32,27 @@ 'redis://foo' => 'cache.adapter.redis', ], ], + 'cache.redis_tag_aware.foo' => [ + 'adapter' => 'cache.adapter.redis_tag_aware', + ], + 'cache.redis_tag_aware.foo2' => [ + 'tags' => true, + 'adapter' => 'cache.adapter.redis_tag_aware', + ], + 'cache.redis_tag_aware.bar' => [ + 'adapter' => 'cache.redis_tag_aware.foo', + ], + 'cache.redis_tag_aware.bar2' => [ + 'tags' => true, + 'adapter' => 'cache.redis_tag_aware.foo', + ], + 'cache.redis_tag_aware.baz' => [ + 'adapter' => 'cache.redis_tag_aware.foo2', + ], + 'cache.redis_tag_aware.baz2' => [ + 'tags' => true, + 'adapter' => 'cache.redis_tag_aware.foo2', + ], ], ], ]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/csrf.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/csrf.php index 886cb657b2dc6..e3f3577c1b430 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/csrf.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/csrf.php @@ -2,7 +2,9 @@ $container->loadFromExtension('framework', [ 'csrf_protection' => true, - 'form' => true, + 'form' => [ + 'legacy_error_messages' => false, + ], 'session' => [ 'handler_id' => null, ], diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/form_legacy_messages.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/form_legacy_messages.php new file mode 100644 index 0000000000000..6e98e3cb6d91f --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/form_legacy_messages.php @@ -0,0 +1,7 @@ +loadFromExtension('framework', [ + 'form' => [ + 'legacy_error_messages' => true, + ], +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/form_no_csrf.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/form_no_csrf.php index e0befdb320612..c6bde28a78af0 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/form_no_csrf.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/form_no_csrf.php @@ -5,5 +5,6 @@ 'csrf_protection' => [ 'enabled' => false, ], + 'legacy_error_messages' => false, ], ]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/full.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/full.php index b11b5e08dcb96..647044e613798 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/full.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/full.php @@ -8,6 +8,7 @@ 'csrf_protection' => [ 'field_name' => '_csrf', ], + 'legacy_error_messages' => false, ], 'http_method_override' => false, 'esi' => [ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_mock_response_factory.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_mock_response_factory.php new file mode 100644 index 0000000000000..5b64c3ae0a1d4 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_mock_response_factory.php @@ -0,0 +1,8 @@ +loadFromExtension('framework', [ + 'http_client' => [ + 'default_options' => null, + 'mock_response_factory' => 'my_response_factory', + ], +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_retry.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_retry.php new file mode 100644 index 0000000000000..eeb9e45b40fa5 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_retry.php @@ -0,0 +1,23 @@ +loadFromExtension('framework', [ + 'http_client' => [ + 'default_options' => [ + 'retry_failed' => [ + 'backoff_service' => null, + 'decider_service' => null, + 'http_codes' => [429, 500], + 'max_retries' => 2, + 'delay' => 100, + 'multiplier' => 2, + 'max_delay' => 0, + ] + ], + 'scoped_clients' => [ + 'foo' => [ + 'base_uri' => 'http://example.com', + 'retry_failed' => ['multiplier' => 4], + ], + ], + ], +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer.php index ef8cdd385cf80..5e3093b33b431 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer.php @@ -7,5 +7,10 @@ 'sender' => 'sender@example.org', 'recipients' => ['redirected@example.org', 'redirected1@example.org'], ], + 'headers' => [ + 'from' => 'from@example.org', + 'bcc' => ['bcc1@example.org', 'bcc2@example.org'], + 'foo' => 'bar', + ], ], ]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer_with_dsn.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer_with_dsn.php index 7eec06a9a0e50..df2ca46e46ee9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer_with_dsn.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer_with_dsn.php @@ -10,6 +10,11 @@ 'sender' => 'sender@example.org', 'recipients' => ['redirected@example.org', 'redirected1@example.org'], ], + 'headers' => [ + 'from' => 'from@example.org', + 'bcc' => ['bcc1@example.org', 'bcc2@example.org'], + 'foo' => 'bar', + ], ], ]); }; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer_with_transports.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer_with_transports.php index 1bc79f3dd204c..8b13bc269b24a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer_with_transports.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer_with_transports.php @@ -13,6 +13,11 @@ 'sender' => 'sender@example.org', 'recipients' => ['redirected@example.org', 'redirected1@example.org'], ], + 'headers' => [ + 'from' => 'from@example.org', + 'bcc' => ['bcc1@example.org', 'bcc2@example.org'], + 'foo' => 'bar', + ], ], ]); }; 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 0aff440e855e9..90c5def3ac100 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 @@ -22,6 +22,7 @@ ], 'failed' => 'in-memory:///', 'redis' => 'redis://127.0.0.1:6379/messages', + 'beanstalkd' => 'beanstalkd://127.0.0.1:11300', ], ], ]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/property_accessor.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/property_accessor.php index 8f431f8735d89..dc6954fe89da4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/property_accessor.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/property_accessor.php @@ -3,6 +3,8 @@ $container->loadFromExtension('framework', [ 'property_access' => [ 'magic_call' => true, + 'magic_get' => true, + 'magic_set' => false, 'throw_exception_on_invalid_index' => true, 'throw_exception_on_invalid_property_path' => false, ], diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/validation_annotations.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/validation_annotations.php index dff03e398e2dc..933410dfee767 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/validation_annotations.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/validation_annotations.php @@ -7,3 +7,5 @@ 'enable_annotations' => true, ], ]); + +$container->setAlias('validator.alias', 'validator')->setPublic(true); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflow_with_no_events_to_dispatch.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflow_with_no_events_to_dispatch.php new file mode 100644 index 0000000000000..0ae6ac69ee7cd --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflow_with_no_events_to_dispatch.php @@ -0,0 +1,42 @@ +loadFromExtension('framework', [ + 'workflows' => [ + 'my_workflow' => [ + 'type' => 'state_machine', + 'marking_store' => [ + 'type' => 'method', + 'property' => 'state' + ], + 'supports' => [ + FrameworkExtensionTest::class, + ], + 'events_to_dispatch' => [], + 'places' => [ + 'one', + 'two', + 'three', + ], + 'transitions' => [ + 'count_to_two' => [ + 'from' => [ + 'one', + ], + 'to' => [ + 'two', + ], + ], + 'count_to_three' => [ + 'from' => [ + 'two', + ], + 'to' => [ + 'three' + ] + ] + ], + ], + ], +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflow_with_specified_events_to_dispatch.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflow_with_specified_events_to_dispatch.php new file mode 100644 index 0000000000000..259ee5087ff2a --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflow_with_specified_events_to_dispatch.php @@ -0,0 +1,45 @@ +loadFromExtension('framework', [ + 'workflows' => [ + 'my_workflow' => [ + 'type' => 'state_machine', + 'marking_store' => [ + 'type' => 'method', + 'property' => 'state' + ], + 'supports' => [ + FrameworkExtensionTest::class, + ], + 'events_to_dispatch' => [ + 'workflow.leave', + 'workflow.completed', + ], + 'places' => [ + 'one', + 'two', + 'three', + ], + 'transitions' => [ + 'count_to_two' => [ + 'from' => [ + 'one', + ], + 'to' => [ + 'two', + ], + ], + 'count_to_three' => [ + 'from' => [ + 'two', + ], + 'to' => [ + 'three' + ] + ] + ], + ], + ], +]); 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 2db74964b53e7..f8d49eb7df645 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/cache.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/cache.xml @@ -17,6 +17,12 @@ + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/csrf.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/csrf.xml index 4cd628eadc15a..4686d9ffc046d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/csrf.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/csrf.xml @@ -8,7 +8,7 @@ - + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_csrf_sets_field_name.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_csrf_sets_field_name.xml index 1bdf2e528432e..1552a3ceb6e42 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_csrf_sets_field_name.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_csrf_sets_field_name.xml @@ -9,6 +9,6 @@ - + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_csrf_under_form_sets_field_name.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_csrf_under_form_sets_field_name.xml index c04193e837b34..dda2e724cc664 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_csrf_under_form_sets_field_name.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_csrf_under_form_sets_field_name.xml @@ -8,7 +8,7 @@ - + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_legacy_messages.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_legacy_messages.xml new file mode 100644 index 0000000000000..4c94a4c79dfff --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_legacy_messages.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_no_csrf.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_no_csrf.xml index 092174a2d9720..3af5322be212f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_no_csrf.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_no_csrf.xml @@ -7,7 +7,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/full.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/full.xml index 10a646049d766..9207066f1c183 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/full.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/full.xml @@ -8,7 +8,7 @@ - + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_mock_response_factory.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_mock_response_factory.xml new file mode 100644 index 0000000000000..6835b2f4b7660 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_mock_response_factory.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_retry.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_retry.xml new file mode 100644 index 0000000000000..9d475da0b7edd --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_retry.xml @@ -0,0 +1,25 @@ + + + + + + + + 429 + 500 + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_dsn.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_dsn.xml index ff4d75c8250bf..be53f59bc3cad 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_dsn.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_dsn.xml @@ -13,6 +13,9 @@ redirected@example.org redirected1@example.org + from@example.org + bcc1@example.org + bar diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_transports.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_transports.xml index a6eb67dc81024..cbe538d33e99c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_transports.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_transports.xml @@ -15,6 +15,9 @@ redirected@example.org redirected1@example.org + from@example.org + bcc1@example.org + bar 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 837db14c1cad4..b0510d580ceaf 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 @@ -20,6 +20,7 @@ + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/property_accessor.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/property_accessor.xml index 07e33ae3e8d96..9406919e92394 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/property_accessor.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/property_accessor.xml @@ -7,6 +7,6 @@ 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/validation_annotations.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/validation_annotations.xml index f993a20d97314..2324b9ca6e374 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/validation_annotations.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/validation_annotations.xml @@ -9,4 +9,8 @@ + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow_with_no_events_to_dispatch.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow_with_no_events_to_dispatch.xml new file mode 100644 index 0000000000000..2f563da4bf96b --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow_with_no_events_to_dispatch.xml @@ -0,0 +1,28 @@ + + + + + + + one + + Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest + + + + + + one + two + + + two + three + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow_with_specified_events_to_dispatch.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow_with_specified_events_to_dispatch.xml new file mode 100644 index 0000000000000..d442828f8cfbf --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow_with_specified_events_to_dispatch.xml @@ -0,0 +1,29 @@ + + + + + + + one + + Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest + workflow.leave + workflow.completed + + + + + one + two + + + two + three + + + + 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 ee20bc74b22d6..4921492d1af83 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/cache.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/cache.yml @@ -23,3 +23,18 @@ framework: - cache.adapter.array - cache.adapter.filesystem - {name: cache.adapter.redis, provider: 'redis://foo'} + cache.redis_tag_aware.foo: + adapter: cache.adapter.redis_tag_aware + cache.redis_tag_aware.foo2: + tags: true + adapter: cache.adapter.redis_tag_aware + cache.redis_tag_aware.bar: + adapter: cache.redis_tag_aware.foo + cache.redis_tag_aware.bar2: + tags: true + adapter: cache.redis_tag_aware.foo + cache.redis_tag_aware.baz: + adapter: cache.redis_tag_aware.foo2 + cache.redis_tag_aware.baz2: + tags: true + adapter: cache.redis_tag_aware.foo2 diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/csrf.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/csrf.yml index dbdd4951946fa..d29019cf48f6d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/csrf.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/csrf.yml @@ -1,5 +1,6 @@ framework: secret: s3cr3t csrf_protection: ~ - form: ~ + form: + legacy_error_messages: false session: ~ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/form_legacy_messages.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/form_legacy_messages.yml new file mode 100644 index 0000000000000..77c04e852fbcf --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/form_legacy_messages.yml @@ -0,0 +1,3 @@ +framework: + form: + legacy_error_messages: true diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/form_no_csrf.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/form_no_csrf.yml index e3ac7e8daf42d..1295018de16f8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/form_no_csrf.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/form_no_csrf.yml @@ -2,3 +2,4 @@ framework: form: csrf_protection: enabled: false + legacy_error_messages: false 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 5ad80a2da4db2..2206585863baa 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/full.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/full.yml @@ -5,6 +5,7 @@ framework: form: csrf_protection: field_name: _csrf + legacy_error_messages: false http_method_override: false esi: enabled: true diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_mock_response_factory.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_mock_response_factory.yml new file mode 100644 index 0000000000000..b958591084136 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_mock_response_factory.yml @@ -0,0 +1,4 @@ +framework: + http_client: + default_options: ~ + mock_response_factory: my_response_factory diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_retry.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_retry.yml new file mode 100644 index 0000000000000..8b81f3d1be3bf --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_retry.yml @@ -0,0 +1,16 @@ +framework: + http_client: + default_options: + retry_failed: + backoff_service: null + decider_service: null + http_codes: [429, 500] + max_retries: 2 + delay: 100 + multiplier: 2 + max_delay: 0 + scoped_clients: + foo: + base_uri: http://example.com + retry_failed: + multiplier: 4 diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer_with_dsn.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer_with_dsn.yml index 07d435d9df30b..f8b3c87c4302c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer_with_dsn.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer_with_dsn.yml @@ -6,3 +6,7 @@ framework: recipients: - redirected@example.org - redirected1@example.org + headers: + from: from@example.org + bcc: [bcc1@example.org, bcc2@example.org] + foo: bar diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer_with_transports.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer_with_transports.yml index 6035988d76e59..bc4657d3a4397 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer_with_transports.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer_with_transports.yml @@ -8,3 +8,7 @@ framework: recipients: - redirected@example.org - redirected1@example.org + headers: + from: from@example.org + bcc: [bcc1@example.org, bcc2@example.org] + foo: bar 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 daab75bd87e40..d00f4a65dd37c 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 @@ -19,3 +19,4 @@ framework: max_delay: 100 failed: 'in-memory:///' redis: 'redis://127.0.0.1:6379/messages' + beanstalkd: 'beanstalkd://127.0.0.1:11300' diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/property_accessor.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/property_accessor.yml index ea527c9821116..931b50383f210 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/property_accessor.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/property_accessor.yml @@ -1,5 +1,7 @@ framework: property_access: magic_call: true + magic_get: true + magic_set: false throw_exception_on_invalid_index: true throw_exception_on_invalid_property_path: false diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/validation_annotations.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/validation_annotations.yml index 41f17969b83ca..97b433f8cfb08 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/validation_annotations.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/validation_annotations.yml @@ -3,3 +3,8 @@ framework: validation: enabled: true enable_annotations: true + +services: + validator.alias: + alias: validator + public: true diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflow_with_no_events_to_dispatch.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflow_with_no_events_to_dispatch.yml new file mode 100644 index 0000000000000..e0a281f27db46 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflow_with_no_events_to_dispatch.yml @@ -0,0 +1,22 @@ +framework: + workflows: + my_workflow: + type: state_machine + initial_marking: one + events_to_dispatch: [] + marking_store: + type: method + property: state + supports: + - Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest + places: + - one + - two + - three + transitions: + count_to_two: + from: one + to: two + count_to_three: + from: two + to: three diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflow_with_specified_events_to_dispatch.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflow_with_specified_events_to_dispatch.yml new file mode 100644 index 0000000000000..d5ff3d5e5fe0d --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflow_with_specified_events_to_dispatch.yml @@ -0,0 +1,22 @@ +framework: + workflows: + my_workflow: + type: state_machine + initial_marking: one + events_to_dispatch: ['workflow.leave', 'workflow.completed'] + marking_store: + type: method + property: state + supports: + - Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest + places: + - one + - two + - three + transitions: + count_to_two: + from: one + to: two + count_to_three: + from: two + to: three diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index c6f158eb8c0d0..146cf8ed4fe58 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -12,7 +12,9 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection; use Doctrine\Common\Annotations\Annotation; +use Psr\Cache\CacheItemPoolInterface; use Psr\Log\LoggerAwareInterface; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddAnnotationsCachedReaderPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\FrameworkExtension; use Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\DummyMessage; @@ -26,6 +28,7 @@ use Symfony\Component\Cache\Adapter\FilesystemAdapter; use Symfony\Component\Cache\Adapter\ProxyAdapter; use Symfony\Component\Cache\Adapter\RedisAdapter; +use Symfony\Component\Cache\Adapter\RedisTagAwareAdapter; use Symfony\Component\Cache\DependencyInjection\CachePoolPass; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; @@ -33,10 +36,13 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Exception\LogicException; use Symfony\Component\DependencyInjection\Loader\ClosureLoader; use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\HttpClient\RetryableHttpClient; use Symfony\Component\HttpClient\ScopingHttpClient; use Symfony\Component\HttpKernel\DependencyInjection\LoggerPass; use Symfony\Component\Messenger\Transport\TransportFactory; @@ -48,16 +54,22 @@ use Symfony\Component\Serializer\Normalizer\DataUriNormalizer; use Symfony\Component\Serializer\Normalizer\DateIntervalNormalizer; use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; +use Symfony\Component\Serializer\Normalizer\FormErrorNormalizer; use Symfony\Component\Serializer\Normalizer\JsonSerializableNormalizer; use Symfony\Component\Serializer\Serializer; use Symfony\Component\Translation\DependencyInjection\TranslatorPass; use Symfony\Component\Validator\DependencyInjection\AddConstraintValidatorsPass; use Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader; use Symfony\Component\Workflow; +use Symfony\Component\Workflow\WorkflowEvents; +use Symfony\Contracts\Cache\CacheInterface; +use Symfony\Contracts\Cache\TagAwareCacheInterface; use Symfony\Component\Workflow\Metadata\InMemoryMetadataStore; abstract class FrameworkExtensionTest extends TestCase { + use ExpectDeprecationTrait; + private static $containerCache = []; abstract protected function loadFromFile(ContainerBuilder $container, $file); @@ -79,7 +91,7 @@ public function testPropertyAccessWithDefaultValue() $container = $this->createContainerFromFile('full'); $def = $container->getDefinition('property_accessor'); - $this->assertFalse($def->getArgument(0)); + $this->assertSame(PropertyAccessor::MAGIC_SET | PropertyAccessor::MAGIC_GET, $def->getArgument(0)); $this->assertFalse($def->getArgument(1)); $this->assertTrue($def->getArgument(3)); } @@ -88,7 +100,7 @@ public function testPropertyAccessWithOverriddenValues() { $container = $this->createContainerFromFile('property_accessor'); $def = $container->getDefinition('property_accessor'); - $this->assertTrue($def->getArgument(0)); + $this->assertSame(PropertyAccessor::MAGIC_GET | PropertyAccessor::MAGIC_CALL, $def->getArgument(0)); $this->assertTrue($def->getArgument(1)); $this->assertFalse($def->getArgument(3)); } @@ -214,6 +226,8 @@ public function testWorkflows() $this->assertTrue($container->hasDefinition('workflow.article'), 'Workflow is registered as a service'); $this->assertSame('workflow.abstract', $container->getDefinition('workflow.article')->getParent()); + $this->assertNull($container->getDefinition('workflow.article')->getArgument('index_4'), 'Workflows has eventsToDispatch=null'); + $this->assertTrue($container->hasDefinition('workflow.article.definition'), 'Workflow definition is registered as a service'); $workflowDefinition = $container->getDefinition('workflow.article.definition'); @@ -232,7 +246,7 @@ public function testWorkflows() ); $this->assertCount(4, $workflowDefinition->getArgument(1)); $this->assertSame(['draft'], $workflowDefinition->getArgument(2)); - $metadataStoreDefinition = $workflowDefinition->getArgument(3); + $metadataStoreDefinition = $container->getDefinition('workflow.article.metadata_store'); $this->assertSame(InMemoryMetadataStore::class, $metadataStoreDefinition->getClass()); $this->assertSame([ 'title' => 'article workflow', @@ -260,8 +274,12 @@ public function testWorkflows() $this->assertCount(9, $stateMachineDefinition->getArgument(1)); $this->assertSame(['start'], $stateMachineDefinition->getArgument(2)); - $metadataStoreDefinition = $stateMachineDefinition->getArgument(3); - $this->assertInstanceOf(Definition::class, $metadataStoreDefinition); + $metadataStoreReference = $stateMachineDefinition->getArgument(3); + $this->assertInstanceOf(Reference::class, $metadataStoreReference); + $this->assertSame('state_machine.pull_request.metadata_store', (string) $metadataStoreReference); + + $metadataStoreDefinition = $container->getDefinition('state_machine.pull_request.metadata_store'); + $this->assertSame(Workflow\Metadata\InMemoryMetadataStore::class, $metadataStoreDefinition->getClass()); $this->assertSame(InMemoryMetadataStore::class, $metadataStoreDefinition->getClass()); $workflowMetadata = $metadataStoreDefinition->getArgument(0); @@ -427,6 +445,24 @@ public function testWorkflowsNamedExplicitlyEnabled() $this->assertTrue($container->hasDefinition('workflow.workflows.definition')); } + public function testWorkflowsWithNoDispatchedEvents() + { + $container = $this->createContainerFromFile('workflow_with_no_events_to_dispatch'); + + $eventsToDispatch = $container->getDefinition('state_machine.my_workflow')->getArgument('index_4'); + + $this->assertSame([], $eventsToDispatch); + } + + public function testWorkflowsWithSpecifiedDispatchedEvents() + { + $container = $this->createContainerFromFile('workflow_with_specified_events_to_dispatch'); + + $eventsToDispatch = $container->getDefinition('state_machine.my_workflow')->getArgument('index_4'); + + $this->assertSame([WorkflowEvents::LEAVE, WorkflowEvents::COMPLETED], $eventsToDispatch); + } + public function testEnabledPhpErrorsConfig() { $container = $this->createContainerFromFile('php_errors_enabled'); @@ -512,7 +548,7 @@ public function testNullSessionHandler() $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']; + $expected = ['session', 'initialized_session', 'logger', 'session_collector']; $this->assertEquals($expected, array_keys($container->getDefinition('session_listener')->getArgument(0)->getValues())); } @@ -643,6 +679,16 @@ public function testMessengerTransports() $this->assertTrue($container->hasDefinition('messenger.transport.redis.factory')); + $this->assertTrue($container->hasDefinition('messenger.transport.beanstalkd')); + $transportFactory = $container->getDefinition('messenger.transport.beanstalkd')->getFactory(); + $transportArguments = $container->getDefinition('messenger.transport.beanstalkd')->getArguments(); + + $this->assertEquals([new Reference('messenger.transport_factory'), 'createTransport'], $transportFactory); + $this->assertCount(3, $transportArguments); + $this->assertSame('beanstalkd://127.0.0.1:11300', $transportArguments[0]); + + $this->assertTrue($container->hasDefinition('messenger.transport.beanstalkd.factory')); + $this->assertSame(10, $container->getDefinition('messenger.retry.multiplier_retry_strategy.customised')->getArgument(0)); $this->assertSame(7, $container->getDefinition('messenger.retry.multiplier_retry_strategy.customised')->getArgument(1)); $this->assertSame(3, $container->getDefinition('messenger.retry.multiplier_retry_strategy.customised')->getArgument(2)); @@ -737,7 +783,7 @@ public function testMessengerInvalidTransportRouting() public function testTranslator() { $container = $this->createContainerFromFile('full'); - $this->assertTrue($container->hasDefinition('translator.default'), '->registerTranslatorConfiguration() loads translation.xml'); + $this->assertTrue($container->hasDefinition('translator.default'), '->registerTranslatorConfiguration() loads translation.php'); $this->assertEquals('translator.default', (string) $container->getAlias('translator'), '->registerTranslatorConfiguration() redefines translator service from identity to real translator'); $options = $container->getDefinition('translator.default')->getArgument(4); @@ -847,7 +893,7 @@ public function testValidationService() { $container = $this->createContainerFromFile('validation_annotations', ['kernel.charset' => 'UTF-8'], false); - $this->assertInstanceOf('Symfony\Component\Validator\Validator\ValidatorInterface', $container->get('validator')); + $this->assertInstanceOf('Symfony\Component\Validator\Validator\ValidatorInterface', $container->get('validator.alias')); } public function testAnnotations() @@ -1023,6 +1069,16 @@ public function testFormsCanBeEnabledWithoutCsrfProtection() $this->assertFalse($container->getParameter('form.type_extension.csrf.enabled')); } + /** + * @group legacy + */ + public function testFormsWithoutImprovedValidationMessages() + { + $this->expectDeprecation('Since symfony/framework-bundle 5.2: Setting the "framework.form.legacy_error_messages" option to "true" is deprecated. It will have no effect as of Symfony 6.0.'); + + $this->createContainerFromFile('form_legacy_messages'); + } + public function testStopwatchEnabledWithDebugModeEnabled() { $container = $this->createContainerFromFile('default_config', [ @@ -1071,7 +1127,7 @@ public function testRegisterSerializerExtractor() $serializerExtractorDefinition = $container->getDefinition('property_info.serializer_extractor'); $this->assertEquals('serializer.mapping.class_metadata_factory', $serializerExtractorDefinition->getArgument(0)->__toString()); - $this->assertFalse($serializerExtractorDefinition->isPublic()); + $this->assertTrue(!$serializerExtractorDefinition->isPublic() || $serializerExtractorDefinition->isPrivate()); $tag = $serializerExtractorDefinition->getTag('property_info.list_extractor'); $this->assertEquals(['priority' => -999], $tag[0]); } @@ -1109,6 +1165,17 @@ public function testDateTimeNormalizerRegistered() $this->assertEquals(-910, $tag[0]['priority']); } + public function testFormErrorNormalizerRegistred() + { + $container = $this->createContainerFromFile('full'); + + $definition = $container->getDefinition('serializer.normalizer.form_error'); + $tag = $definition->getTag('serializer.normalizer'); + + $this->assertEquals(FormErrorNormalizer::class, $definition->getClass()); + $this->assertEquals(-915, $tag[0]['priority']); + } + public function testJsonSerializableNormalizerRegistered() { $container = $this->createContainerFromFile('full'); @@ -1293,6 +1360,41 @@ public function testCachePoolServices() $this->assertEquals($expected, $chain->getArguments()); } + public function testRedisTagAwareAdapter(): void + { + $container = $this->createContainerFromFile('cache', [], true); + + $aliasesForArguments = []; + $argNames = [ + 'cacheRedisTagAwareFoo', + 'cacheRedisTagAwareFoo2', + 'cacheRedisTagAwareBar', + 'cacheRedisTagAwareBar2', + 'cacheRedisTagAwareBaz', + 'cacheRedisTagAwareBaz2', + ]; + foreach ($argNames as $argumentName) { + $aliasesForArguments[] = sprintf('%s $%s', TagAwareCacheInterface::class, $argumentName); + $aliasesForArguments[] = sprintf('%s $%s', CacheInterface::class, $argumentName); + $aliasesForArguments[] = sprintf('%s $%s', CacheItemPoolInterface::class, $argumentName); + } + + foreach ($aliasesForArguments as $aliasForArgumentStr) { + $aliasForArgument = $container->getAlias($aliasForArgumentStr); + $this->assertNotNull($aliasForArgument, sprintf("No alias found for '%s'", $aliasForArgumentStr)); + + $def = $container->getDefinition((string) $aliasForArgument); + $this->assertInstanceOf(ChildDefinition::class, $def, sprintf("No definition found for '%s'", $aliasForArgumentStr)); + + $defParent = $container->getDefinition($def->getParent()); + if ($defParent instanceof ChildDefinition) { + $defParent = $container->getDefinition($defParent->getParent()); + } + + $this->assertSame(RedisTagAwareAdapter::class, $defParent->getClass(), sprintf("'%s' is not %s", $aliasForArgumentStr, RedisTagAwareAdapter::class)); + } + } + public function testRemovesResourceCheckerConfigCacheFactoryArgumentOnlyIfNoDebug() { $container = $this->createContainer(['kernel.debug' => true]); @@ -1324,7 +1426,7 @@ public function testSessionCookieSecureAuto() { $container = $this->createContainerFromFile('session_cookie_secure_auto'); - $expected = ['session', 'initialized_session', 'logger', 'session_storage', 'request_stack']; + $expected = ['session', 'initialized_session', 'logger', 'session_collector', 'session_storage', 'request_stack']; $this->assertEquals($expected, array_keys($container->getDefinition('session_listener')->getArgument(0)->getValues())); } @@ -1390,6 +1492,23 @@ public function testHttpClientOverrideDefaultOptions() $this->assertSame($expected, $container->getDefinition('foo')->getArgument(2)); } + public function testHttpClientRetry() + { + if (!class_exists(RetryableHttpClient::class)) { + $this->expectException(LogicException::class); + } + $container = $this->createContainerFromFile('http_client_retry'); + + $this->assertSame([429, 500], $container->getDefinition('http_client.retry.decider')->getArgument(0)); + $this->assertSame(100, $container->getDefinition('http_client.retry.exponential_backoff')->getArgument(0)); + $this->assertSame(2, $container->getDefinition('http_client.retry.exponential_backoff')->getArgument(1)); + $this->assertSame(0, $container->getDefinition('http_client.retry.exponential_backoff')->getArgument(2)); + $this->assertSame(2, $container->getDefinition('http_client.retry')->getArgument(3)); + + $this->assertSame(RetryableHttpClient::class, $container->getDefinition('foo.retry')->getClass()); + $this->assertSame(4, $container->getDefinition('foo.retry.exponential_backoff')->getArgument(1)); + } + public function testHttpClientWithQueryParameterKey() { $container = $this->createContainerFromFile('http_client_xml_key'); @@ -1461,6 +1580,11 @@ public function testMailer(string $configFile, array $expectedTransports): void $this->assertSame('sender@example.org', $l->getArgument(0)); $this->assertSame(['redirected@example.org', 'redirected1@example.org'], $l->getArgument(1)); $this->assertEquals(new Reference('messenger.default_bus', ContainerInterface::NULL_ON_INVALID_REFERENCE), $container->getDefinition('mailer.mailer')->getArgument(1)); + + $this->assertTrue($container->hasDefinition('mailer.message_listener')); + $l = $container->getDefinition('mailer.message_listener'); + $h = $l->getArgument(0); + $this->assertCount(3, $h->getMethodCalls()); } public function testMailerWithDisabledMessageBus(): void @@ -1477,12 +1601,28 @@ public function testMailerWithSpecificMessageBus(): void $this->assertEquals(new Reference('app.another_bus'), $container->getDefinition('mailer.mailer')->getArgument(1)); } + public function testHttpClientMockResponseFactory() + { + $container = $this->createContainerFromFile('http_client_mock_response_factory'); + + $definition = $container->getDefinition('http_client'); + + $this->assertSame(MockHttpClient::class, $definition->getClass()); + $this->assertCount(1, $definition->getArguments()); + + $argument = $definition->getArgument(0); + + $this->assertInstanceOf(Reference::class, $argument); + $this->assertSame('my_response_factory', (string) $argument); + } + protected function createContainer(array $data = []) { return new ContainerBuilder(new EnvPlaceholderParameterBag(array_merge([ 'kernel.bundles' => ['FrameworkBundle' => 'Symfony\\Bundle\\FrameworkBundle\\FrameworkBundle'], 'kernel.bundles_metadata' => ['FrameworkBundle' => ['namespace' => 'Symfony\\Bundle\\FrameworkBundle', 'path' => __DIR__.'/../..']], 'kernel.cache_dir' => __DIR__, + 'kernel.build_dir' => __DIR__, 'kernel.project_dir' => __DIR__, 'kernel.debug' => false, 'kernel.environment' => 'test', diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/BundlePathsTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/BundlePathsTest.php index f447300c2c69c..65f2361451619 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/BundlePathsTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/BundlePathsTest.php @@ -40,7 +40,7 @@ public function testBundlePublicDir() public function testBundleTwigTemplatesDir() { static::bootKernel(['test_case' => 'BundlePaths']); - $twig = static::$container->get('twig'); + $twig = static::$container->get('twig.alias'); $bundlesMetadata = static::$container->getParameter('kernel.bundles_metadata'); $this->assertSame([$bundlesMetadata['LegacyBundle']['path'].'/Resources/views'], $twig->getLoader()->getPaths('Legacy')); @@ -53,7 +53,7 @@ public function testBundleTwigTemplatesDir() public function testBundleTranslationsDir() { static::bootKernel(['test_case' => 'BundlePaths']); - $translator = static::$container->get('translator'); + $translator = static::$container->get('translator.alias'); $this->assertSame('OK', $translator->trans('ok_label', [], 'legacy')); $this->assertSame('OK', $translator->trans('ok_label', [], 'modern')); @@ -62,7 +62,7 @@ public function testBundleTranslationsDir() public function testBundleValidationConfigDir() { static::bootKernel(['test_case' => 'BundlePaths']); - $validator = static::$container->get('validator'); + $validator = static::$container->get('validator.alias'); $this->assertTrue($validator->hasMetadataFor(LegacyPerson::class)); $this->assertCount(1, $constraintViolationList = $validator->validate(new LegacyPerson('john', 5))); @@ -76,7 +76,7 @@ public function testBundleValidationConfigDir() public function testBundleSerializationConfigDir() { static::bootKernel(['test_case' => 'BundlePaths']); - $serializer = static::$container->get('serializer'); + $serializer = static::$container->get('serializer.alias'); $this->assertEquals(['full_name' => 'john', 'age' => 5], $serializer->normalize(new LegacyPerson('john', 5), 'json')); $this->assertEquals(['full_name' => 'john', 'age' => 5], $serializer->normalize(new ModernPerson('john', 5), 'json')); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/CachePoolsTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/CachePoolsTest.php index e6f6bbb3158d8..8fa9374ab8d51 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/CachePoolsTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/CachePoolsTest.php @@ -96,7 +96,7 @@ private function doTestCachePools($options, $adapterClass) $pool2 = $container->get('cache.pool2'); $pool2->save($item); - $container->get('cache_clearer')->clear($container->getParameter('kernel.cache_dir')); + $container->get('cache_clearer.alias')->clear($container->getParameter('kernel.cache_dir')); $item = $pool1->getItem($key); $this->assertFalse($item->isHit()); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SerializerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SerializerTest.php index b0d774bdd55e8..bbb66e53845aa 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SerializerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SerializerTest.php @@ -20,7 +20,7 @@ public function testDeserializeArrayOfObject() { static::bootKernel(['test_case' => 'Serializer']); - $result = static::$container->get('serializer')->deserialize('{"bars": [{"id": 1}, {"id": 2}]}', Foo::class, 'json'); + $result = static::$container->get('serializer.alias')->deserialize('{"bars": [{"id": 1}, {"id": 2}]}', Foo::class, 'json'); $bar1 = new Bar(); $bar1->id = 1; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/BundlePaths/config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/BundlePaths/config.yml index 94994ba60c798..82a5a5422ab7d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/BundlePaths/config.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/BundlePaths/config.yml @@ -8,3 +8,19 @@ framework: twig: strict_variables: '%kernel.debug%' + +services: + twig.alias: + alias: twig + + validator.alias: + alias: validator + public: true + + serializer.alias: + alias: serializer + public: true + + translator.alias: + alias: translator + public: true diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/config.yml index 8c7bcb4eb1fac..83c2b38df7248 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/config.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/config.yml @@ -1,5 +1,5 @@ imports: - - { resource: ../config/default.yml } + - { resource: default.yml } framework: cache: diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/default.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/default.yml new file mode 100644 index 0000000000000..c03efedd02bf7 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/default.yml @@ -0,0 +1,7 @@ +imports: + - { resource: ../config/default.yml } + +services: + cache_clearer.alias: + alias: cache_clearer + public: true diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/redis_config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/redis_config.yml index 30c69163d4f2f..e5ec8c4effbc4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/redis_config.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/redis_config.yml @@ -1,5 +1,5 @@ imports: - - { resource: ../config/default.yml } + - { resource: default.yml } parameters: env(REDIS_HOST): 'localhost' diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/redis_custom_config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/redis_custom_config.yml index df20c5357f7a4..84f90c8d2cede 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/redis_custom_config.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/redis_custom_config.yml @@ -1,5 +1,5 @@ imports: - - { resource: ../config/default.yml } + - { resource: default.yml } parameters: env(REDIS_HOST): 'localhost' diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Mailer/config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Mailer/config.yml index c2c3ace06f179..b464a41a0d857 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Mailer/config.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Mailer/config.yml @@ -9,3 +9,4 @@ framework: sender: sender@example.org recipients: - redirected@example.org + profiler: ~ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Serializer/config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Serializer/config.yml index cac135c315d00..7878c2ecb68d4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Serializer/config.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Serializer/config.yml @@ -4,3 +4,8 @@ imports: framework: serializer: { enabled: true } property_info: { enabled: true } + +services: + serializer.alias: + alias: serializer + public: true 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 1c42894a24d9c..50078d4fd59c4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/config/framework.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/config/framework.yml @@ -3,7 +3,9 @@ framework: router: { resource: "%kernel.project_dir%/%kernel.test_case%/routing.yml", utf8: true } validation: { enabled: true, enable_annotations: true } csrf_protection: true - form: true + form: + enabled: true + legacy_error_messages: false test: true default_locale: en session: diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/ConcreteMicroKernel.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/ConcreteMicroKernel.php index 2cab4749d3816..758ca34784033 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/ConcreteMicroKernel.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/ConcreteMicroKernel.php @@ -17,7 +17,6 @@ use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\Kernel; @@ -74,12 +73,6 @@ public function __wakeup() throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); } - public function __destruct() - { - $fs = new Filesystem(); - $fs->remove($this->cacheDir); - } - protected function configureRoutes(RoutingConfigurator $routes): void { $routes->add('halloween', '/')->controller('kernel::halloweenAction'); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/MicroKernelTraitTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/MicroKernelTraitTest.php index 67b6425545eb8..4ce3f35c0bf71 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/MicroKernelTraitTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/MicroKernelTraitTest.php @@ -19,6 +19,7 @@ use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; use Symfony\Component\DependencyInjection\Loader\ClosureLoader; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; +use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\HttpKernelInterface; @@ -29,9 +30,21 @@ class MicroKernelTraitTest extends TestCase { + private $kernel; + + protected function tearDown(): void + { + if ($this->kernel) { + $kernel = $this->kernel; + $this->kernel = null; + $fs = new Filesystem(); + $fs->remove($kernel->getCacheDir()); + } + } + public function test() { - $kernel = new ConcreteMicroKernel('test', false); + $kernel = $this->kernel = new ConcreteMicroKernel('test', false); $kernel->boot(); $request = Request::create('/'); @@ -44,7 +57,7 @@ public function test() public function testAsEventSubscriber() { - $kernel = new ConcreteMicroKernel('test', false); + $kernel = $this->kernel = new ConcreteMicroKernel('test', false); $kernel->boot(); $request = Request::create('/danger'); @@ -62,7 +75,7 @@ public function testRoutingRouteLoaderTagIsAdded() ->willReturn('framework'); $container = new ContainerBuilder(); $container->registerExtension($frameworkExtension); - $kernel = new ConcreteMicroKernel('test', false); + $kernel = $this->kernel = new ConcreteMicroKernel('test', false); $kernel->registerContainerConfiguration(new ClosureLoader($container)); $this->assertTrue($container->getDefinition('kernel')->hasTag('routing.route_loader')); } @@ -80,7 +93,7 @@ public function testFlexStyle() public function testSecretLoadedFromExtension() { - $kernel = new ConcreteMicroKernel('test', false); + $kernel = $this->kernel = new ConcreteMicroKernel('test', false); $kernel->boot(); self::assertSame('$ecret', $kernel->getContainer()->getParameter('kernel.secret')); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Test/WebTestCaseTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Test/WebTestCaseTest.php index efa3fbfb1f677..96e1d8779b31e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Test/WebTestCaseTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Test/WebTestCaseTest.php @@ -220,6 +220,38 @@ public function testAssertInputValueNotSame() $this->getCrawlerTester(new Crawler('
'))->assertInputValueNotSame('password', 'pa$$'); } + public function testAssertCheckboxChecked() + { + $this->getCrawlerTester(new Crawler(''))->assertCheckboxChecked('rememberMe'); + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('matches selector "input[name="rememberMe"]" and has a node matching selector "input[name="rememberMe"]" with attribute "checked" of value "checked".'); + $this->getCrawlerTester(new Crawler(''))->assertCheckboxChecked('rememberMe'); + } + + public function testAssertCheckboxNotChecked() + { + $this->getCrawlerTester(new Crawler(''))->assertCheckboxNotChecked('rememberMe'); + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('matches selector "input[name="rememberMe"]" and does not have a node matching selector "input[name="rememberMe"]" with attribute "checked" of value "checked".'); + $this->getCrawlerTester(new Crawler(''))->assertCheckboxNotChecked('rememberMe'); + } + + public function testAssertFormValue() + { + $this->getCrawlerTester(new Crawler('', 'http://localhost'))->assertFormValue('#form', 'username', 'Fabien'); + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Failed asserting that two strings are identical.'); + $this->getCrawlerTester(new Crawler('', 'http://localhost'))->assertFormValue('#form', 'username', 'Jane'); + } + + public function testAssertNoFormValue() + { + $this->getCrawlerTester(new Crawler('', 'http://localhost'))->assertNoFormValue('#form', 'rememberMe'); + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Field "rememberMe" has a value in form "#form".'); + $this->getCrawlerTester(new Crawler('', 'http://localhost'))->assertNoFormValue('#form', 'rememberMe'); + } + public function testAssertRequestAttributeValueSame() { $this->getRequestTester()->assertRequestAttributeValueSame('foo', 'bar'); diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index 62555c0c6fa26..6a24ffedc75bb 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -18,13 +18,13 @@ "require": { "php": ">=7.2.5", "ext-xml": "*", - "symfony/cache": "^4.4|^5.0", + "symfony/cache": "^5.2", "symfony/config": "^5.0", - "symfony/dependency-injection": "^5.1", + "symfony/dependency-injection": "^5.2", "symfony/event-dispatcher": "^5.1", "symfony/error-handler": "^4.4.1|^5.0.1", - "symfony/http-foundation": "^4.4|^5.0", - "symfony/http-kernel": "^5.0", + "symfony/http-foundation": "^5.2", + "symfony/http-kernel": "^5.2", "symfony/polyfill-mbstring": "~1.0", "symfony/polyfill-php80": "^1.15", "symfony/filesystem": "^4.4|^5.0", @@ -36,33 +36,33 @@ "doctrine/cache": "~1.0", "symfony/asset": "^5.1", "symfony/browser-kit": "^4.4|^5.0", - "symfony/console": "^4.4|^5.0", + "symfony/console": "^5.2", "symfony/css-selector": "^4.4|^5.0", "symfony/dom-crawler": "^4.4|^5.0", "symfony/dotenv": "^5.1", "symfony/polyfill-intl-icu": "~1.0", - "symfony/form": "^4.4|^5.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": "^4.4|^5.0", - "symfony/messenger": "^4.4|^5.0", + "symfony/mailer": "^5.2", + "symfony/messenger": "^5.2", "symfony/mime": "^4.4|^5.0", "symfony/process": "^4.4|^5.0", "symfony/security-bundle": "^5.1", "symfony/security-csrf": "^4.4|^5.0", "symfony/security-http": "^4.4|^5.0", - "symfony/serializer": "^4.4|^5.0", + "symfony/serializer": "^5.2", "symfony/stopwatch": "^4.4|^5.0", "symfony/string": "^5.0", "symfony/translation": "^5.0", "symfony/twig-bundle": "^4.4|^5.0", - "symfony/validator": "^4.4|^5.0", - "symfony/workflow": "^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", - "phpdocumentor/reflection-docblock": "^3.0|^4.0", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", "paragonie/sodium_compat": "^1.8", "twig/twig": "^2.10|^3.0" }, @@ -73,24 +73,25 @@ "phpunit/phpunit": "<5.4.3", "symfony/asset": "<5.1", "symfony/browser-kit": "<4.4", - "symfony/console": "<4.4", + "symfony/console": "<5.2", "symfony/dotenv": "<5.1", "symfony/dom-crawler": "<4.4", "symfony/http-client": "<4.4", - "symfony/form": "<4.4", + "symfony/form": "<5.2", "symfony/lock": "<4.4", - "symfony/mailer": "<4.4", + "symfony/mailer": "<5.2", "symfony/messenger": "<4.4", "symfony/mime": "<4.4", "symfony/property-info": "<4.4", - "symfony/serializer": "<4.4", + "symfony/property-access": "<5.2", + "symfony/serializer": "<5.2", "symfony/stopwatch": "<4.4", "symfony/translation": "<5.0", "symfony/twig-bridge": "<4.4", "symfony/twig-bundle": "<4.4", - "symfony/validator": "<4.4", + "symfony/validator": "<5.2", "symfony/web-profiler-bundle": "<4.4", - "symfony/workflow": "<4.4" + "symfony/workflow": "<5.2" }, "suggest": { "ext-apcu": "For best performance of the system caches", @@ -111,7 +112,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "5.1-dev" + "dev-master": "5.2-dev" } } } diff --git a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md index ae7c1d9164702..2fe6c20335385 100644 --- a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md @@ -1,6 +1,14 @@ CHANGELOG ========= +5.2.0 +----- + + * Added `FirewallListenerFactoryInterface`, which can be implemented by security factories to add firewall listeners + * Added `SortFirewallListenersPass` to make the execution order of firewall listeners configurable by + leveraging `Symfony\Component\Security\Http\Firewall\FirewallListenerInterface` + * Added ability to use comma separated ip address list for `security.access_control` + 5.1.0 ----- diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/SortFirewallListenersPass.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/SortFirewallListenersPass.php new file mode 100644 index 0000000000000..6d49320445c10 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/SortFirewallListenersPass.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Argument\IteratorArgument; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\Security\Http\Firewall\FirewallListenerInterface; + +/** + * Sorts firewall listeners based on the execution order provided by FirewallListenerInterface::getPriority(). + * + * @author Christian Scheb + */ +class SortFirewallListenersPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container): void + { + if (!$container->hasParameter('security.firewalls')) { + return; + } + + foreach ($container->getParameter('security.firewalls') as $firewallName) { + $firewallContextDefinition = $container->getDefinition('security.firewall.map.context.'.$firewallName); + $this->sortFirewallContextListeners($firewallContextDefinition, $container); + } + } + + private function sortFirewallContextListeners(Definition $definition, ContainerBuilder $container): void + { + /** @var IteratorArgument $listenerIteratorArgument */ + $listenerIteratorArgument = $definition->getArgument(0); + $prioritiesByServiceId = $this->getListenerPriorities($listenerIteratorArgument, $container); + + $listeners = $listenerIteratorArgument->getValues(); + usort($listeners, function (Reference $a, Reference $b) use ($prioritiesByServiceId) { + return $prioritiesByServiceId[(string) $b] <=> $prioritiesByServiceId[(string) $a]; + }); + + $listenerIteratorArgument->setValues(array_values($listeners)); + } + + private function getListenerPriorities(IteratorArgument $listeners, ContainerBuilder $container): array + { + $priorities = []; + + foreach ($listeners->getValues() as $reference) { + $id = (string) $reference; + $def = $container->getDefinition($id); + + // We must assume that the class value has been correctly filled, even if the service is created by a factory + $class = $def->getClass(); + + if (!$r = $container->getReflectionClass($class)) { + throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id)); + } + + $priority = 0; + if ($r->isSubclassOf(FirewallListenerInterface::class)) { + $priority = $r->getMethod('getPriority')->invoke(null); + } + + $priorities[$id] = $priority; + } + + return $priorities; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php index a5d6f7e45ea6e..c96dc76d7ba98 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php @@ -170,7 +170,7 @@ protected function createAuthenticationSuccessHandler(ContainerBuilder $containe } else { $successHandler = $container->setDefinition($successHandlerId, new ChildDefinition('security.authentication.success_handler')); $successHandler->addMethodCall('setOptions', [$options]); - $successHandler->addMethodCall('setProviderKey', [$id]); + $successHandler->addMethodCall('setFirewallName', [$id]); } return $successHandlerId; diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/EntryPointFactoryInterface.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/EntryPointFactoryInterface.php index d7e726b02b028..f352e14755652 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/EntryPointFactoryInterface.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/EntryPointFactoryInterface.php @@ -26,5 +26,5 @@ interface EntryPointFactoryInterface * This does not mean that the entry point is also used. This is managed * by the "entry_point" firewall setting. */ - public function registerEntryPoint(ContainerBuilder $container, string $id, array $config): ?string; + public function registerEntryPoint(ContainerBuilder $container, string $firewallName, array $config): ?string; } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FirewallListenerFactoryInterface.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FirewallListenerFactoryInterface.php new file mode 100644 index 0000000000000..c4842010a779f --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FirewallListenerFactoryInterface.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\Bundle\SecurityBundle\DependencyInjection\Security\Factory; + +use Symfony\Component\DependencyInjection\ContainerBuilder; + +/** + * Can be implemented by a security factory to add a listener to the firewall. + * + * @author Christian Scheb + */ +interface FirewallListenerFactoryInterface +{ + /** + * Creates the firewall listener services for the provided configuration. + * + * @return string[] The listener service IDs to be used by the firewall + */ + public function createListeners(ContainerBuilder $container, string $firewallName, array $config): array; +} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php index 1f1be87c64b03..76dde71424a9c 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php @@ -97,9 +97,9 @@ protected function createEntryPoint(ContainerBuilder $container, string $id, arr return $this->registerEntryPoint($container, $id, $config); } - public function registerEntryPoint(ContainerBuilder $container, string $id, array $config): string + public function registerEntryPoint(ContainerBuilder $container, string $firewallName, array $config): string { - $entryPointId = 'security.authentication.form_entry_point.'.$id; + $entryPointId = 'security.authentication.form_entry_point.'.$firewallName; $container ->setDefinition($entryPointId, new ChildDefinition('security.authentication.form_entry_point')) ->addArgument(new Reference('security.http_utils')) diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardAuthenticationFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardAuthenticationFactory.php index 0b4e12145f951..51a2b3bb97f54 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardAuthenticationFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardAuthenticationFactory.php @@ -114,7 +114,7 @@ public function createAuthenticator(ContainerBuilder $container, string $firewal return $authenticatorIds; } - public function registerEntryPoint(ContainerBuilder $container, string $id, array $config): ?string + public function registerEntryPoint(ContainerBuilder $container, string $firewallName, array $config): ?string { try { return $this->determineEntryPoint(null, $config); diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php index 1973f83f049aa..718076ced42f7 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php @@ -82,9 +82,9 @@ public function addConfiguration(NodeDefinition $node) ; } - public function registerEntryPoint(ContainerBuilder $container, string $id, array $config): string + public function registerEntryPoint(ContainerBuilder $container, string $firewallName, array $config): string { - $entryPointId = 'security.authentication.basic_entry_point.'.$id; + $entryPointId = 'security.authentication.basic_entry_point.'.$firewallName; $container ->setDefinition($entryPointId, new ChildDefinition('security.authentication.basic_entry_point')) ->addArgument($config['realm']) diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LoginLinkFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LoginLinkFactory.php new file mode 100644 index 0000000000000..f5c323be033c2 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LoginLinkFactory.php @@ -0,0 +1,159 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory; + +use Symfony\Component\Config\Definition\Builder\NodeBuilder; +use Symfony\Component\Config\Definition\Builder\NodeDefinition; +use Symfony\Component\Config\FileLocator; +use Symfony\Component\DependencyInjection\ChildDefinition; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface; +use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface; +use Symfony\Component\Security\Http\LoginLink\LoginLinkHandler; + +/** + * @internal + * @experimental in 5.2 + */ +class LoginLinkFactory extends AbstractFactory implements AuthenticatorFactoryInterface +{ + public function addConfiguration(NodeDefinition $node) + { + /** @var NodeBuilder $builder */ + $builder = $node->children(); + + $builder + ->scalarNode('check_route') + ->isRequired() + ->info('Route that will validate the login link - e.g. app_login_link_verify') + ->end() + ->arrayNode('signature_properties') + ->prototype('scalar')->end() + ->requiresAtLeastOneElement() + ->info('An array of properties on your User that are used to sign the link. If any of these change, all existing links will become invalid') + ->example(['email', 'password']) + ->end() + ->integerNode('lifetime') + ->defaultValue(600) + ->info('The lifetime of the login link in seconds') + ->end() + ->integerNode('max_uses') + ->defaultNull() + ->info('Max number of times a login link can be used - null means unlimited within lifetime.') + ->end() + ->scalarNode('used_link_cache') + ->info('Cache service id used to expired links of max_uses is set') + ->end() + ->scalarNode('success_handler') + ->info(sprintf('A service id that implements %s', AuthenticationSuccessHandlerInterface::class)) + ->end() + ->scalarNode('failure_handler') + ->info(sprintf('A service id that implements %s', AuthenticationFailureHandlerInterface::class)) + ->end() + ->scalarNode('provider') + ->info('the user provider to load users from.') + ->end() + ; + + foreach (array_merge($this->defaultSuccessHandlerOptions, $this->defaultFailureHandlerOptions) as $name => $default) { + if (\is_bool($default)) { + $builder->booleanNode($name)->defaultValue($default); + } else { + $builder->scalarNode($name)->defaultValue($default); + } + } + } + + public function getKey() + { + return 'login-link'; + } + + public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId): string + { + if (!class_exists(LoginLinkHandler::class)) { + throw new \LogicException('Login login link requires symfony/security-http:^5.2.'); + } + + if (!$container->hasDefinition('security.authenticator.login_link')) { + $loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__).'/../../Resources/config')); + $loader->load('security_authenticator_login_link.php'); + } + + if (null !== $config['max_uses'] && !isset($config['used_link_cache'])) { + $config['used_link_cache'] = 'security.authenticator.cache.expired_links'; + } + + $expiredStorageId = null; + if (isset($config['used_link_cache'])) { + $expiredStorageId = 'security.authenticator.expired_login_link_storage.'.$firewallName; + $container + ->setDefinition($expiredStorageId, new ChildDefinition('security.authenticator.expired_login_link_storage')) + ->replaceArgument(0, new Reference($config['used_link_cache'])) + ->replaceArgument(1, $config['lifetime']); + } + + $linkerId = 'security.authenticator.login_link_handler.'.$firewallName; + $linkerOptions = [ + 'route_name' => $config['check_route'], + 'lifetime' => $config['lifetime'], + 'max_uses' => $config['max_uses'] ?? null, + ]; + $container + ->setDefinition($linkerId, new ChildDefinition('security.authenticator.abstract_login_link_handler')) + ->replaceArgument(1, new Reference($userProviderId)) + ->replaceArgument(3, $config['signature_properties']) + ->replaceArgument(5, $linkerOptions) + ->replaceArgument(6, $expiredStorageId ? new Reference($expiredStorageId) : null) + ->addTag('security.authenticator.login_linker', ['firewall' => $firewallName]) + ; + + $authenticatorId = 'security.authenticator.login_link.'.$firewallName; + $container + ->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.login_link')) + ->replaceArgument(0, new Reference($linkerId)) + ->replaceArgument(2, new Reference($this->createAuthenticationSuccessHandler($container, $firewallName, $config))) + ->replaceArgument(3, new Reference($this->createAuthenticationFailureHandler($container, $firewallName, $config))) + ->replaceArgument(4, [ + 'check_route' => $config['check_route'], + ]); + + return $authenticatorId; + } + + public function getPosition() + { + return 'form'; + } + + protected function createAuthProvider(ContainerBuilder $container, string $id, array $config, string $userProviderId) + { + throw new \Exception('The old authentication system is not supported with login_link.'); + } + + protected function getListenerId() + { + throw new \Exception('The old authentication system is not supported with login_link.'); + } + + protected function createListener(ContainerBuilder $container, string $id, array $config, string $userProvider) + { + throw new \Exception('The old authentication system is not supported with login_link.'); + } + + protected function createEntryPoint(ContainerBuilder $container, string $id, array $config, ?string $defaultEntryPointId) + { + 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 new file mode 100644 index 0000000000000..df30cd05786fc --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LoginThrottlingFactory.php @@ -0,0 +1,98 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory; + +use Symfony\Bundle\FrameworkBundle\DependencyInjection\FrameworkExtension; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\Config\Definition\Builder\NodeDefinition; +use Symfony\Component\DependencyInjection\ChildDefinition; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\HttpFoundation\RateLimiter\RequestRateLimiterInterface; +use Symfony\Component\RateLimiter\Limiter; +use Symfony\Component\Security\Http\EventListener\LoginThrottlingListener; +use Symfony\Component\Security\Http\RateLimiter\DefaultLoginRateLimiter; + +/** + * @author Wouter de Jong + * + * @internal + */ +class LoginThrottlingFactory implements AuthenticatorFactoryInterface, SecurityFactoryInterface +{ + public function create(ContainerBuilder $container, string $id, array $config, string $userProvider, ?string $defaultEntryPoint) + { + throw new \LogicException('Login throttling is not supported when "security.enable_authenticator_manager" is not set to true.'); + } + + public function getPosition(): string + { + // this factory doesn't register any authenticators, this position doesn't matter + return 'pre_auth'; + } + + public function getKey(): string + { + return 'login_throttling'; + } + + /** + * @param ArrayNodeDefinition $builder + */ + public function addConfiguration(NodeDefinition $builder) + { + $builder + ->children() + ->scalarNode('limiter')->info(sprintf('A service id implementing "%s".', RequestRateLimiterInterface::class))->end() + ->integerNode('max_attempts')->defaultValue(5)->end() + ->end(); + } + + public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId): array + { + if (!class_exists(LoginThrottlingListener::class)) { + throw new \LogicException('Login throttling requires symfony/security-http:^5.2.'); + } + + if (!class_exists(Limiter::class)) { + throw new \LogicException('Login throttling requires symfony/rate-limiter to be installed and enabled.'); + } + + if (!isset($config['limiter'])) { + if (!class_exists(FrameworkExtension::class) || !method_exists(FrameworkExtension::class, 'registerRateLimiter')) { + throw new \LogicException('You must either configure a rate limiter for "security.firewalls.'.$firewallName.'.login_throttling" or install symfony/framework-bundle:^5.2.'); + } + + $limiterOptions = [ + 'strategy' => 'fixed_window', + 'limit' => $config['max_attempts'], + 'interval' => '1 minute', + ]; + FrameworkExtension::registerRateLimiter($container, $localId = '_login_local_'.$firewallName, $limiterOptions); + + $limiterOptions['limit'] = 5 * $config['max_attempts']; + FrameworkExtension::registerRateLimiter($container, $globalId = '_login_global_'.$firewallName, $limiterOptions); + + $container->register($config['limiter'] = 'security.login_throttling.'.$firewallName.'.limiter', DefaultLoginRateLimiter::class) + ->addArgument(new Reference('limiter.'.$globalId)) + ->addArgument(new Reference('limiter.'.$localId)) + ; + } + + $container + ->setDefinition('security.listener.login_throttling.'.$firewallName, new ChildDefinition('security.listener.login_throttling')) + ->replaceArgument(1, new Reference($config['limiter'])) + ->addTag('kernel.event_subscriber', ['dispatcher' => 'security.event_dispatcher.'.$firewallName]); + + return []; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 7202b2e5a95e0..f9d28a1d7b35c 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -13,6 +13,7 @@ use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AuthenticatorFactoryInterface; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\EntryPointFactoryInterface; +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; @@ -29,11 +30,10 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; -use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; +use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\HttpKernel\DependencyInjection\Extension; -use Symfony\Component\Ldap\Entry; use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder; use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder; @@ -41,6 +41,7 @@ use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Http\Controller\UserValueResolver; use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface; +use Symfony\Component\Security\Http\Event\CheckPassportEvent; use Twig\Extension\AbstractExtension; /** @@ -105,17 +106,18 @@ public function load(array $configs, ContainerBuilder $container) $config = $this->processConfiguration($mainConfig, $configs); // load services - $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); - $loader->load('security.xml'); - $loader->load('security_listeners.xml'); - $loader->load('security_rememberme.xml'); + $loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__).'/Resources/config')); + + $loader->load('security.php'); + $loader->load('security_listeners.php'); + $loader->load('security_rememberme.php'); if ($this->authenticatorManagerEnabled = $config['enable_authenticator_manager']) { if ($config['always_authenticate_before_granting']) { throw new InvalidConfigurationException('The security option "always_authenticate_before_granting" cannot be used when "enable_authenticator_manager" is set to true. If you rely on this behavior, set it to false.'); } - $loader->load('security_authenticator.xml'); + $loader->load('security_authenticator.php'); // The authenticator system no longer has anonymous tokens. This makes sure AccessListener // and AuthorizationChecker do not throw AuthenticationCredentialsNotFoundException when no @@ -124,18 +126,18 @@ public function load(array $configs, ContainerBuilder $container) $container->getDefinition('security.authorization_checker')->setArgument(4, false); $container->getDefinition('security.authorization_checker')->setArgument(5, false); } else { - $loader->load('security_legacy.xml'); + $loader->load('security_legacy.php'); } if (class_exists(AbstractExtension::class)) { - $loader->load('templating_twig.xml'); + $loader->load('templating_twig.php'); } - $loader->load('collectors.xml'); - $loader->load('guard.xml'); + $loader->load('collectors.php'); + $loader->load('guard.php'); if ($container->hasParameter('kernel.debug') && $container->getParameter('kernel.debug')) { - $loader->load('security_debug.xml'); + $loader->load('security_debug.php'); } if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) { @@ -149,7 +151,7 @@ public function load(array $configs, ContainerBuilder $container) $container->setParameter('security.authentication.session_strategy.strategy', $config['session_fixation_strategy']); if (isset($config['access_decision_manager']['service'])) { - $container->setAlias('security.access.decision_manager', $config['access_decision_manager']['service'])->setPrivate(true); + $container->setAlias('security.access.decision_manager', $config['access_decision_manager']['service']); } else { $container ->getDefinition('security.access.decision_manager') @@ -173,7 +175,7 @@ public function load(array $configs, ContainerBuilder $container) } if (class_exists(Application::class)) { - $loader->load('console.xml'); + $loader->load('console.php'); $container->getDefinition('security.command.user_password_encoder')->replaceArgument(1, array_keys($config['encoders'])); } @@ -341,6 +343,12 @@ private function createFirewall(ContainerBuilder $container, string $id, array $ throw new InvalidConfigurationException(sprintf('Invalid firewall "%s": user provider "%s" not found.', $id, $firewall['provider'])); } $defaultProvider = $providerIds[$normalizedName]; + + if ($this->authenticatorManagerEnabled) { + $container->setDefinition('security.listener.'.$id.'.user_provider', new ChildDefinition('security.listener.user_provider.abstract')) + ->addTag('kernel.event_listener', ['event' => CheckPassportEvent::class, 'priority' => 2048, 'method' => 'checkPassport']) + ->replaceArgument(0, new Reference($defaultProvider)); + } } elseif (1 === \count($providerIds)) { $defaultProvider = reset($providerIds); } @@ -572,6 +580,14 @@ private function createAuthenticationListeners(ContainerBuilder $container, stri $listeners[] = new Reference($listenerId); $authenticationProviders[] = $provider; } + + if ($factory instanceof FirewallListenerFactoryInterface) { + $firewallListenerIds = $factory->createListeners($container, $id, $firewall[$key]); + foreach ($firewallListenerIds as $firewallListenerId) { + $listeners[] = new Reference($firewallListenerId); + } + } + $hasListeners = true; } } @@ -623,7 +639,7 @@ private function getUserProvider(ContainerBuilder $container, string $id, array return $userProvider; } - if ('remember_me' === $factoryKey || 'anonymous' === $factoryKey) { + if ('remember_me' === $factoryKey || 'anonymous' === $factoryKey || 'custom_authenticators' === $factoryKey) { return 'security.user_providers'; } diff --git a/src/Symfony/Bundle/SecurityBundle/LoginLink/FirewallAwareLoginLinkHandler.php b/src/Symfony/Bundle/SecurityBundle/LoginLink/FirewallAwareLoginLinkHandler.php new file mode 100644 index 0000000000000..eaa52058f23db --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/LoginLink/FirewallAwareLoginLinkHandler.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\LoginLink; + +use Psr\Container\ContainerInterface; +use Symfony\Bundle\SecurityBundle\Security\FirewallMap; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Http\LoginLink\LoginLinkDetails; +use Symfony\Component\Security\Http\LoginLink\LoginLinkHandlerInterface; + +/** + * Decorates the login link handler for the current firewall. + * + * @author Ryan Weaver + */ +class FirewallAwareLoginLinkHandler implements LoginLinkHandlerInterface +{ + private $firewallMap; + private $loginLinkHandlerLocator; + private $requestStack; + + public function __construct(FirewallMap $firewallMap, ContainerInterface $loginLinkHandlerLocator, RequestStack $requestStack) + { + $this->firewallMap = $firewallMap; + $this->loginLinkHandlerLocator = $loginLinkHandlerLocator; + $this->requestStack = $requestStack; + } + + public function createLoginLink(UserInterface $user): LoginLinkDetails + { + return $this->getLoginLinkHandler()->createLoginLink($user); + } + + public function consumeLoginLink(Request $request): UserInterface + { + return $this->getLoginLinkHandler()->consumeLoginLink($request); + } + + private function getLoginLinkHandler(): LoginLinkHandlerInterface + { + if (null === $request = $this->requestStack->getCurrentRequest()) { + throw new \LogicException('Cannot determine the correct LoginLinkHandler to use: there is no active Request and so, the firewall cannot be determined. Try using the specific login link handler service.'); + } + + $firewallName = $this->firewallMap->getFirewallConfig($request)->getName(); + if (!$this->loginLinkHandlerLocator->has($firewallName)) { + throw new \InvalidArgumentException(sprintf('No login link handler found. Did you add a login_link key under your "%s" firewall?', $firewallName)); + } + + return $this->loginLinkHandlerLocator->get($firewallName); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/collectors.php b/src/Symfony/Bundle/SecurityBundle/Resources/config/collectors.php new file mode 100644 index 0000000000000..3619779311ae2 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/collectors.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Bundle\SecurityBundle\DataCollector\SecurityDataCollector; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('data_collector.security', SecurityDataCollector::class) + ->args([ + service('security.untracked_token_storage'), + service('security.role_hierarchy'), + service('security.logout_url_generator'), + service('security.access.decision_manager'), + service('security.firewall.map'), + service('debug.security.firewall')->nullOnInvalid(), + ]) + ->tag('data_collector', [ + 'template' => '@Security/Collector/security.html.twig', + 'id' => 'security', + 'priority' => 270, + ]) + ; +}; diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/collectors.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/collectors.xml deleted file mode 100644 index 811c6dfc5cfdc..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/collectors.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/console.php b/src/Symfony/Bundle/SecurityBundle/Resources/config/console.php new file mode 100644 index 0000000000000..a5ea6868a8bb6 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/console.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Bundle\SecurityBundle\Command\UserPasswordEncoderCommand; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('security.command.user_password_encoder', UserPasswordEncoderCommand::class) + ->args([ + service('security.encoder_factory'), + abstract_arg('encoders user classes'), + ]) + ->tag('console.command', ['command' => 'security:encode-password']) + ; +}; diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/console.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/console.xml deleted file mode 100644 index 2e28eda8890fa..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/console.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/guard.php b/src/Symfony/Bundle/SecurityBundle/Resources/config/guard.php new file mode 100644 index 0000000000000..f8b79cb3569d2 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/guard.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\Security\Guard\Firewall\GuardAuthenticationListener; +use Symfony\Component\Security\Guard\GuardAuthenticatorHandler; +use Symfony\Component\Security\Guard\Provider\GuardAuthenticationProvider; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('security.authentication.guard_handler', GuardAuthenticatorHandler::class) + ->args([ + service('security.token_storage'), + service('event_dispatcher')->nullOnInvalid(), + abstract_arg('stateless firewall keys'), + ]) + ->call('setSessionAuthenticationStrategy', [service('security.authentication.session_strategy')]) + + ->alias(GuardAuthenticatorHandler::class, 'security.authentication.guard_handler') + + ->set('security.authentication.provider.guard', GuardAuthenticationProvider::class) + ->abstract() + ->args([ + abstract_arg('Authenticators'), + abstract_arg('User Provider'), + abstract_arg('Provider-shared Key'), + abstract_arg('User Checker'), + service('security.password_encoder'), + ]) + + ->set('security.authentication.listener.guard', GuardAuthenticationListener::class) + ->abstract() + ->args([ + service('security.authentication.guard_handler'), + service('security.authentication.manager'), + abstract_arg('Provider-shared Key'), + abstract_arg('Authenticators'), + service('logger')->nullOnInvalid(), + ]) + ->tag('monolog.logger', ['channel' => 'security']) + ; +}; diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/guard.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/guard.xml deleted file mode 100644 index c9bb06d179874..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/guard.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.php b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.php new file mode 100644 index 0000000000000..1a9fceb0af3eb --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.php @@ -0,0 +1,286 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Bundle\SecurityBundle\CacheWarmer\ExpressionCacheWarmer; +use Symfony\Bundle\SecurityBundle\EventListener\FirewallEventBubblingListener; +use Symfony\Bundle\SecurityBundle\EventListener\FirewallListener; +use Symfony\Bundle\SecurityBundle\Security\FirewallConfig; +use Symfony\Bundle\SecurityBundle\Security\FirewallContext; +use Symfony\Bundle\SecurityBundle\Security\FirewallMap; +use Symfony\Bundle\SecurityBundle\Security\LazyFirewallContext; +use Symfony\Component\Ldap\Security\LdapUserProvider; +use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolver; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\Authentication\Token\Storage\UsageTrackingTokenStorage; +use Symfony\Component\Security\Core\Authorization\AccessDecisionManager; +use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; +use Symfony\Component\Security\Core\Authorization\AuthorizationChecker; +use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; +use Symfony\Component\Security\Core\Authorization\ExpressionLanguage; +use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter; +use Symfony\Component\Security\Core\Authorization\Voter\ExpressionVoter; +use Symfony\Component\Security\Core\Authorization\Voter\RoleHierarchyVoter; +use Symfony\Component\Security\Core\Authorization\Voter\RoleVoter; +use Symfony\Component\Security\Core\Encoder\EncoderFactory; +use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; +use Symfony\Component\Security\Core\Encoder\UserPasswordEncoder; +use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; +use Symfony\Component\Security\Core\Role\RoleHierarchy; +use Symfony\Component\Security\Core\Role\RoleHierarchyInterface; +use Symfony\Component\Security\Core\Security; +use Symfony\Component\Security\Core\User\ChainUserProvider; +use Symfony\Component\Security\Core\User\InMemoryUserProvider; +use Symfony\Component\Security\Core\User\MissingUserProvider; +use Symfony\Component\Security\Core\User\UserChecker; +use Symfony\Component\Security\Core\Validator\Constraints\UserPasswordValidator; +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\HttpUtils; +use Symfony\Component\Security\Http\Impersonate\ImpersonateUrlGenerator; +use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator; +use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategy; +use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface; + +return static function (ContainerConfigurator $container) { + $container->parameters() + ->set('security.role_hierarchy.roles', []) + ; + + $container->services() + ->set('security.authorization_checker', AuthorizationChecker::class) + ->public() + ->args([ + service('security.token_storage'), + service('security.authentication.manager'), + service('security.access.decision_manager'), + param('security.access.always_authenticate_before_granting'), + ]) + ->alias(AuthorizationCheckerInterface::class, 'security.authorization_checker') + + ->set('security.token_storage', UsageTrackingTokenStorage::class) + ->public() + ->args([ + service('security.untracked_token_storage'), + service_locator([ + 'session' => service('session'), + ]), + ]) + ->tag('kernel.reset', ['method' => 'disableUsageTracking']) + ->tag('kernel.reset', ['method' => 'setToken']) + ->alias(TokenStorageInterface::class, 'security.token_storage') + + ->set('security.untracked_token_storage', TokenStorage::class) + + ->set('security.helper', Security::class) + ->args([service_locator([ + 'security.token_storage' => service('security.token_storage'), + 'security.authorization_checker' => service('security.authorization_checker'), + ])]) + ->alias(Security::class, 'security.helper') + + ->set('security.user_value_resolver', UserValueResolver::class) + ->args([ + service('security.token_storage'), + ]) + ->tag('controller.argument_value_resolver', ['priority' => 40]) + + // Authentication related services + ->set('security.authentication.trust_resolver', AuthenticationTrustResolver::class) + + ->set('security.authentication.session_strategy', SessionAuthenticationStrategy::class) + ->args([param('security.authentication.session_strategy.strategy')]) + ->alias(SessionAuthenticationStrategyInterface::class, 'security.authentication.session_strategy') + + ->set('security.authentication.session_strategy_noop', SessionAuthenticationStrategy::class) + ->args(['none']) + + ->set('security.encoder_factory.generic', EncoderFactory::class) + ->args([ + [], + ]) + ->alias('security.encoder_factory', 'security.encoder_factory.generic') + ->alias(EncoderFactoryInterface::class, 'security.encoder_factory') + + ->set('security.user_password_encoder.generic', UserPasswordEncoder::class) + ->args([service('security.encoder_factory')]) + ->alias('security.password_encoder', 'security.user_password_encoder.generic')->public() + ->alias(UserPasswordEncoderInterface::class, 'security.password_encoder') + + ->set('security.user_checker', UserChecker::class) + + ->set('security.expression_language', ExpressionLanguage::class) + ->args([service('cache.security_expression_language')->nullOnInvalid()]) + + ->set('security.authentication_utils', AuthenticationUtils::class) + ->args([service('request_stack')]) + ->alias(AuthenticationUtils::class, 'security.authentication_utils') + + ->set('security.event_dispatcher.event_bubbling_listener', FirewallEventBubblingListener::class) + ->abstract() + ->args([service('event_dispatcher')]) + + // Authorization related services + ->set('security.access.decision_manager', AccessDecisionManager::class) + ->args([[]]) + ->alias(AccessDecisionManagerInterface::class, 'security.access.decision_manager') + + ->set('security.role_hierarchy', RoleHierarchy::class) + ->args([param('security.role_hierarchy.roles')]) + ->alias(RoleHierarchyInterface::class, 'security.role_hierarchy') + + // Security Voters + ->set('security.access.simple_role_voter', RoleVoter::class) + ->tag('security.voter', ['priority' => 245]) + + ->set('security.access.authenticated_voter', AuthenticatedVoter::class) + ->args([service('security.authentication.trust_resolver')]) + ->tag('security.voter', ['priority' => 250]) + + ->set('security.access.role_hierarchy_voter', RoleHierarchyVoter::class) + ->args([service('security.role_hierarchy')]) + ->tag('security.voter', ['priority' => 245]) + + ->set('security.access.expression_voter', ExpressionVoter::class) + ->args([ + service('security.expression_language'), + service('security.authentication.trust_resolver'), + service('security.authorization_checker'), + service('security.role_hierarchy')->nullOnInvalid(), + ]) + ->tag('security.voter', ['priority' => 245]) + + ->set('security.impersonate_url_generator', ImpersonateUrlGenerator::class) + ->args([ + service('request_stack'), + service('security.firewall.map'), + service('security.token_storage'), + ]) + + // Firewall related services + ->set('security.firewall', FirewallListener::class) + ->args([ + service('security.firewall.map'), + service('event_dispatcher'), + service('security.logout_url_generator'), + ]) + ->tag('kernel.event_subscriber') + ->alias(Firewall::class, 'security.firewall') + + ->set('security.firewall.map', FirewallMap::class) + ->args([ + abstract_arg('Firewall context locator'), + abstract_arg('Request matchers'), + ]) + + ->set('security.firewall.context', FirewallContext::class) + ->abstract() + ->args([ + [], + service('security.exception_listener'), + abstract_arg('LogoutListener'), + abstract_arg('FirewallConfig'), + ]) + + ->set('security.firewall.lazy_context', LazyFirewallContext::class) + ->abstract() + ->args([ + [], + service('security.exception_listener'), + abstract_arg('LogoutListener'), + abstract_arg('FirewallConfig'), + service('security.untracked_token_storage'), + ]) + + ->set('security.firewall.config', FirewallConfig::class) + ->abstract() + ->args([ + abstract_arg('name'), + abstract_arg('user_checker'), + abstract_arg('request_matcher'), + false, // security enabled + false, // stateless + null, + null, + null, + null, + null, + [], // listeners + null, // switch_user + ]) + + ->set('security.logout_url_generator', LogoutUrlGenerator::class) + ->args([ + service('request_stack')->nullOnInvalid(), + service('router')->nullOnInvalid(), + service('security.token_storage')->nullOnInvalid(), + ]) + + // Provisioning + ->set('security.user.provider.missing', MissingUserProvider::class) + ->abstract() + ->args([ + abstract_arg('firewall'), + ]) + + ->set('security.user.provider.in_memory', InMemoryUserProvider::class) + ->abstract() + + ->set('security.user.provider.ldap', LdapUserProvider::class) + ->abstract() + ->args([ + abstract_arg('security.ldap.ldap'), + abstract_arg('base dn'), + abstract_arg('search dn'), + abstract_arg('search password'), + abstract_arg('default_roles'), + abstract_arg('uid key'), + abstract_arg('filter'), + abstract_arg('password_attribute'), + abstract_arg('extra_fields (email etc)'), + ]) + + ->set('security.user.provider.chain', ChainUserProvider::class) + ->abstract() + + ->set('security.http_utils', HttpUtils::class) + ->args([ + service('router')->nullOnInvalid(), + service('router')->nullOnInvalid(), + ]) + ->alias(HttpUtils::class, 'security.http_utils') + + // Validator + ->set('security.validator.user_password', UserPasswordValidator::class) + ->args([ + service('security.token_storage'), + service('security.encoder_factory'), + ]) + ->tag('validator.constraint_validator', ['alias' => 'security.validator.user_password']) + + // Cache + ->set('cache.security_expression_language') + ->parent('cache.system') + ->private() + ->tag('cache.pool') + + // Cache Warmers + ->set('security.cache_warmer.expression', ExpressionCacheWarmer::class) + ->args([ + [], + service('security.expression_language'), + ]) + ->tag('kernel.cache_warmer') + ; +}; diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml deleted file mode 100644 index 44e598bd1d0da..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml +++ /dev/null @@ -1,220 +0,0 @@ - - - - - - - - - - - - - - - - %security.access.always_authenticate_before_granting% - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - %security.authentication.session_strategy.strategy% - - - - - none - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - %security.role_hierarchy.roles% - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - false - false - - - - - - - null - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.php b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.php new file mode 100644 index 0000000000000..3d0c6ddcb4f9e --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.php @@ -0,0 +1,189 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Bundle\SecurityBundle\Security\UserAuthenticator; +use Symfony\Component\DependencyInjection\ServiceLocator; +use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; +use Symfony\Component\Security\Http\Authentication\AuthenticatorManager; +use Symfony\Component\Security\Http\Authentication\NoopAuthenticationManager; +use Symfony\Component\Security\Http\Authentication\UserAuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\FormLoginAuthenticator; +use Symfony\Component\Security\Http\Authenticator\HttpBasicAuthenticator; +use Symfony\Component\Security\Http\Authenticator\JsonLoginAuthenticator; +use Symfony\Component\Security\Http\Authenticator\RememberMeAuthenticator; +use Symfony\Component\Security\Http\Authenticator\RemoteUserAuthenticator; +use Symfony\Component\Security\Http\Authenticator\X509Authenticator; +use Symfony\Component\Security\Http\Event\CheckPassportEvent; +use Symfony\Component\Security\Http\EventListener\CheckCredentialsListener; +use Symfony\Component\Security\Http\EventListener\LoginThrottlingListener; +use Symfony\Component\Security\Http\EventListener\PasswordMigratingListener; +use Symfony\Component\Security\Http\EventListener\RememberMeListener; +use Symfony\Component\Security\Http\EventListener\SessionStrategyListener; +use Symfony\Component\Security\Http\EventListener\UserCheckerListener; +use Symfony\Component\Security\Http\EventListener\UserProviderListener; +use Symfony\Component\Security\Http\Firewall\AuthenticatorManagerListener; + +return static function (ContainerConfigurator $container) { + $container->services() + + // Manager + ->set('security.authenticator.manager', AuthenticatorManager::class) + ->abstract() + ->args([ + abstract_arg('authenticators'), + service('security.token_storage'), + service('event_dispatcher'), + abstract_arg('provider key'), + service('logger')->nullOnInvalid(), + param('security.authentication.manager.erase_credentials'), + ]) + ->tag('monolog.logger', ['channel' => 'security']) + + ->set('security.authenticator.managers_locator', ServiceLocator::class) + ->args([[]]) + + ->set('security.user_authenticator', UserAuthenticator::class) + ->args([ + service('security.firewall.map'), + service('security.authenticator.managers_locator'), + service('request_stack'), + ]) + ->alias(UserAuthenticatorInterface::class, 'security.user_authenticator') + + ->set('security.authentication.manager', NoopAuthenticationManager::class) + ->alias(AuthenticationManagerInterface::class, 'security.authentication.manager') + + ->set('security.firewall.authenticator', AuthenticatorManagerListener::class) + ->abstract() + ->args([ + abstract_arg('authenticator manager'), + ]) + + // Listeners + ->set('security.listener.check_authenticator_credentials', CheckCredentialsListener::class) + ->args([ + service('security.encoder_factory'), + ]) + ->tag('kernel.event_subscriber') + + ->set('security.listener.user_provider', UserProviderListener::class) + ->args([ + service('security.user_providers'), + ]) + ->tag('kernel.event_listener', ['event' => CheckPassportEvent::class, 'priority' => 1024, 'method' => 'checkPassport']) + + ->set('security.listener.user_provider.abstract', UserProviderListener::class) + ->abstract() + ->args([ + abstract_arg('user provider'), + ]) + + ->set('security.listener.password_migrating', PasswordMigratingListener::class) + ->args([ + service('security.encoder_factory'), + ]) + ->tag('kernel.event_subscriber') + + ->set('security.listener.user_checker', UserCheckerListener::class) + ->abstract() + ->args([ + abstract_arg('user checker'), + ]) + + ->set('security.listener.session', SessionStrategyListener::class) + ->abstract() + ->args([ + service('security.authentication.session_strategy'), + ]) + + ->set('security.listener.remember_me', RememberMeListener::class) + ->abstract() + ->args([ + abstract_arg('remember me services'), + service('logger')->nullOnInvalid(), + ]) + ->tag('monolog.logger', ['channel' => 'security']) + + ->set('security.listener.login_throttling', LoginThrottlingListener::class) + ->abstract() + ->args([ + service('request_stack'), + abstract_arg('request rate limiter'), + ]) + + // Authenticators + ->set('security.authenticator.http_basic', HttpBasicAuthenticator::class) + ->abstract() + ->args([ + abstract_arg('realm name'), + abstract_arg('user provider'), + service('logger')->nullOnInvalid(), + ]) + ->tag('monolog.logger', ['channel' => 'security']) + + ->set('security.authenticator.form_login', FormLoginAuthenticator::class) + ->abstract() + ->args([ + service('security.http_utils'), + abstract_arg('user provider'), + abstract_arg('authentication success handler'), + abstract_arg('authentication failure handler'), + abstract_arg('options'), + ]) + + ->set('security.authenticator.json_login', JsonLoginAuthenticator::class) + ->abstract() + ->args([ + service('security.http_utils'), + abstract_arg('user provider'), + abstract_arg('authentication success handler'), + abstract_arg('authentication failure handler'), + abstract_arg('options'), + service('property_accessor')->nullOnInvalid(), + ]) + ->call('setTranslator', [service('translator')->ignoreOnInvalid()]) + + ->set('security.authenticator.remember_me', RememberMeAuthenticator::class) + ->abstract() + ->args([ + abstract_arg('remember me services'), + param('kernel.secret'), + service('security.token_storage'), + abstract_arg('options'), + service('security.authentication.session_strategy'), + ]) + + ->set('security.authenticator.x509', X509Authenticator::class) + ->abstract() + ->args([ + abstract_arg('user provider'), + service('security.token_storage'), + abstract_arg('firewall name'), + abstract_arg('user key'), + abstract_arg('credentials key'), + service('logger')->nullOnInvalid(), + ]) + ->tag('monolog.logger', ['channel' => 'security']) + + ->set('security.authenticator.remote_user', RemoteUserAuthenticator::class) + ->abstract() + ->args([ + abstract_arg('user provider'), + service('security.token_storage'), + abstract_arg('firewall name'), + abstract_arg('user key'), + service('logger')->nullOnInvalid(), + ]) + ->tag('monolog.logger', ['channel' => 'security']) + ; +}; diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml deleted file mode 100644 index 98936b87ff431..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.xml +++ /dev/null @@ -1,140 +0,0 @@ - - - - - - - - - authenticators - - - provider key - - %security.authentication.manager.erase_credentials% - - - - - - - - - - - - - - - - - - authenticator manager - - - - - - - - - - - - - - - - user checker - - - - - - - - - remember me services - - - - - - - - realm name - user provider - - - - - - user provider - authentication success handler - authentication failure handler - options - - - - - user provider - authentication success handler - authentication failure handler - options - - - - - remember me services - %kernel.secret% - - options - - - - - - user provider - - firewall name - user key - credentials key - - - - - - user provider - - firewall name - user key - - - - diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator_login_link.php b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator_login_link.php new file mode 100644 index 0000000000000..2efb089262cfe --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator_login_link.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Bundle\SecurityBundle\LoginLink\FirewallAwareLoginLinkHandler; +use Symfony\Component\Security\Http\Authenticator\LoginLinkAuthenticator; +use Symfony\Component\Security\Http\LoginLink\ExpiredLoginLinkStorage; +use Symfony\Component\Security\Http\LoginLink\LoginLinkHandler; +use Symfony\Component\Security\Http\LoginLink\LoginLinkHandlerInterface; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('security.authenticator.login_link', LoginLinkAuthenticator::class) + ->abstract() + ->args([ + abstract_arg('the login link handler instance'), + service('security.http_utils'), + abstract_arg('authentication success handler'), + abstract_arg('authentication failure handler'), + abstract_arg('options'), + ]) + + ->set('security.authenticator.abstract_login_link_handler', LoginLinkHandler::class) + ->abstract() + ->args([ + service('router'), + abstract_arg('user provider'), + service('property_accessor'), + abstract_arg('signature properties'), + '%kernel.secret%', + abstract_arg('options'), + abstract_arg('expired login link storage'), + ]) + + ->set('security.authenticator.expired_login_link_storage', ExpiredLoginLinkStorage::class) + ->abstract() + ->args([ + abstract_arg('cache pool service'), + abstract_arg('expired login link storage'), + ]) + + ->set('security.authenticator.cache.expired_links') + ->parent('cache.app') + ->private() + ->tag('cache.pool') + + ->set('security.authenticator.firewall_aware_login_link_handler', FirewallAwareLoginLinkHandler::class) + ->args([ + service('security.firewall.map'), + tagged_locator('security.authenticator.login_linker', 'firewall'), + service('request_stack'), + ]) + ->alias(LoginLinkHandlerInterface::class, 'security.authenticator.firewall_aware_login_link_handler') + + ->set('security.authenticator.entity_login_link_user_handler', EntityLoginLinkUserHandler::class) + ->abstract() + ->args([ + service('doctrine'), + abstract_arg('user entity class name'), + ]) + + ; +}; diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_debug.php b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_debug.php new file mode 100644 index 0000000000000..dc668b15e9ded --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_debug.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\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Bundle\SecurityBundle\Debug\TraceableFirewallListener; +use Symfony\Bundle\SecurityBundle\EventListener\VoteListener; +use Symfony\Component\Security\Core\Authorization\TraceableAccessDecisionManager; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('debug.security.access.decision_manager', TraceableAccessDecisionManager::class) + ->decorate('security.access.decision_manager') + ->args([ + service('debug.security.access.decision_manager.inner'), + ]) + + ->set('debug.security.voter.vote_listener', VoteListener::class) + ->args([ + service('debug.security.access.decision_manager'), + ]) + ->tag('kernel.event_subscriber') + + ->set('debug.security.firewall', TraceableFirewallListener::class) + ->args([ + service('security.firewall.map'), + service('event_dispatcher'), + service('security.logout_url_generator'), + ]) + ->tag('kernel.event_subscriber') + ->alias('security.firewall', 'debug.security.firewall') + ; +}; diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_debug.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_debug.xml deleted file mode 100644 index e348cb8b7406a..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_debug.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_legacy.php b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_legacy.php new file mode 100644 index 0000000000000..1c3ee242cc0a7 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_legacy.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\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; +use Symfony\Component\Security\Core\Authentication\AuthenticationProviderManager; + +return static function (ContainerConfigurator $container) { + $container->services() + + // Authentication related services + ->set('security.authentication.manager', AuthenticationProviderManager::class) + ->args([ + abstract_arg('providers'), + param('security.authentication.manager.erase_credentials'), + ]) + ->call('setEventDispatcher', [service('event_dispatcher')]) + ->alias(AuthenticationManagerInterface::class, 'security.authentication.manager') + ; +}; diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_legacy.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_legacy.xml deleted file mode 100644 index 85d672a078dab..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_legacy.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - %security.authentication.manager.erase_credentials% - - - - - - - diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.php b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.php new file mode 100644 index 0000000000000..7683ea2484031 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.php @@ -0,0 +1,288 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\Security\Core\Authentication\Provider\AnonymousAuthenticationProvider; +use Symfony\Component\Security\Core\Authentication\Provider\DaoAuthenticationProvider; +use Symfony\Component\Security\Core\Authentication\Provider\LdapBindAuthenticationProvider; +use Symfony\Component\Security\Core\Authentication\Provider\PreAuthenticatedAuthenticationProvider; +use Symfony\Component\Security\Http\AccessMap; +use Symfony\Component\Security\Http\Authentication\CustomAuthenticationFailureHandler; +use Symfony\Component\Security\Http\Authentication\CustomAuthenticationSuccessHandler; +use Symfony\Component\Security\Http\Authentication\DefaultAuthenticationFailureHandler; +use Symfony\Component\Security\Http\Authentication\DefaultAuthenticationSuccessHandler; +use Symfony\Component\Security\Http\EntryPoint\BasicAuthenticationEntryPoint; +use Symfony\Component\Security\Http\EntryPoint\FormAuthenticationEntryPoint; +use Symfony\Component\Security\Http\EntryPoint\RetryAuthenticationEntryPoint; +use Symfony\Component\Security\Http\EventListener\CookieClearingLogoutListener; +use Symfony\Component\Security\Http\EventListener\DefaultLogoutListener; +use Symfony\Component\Security\Http\EventListener\SessionLogoutListener; +use Symfony\Component\Security\Http\Firewall\AccessListener; +use Symfony\Component\Security\Http\Firewall\AnonymousAuthenticationListener; +use Symfony\Component\Security\Http\Firewall\BasicAuthenticationListener; +use Symfony\Component\Security\Http\Firewall\ChannelListener; +use Symfony\Component\Security\Http\Firewall\ContextListener; +use Symfony\Component\Security\Http\Firewall\ExceptionListener; +use Symfony\Component\Security\Http\Firewall\LogoutListener; +use Symfony\Component\Security\Http\Firewall\RemoteUserAuthenticationListener; +use Symfony\Component\Security\Http\Firewall\SwitchUserListener; +use Symfony\Component\Security\Http\Firewall\UsernamePasswordFormAuthenticationListener; +use Symfony\Component\Security\Http\Firewall\UsernamePasswordJsonAuthenticationListener; +use Symfony\Component\Security\Http\Firewall\X509AuthenticationListener; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('security.authentication.listener.anonymous', AnonymousAuthenticationListener::class) + ->args([ + service('security.untracked_token_storage'), + abstract_arg('Key'), + service('logger')->nullOnInvalid(), + service('security.authentication.manager'), + ]) + ->tag('monolog.logger', ['channel' => 'security']) + + ->set('security.authentication.provider.anonymous', AnonymousAuthenticationProvider::class) + ->args([abstract_arg('Key')]) + + ->set('security.authentication.retry_entry_point', RetryAuthenticationEntryPoint::class) + ->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(), + ]) + ->tag('monolog.logger', ['channel' => 'security']) + + ->set('security.access_map', AccessMap::class) + + ->set('security.context_listener', ContextListener::class) + ->args([ + service('security.untracked_token_storage'), + [], + abstract_arg('Provider Key'), + service('logger')->nullOnInvalid(), + service('event_dispatcher')->nullOnInvalid(), + service('security.authentication.trust_resolver'), + ]) + ->tag('monolog.logger', ['channel' => 'security']) + + ->set('security.logout_listener', LogoutListener::class) + ->abstract() + ->args([ + service('security.token_storage'), + service('security.http_utils'), + abstract_arg('event dispatcher'), + [], // Options + ]) + + ->set('security.logout.listener.session', SessionLogoutListener::class) + ->abstract() + + ->set('security.logout.listener.cookie_clearing', CookieClearingLogoutListener::class) + ->abstract() + + ->set('security.logout.listener.default', DefaultLogoutListener::class) + ->abstract() + ->args([ + service('security.http_utils'), + abstract_arg('target url'), + ]) + + ->set('security.authentication.form_entry_point', FormAuthenticationEntryPoint::class) + ->abstract() + ->args([ + service('http_kernel'), + ]) + + ->set('security.authentication.listener.abstract') + ->abstract() + ->args([ + service('security.token_storage'), + service('security.authentication.manager'), + service('security.authentication.session_strategy'), + service('security.http_utils'), + abstract_arg('Provider-shared Key'), + service('security.authentication.success_handler'), + service('security.authentication.failure_handler'), + [], + service('logger')->nullOnInvalid(), + service('event_dispatcher')->nullOnInvalid(), + ]) + ->tag('monolog.logger', ['channel' => 'security']) + + ->set('security.authentication.custom_success_handler', CustomAuthenticationSuccessHandler::class) + ->abstract() + ->args([ + abstract_arg('The custom success handler service'), + [], // Options + abstract_arg('Provider-shared Key'), + ]) + + ->set('security.authentication.success_handler', DefaultAuthenticationSuccessHandler::class) + ->abstract() + ->args([ + service('security.http_utils'), + [], // Options + ]) + + ->set('security.authentication.custom_failure_handler', CustomAuthenticationFailureHandler::class) + ->abstract() + ->args([ + abstract_arg('The custom failure handler service'), + [], // Options + ]) + + ->set('security.authentication.failure_handler', DefaultAuthenticationFailureHandler::class) + ->abstract() + ->args([ + service('http_kernel'), + service('security.http_utils'), + [], // Options + service('logger')->nullOnInvalid(), + ]) + ->tag('monolog.logger', ['channel' => 'security']) + + ->set('security.authentication.listener.form', UsernamePasswordFormAuthenticationListener::class) + ->parent('security.authentication.listener.abstract') + ->abstract() + + ->set('security.authentication.listener.x509', X509AuthenticationListener::class) + ->abstract() + ->args([ + service('security.token_storage'), + service('security.authentication.manager'), + abstract_arg('Provider-shared Key'), + abstract_arg('x509 user'), + abstract_arg('x509 credentials'), + service('logger')->nullOnInvalid(), + service('event_dispatcher')->nullOnInvalid(), + ]) + ->tag('monolog.logger', ['channel' => 'security']) + + ->set('security.authentication.listener.json', UsernamePasswordJsonAuthenticationListener::class) + ->abstract() + ->args([ + service('security.token_storage'), + service('security.authentication.manager'), + service('security.http_utils'), + abstract_arg('Provider-shared Key'), + abstract_arg('Failure handler'), + abstract_arg('Success Handler'), + [], // Options + service('logger')->nullOnInvalid(), + service('event_dispatcher')->nullOnInvalid(), + service('property_accessor')->nullOnInvalid(), + ]) + ->call('setTranslator', [service('translator')->ignoreOnInvalid()]) + ->tag('monolog.logger', ['channel' => 'security']) + + ->set('security.authentication.listener.remote_user', RemoteUserAuthenticationListener::class) + ->abstract() + ->args([ + service('security.token_storage'), + service('security.authentication.manager'), + abstract_arg('Provider-shared Key'), + abstract_arg('REMOTE_USER server env var'), + service('logger')->nullOnInvalid(), + service('event_dispatcher')->nullOnInvalid(), + ]) + ->tag('monolog.logger', ['channel' => 'security']) + + ->set('security.authentication.listener.basic', BasicAuthenticationListener::class) + ->abstract() + ->args([ + service('security.token_storage'), + service('security.authentication.manager'), + abstract_arg('Provider-shared Key'), + abstract_arg('Entry Point'), + service('logger')->nullOnInvalid(), + ]) + ->tag('monolog.logger', ['channel' => 'security']) + + ->set('security.authentication.provider.dao', DaoAuthenticationProvider::class) + ->abstract() + ->args([ + abstract_arg('User Provider'), + abstract_arg('User Checker'), + abstract_arg('Provider-shared Key'), + service('security.encoder_factory'), + param('security.authentication.hide_user_not_found'), + ]) + + ->set('security.authentication.provider.ldap_bind', LdapBindAuthenticationProvider::class) + ->abstract() + ->args([ + abstract_arg('User Provider'), + abstract_arg('UserChecker'), + abstract_arg('Provider-shared Key'), + abstract_arg('LDAP'), + abstract_arg('Base DN'), + param('security.authentication.hide_user_not_found'), + abstract_arg('search dn'), + abstract_arg('search password'), + ]) + + ->set('security.authentication.provider.pre_authenticated', PreAuthenticatedAuthenticationProvider::class) + ->abstract() + ->args([ + abstract_arg('User Provider'), + abstract_arg('UserChecker'), + ]) + + ->set('security.exception_listener', ExceptionListener::class) + ->abstract() + ->args([ + service('security.token_storage'), + service('security.authentication.trust_resolver'), + service('security.http_utils'), + abstract_arg('Provider-shared Key'), + service('security.authentication.entry_point')->nullOnInvalid(), + param('security.access.denied_url'), + service('security.access.denied_handler')->nullOnInvalid(), + service('logger')->nullOnInvalid(), + false, // Stateless + ]) + ->tag('monolog.logger', ['channel' => 'security']) + + ->set('security.authentication.switchuser_listener', SwitchUserListener::class) + ->abstract() + ->args([ + service('security.token_storage'), + abstract_arg('User Provider'), + abstract_arg('User Checker'), + abstract_arg('Provider Key'), + service('security.access.decision_manager'), + service('logger')->nullOnInvalid(), + '_switch_user', + 'ROLE_ALLOWED_TO_SWITCH', + service('event_dispatcher')->nullOnInvalid(), + false, // Stateless + ]) + ->tag('monolog.logger', ['channel' => 'security']) + + ->set('security.access_listener', AccessListener::class) + ->args([ + service('security.token_storage'), + service('security.access.decision_manager'), + service('security.access_map'), + service('security.authentication.manager'), + ]) + ->tag('monolog.logger', ['channel' => 'security']) + ; +}; diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml deleted file mode 100644 index c8e5d9d5a093f..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml +++ /dev/null @@ -1,215 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - / - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - %security.authentication.hide_user_not_found% - - - - - - - - - %security.authentication.hide_user_not_found% - - - - - - - - - - - - - - - - - %security.access.denied_url% - - - false - - - - - - - - - - - _switch_user - ROLE_ALLOWED_TO_SWITCH - - false - - - - - - - - - - - diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_rememberme.php b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_rememberme.php new file mode 100644 index 0000000000000..e1b279d09a9b6 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_rememberme.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\Security\Core\Authentication\Provider\RememberMeAuthenticationProvider; +use Symfony\Component\Security\Core\Authentication\RememberMe\InMemoryTokenProvider; +use Symfony\Component\Security\Http\Firewall\RememberMeListener; +use Symfony\Component\Security\Http\RememberMe\PersistentTokenBasedRememberMeServices; +use Symfony\Component\Security\Http\RememberMe\ResponseListener; +use Symfony\Component\Security\Http\RememberMe\TokenBasedRememberMeServices; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('security.authentication.listener.rememberme', RememberMeListener::class) + ->abstract() + ->args([ + service('security.untracked_token_storage'), + service('security.authentication.rememberme'), + service('security.authentication.manager'), + service('logger')->nullOnInvalid(), + service('event_dispatcher')->nullOnInvalid(), + abstract_arg('Catch exception flag set in RememberMeFactory'), + service('security.authentication.session_strategy'), + ]) + ->tag('monolog.logger', ['channel' => 'security']) + + ->set('security.authentication.provider.rememberme', RememberMeAuthenticationProvider::class) + ->abstract() + ->args([abstract_arg('User Checker')]) + + ->set('security.rememberme.token.provider.in_memory', InMemoryTokenProvider::class) + + ->set('security.authentication.rememberme.services.abstract') + ->abstract() + ->args([ + [], // User Providers + abstract_arg('Shared Token Key'), + abstract_arg('Shared Provider Key'), + [], // Options + service('logger')->nullOnInvalid(), + ]) + ->tag('monolog.logger', ['channel' => 'security']) + + ->set('security.authentication.rememberme.services.persistent', PersistentTokenBasedRememberMeServices::class) + ->parent('security.authentication.rememberme.services.abstract') + ->abstract() + + ->set('security.authentication.rememberme.services.simplehash', TokenBasedRememberMeServices::class) + ->parent('security.authentication.rememberme.services.abstract') + ->abstract() + + ->set('security.rememberme.response_listener', ResponseListener::class) + ->tag('kernel.event_subscriber') + ; +}; diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_rememberme.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_rememberme.xml deleted file mode 100644 index 94aa3a3824ba4..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_rememberme.xml +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/templating_twig.php b/src/Symfony/Bundle/SecurityBundle/Resources/config/templating_twig.php new file mode 100644 index 0000000000000..05a74d086e820 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/templating_twig.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Bridge\Twig\Extension\LogoutUrlExtension; +use Symfony\Bridge\Twig\Extension\SecurityExtension; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('twig.extension.logout_url', LogoutUrlExtension::class) + ->args([ + service('security.logout_url_generator'), + ]) + ->tag('twig.extension') + + ->set('twig.extension.security', SecurityExtension::class) + ->args([ + service('security.authorization_checker')->ignoreOnInvalid(), + service('security.impersonate_url_generator')->ignoreOnInvalid(), + ]) + ->tag('twig.extension') + ; +}; diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/templating_twig.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/templating_twig.xml deleted file mode 100644 index c07547fa17902..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/templating_twig.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php b/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php index 9388ec3331f14..27ee29df549b9 100644 --- a/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php +++ b/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php @@ -18,6 +18,7 @@ use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterGlobalSecurityEventListenersPass; use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterLdapLocatorPass; use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterTokenUsageTrackingPass; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\SortFirewallListenersPass; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AnonymousFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\CustomAuthenticatorFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FormLoginFactory; @@ -27,6 +28,8 @@ use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\HttpBasicLdapFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\JsonLoginFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\JsonLoginLdapFactory; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\LoginLinkFactory; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\LoginThrottlingFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\RememberMeFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\RemoteUserFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\X509Factory; @@ -37,10 +40,6 @@ use Symfony\Component\EventDispatcher\DependencyInjection\AddEventAliasesPass; use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\Security\Core\AuthenticationEvents; -use Symfony\Component\Security\Core\Event\AuthenticationFailureEvent; -use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent; -use Symfony\Component\Security\Http\Event\InteractiveLoginEvent; -use Symfony\Component\Security\Http\Event\SwitchUserEvent; use Symfony\Component\Security\Http\SecurityEvents; /** @@ -67,6 +66,8 @@ public function build(ContainerBuilder $container) $extension->addSecurityListenerFactory(new GuardAuthenticationFactory()); $extension->addSecurityListenerFactory(new AnonymousFactory()); $extension->addSecurityListenerFactory(new CustomAuthenticatorFactory()); + $extension->addSecurityListenerFactory(new LoginThrottlingFactory()); + $extension->addSecurityListenerFactory(new LoginLinkFactory()); $extension->addUserProviderFactory(new InMemoryFactory()); $extension->addUserProviderFactory(new LdapFactory()); @@ -78,12 +79,12 @@ public function build(ContainerBuilder $container) $container->addCompilerPass(new RegisterLdapLocatorPass()); // must be registered after RegisterListenersPass (in the FrameworkBundle) $container->addCompilerPass(new RegisterGlobalSecurityEventListenersPass(), PassConfig::TYPE_BEFORE_REMOVING, -200); + // execute after ResolveChildDefinitionsPass optimization pass, to ensure class names are set + $container->addCompilerPass(new SortFirewallListenersPass(), PassConfig::TYPE_BEFORE_REMOVING); - $container->addCompilerPass(new AddEventAliasesPass([ - AuthenticationSuccessEvent::class => AuthenticationEvents::AUTHENTICATION_SUCCESS, - AuthenticationFailureEvent::class => AuthenticationEvents::AUTHENTICATION_FAILURE, - InteractiveLoginEvent::class => SecurityEvents::INTERACTIVE_LOGIN, - SwitchUserEvent::class => SecurityEvents::SWITCH_USER, - ])); + $container->addCompilerPass(new AddEventAliasesPass(array_merge( + AuthenticationEvents::ALIASES, + SecurityEvents::ALIASES + ))); } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/AddSessionDomainConstraintPassTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/AddSessionDomainConstraintPassTest.php index 66f942273204f..cfa6b5e0282b5 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/AddSessionDomainConstraintPassTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/AddSessionDomainConstraintPassTest.php @@ -125,6 +125,7 @@ private function createContainer($sessionStorageOptions) $container = new ContainerBuilder(); $container->setParameter('kernel.bundles_metadata', []); $container->setParameter('kernel.cache_dir', __DIR__); + $container->setParameter('kernel.build_dir', __DIR__); $container->setParameter('kernel.charset', 'UTF-8'); $container->setParameter('kernel.container_class', 'cc'); $container->setParameter('kernel.debug', true); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/SortFirewallListenersPassTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/SortFirewallListenersPassTest.php new file mode 100644 index 0000000000000..32f11f45cb58d --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/SortFirewallListenersPassTest.php @@ -0,0 +1,82 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\DependencyInjection\Compiler; + +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\SortFirewallListenersPass; +use Symfony\Bundle\SecurityBundle\Security\FirewallContext; +use Symfony\Component\DependencyInjection\Argument\IteratorArgument; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\Security\Http\Firewall\FirewallListenerInterface; + +class SortFirewallListenersPassTest extends TestCase +{ + public function testSortFirewallListeners() + { + $container = new ContainerBuilder(); + $container->setParameter('security.firewalls', ['main']); + + $container->register('listener_priority_minus1', FirewallListenerPriorityMinus1::class); + $container->register('listener_priority_1', FirewallListenerPriority1::class); + $container->register('listener_priority_2', FirewallListenerPriority2::class); + $container->register('listener_interface_not_implemented', \stdClass::class); + + $firewallContext = $container->register('security.firewall.map.context.main', FirewallContext::class); + $firewallContext->addTag('security.firewall_map_context'); + + $listeners = new IteratorArgument([ + new Reference('listener_priority_minus1'), + new Reference('listener_priority_1'), + new Reference('listener_priority_2'), + new Reference('listener_interface_not_implemented'), + ]); + + $firewallContext->setArgument(0, $listeners); + + $compilerPass = new SortFirewallListenersPass(); + $compilerPass->process($container); + + $sortedListeners = $firewallContext->getArgument(0); + $expectedSortedlisteners = [ + new Reference('listener_priority_2'), + new Reference('listener_priority_1'), + new Reference('listener_interface_not_implemented'), + new Reference('listener_priority_minus1'), + ]; + $this->assertEquals($expectedSortedlisteners, $sortedListeners->getValues()); + } +} + +class FirewallListenerPriorityMinus1 implements FirewallListenerInterface +{ + public static function getPriority(): int + { + return -1; + } +} + +class FirewallListenerPriority1 implements FirewallListenerInterface +{ + public static function getPriority(): int + { + return 1; + } +} + +class FirewallListenerPriority2 implements FirewallListenerInterface +{ + public static function getPriority(): int + { + return 2; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/AbstractFactoryTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/AbstractFactoryTest.php index 01e03b0312bd9..364a9249224cb 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/AbstractFactoryTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/AbstractFactoryTest.php @@ -110,7 +110,7 @@ public function testDefaultSuccessHandler($serviceId, $defaultHandlerInjection) if ($defaultHandlerInjection) { $this->assertEquals('setOptions', $methodCalls[0][0]); $this->assertEquals(['default_target_path' => '/bar'], $methodCalls[0][1][0]); - $this->assertEquals('setProviderKey', $methodCalls[1][0]); + $this->assertEquals('setFirewallName', $methodCalls[1][0]); $this->assertEquals(['foo'], $methodCalls[1][1]); } else { $this->assertCount(0, $methodCalls); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/LoginLinkFactoryTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/LoginLinkFactoryTest.php new file mode 100644 index 0000000000000..8f9450a97d25c --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/LoginLinkFactoryTest.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\DependencyInjection\Security\Factory; + +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\LoginLinkFactory; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +class LoginLinkFactoryTest extends TestCase +{ + public function testBasicServiceConfiguration() + { + $container = new ContainerBuilder(); + + $config = [ + 'check_route' => 'app_check_login_link', + 'lifetime' => 500, + 'signature_properties' => ['email', 'password'], + 'success_handler' => 'success_handler_service_id', + 'failure_handler' => 'failure_handler_service_id', + ]; + + $factory = new LoginLinkFactory(); + $finalizedConfig = $this->processConfig($config, $factory); + $factory->createAuthenticator($container, 'firewall1', $finalizedConfig, 'userprovider'); + + $this->assertTrue($container->hasDefinition('security.authenticator.login_link')); + $this->assertTrue($container->hasDefinition('security.authenticator.login_link_handler.firewall1')); + } + + private function processConfig(array $config, LoginLinkFactory $factory) + { + $nodeDefinition = new ArrayNodeDefinition('login-link'); + $factory->addConfiguration($nodeDefinition); + + $node = $nodeDefinition->getNode(); + $normalizedConfig = $node->normalize($config); + + return $node->finalize($normalizedConfig); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php index 6eb936c48bae6..616abed29e65b 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php @@ -13,13 +13,17 @@ use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\DependencyInjection\FrameworkExtension; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FirewallListenerFactoryInterface; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface; use Symfony\Bundle\SecurityBundle\DependencyInjection\SecurityExtension; use Symfony\Bundle\SecurityBundle\SecurityBundle; use Symfony\Bundle\SecurityBundle\Tests\DependencyInjection\Fixtures\UserProvider\DummyProvider; use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\FirewallEntryPointBundle\Security\EntryPointStub; use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\GuardedBundle\AppCustomAuthenticator; +use Symfony\Component\Config\Definition\Builder\NodeDefinition; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\DependencyInjection\Argument\IteratorArgument; +use Symfony\Component\DependencyInjection\Compiler\ResolveChildDefinitionsPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\ExpressionLanguage\Expression; @@ -635,6 +639,29 @@ public function provideUserCheckerConfig() yield [['user_checker' => TestUserChecker::class], TestUserChecker::class]; } + public function testConfigureCustomFirewallListener(): void + { + $container = $this->getRawContainer(); + /** @var SecurityExtension $extension */ + $extension = $container->getExtension('security'); + $extension->addSecurityListenerFactory(new TestFirewallListenerFactory()); + + $container->loadFromExtension('security', [ + 'firewalls' => [ + 'main' => [ + 'custom_listener' => true, + ], + ], + ]); + + $container->compile(); + + /** @var IteratorArgument $listenersIteratorArgument */ + $listenersIteratorArgument = $container->getDefinition('security.firewall.map.context.main')->getArgument(0); + $firewallListeners = array_map('strval', $listenersIteratorArgument->getValues()); + $this->assertContains('custom_firewall_listener_id', $firewallListeners); + } + protected function getRawContainer() { $container = new ContainerBuilder(); @@ -646,7 +673,7 @@ protected function getRawContainer() $bundle = new SecurityBundle(); $bundle->build($container); - $container->getCompilerPassConfig()->setOptimizationPasses([]); + $container->getCompilerPassConfig()->setOptimizationPasses([new ResolveChildDefinitionsPass()]); $container->getCompilerPassConfig()->setRemovingPasses([]); $container->getCompilerPassConfig()->setAfterRemovingPasses([]); @@ -734,3 +761,35 @@ public function checkPostAuth(UserInterface $user) { } } + +class TestFirewallListenerFactory implements SecurityFactoryInterface, FirewallListenerFactoryInterface +{ + public function createListeners(ContainerBuilder $container, string $firewallName, array $config): array + { + $container->register('custom_firewall_listener_id', \stdClass::class); + + return ['custom_firewall_listener_id']; + } + + public function create(ContainerBuilder $container, string $id, array $config, string $userProvider, ?string $defaultEntryPoint) + { + $container->register('provider_id', \stdClass::class); + $container->register('listener_id', \stdClass::class); + + return ['provider_id', 'listener_id', $defaultEntryPoint]; + } + + public function getPosition() + { + return 'form'; + } + + public function getKey() + { + return 'custom_listener'; + } + + public function addConfiguration(NodeDefinition $builder) + { + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AuthenticatorTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AuthenticatorTest.php new file mode 100644 index 0000000000000..201e446e04370 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AuthenticatorTest.php @@ -0,0 +1,52 @@ + + * + * 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; + +class AuthenticatorTest extends AbstractWebTestCase +{ + /** + * @dataProvider provideEmails + */ + public function testGlobalUserProvider($email) + { + $client = $this->createClient(['test_case' => 'Authenticator', 'root_config' => 'implicit_user_provider.yml']); + + $client->request('GET', '/profile', [], [], [ + 'HTTP_X-USER-EMAIL' => $email, + ]); + $this->assertJsonStringEqualsJsonString('{"email":"'.$email.'"}', $client->getResponse()->getContent()); + } + + /** + * @dataProvider provideEmails + */ + public function testFirewallUserProvider($email, $withinFirewall) + { + $client = $this->createClient(['test_case' => 'Authenticator', 'root_config' => 'firewall_user_provider.yml']); + + $client->request('GET', '/profile', [], [], [ + 'HTTP_X-USER-EMAIL' => $email, + ]); + + if ($withinFirewall) { + $this->assertJsonStringEqualsJsonString('{"email":"'.$email.'"}', $client->getResponse()->getContent()); + } else { + $this->assertJsonStringEqualsJsonString('{"error":"Username could not be found."}', $client->getResponse()->getContent()); + } + } + + public function provideEmails() + { + yield ['jane@example.org', true]; + yield ['john@example.org', false]; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AuthenticatorBundle/ApiAuthenticator.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AuthenticatorBundle/ApiAuthenticator.php new file mode 100644 index 0000000000000..6bff3145c9dd5 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AuthenticatorBundle/ApiAuthenticator.php @@ -0,0 +1,53 @@ + + * + * 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\AuthenticatorBundle; + +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\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Core\Exception\BadCredentialsException; +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\SelfValidatingPassport; + +class ApiAuthenticator extends AbstractAuthenticator +{ + public function supports(Request $request): ?bool + { + return $request->headers->has('X-USER-EMAIL'); + } + + public function authenticate(Request $request): PassportInterface + { + $email = $request->headers->get('X-USER-EMAIL'); + if (false === strpos($email, '@')) { + throw new BadCredentialsException('Email is not a valid email address.'); + } + + return new SelfValidatingPassport(new UserBadge($email)); + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response + { + return null; + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response + { + return new JsonResponse([ + 'error' => $exception->getMessageKey(), + ], JsonResponse::HTTP_FORBIDDEN); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AuthenticatorBundle/ProfileController.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AuthenticatorBundle/ProfileController.php new file mode 100644 index 0000000000000..3e23d86e37483 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AuthenticatorBundle/ProfileController.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle; + +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + +class ProfileController extends AbstractController +{ + public function __invoke() + { + $this->denyAccessUnlessGranted('ROLE_USER'); + + return $this->json(['email' => $this->getUser()->getUsername()]); + } +} 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 bafa0f68ce33d..f6f7aca9d5ec2 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 @@ -11,14 +11,21 @@ namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\CsrfFormLoginBundle\Controller; -use Symfony\Component\DependencyInjection\ContainerAwareInterface; -use Symfony\Component\DependencyInjection\ContainerAwareTrait; +use Psr\Container\ContainerInterface; +use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Exception\AccessDeniedException; +use Symfony\Contracts\Service\ServiceSubscriberInterface; +use Twig\Environment; -class LoginController implements ContainerAwareInterface +class LoginController implements ServiceSubscriberInterface { - use ContainerAwareTrait; + private $container; + + public function __construct(ContainerInterface $container) + { + $this->container = $container; + } public function loginAction() { @@ -43,4 +50,15 @@ public function secureAction() { throw new \Exception('Wrapper', 0, new \Exception('Another Wrapper', 0, new AccessDeniedException())); } + + /** + * {@inheritdoc} + */ + public static function getSubscribedServices() + { + return [ + 'form.factory' => FormFactoryInterface::class, + 'twig' => Environment::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 cf0e1150aff9a..5904183581517 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 @@ -11,15 +11,21 @@ namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\FormLoginBundle\Controller; -use Symfony\Component\DependencyInjection\ContainerAwareInterface; -use Symfony\Component\DependencyInjection\ContainerAwareTrait; +use Psr\Container\ContainerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Security; +use Symfony\Contracts\Service\ServiceSubscriberInterface; +use Twig\Environment; -class LocalizedController implements ContainerAwareInterface +class LocalizedController implements ServiceSubscriberInterface { - use ContainerAwareTrait; + private $container; + + public function __construct(ContainerInterface $container) + { + $this->container = $container; + } public function loginAction(Request $request) { @@ -61,4 +67,14 @@ public function homepageAction() { return (new Response('Homepage'))->setPublic(); } + + /** + * {@inheritdoc} + */ + public static function getSubscribedServices() + { + 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 60eef86718775..99183293fb1e8 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 @@ -11,17 +11,23 @@ namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\FormLoginBundle\Controller; -use Symfony\Component\DependencyInjection\ContainerAwareInterface; -use Symfony\Component\DependencyInjection\ContainerAwareTrait; +use Psr\Container\ContainerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Contracts\Service\ServiceSubscriberInterface; +use Twig\Environment; -class LoginController implements ContainerAwareInterface +class LoginController implements ServiceSubscriberInterface { - use ContainerAwareTrait; + private $container; + + public function __construct(ContainerInterface $container) + { + $this->container = $container; + } public function loginAction(Request $request, UserInterface $user = null) { @@ -53,4 +59,14 @@ public function secureAction() { throw new \Exception('Wrapper', 0, new \Exception('Another Wrapper', 0, new AccessDeniedException())); } + + /** + * {@inheritdoc} + */ + public static function getSubscribedServices() + { + return [ + 'twig' => Environment::class, + ]; + } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/FormLoginBundle.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/FormLoginBundle.php index eab1913ec184d..c330723adff15 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/FormLoginBundle.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/FormLoginBundle.php @@ -11,8 +11,28 @@ namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\FormLoginBundle; +use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\FormLoginBundle\Controller\LocalizedController; +use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\FormLoginBundle\Controller\LoginController; +use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; class FormLoginBundle extends Bundle { + /** + * {@inheritdoc} + */ + public function build(ContainerBuilder $container) + { + parent::build($container); + + $container + ->register(LoginController::class) + ->setPublic(true) + ->addTag('container.service_subscriber'); + + $container + ->register(LocalizedController::class) + ->setPublic(true) + ->addTag('container.service_subscriber'); + } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Resources/views/Login/login.html.twig b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Resources/views/Login/login.html.twig index 059f5f2bca1d2..34ea19f2bde62 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Resources/views/Login/login.html.twig +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Resources/views/Login/login.html.twig @@ -3,7 +3,8 @@ {% block body %} {% if error %} -
{{ error.message }}
+
{{ error.messageKey }}
+
{{ error.messageKey|replace(error.messageData) }}
{% endif %} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/LoginLink/TestCustomLoginLinkSuccessHandler.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/LoginLink/TestCustomLoginLinkSuccessHandler.php new file mode 100644 index 0000000000000..329a288584a77 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/LoginLink/TestCustomLoginLinkSuccessHandler.php @@ -0,0 +1,16 @@ + sprintf('Welcome %s!', $token->getUsername())]); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/CsrfFormLoginTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/CsrfFormLoginTest.php index f252314b0c4c1..0302c8a1e3943 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/CsrfFormLoginTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/CsrfFormLoginTest.php @@ -51,10 +51,6 @@ public function testFormLoginWithInvalidCsrfToken($options) $client = $this->createClient($options); $form = $client->request('GET', '/login')->selectButton('login')->form(); - if ($options['enable_authenticator_manager'] ?? false) { - $form['user_login[username]'] = 'johannes'; - $form['user_login[password]'] = 'test'; - } $form['user_login[_token]'] = ''; $client->submit($form); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/FormLoginTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/FormLoginTest.php index 45d74fc72261f..f49cfa84c07e1 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/FormLoginTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/FormLoginTest.php @@ -11,6 +11,8 @@ namespace Symfony\Bundle\SecurityBundle\Tests\Functional; +use Symfony\Component\Security\Http\EventListener\LoginThrottlingListener; + class FormLoginTest extends AbstractWebTestCase { /** @@ -106,6 +108,37 @@ public function testFormLoginRedirectsToProtectedResourceAfterLogin(array $optio $this->assertStringContainsString('You\'re browsing to path "/protected_resource".', $text); } + /** + * @dataProvider provideInvalidCredentials + */ + public function testLoginThrottling($username, $password) + { + if (!class_exists(LoginThrottlingListener::class)) { + $this->markTestSkipped('Login throttling requires symfony/security-http:^5.2'); + } + + $client = $this->createClient(['test_case' => 'StandardFormLogin', 'root_config' => 'login_throttling.yml', 'enable_authenticator_manager' => true]); + + $form = $client->request('GET', '/login')->selectButton('login')->form(); + $form['_username'] = $username; + $form['_password'] = $password; + $client->submit($form); + + $client->followRedirect()->selectButton('login')->form(); + $form['_username'] = $username; + $form['_password'] = $password; + $client->submit($form); + + $text = $client->followRedirect()->text(null, true); + $this->assertStringContainsString('Too many failed login attempts, please try again in 1 minute.', $text); + } + + public function provideInvalidCredentials() + { + yield 'invalid_password' => ['johannes', 'wrong']; + yield 'invalid_username' => ['wrong', 'wrong']; + } + public function provideClientOptions() { yield [['test_case' => 'StandardFormLogin', 'root_config' => 'config.yml', 'enable_authenticator_manager' => true]]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/LoginLinkAuthenticationTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/LoginLinkAuthenticationTest.php new file mode 100644 index 0000000000000..77049f33e3fda --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/LoginLinkAuthenticationTest.php @@ -0,0 +1,58 @@ + + * + * 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; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\Security\Core\User\User; +use Symfony\Component\Security\Http\LoginLink\LoginLinkHandler; +use Symfony\Component\Security\Http\LoginLink\LoginLinkHandlerInterface; + +/** + * @author Ryan Weaver + */ +class LoginLinkAuthenticationTest extends AbstractWebTestCase +{ + public function testLoginLinkSuccess() + { + if (!class_exists(LoginLinkHandler::class)) { + $this->markTestSkipped('Login link auth requires symfony/security-http:^5.2'); + } + + $client = $this->createClient(['test_case' => 'LoginLink', 'root_config' => 'config.yml']); + + // we need an active request that is under the firewall to use the linker + $request = Request::create('/get-login-link'); + self::$container->get(RequestStack::class)->push($request); + + /** @var LoginLinkHandlerInterface $loginLinkHandler */ + $loginLinkHandler = self::$container->get(LoginLinkHandlerInterface::class); + $user = new User('weaverryan', 'foo'); + $loginLink = $loginLinkHandler->createLoginLink($user); + $this->assertStringContainsString('user=weaverryan', $loginLink); + $this->assertStringContainsString('hash=', $loginLink); + $this->assertStringContainsString('expires=', $loginLink); + $client->request('GET', $loginLink->getUrl()); + $response = $client->getResponse(); + + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame(['message' => 'Welcome weaverryan!'], json_decode($response->getContent(), true)); + + $client->request('GET', $loginLink->getUrl()); + $response = $client->getResponse(); + $this->assertSame(200, $response->getStatusCode()); + + $client->request('GET', $loginLink->getUrl()); + $response = $client->getResponse(); + $this->assertSame(302, $response->getStatusCode(), 'Should redirect with an error because max uses are only 2'); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/bundles.php new file mode 100644 index 0000000000000..d1e9eb7e0d36a --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/bundles.php @@ -0,0 +1,15 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +return [ + new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), + new Symfony\Bundle\SecurityBundle\SecurityBundle(), +]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/config.yml new file mode 100644 index 0000000000000..5e55d065fffd6 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/config.yml @@ -0,0 +1,33 @@ +framework: + secret: test + router: { resource: "%kernel.project_dir%/%kernel.test_case%/routing.yml", utf8: true } + test: ~ + default_locale: en + profiler: false + session: + storage_id: session.storage.mock_file + +services: + logger: { class: Psr\Log\NullLogger } + Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ProfileController: + public: true + calls: + - ['setContainer', ['@Psr\Container\ContainerInterface']] + tags: [container.service_subscriber] + Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ApiAuthenticator: ~ + +security: + enable_authenticator_manager: true + + encoders: + Symfony\Component\Security\Core\User\User: plaintext + + providers: + in_memory: + memory: + users: + 'jane@example.org': { password: test, roles: [ROLE_USER] } + in_memory2: + memory: + users: + 'john@example.org': { password: test, roles: [ROLE_USER] } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/firewall_user_provider.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/firewall_user_provider.yml new file mode 100644 index 0000000000000..59e5e5b536e2b --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/firewall_user_provider.yml @@ -0,0 +1,10 @@ +imports: +- { resource: ./config.yml } + +security: + firewalls: + api: + pattern: / + provider: in_memory + custom_authenticator: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ApiAuthenticator + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/implicit_user_provider.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/implicit_user_provider.yml new file mode 100644 index 0000000000000..ce62733725055 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/implicit_user_provider.yml @@ -0,0 +1,9 @@ +imports: +- { resource: ./config.yml } + +security: + firewalls: + api: + pattern: / + custom_authenticator: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ApiAuthenticator + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/routing.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/routing.yml new file mode 100644 index 0000000000000..2fd12cf5607a3 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/routing.yml @@ -0,0 +1,4 @@ +profile: + path: /profile + defaults: + _controller: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ProfileController diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/CsrfFormLogin/base_config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/CsrfFormLogin/base_config.yml index d6a80d5059471..6b82dea8de8ec 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/CsrfFormLogin/base_config.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/CsrfFormLogin/base_config.yml @@ -9,6 +9,11 @@ services: tags: - { name: form.type } + Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\CsrfFormLoginBundle\Controller\LoginController: + public: true + tags: + - { name: container.service_subscriber } + security: encoders: Symfony\Component\Security\Core\User\User: plaintext diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/FirewallEntryPoint/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/FirewallEntryPoint/config.yml index 302d7382762d8..25ef98650e419 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/FirewallEntryPoint/config.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/FirewallEntryPoint/config.yml @@ -3,7 +3,9 @@ framework: router: { resource: "%kernel.project_dir%/%kernel.test_case%/routing.yml", utf8: true } validation: { enabled: true, enable_annotations: true } csrf_protection: true - form: true + form: + enabled: true + legacy_error_messages: false test: ~ default_locale: en session: diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LoginLink/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LoginLink/bundles.php new file mode 100644 index 0000000000000..bcfd17425cfd1 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LoginLink/bundles.php @@ -0,0 +1,15 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +return [ + new Symfony\Bundle\SecurityBundle\SecurityBundle(), + new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), +]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LoginLink/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LoginLink/config.yml new file mode 100644 index 0000000000000..969c669eabcfc --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LoginLink/config.yml @@ -0,0 +1,28 @@ +imports: + - { resource: ./../config/framework.yml } + +security: + enable_authenticator_manager: true + + providers: + in_memory: + memory: + users: + weaverryan: { password: foo, roles: [ROLE_USER] } + + firewalls: + main: + pattern: ^/ + login_link: + check_route: login_link_check + signature_properties: ['password'] + max_uses: 2 + success_handler: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\LoginLink\TestCustomLoginLinkSuccessHandler + +services: + Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\LoginLink\TestCustomLoginLinkSuccessHandler: null + # needed so LoginLinkHandlerInterface has *some* reference, and so isn't + # entirely removed from the container (so we can fetch it in the test container) + login_link_handler: + alias: 'Symfony\Component\Security\Http\LoginLink\LoginLinkHandlerInterface' + public: true diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LoginLink/routing.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LoginLink/routing.yml new file mode 100644 index 0000000000000..890d2bef3d4c7 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LoginLink/routing.yml @@ -0,0 +1,2 @@ +login_link_check: + path: /login-link-check diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/config.yml index 328242d279722..b35ad3f4c91d2 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/config.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/config.yml @@ -3,6 +3,7 @@ imports: parameters: env(APP_IP): '127.0.0.1' + env(APP_IPS): '127.0.0.1, ::1' security: encoders: @@ -47,7 +48,9 @@ security: - { 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: ^/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: .*, roles: IS_AUTHENTICATED_FULLY } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/login_throttling.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/login_throttling.yml new file mode 100644 index 0000000000000..4848567cf3360 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/login_throttling.yml @@ -0,0 +1,12 @@ +imports: + - { resource: ./config.yml } + +framework: + lock: ~ + rate_limiter: ~ + +security: + firewalls: + default: + login_throttling: + max_attempts: 1 diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/config/framework.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/config/framework.yml index 3c60329efb3f1..e145253080d71 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/config/framework.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/config/framework.yml @@ -4,7 +4,9 @@ framework: validation: { enabled: true, enable_annotations: true } assets: ~ csrf_protection: true - form: true + form: + enabled: true + legacy_error_messages: false test: ~ default_locale: en session: diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/LoginLink/FirewallAwareLoginLinkHandlerTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/LoginLink/FirewallAwareLoginLinkHandlerTest.php new file mode 100644 index 0000000000000..e2a6bb0116977 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/LoginLink/FirewallAwareLoginLinkHandlerTest.php @@ -0,0 +1,83 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\LoginLink; + +use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerInterface; +use Symfony\Bundle\SecurityBundle\LoginLink\FirewallAwareLoginLinkHandler; +use Symfony\Bundle\SecurityBundle\Security\FirewallConfig; +use Symfony\Bundle\SecurityBundle\Security\FirewallMap; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Http\LoginLink\LoginLinkDetails; +use Symfony\Component\Security\Http\LoginLink\LoginLinkHandlerInterface; + +class FirewallAwareLoginLinkHandlerTest extends TestCase +{ + public function testSuccessfulDecoration() + { + $user = $this->createMock(UserInterface::class); + $linkDetails = new LoginLinkDetails('http://example.com', new \DateTimeImmutable()); + $request = Request::create('http://example.com/verify'); + + $firewallMap = $this->createFirewallMap('main_firewall'); + $loginLinkHandler = $this->createMock(LoginLinkHandlerInterface::class); + $loginLinkHandler->expects($this->once()) + ->method('createLoginLink') + ->with($user) + ->willReturn($linkDetails); + $loginLinkHandler->expects($this->once()) + ->method('consumeLoginLink') + ->with($request) + ->willReturn($user); + $locator = $this->createLocator([ + 'main_firewall' => $loginLinkHandler, + ]); + $requestStack = new RequestStack(); + $requestStack->push($request); + + $linker = new FirewallAwareLoginLinkHandler($firewallMap, $locator, $requestStack); + $actualLinkDetails = $linker->createLoginLink($user); + $this->assertSame($linkDetails, $actualLinkDetails); + + $actualUser = $linker->consumeLoginLink($request); + $this->assertSame($user, $actualUser); + } + + private function createFirewallMap(string $firewallName) + { + $map = $this->createMock(FirewallMap::class); + $map->expects($this->any()) + ->method('getFirewallConfig') + ->willReturn($config = new FirewallConfig($firewallName, 'user_checker')); + + return $map; + } + + private function createLocator(array $linkers) + { + $locator = $this->createMock(ContainerInterface::class); + $locator->expects($this->any()) + ->method('has') + ->willReturnCallback(function ($firewallName) use ($linkers) { + return isset($linkers[$firewallName]); + }); + $locator->expects($this->any()) + ->method('get') + ->willReturnCallback(function ($firewallName) use ($linkers) { + return $linkers[$firewallName]; + }); + + return $locator; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/composer.json b/src/Symfony/Bundle/SecurityBundle/composer.json index 7b2575a51f103..7402d7656cfd7 100644 --- a/src/Symfony/Bundle/SecurityBundle/composer.json +++ b/src/Symfony/Bundle/SecurityBundle/composer.json @@ -19,14 +19,14 @@ "php": ">=7.2.5", "ext-xml": "*", "symfony/config": "^4.4|^5.0", - "symfony/dependency-injection": "^5.1", + "symfony/dependency-injection": "^5.2", "symfony/event-dispatcher": "^5.1", "symfony/http-kernel": "^5.0", "symfony/polyfill-php80": "^1.15", - "symfony/security-core": "^5.1", + "symfony/security-core": "^5.2", "symfony/security-csrf": "^4.4|^5.0", "symfony/security-guard": "^5.1", - "symfony/security-http": "^5.1,>=5.1.2" + "symfony/security-http": "^5.2" }, "require-dev": { "doctrine/doctrine-bundle": "^2.0", @@ -37,8 +37,9 @@ "symfony/dom-crawler": "^4.4|^5.0", "symfony/expression-language": "^4.4|^5.0", "symfony/form": "^4.4|^5.0", - "symfony/framework-bundle": "^4.4|^5.0", + "symfony/framework-bundle": "^5.2", "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", @@ -63,7 +64,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "5.1-dev" + "dev-master": "5.2-dev" } } } diff --git a/src/Symfony/Bundle/TwigBundle/CHANGELOG.md b/src/Symfony/Bundle/TwigBundle/CHANGELOG.md index f07bf65d3e1fc..be47f246de147 100644 --- a/src/Symfony/Bundle/TwigBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/TwigBundle/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +5.2.0 +----- + + * deprecated the public `twig` service to private + 5.0.0 ----- diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExtensionPass.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExtensionPass.php index 6c955aae6fa50..8803bb422d06c 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExtensionPass.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExtensionPass.php @@ -42,11 +42,6 @@ public function process(ContainerBuilder $container) $viewDir = \dirname((new \ReflectionClass('Symfony\Bridge\Twig\Extension\FormExtension'))->getFileName(), 2).'/Resources/views'; $templateIterator = $container->getDefinition('twig.template_iterator'); $templatePaths = $templateIterator->getArgument(1); - $cacheWarmer = null; - if ($container->hasDefinition('twig.cache_warmer')) { - $cacheWarmer = $container->getDefinition('twig.cache_warmer'); - $cacheWarmerPaths = $cacheWarmer->getArgument(2); - } $loader = $container->getDefinition('twig.loader.native_filesystem'); if ($container->has('mailer')) { @@ -54,9 +49,6 @@ public function process(ContainerBuilder $container) $loader->addMethodCall('addPath', [$emailPath, 'email']); $loader->addMethodCall('addPath', [$emailPath, '!email']); $templatePaths[$emailPath] = 'email'; - if ($cacheWarmer) { - $cacheWarmerPaths[$emailPath] = 'email'; - } } if ($container->has('form.extension')) { @@ -65,15 +57,9 @@ public function process(ContainerBuilder $container) $coreThemePath = $viewDir.'/Form'; $loader->addMethodCall('addPath', [$coreThemePath]); $templatePaths[$coreThemePath] = null; - if ($cacheWarmer) { - $cacheWarmerPaths[$coreThemePath] = null; - } } $templateIterator->replaceArgument(1, $templatePaths); - if ($cacheWarmer) { - $container->getDefinition('twig.cache_warmer')->replaceArgument(2, $cacheWarmerPaths); - } if ($container->has('router')) { $container->getDefinition('twig.extension.routing')->addTag('twig.extension'); @@ -90,10 +76,6 @@ public function process(ContainerBuilder $container) } } - if (!$container->has('http_kernel')) { - $container->removeDefinition('twig.controller.preview_error'); - } - if ($container->has('request_stack')) { $container->getDefinition('twig.extension.httpfoundation')->addTag('twig.extension'); } diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/TwigLoaderPass.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/TwigLoaderPass.php index bd0c606a86d38..abdfe509c3ad4 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/TwigLoaderPass.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/TwigLoaderPass.php @@ -43,7 +43,7 @@ public function process(ContainerBuilder $container) } if (1 === $found) { - $container->setAlias('twig.loader', $id)->setPrivate(true); + $container->setAlias('twig.loader', $id); } else { $chainLoader = $container->getDefinition('twig.loader.chain'); krsort($prioritizedLoaders); @@ -54,7 +54,7 @@ public function process(ContainerBuilder $container) } } - $container->setAlias('twig.loader', 'twig.loader.chain')->setPrivate(true); + $container->setAlias('twig.loader', 'twig.loader.chain'); } } } diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php index a70fd31afe3f8..13826834c02be 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php @@ -15,7 +15,7 @@ use Symfony\Component\Config\Resource\FileExistenceResource; use Symfony\Component\Console\Application; use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; +use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\Mailer\Mailer; @@ -34,19 +34,19 @@ class TwigExtension extends Extension { public function load(array $configs, ContainerBuilder $container) { - $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); - $loader->load('twig.xml'); + $loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); + $loader->load('twig.php'); if (class_exists('Symfony\Component\Form\Form')) { - $loader->load('form.xml'); + $loader->load('form.php'); } if (class_exists(Application::class)) { - $loader->load('console.xml'); + $loader->load('console.php'); } if (class_exists(Mailer::class)) { - $loader->load('mailer.xml'); + $loader->load('mailer.php'); } if (!class_exists(Translator::class)) { diff --git a/src/Symfony/Bundle/TwigBundle/Resources/config/console.php b/src/Symfony/Bundle/TwigBundle/Resources/config/console.php new file mode 100644 index 0000000000000..9abd75da19ffc --- /dev/null +++ b/src/Symfony/Bundle/TwigBundle/Resources/config/console.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Bridge\Twig\Command\DebugCommand; +use Symfony\Bundle\TwigBundle\Command\LintCommand; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('twig.command.debug', DebugCommand::class) + ->args([ + service('twig'), + param('kernel.project_dir'), + param('kernel.bundles_metadata'), + param('twig.default_path'), + service('debug.file_link_formatter')->nullOnInvalid(), + ]) + ->tag('console.command', ['command' => 'debug:twig']) + + ->set('twig.command.lint', LintCommand::class) + ->args([service('twig')]) + ->tag('console.command', ['command' => 'lint:twig']) + ; +}; diff --git a/src/Symfony/Bundle/TwigBundle/Resources/config/console.xml b/src/Symfony/Bundle/TwigBundle/Resources/config/console.xml deleted file mode 100644 index 68afbcc30f2c6..0000000000000 --- a/src/Symfony/Bundle/TwigBundle/Resources/config/console.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - %kernel.project_dir% - %kernel.bundles_metadata% - %twig.default_path% - - - - - - - - - - diff --git a/src/Symfony/Bundle/TwigBundle/Resources/config/form.php b/src/Symfony/Bundle/TwigBundle/Resources/config/form.php new file mode 100644 index 0000000000000..9f2efdf94105c --- /dev/null +++ b/src/Symfony/Bundle/TwigBundle/Resources/config/form.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Bridge\Twig\Extension\FormExtension; +use Symfony\Bridge\Twig\Form\TwigRendererEngine; +use Symfony\Component\Form\FormRenderer; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('twig.extension.form', FormExtension::class) + ->args([service('translator')->nullOnInvalid()]) + + ->set('twig.form.engine', TwigRendererEngine::class) + ->args([param('twig.form.resources'), service('twig')]) + + ->set('twig.form.renderer', FormRenderer::class) + ->args([service('twig.form.engine'), service('security.csrf.token_manager')->nullOnInvalid()]) + ->tag('twig.runtime') + ; +}; diff --git a/src/Symfony/Bundle/TwigBundle/Resources/config/form.xml b/src/Symfony/Bundle/TwigBundle/Resources/config/form.xml deleted file mode 100644 index 8fe29572c687c..0000000000000 --- a/src/Symfony/Bundle/TwigBundle/Resources/config/form.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - %twig.form.resources% - - - - - - - - - - diff --git a/src/Symfony/Bundle/TwigBundle/Resources/config/mailer.php b/src/Symfony/Bundle/TwigBundle/Resources/config/mailer.php new file mode 100644 index 0000000000000..1444481f2c0ba --- /dev/null +++ b/src/Symfony/Bundle/TwigBundle/Resources/config/mailer.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\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Bridge\Twig\Mime\BodyRenderer; +use Symfony\Component\Mailer\EventListener\MessageListener; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('twig.mailer.message_listener', MessageListener::class) + ->args([null, service('twig.mime_body_renderer')]) + ->tag('kernel.event_subscriber') + + ->set('twig.mime_body_renderer', BodyRenderer::class) + ->args([service('twig')]) + ; +}; diff --git a/src/Symfony/Bundle/TwigBundle/Resources/config/mailer.xml b/src/Symfony/Bundle/TwigBundle/Resources/config/mailer.xml deleted file mode 100644 index 0e425952ffe59..0000000000000 --- a/src/Symfony/Bundle/TwigBundle/Resources/config/mailer.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - null - - - - - - - - - diff --git a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.php b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.php new file mode 100644 index 0000000000000..a7124a30c20aa --- /dev/null +++ b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.php @@ -0,0 +1,164 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Psr\Container\ContainerInterface; +use Symfony\Bridge\Twig\AppVariable; +use Symfony\Bridge\Twig\DataCollector\TwigDataCollector; +use Symfony\Bridge\Twig\ErrorRenderer\TwigErrorRenderer; +use Symfony\Bridge\Twig\Extension\AssetExtension; +use Symfony\Bridge\Twig\Extension\CodeExtension; +use Symfony\Bridge\Twig\Extension\ExpressionExtension; +use Symfony\Bridge\Twig\Extension\HttpFoundationExtension; +use Symfony\Bridge\Twig\Extension\HttpKernelExtension; +use Symfony\Bridge\Twig\Extension\HttpKernelRuntime; +use Symfony\Bridge\Twig\Extension\ProfilerExtension; +use Symfony\Bridge\Twig\Extension\RoutingExtension; +use Symfony\Bridge\Twig\Extension\StopwatchExtension; +use Symfony\Bridge\Twig\Extension\TranslationExtension; +use Symfony\Bridge\Twig\Extension\WebLinkExtension; +use Symfony\Bridge\Twig\Extension\WorkflowExtension; +use Symfony\Bridge\Twig\Extension\YamlExtension; +use Symfony\Bridge\Twig\Translation\TwigExtractor; +use Symfony\Bundle\TwigBundle\CacheWarmer\TemplateCacheWarmer; +use Symfony\Bundle\TwigBundle\DependencyInjection\Configurator\EnvironmentConfigurator; +use Symfony\Bundle\TwigBundle\TemplateIterator; +use Twig\Cache\FilesystemCache; +use Twig\Environment; +use Twig\Extension\CoreExtension; +use Twig\Extension\DebugExtension; +use Twig\Extension\EscaperExtension; +use Twig\Extension\OptimizerExtension; +use Twig\Extension\StagingExtension; +use Twig\ExtensionSet; +use Twig\Loader\ChainLoader; +use Twig\Loader\FilesystemLoader; +use Twig\Profiler\Profile; +use Twig\RuntimeLoader\ContainerRuntimeLoader; +use Twig\Template; +use Twig\TemplateWrapper; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('twig', Environment::class) + ->public() + ->args([service('twig.loader'), abstract_arg('Twig options')]) + ->call('addGlobal', ['app', service('twig.app_variable')]) + ->call('addRuntimeLoader', [service('twig.runtime_loader')]) + ->configurator([service('twig.configurator.environment'), 'configure']) + ->tag('container.preload', ['class' => FilesystemCache::class]) + ->tag('container.preload', ['class' => CoreExtension::class]) + ->tag('container.preload', ['class' => EscaperExtension::class]) + ->tag('container.preload', ['class' => OptimizerExtension::class]) + ->tag('container.preload', ['class' => StagingExtension::class]) + ->tag('container.preload', ['class' => ExtensionSet::class]) + ->tag('container.preload', ['class' => Template::class]) + ->tag('container.preload', ['class' => TemplateWrapper::class]) + ->tag('container.private', ['package' => 'symfony/twig-bundle', 'version' => '5.2']) + + ->alias('Twig_Environment', 'twig') + ->alias(Environment::class, 'twig') + + ->set('twig.app_variable', AppVariable::class) + ->call('setEnvironment', [param('kernel.environment')]) + ->call('setDebug', [param('kernel.debug')]) + ->call('setTokenStorage', [service('security.token_storage')->ignoreOnInvalid()]) + ->call('setRequestStack', [service('request_stack')->ignoreOnInvalid()]) + + ->set('twig.template_iterator', TemplateIterator::class) + ->args([service('kernel'), abstract_arg('Twig paths'), param('twig.default_path')]) + + ->set('twig.template_cache_warmer', TemplateCacheWarmer::class) + ->args([service(ContainerInterface::class), service('twig.template_iterator')]) + ->tag('kernel.cache_warmer') + ->tag('container.service_subscriber', ['id' => 'twig']) + + ->set('twig.loader.native_filesystem', FilesystemLoader::class) + ->args([[], param('kernel.project_dir')]) + ->tag('twig.loader') + + ->set('twig.loader.chain', ChainLoader::class) + + ->set('twig.extension.profiler', ProfilerExtension::class) + ->args([service('twig.profile'), service('debug.stopwatch')->ignoreOnInvalid()]) + + ->set('twig.profile', Profile::class) + + ->set('data_collector.twig', TwigDataCollector::class) + ->args([service('twig.profile'), service('twig')]) + ->tag('data_collector', ['template' => '@WebProfiler/Collector/twig.html.twig', 'id' => 'twig', 'priority' => 257]) + + ->set('twig.extension.trans', TranslationExtension::class) + ->args([service('translator')->nullOnInvalid()]) + ->tag('twig.extension') + + ->set('twig.extension.assets', AssetExtension::class) + ->args([service('assets.packages')]) + + ->set('twig.extension.code', CodeExtension::class) + ->args([service('debug.file_link_formatter')->ignoreOnInvalid(), param('kernel.project_dir'), param('kernel.charset')]) + ->tag('twig.extension') + + ->set('twig.extension.routing', RoutingExtension::class) + ->args([service('router')]) + + ->set('twig.extension.yaml', YamlExtension::class) + + ->set('twig.extension.debug.stopwatch', StopwatchExtension::class) + ->args([service('debug.stopwatch')->ignoreOnInvalid(), param('kernel.debug')]) + + ->set('twig.extension.expression', ExpressionExtension::class) + + ->set('twig.extension.httpkernel', HttpKernelExtension::class) + + ->set('twig.runtime.httpkernel', HttpKernelRuntime::class) + ->args([service('fragment.handler')]) + + ->set('twig.extension.httpfoundation', HttpFoundationExtension::class) + ->args([service('url_helper')]) + + ->set('twig.extension.debug', DebugExtension::class) + + ->set('twig.extension.weblink', WebLinkExtension::class) + ->args([service('request_stack')]) + + ->set('twig.translation.extractor', TwigExtractor::class) + ->args([service('twig')]) + ->tag('translation.extractor', ['alias' => 'twig']) + + ->set('workflow.twig_extension', WorkflowExtension::class) + ->args([service('workflow.registry')]) + + ->set('twig.configurator.environment', EnvironmentConfigurator::class) + ->args([ + abstract_arg('date format, set in TwigExtension'), + abstract_arg('interval format, set in TwigExtension'), + abstract_arg('timezone, set in TwigExtension'), + abstract_arg('decimals, set in TwigExtension'), + abstract_arg('decimal point, set in TwigExtension'), + abstract_arg('thousands separator, set in TwigExtension'), + ]) + + ->set('twig.runtime_loader', ContainerRuntimeLoader::class) + ->args([abstract_arg('runtime locator')]) + + ->set('twig.error_renderer.html', TwigErrorRenderer::class) + ->decorate('error_renderer.html') + ->args([ + service('twig'), + service('twig.error_renderer.html.inner'), + inline_service(TwigErrorRenderer::class) + ->factory([TwigErrorRenderer::class, 'isDebug']) + ->args([service('request_stack'), param('kernel.debug')]), + ]) + ; +}; diff --git a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml deleted file mode 100644 index cb30219365e4e..0000000000000 --- a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml +++ /dev/null @@ -1,153 +0,0 @@ - - - - - - - - - - - - app - - - - - - - - - - - - - - - - - - - - %kernel.environment% - %kernel.debug% - - - - - - - - %twig.default_path% - - - - - - - - - - - - %kernel.project_dir% - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - %kernel.project_dir% - %kernel.charset% - - - - - - - - - - - %kernel.debug% - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - %kernel.debug% - - - - - diff --git a/src/Symfony/Bundle/TwigBundle/Tests/Functional/NoTemplatingEntryTest.php b/src/Symfony/Bundle/TwigBundle/Tests/Functional/NoTemplatingEntryTest.php index 5d73f73e7ed22..b571d1cc76094 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/Functional/NoTemplatingEntryTest.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/Functional/NoTemplatingEntryTest.php @@ -15,6 +15,7 @@ use Symfony\Bundle\TwigBundle\Tests\TestCase; use Symfony\Bundle\TwigBundle\TwigBundle; use Symfony\Component\Config\Loader\LoaderInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpKernel\Kernel; @@ -26,7 +27,7 @@ public function test() $kernel->boot(); $container = $kernel->getContainer(); - $content = $container->get('twig')->render('index.html.twig'); + $content = $container->get('twig.alias')->render('index.html.twig'); $this->assertStringContainsString('{ a: b }', $content); } @@ -60,7 +61,7 @@ public function registerBundles(): iterable public function registerContainerConfiguration(LoaderInterface $loader) { - $loader->load(function ($container) { + $loader->load(function (ContainerBuilder $container) { $container ->loadFromExtension('framework', [ 'secret' => '$ecret', @@ -69,6 +70,7 @@ public function registerContainerConfiguration(LoaderInterface $loader) ->loadFromExtension('twig', [ 'default_path' => __DIR__.'/templates', ]) + ->setAlias('twig.alias', 'twig')->setPublic(true) ; }); } diff --git a/src/Symfony/Bundle/TwigBundle/composer.json b/src/Symfony/Bundle/TwigBundle/composer.json index f5f34b02921f7..d03cd5c73ba33 100644 --- a/src/Symfony/Bundle/TwigBundle/composer.json +++ b/src/Symfony/Bundle/TwigBundle/composer.json @@ -27,7 +27,7 @@ "require-dev": { "symfony/asset": "^4.4|^5.0", "symfony/stopwatch": "^4.4|^5.0", - "symfony/dependency-injection": "^4.4|^5.0", + "symfony/dependency-injection": "^5.2", "symfony/expression-language": "^4.4|^5.0", "symfony/finder": "^4.4|^5.0", "symfony/form": "^4.4|^5.0", @@ -40,7 +40,7 @@ "doctrine/cache": "~1.0" }, "conflict": { - "symfony/dependency-injection": "<4.4", + "symfony/dependency-injection": "<5.2", "symfony/framework-bundle": "<5.0", "symfony/translation": "<5.0" }, @@ -53,7 +53,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "5.1-dev" + "dev-master": "5.2-dev" } } } diff --git a/src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md b/src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md index 5418767fc9e03..028537ead68cd 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +5.2.0 +----- + + * added session usage + 5.0.0 ----- diff --git a/src/Symfony/Bundle/WebProfilerBundle/DependencyInjection/WebProfilerExtension.php b/src/Symfony/Bundle/WebProfilerBundle/DependencyInjection/WebProfilerExtension.php index 5d2be199094d0..0bb949c095a36 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/DependencyInjection/WebProfilerExtension.php +++ b/src/Symfony/Bundle/WebProfilerBundle/DependencyInjection/WebProfilerExtension.php @@ -16,7 +16,7 @@ use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\Extension; -use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; +use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; use Symfony\Component\DependencyInjection\Reference; /** @@ -43,11 +43,11 @@ public function load(array $configs, ContainerBuilder $container) $configuration = $this->getConfiguration($configs, $container); $config = $this->processConfiguration($configuration, $configs); - $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); - $loader->load('profiler.xml'); + $loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); + $loader->load('profiler.php'); if ($config['toolbar'] || $config['intercept_redirects']) { - $loader->load('toolbar.xml'); + $loader->load('toolbar.php'); $container->getDefinition('web_profiler.debug_toolbar')->replaceArgument(4, $config['excluded_ajax_paths']); $container->setParameter('web_profiler.debug_toolbar.intercept_redirects', $config['intercept_redirects']); $container->setParameter('web_profiler.debug_toolbar.mode', $config['toolbar'] ? WebDebugToolbarListener::ENABLED : WebDebugToolbarListener::DISABLED); diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/config/profiler.php b/src/Symfony/Bundle/WebProfilerBundle/Resources/config/profiler.php new file mode 100644 index 0000000000000..85c64f268b576 --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/config/profiler.php @@ -0,0 +1,83 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Bundle\WebProfilerBundle\Controller\ExceptionPanelController; +use Symfony\Bundle\WebProfilerBundle\Controller\ProfilerController; +use Symfony\Bundle\WebProfilerBundle\Controller\RouterController; +use Symfony\Bundle\WebProfilerBundle\Csp\ContentSecurityPolicyHandler; +use Symfony\Bundle\WebProfilerBundle\Csp\NonceGenerator; +use Symfony\Bundle\WebProfilerBundle\Twig\WebProfilerExtension; +use Symfony\Component\HttpKernel\Debug\FileLinkFormatter; +use Symfony\Component\VarDumper\Dumper\HtmlDumper; + +return static function (ContainerConfigurator $container) { + $container->services() + + ->set('web_profiler.controller.profiler', ProfilerController::class) + ->public() + ->args([ + service('router')->nullOnInvalid(), + service('profiler')->nullOnInvalid(), + service('twig'), + param('data_collector.templates'), + service('web_profiler.csp.handler'), + param('kernel.project_dir'), + ]) + + ->set('web_profiler.controller.router', RouterController::class) + ->public() + ->args([ + service('profiler')->nullOnInvalid(), + service('twig'), + service('router')->nullOnInvalid(), + null, + tagged_iterator('routing.expression_language_provider'), + ]) + + ->set('web_profiler.controller.exception_panel', ExceptionPanelController::class) + ->public() + ->args([ + service('error_handler.error_renderer.html'), + service('profiler')->nullOnInvalid(), + ]) + + ->set('web_profiler.csp.handler', ContentSecurityPolicyHandler::class) + ->args([ + inline_service(NonceGenerator::class), + ]) + + ->set('twig.extension.webprofiler', WebProfilerExtension::class) + ->args([ + inline_service(HtmlDumper::class) + ->args([null, param('kernel.charset'), HtmlDumper::DUMP_LIGHT_ARRAY]) + ->call('setDisplayOptions', [['maxStringLength' => 4096, 'fileLinkFormat' => service('debug.file_link_formatter')]]), + ]) + ->tag('twig.extension') + + ->set('debug.file_link_formatter', FileLinkFormatter::class) + ->args([ + param('debug.file_link_format'), + service('request_stack')->ignoreOnInvalid(), + param('kernel.project_dir'), + '/_profiler/open?file=%%f&line=%%l#line%%l', + ]) + + ->set('debug.file_link_formatter.url_format', 'string') + ->factory([FileLinkFormatter::class, 'generateUrlFormat']) + ->args([ + service('router'), + '_profiler_open_file', + '?file=%%f&line=%%l#line%%l', + ]) + ; +}; diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/config/profiler.xml b/src/Symfony/Bundle/WebProfilerBundle/Resources/config/profiler.xml deleted file mode 100644 index c1409f9ff809a..0000000000000 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/config/profiler.xml +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - - - - - - - %data_collector.templates% - - %kernel.project_dir% - - - - - - - null - - - - - - - - - - - - - - - - - - - null - %kernel.charset% - Symfony\Component\VarDumper\Dumper\HtmlDumper::DUMP_LIGHT_ARRAY - - - 4096 - - - - - - - - - %debug.file_link_format% - - %kernel.project_dir% - /_profiler/open?file=%%f&line=%%l#line%%l - - - - - - _profiler_open_file - ?file=%%f&line=%%l#line%%l - - - diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/config/toolbar.php b/src/Symfony/Bundle/WebProfilerBundle/Resources/config/toolbar.php new file mode 100644 index 0000000000000..626f6feeceec3 --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/config/toolbar.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Bundle\WebProfilerBundle\EventListener\WebDebugToolbarListener; + +return static function (ContainerConfigurator $container) { + $container->services() + + ->set('web_profiler.debug_toolbar', WebDebugToolbarListener::class) + ->args([ + service('twig'), + param('web_profiler.debug_toolbar.intercept_redirects'), + param('web_profiler.debug_toolbar.mode'), + service('router')->ignoreOnInvalid(), + abstract_arg('paths that should be excluded from the AJAX requests shown in the toolbar'), + service('web_profiler.csp.handler'), + ]) + ->tag('kernel.event_subscriber') + ; +}; diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/config/toolbar.xml b/src/Symfony/Bundle/WebProfilerBundle/Resources/config/toolbar.xml deleted file mode 100644 index c38db2056c1a4..0000000000000 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/config/toolbar.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - %web_profiler.debug_toolbar.intercept_redirects% - %web_profiler.debug_toolbar.mode% - - - - - - 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 f3d0f7cad4c14..fc878fdba491d 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/logger.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/logger.html.twig @@ -5,7 +5,7 @@ {% block toolbar %} {% if collector.counterrors or collector.countdeprecations or collector.countwarnings %} {% set icon %} - {% set status_color = collector.counterrors ? 'red' : 'yellow' %} + {% set status_color = collector.counterrors ? 'red' : collector.countwarnings ? 'yellow' : 'none' %} {{ include('@WebProfiler/Icon/logger.svg') }} {{ collector.counterrors ?: (collector.countdeprecations + collector.countwarnings) }} {% endset %} @@ -23,7 +23,7 @@
Deprecations - {{ collector.countdeprecations|default(0) }} + {{ collector.countdeprecations|default(0) }}
{% endset %} @@ -32,7 +32,7 @@ {% endblock %} {% block menu %} - + {{ include('@WebProfiler/Icon/logger.svg') }} Logs {% if collector.counterrors or collector.countdeprecations or collector.countwarnings %} @@ -88,7 +88,7 @@
{# '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) }}

+

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

Log messages generated by using features marked as deprecated.

diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/notifier.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/notifier.html.twig new file mode 100644 index 0000000000000..453f226992d1b --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/notifier.html.twig @@ -0,0 +1,168 @@ +{% extends '@WebProfiler/Profiler/layout.html.twig' %} + +{% block toolbar %} + {% set events = collector.events %} + + {% if events.messages|length %} + {% set icon %} + {% include('@WebProfiler/Icon/mailer.svg') %} + {{ events.messages|length }} + {% endset %} + + {% set text %} +
+ Sent notifications + {{ events.messages|length }} +
+ + {% for transport in events.transports %} +
+ {{ transport }} + {{ events.messages(transport)|length }} +
+ {% endfor %} + {% endset %} + + {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { 'link': profiler_url }) }} + {% endif %} +{% endblock %} + +{% block head %} + {{ parent() }} + +{% endblock %} + +{% block menu %} + {% set events = collector.events %} + + + {{ include('@WebProfiler/Icon/mailer.svg') }} + + Notifications + {% if events.messages|length > 0 %} + + {{ events.messages|length }} + + {% endif %} + +{% endblock %} + +{% block panel %} + {% set events = collector.events %} + +

Notifications

+ + {% if not events.messages|length %} +
+

No notifications were sent.

+
+ {% endif %} + +
+ {% for transport in events.transports %} +
+ {{ events.messages(transport)|length }} + {{ transport }} +
+ {% endfor %} +
+ + {% for transport in events.transports %} +

{{ transport }}

+ +
+
+ {% for event in events.events(transport) %} + {% set message = event.message %} +
+

Message #{{ loop.index }} ({{ event.isQueued() ? 'queued' : 'sent' }})

+
+
+
+ Subject +

{{ message.getSubject() ?? '(empty)' }}

+
+ {% if message.getNotification is defined %} +
+
+
+ Content +
{{ message.getNotification().getContent() ?? '(empty)' }}
+ Importance +
{{ message.getNotification().getImportance() }}
+
+
+
+ {% endif %} +
+
+ {% if message.getNotification is defined %} +
+

Notification

+ {% set notification = event.message.getNotification() %} +
+
+                                                            {{- 'Subject: ' ~ notification.getSubject() }}
+ {{- 'Content: ' ~ notification.getContent() }}
+ {{- 'Importance: ' ~ notification.getImportance() }}
+ {{- 'Emoji: ' ~ (notification.getEmoji() is empty ? '(empty)' : notification.getEmoji()) }}
+ {{- 'Exception: ' ~ notification.getException() ?? '(empty)' }}
+ {{- 'ExceptionAsString: ' ~ (notification.getExceptionAsString() is empty ? '(empty)' : notification.getExceptionAsString()) }} +
+
+
+ {% endif %} +
+

Message Options

+
+
+                                                            {%- if message.getOptions() is null %}
+                                                                {{- '(empty)' }}
+                                                            {%- else %}
+                                                                {{- message.getOptions()|json_encode(constant('JSON_PRETTY_PRINT')) }}
+                                                            {%- endif %}
+                                                        
+
+
+
+
+
+
+
+ {% endfor %} +
+
+ {% endfor %} +{% endblock %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/request.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/request.html.twig index eb5c5595c4cdf..18311c169fece 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/request.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/request.html.twig @@ -59,6 +59,11 @@ Has session {% if collector.sessionmetadata|length %}yes{% else %}no{% endif %}
+ +
+ Stateless Check + {% if collector.statelesscheck %}yes{% else %}no{% endif %} +
{% if redirect_handler is defined -%} @@ -228,7 +233,7 @@ 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 db3791abdab6f..5f623a406bfcb 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 @@ -473,7 +473,11 @@ } }; } - addEventListener(document.getElementById('sfToolbarHideButton-' + newToken), 'click', function (event) { + 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; @@ -482,7 +486,11 @@ document.getElementById('sfMiniToolbar-' + newToken).style.display = 'block'; setPreference('toolbar/displayState', 'none'); }); - addEventListener(document.getElementById('sfToolbarMiniToggler-' + newToken), 'click', function (event) { + 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; diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar.css.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar.css.twig index 6669cd721fe73..be545da0e215e 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar.css.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar.css.twig @@ -14,8 +14,10 @@ z-index: 99999; } -.sf-minitoolbar a { - display: block; +.sf-minitoolbar button { + background-color: transparent; + padding: 0; + border: none; } .sf-minitoolbar svg, .sf-minitoolbar img { @@ -81,10 +83,13 @@ height: 36px; cursor: pointer; text-align: center; + border: none; + margin: 0; + padding: 0; } .sf-toolbarreset .hide-button svg { max-height: 18px; - margin-top: 10px; + margin-top: 1px; } .sf-toolbar-block { diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar.html.twig index a211617d7234d..efc89db286816 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar.html.twig @@ -1,8 +1,8 @@
@@ -23,8 +23,8 @@ {% endif %} {% endfor %} - + diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/DependencyInjection/WebProfilerExtensionTest.php b/src/Symfony/Bundle/WebProfilerBundle/Tests/DependencyInjection/WebProfilerExtensionTest.php index 6c6fc7f0b470d..0c81d82d7d42e 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Tests/DependencyInjection/WebProfilerExtensionTest.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Tests/DependencyInjection/WebProfilerExtensionTest.php @@ -62,6 +62,7 @@ protected function setUp(): void $this->container->register('twig', 'Twig\Environment')->addArgument(new Reference('twig_loader'))->setPublic(true); $this->container->setParameter('kernel.bundles', []); $this->container->setParameter('kernel.cache_dir', __DIR__); + $this->container->setParameter('kernel.build_dir', __DIR__); $this->container->setParameter('kernel.debug', false); $this->container->setParameter('kernel.project_dir', __DIR__); $this->container->setParameter('kernel.charset', 'UTF-8'); diff --git a/src/Symfony/Bundle/WebProfilerBundle/composer.json b/src/Symfony/Bundle/WebProfilerBundle/composer.json index 072c41fa644f0..7c5ffe163faed 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/composer.json +++ b/src/Symfony/Bundle/WebProfilerBundle/composer.json @@ -19,7 +19,7 @@ "php": ">=7.2.5", "symfony/config": "^4.4|^5.0", "symfony/framework-bundle": "^5.1", - "symfony/http-kernel": "^4.4|^5.0", + "symfony/http-kernel": "^5.2", "symfony/routing": "^4.4|^5.0", "symfony/twig-bundle": "^4.4|^5.0", "twig/twig": "^2.10|^3.0" @@ -32,7 +32,8 @@ }, "conflict": { "symfony/form": "<4.4", - "symfony/messenger": "<4.4" + "symfony/messenger": "<4.4", + "symfony/dependency-injection": "<5.2" }, "autoload": { "psr-4": { "Symfony\\Bundle\\WebProfilerBundle\\": "" }, @@ -43,7 +44,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "5.1-dev" + "dev-master": "5.2-dev" } } } diff --git a/src/Symfony/Component/Asset/composer.json b/src/Symfony/Component/Asset/composer.json index 5514494dc5890..78d0e273b50a0 100644 --- a/src/Symfony/Component/Asset/composer.json +++ b/src/Symfony/Component/Asset/composer.json @@ -35,7 +35,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "5.1-dev" + "dev-master": "5.2-dev" } } } diff --git a/src/Symfony/Component/BrowserKit/HttpBrowser.php b/src/Symfony/Component/BrowserKit/HttpBrowser.php index 6f5749c2642a8..0ad87b5c33a62 100644 --- a/src/Symfony/Component/BrowserKit/HttpBrowser.php +++ b/src/Symfony/Component/BrowserKit/HttpBrowser.php @@ -94,7 +94,7 @@ private function getBodyAndExtraHeaders(Request $request, array $headers): array return [http_build_query($fields, '', '&', \PHP_QUERY_RFC1738), ['Content-Type' => 'application/x-www-form-urlencoded']]; } - private function getHeaders(Request $request): array + protected function getHeaders(Request $request): array { $headers = []; foreach ($request->getServer() as $key => $value) { diff --git a/src/Symfony/Component/BrowserKit/Request.php b/src/Symfony/Component/BrowserKit/Request.php index 4dd0bc406f8cf..c2eeba8ee4f4a 100644 --- a/src/Symfony/Component/BrowserKit/Request.php +++ b/src/Symfony/Component/BrowserKit/Request.php @@ -37,6 +37,11 @@ public function __construct(string $uri, string $method, array $parameters = [], { $this->uri = $uri; $this->method = $method; + + array_walk_recursive($parameters, static function (&$value) { + $value = (string) $value; + }); + $this->parameters = $parameters; $this->files = $files; $this->cookies = $cookies; diff --git a/src/Symfony/Component/BrowserKit/Tests/RequestTest.php b/src/Symfony/Component/BrowserKit/Tests/RequestTest.php index e7718eef87ad6..5d7c7d6b894d3 100644 --- a/src/Symfony/Component/BrowserKit/Tests/RequestTest.php +++ b/src/Symfony/Component/BrowserKit/Tests/RequestTest.php @@ -51,4 +51,24 @@ public function testGetServer() $request = new Request('http://www.example.com/', 'get', [], [], [], ['foo' => 'bar']); $this->assertEquals(['foo' => 'bar'], $request->getServer(), '->getServer() returns the server parameters of the request'); } + + public function testAllParameterValuesAreConvertedToString(): void + { + $parameters = [ + 'foo' => 1, + 'bar' => [ + 'baz' => 2, + ], + ]; + + $expected = [ + 'foo' => '1', + 'bar' => [ + 'baz' => '2', + ], + ]; + + $request = new Request('http://www.example.com/', 'get', $parameters); + $this->assertSame($expected, $request->getParameters()); + } } diff --git a/src/Symfony/Component/BrowserKit/composer.json b/src/Symfony/Component/BrowserKit/composer.json index 38150e485dbca..7b3cc36623354 100644 --- a/src/Symfony/Component/BrowserKit/composer.json +++ b/src/Symfony/Component/BrowserKit/composer.json @@ -37,7 +37,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "5.1-dev" + "dev-master": "5.2-dev" } } } diff --git a/src/Symfony/Component/Cache/Adapter/RedisTagAwareAdapter.php b/src/Symfony/Component/Cache/Adapter/RedisTagAwareAdapter.php index 4bcaaddb013bc..0f541a2603559 100644 --- a/src/Symfony/Component/Cache/Adapter/RedisTagAwareAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/RedisTagAwareAdapter.php @@ -149,7 +149,11 @@ protected function doDeleteYieldTags(array $ids): iterable { $lua = <<<'EOLUA' local v = redis.call('GET', KEYS[1]) - redis.call('DEL', KEYS[1]) + local e = redis.pcall('UNLINK', KEYS[1]) + + if type(e) ~= 'number' then + redis.call('DEL', KEYS[1]) + end if not v or v:len() <= 13 or v:byte(1) ~= 0x9D or v:byte(6) ~= 0 or v:byte(10) ~= 0x5F then return '' diff --git a/src/Symfony/Component/Cache/CHANGELOG.md b/src/Symfony/Component/Cache/CHANGELOG.md index 2295089ad453f..33889dbe777fd 100644 --- a/src/Symfony/Component/Cache/CHANGELOG.md +++ b/src/Symfony/Component/Cache/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +5.2.0 +----- + + * added integration with Messenger to allow computing cached values in a worker + 5.1.0 ----- diff --git a/src/Symfony/Component/Cache/DependencyInjection/CachePoolPass.php b/src/Symfony/Component/Cache/DependencyInjection/CachePoolPass.php index f52d0271e4117..05bee86412be7 100644 --- a/src/Symfony/Component/Cache/DependencyInjection/CachePoolPass.php +++ b/src/Symfony/Component/Cache/DependencyInjection/CachePoolPass.php @@ -14,6 +14,7 @@ use Symfony\Component\Cache\Adapter\AbstractAdapter; use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Cache\Adapter\ChainAdapter; +use Symfony\Component\Cache\Messenger\EarlyExpirationDispatcher; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -32,8 +33,11 @@ class CachePoolPass implements CompilerPassInterface private $cachePoolClearerTag; private $cacheSystemClearerId; private $cacheSystemClearerTag; + private $reverseContainerId; + private $reversibleTag; + private $messageHandlerId; - public function __construct(string $cachePoolTag = 'cache.pool', string $kernelResetTag = 'kernel.reset', string $cacheClearerId = 'cache.global_clearer', string $cachePoolClearerTag = 'cache.pool.clearer', string $cacheSystemClearerId = 'cache.system_clearer', string $cacheSystemClearerTag = 'kernel.cache_clearer') + public function __construct(string $cachePoolTag = 'cache.pool', string $kernelResetTag = 'kernel.reset', string $cacheClearerId = 'cache.global_clearer', string $cachePoolClearerTag = 'cache.pool.clearer', string $cacheSystemClearerId = 'cache.system_clearer', string $cacheSystemClearerTag = 'kernel.cache_clearer', string $reverseContainerId = 'reverse_container', string $reversibleTag = 'container.reversible', string $messageHandlerId = 'cache.early_expiration_handler') { $this->cachePoolTag = $cachePoolTag; $this->kernelResetTag = $kernelResetTag; @@ -41,6 +45,9 @@ public function __construct(string $cachePoolTag = 'cache.pool', string $kernelR $this->cachePoolClearerTag = $cachePoolClearerTag; $this->cacheSystemClearerId = $cacheSystemClearerId; $this->cacheSystemClearerTag = $cacheSystemClearerTag; + $this->reverseContainerId = $reverseContainerId; + $this->reversibleTag = $reversibleTag; + $this->messageHandlerId = $messageHandlerId; } /** @@ -49,12 +56,13 @@ public function __construct(string $cachePoolTag = 'cache.pool', string $kernelR public function process(ContainerBuilder $container) { if ($container->hasParameter('cache.prefix.seed')) { - $seed = '.'.$container->getParameterBag()->resolveValue($container->getParameter('cache.prefix.seed')); + $seed = $container->getParameterBag()->resolveValue($container->getParameter('cache.prefix.seed')); } else { $seed = '_'.$container->getParameter('kernel.project_dir'); + $seed .= '.'.$container->getParameter('kernel.container_class'); } - $seed .= '.'.$container->getParameter('kernel.container_class'); + $needsMessageHandler = false; $allPools = []; $clearers = []; $attributes = [ @@ -62,6 +70,7 @@ public function process(ContainerBuilder $container) 'name', 'namespace', 'default_lifetime', + 'early_expiration_message_bus', 'reset', ]; foreach ($container->findTaggedServiceIds($this->cachePoolTag) as $id => $tags) { @@ -155,13 +164,24 @@ public function process(ContainerBuilder $container) if ($tags[0][$attr]) { $pool->addTag($this->kernelResetTag, ['method' => $tags[0][$attr]]); } + } elseif ('early_expiration_message_bus' === $attr) { + $needsMessageHandler = true; + $pool->addMethodCall('setCallbackWrapper', [(new Definition(EarlyExpirationDispatcher::class)) + ->addArgument(new Reference($tags[0]['early_expiration_message_bus'])) + ->addArgument(new Reference($this->reverseContainerId)) + ->addArgument((new Definition('callable')) + ->setFactory([new Reference($id), 'setCallbackWrapper']) + ->addArgument(null) + ), + ]); + $pool->addTag($this->reversibleTag); } elseif ('namespace' !== $attr || ArrayAdapter::class !== $class) { $pool->replaceArgument($i++, $tags[0][$attr]); } unset($tags[0][$attr]); } if (!empty($tags[0])) { - throw new InvalidArgumentException(sprintf('Invalid "%s" tag for service "%s": accepted attributes are "clearer", "provider", "name", "namespace", "default_lifetime" and "reset", found "%s".', $this->cachePoolTag, $id, implode('", "', array_keys($tags[0])))); + throw new InvalidArgumentException(sprintf('Invalid "%s" tag for service "%s": accepted attributes are "clearer", "provider", "name", "namespace", "default_lifetime", "early_expiration_message_bus" and "reset", found "%s".', $this->cachePoolTag, $id, implode('", "', array_keys($tags[0])))); } if (null !== $clearer) { @@ -171,6 +191,10 @@ public function process(ContainerBuilder $container) $allPools[$name] = new Reference($id, $container::IGNORE_ON_UNINITIALIZED_REFERENCE); } + if (!$needsMessageHandler) { + $container->removeDefinition($this->messageHandlerId); + } + $notAliasedCacheClearerId = $this->cacheClearerId; while ($container->hasAlias($this->cacheClearerId)) { $this->cacheClearerId = (string) $container->getAlias($this->cacheClearerId); diff --git a/src/Symfony/Component/Cache/Messenger/EarlyExpirationDispatcher.php b/src/Symfony/Component/Cache/Messenger/EarlyExpirationDispatcher.php new file mode 100644 index 0000000000000..6f11b8b5a2078 --- /dev/null +++ b/src/Symfony/Component/Cache/Messenger/EarlyExpirationDispatcher.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Messenger; + +use Psr\Log\LoggerInterface; +use Symfony\Component\Cache\Adapter\AdapterInterface; +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\DependencyInjection\ReverseContainer; +use Symfony\Component\Messenger\MessageBusInterface; +use Symfony\Component\Messenger\Stamp\HandledStamp; + +/** + * Sends the computation of cached values to a message bus. + */ +class EarlyExpirationDispatcher +{ + private $bus; + private $reverseContainer; + private $callbackWrapper; + + public function __construct(MessageBusInterface $bus, ReverseContainer $reverseContainer, callable $callbackWrapper = null) + { + $this->bus = $bus; + $this->reverseContainer = $reverseContainer; + $this->callbackWrapper = $callbackWrapper; + } + + public function __invoke(callable $callback, CacheItem $item, bool &$save, AdapterInterface $pool, \Closure $setMetadata, LoggerInterface $logger = null) + { + if (!$item->isHit() || null === $message = EarlyExpirationMessage::create($this->reverseContainer, $callback, $item, $pool)) { + // The item is stale or the callback cannot be reversed: we must compute the value now + $logger && $logger->info('Computing item "{key}" online: '.($item->isHit() ? 'callback cannot be reversed' : 'item is stale'), ['key' => $item->getKey()]); + + return null !== $this->callbackWrapper ? ($this->callbackWrapper)($callback, $item, $save, $pool, $setMetadata, $logger) : $callback($item, $save); + } + + $envelope = $this->bus->dispatch($message); + + if ($logger) { + if ($envelope->last(HandledStamp::class)) { + $logger->info('Item "{key}" was computed online', ['key' => $item->getKey()]); + } else { + $logger->info('Item "{key}" sent for recomputation', ['key' => $item->getKey()]); + } + } + + // The item's value is not stale, no need to write it to the backend + $save = false; + + return $message->getItem()->get() ?? $item->get(); + } +} diff --git a/src/Symfony/Component/Cache/Messenger/EarlyExpirationHandler.php b/src/Symfony/Component/Cache/Messenger/EarlyExpirationHandler.php new file mode 100644 index 0000000000000..d7c4632e228f4 --- /dev/null +++ b/src/Symfony/Component/Cache/Messenger/EarlyExpirationHandler.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Messenger; + +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\DependencyInjection\ReverseContainer; +use Symfony\Component\Messenger\Handler\MessageHandlerInterface; + +/** + * Computes cached values sent to a message bus. + */ +class EarlyExpirationHandler implements MessageHandlerInterface +{ + private $reverseContainer; + private $processedNonces = []; + + public function __construct(ReverseContainer $reverseContainer) + { + $this->reverseContainer = $reverseContainer; + } + + public function __invoke(EarlyExpirationMessage $message) + { + $item = $message->getItem(); + $metadata = $item->getMetadata(); + $expiry = $metadata[CacheItem::METADATA_EXPIRY] ?? 0; + $ctime = $metadata[CacheItem::METADATA_CTIME] ?? 0; + + if ($expiry && $ctime) { + // skip duplicate or expired messages + + $processingNonce = [$expiry, $ctime]; + $pool = $message->getPool(); + $key = $item->getKey(); + + if (($this->processedNonces[$pool][$key] ?? null) === $processingNonce) { + return; + } + + if (microtime(true) >= $expiry) { + return; + } + + $this->processedNonces[$pool] = [$key => $processingNonce] + ($this->processedNonces[$pool] ?? []); + + if (\count($this->processedNonces[$pool]) > 100) { + array_pop($this->processedNonces[$pool]); + } + } + + static $setMetadata; + + $setMetadata = $setMetadata ?? \Closure::bind( + function (CacheItem $item, float $startTime) { + if ($item->expiry > $endTime = microtime(true)) { + $item->newMetadata[CacheItem::METADATA_EXPIRY] = $item->expiry; + $item->newMetadata[CacheItem::METADATA_CTIME] = (int) ceil(1000 * ($endTime - $startTime)); + } + }, + null, + CacheItem::class + ); + + $startTime = microtime(true); + $pool = $message->findPool($this->reverseContainer); + $callback = $message->findCallback($this->reverseContainer); + $value = $callback($item); + $setMetadata($item, $startTime); + $pool->save($item->set($value)); + } +} diff --git a/src/Symfony/Component/Cache/Messenger/EarlyExpirationMessage.php b/src/Symfony/Component/Cache/Messenger/EarlyExpirationMessage.php new file mode 100644 index 0000000000000..e25c07e9a66be --- /dev/null +++ b/src/Symfony/Component/Cache/Messenger/EarlyExpirationMessage.php @@ -0,0 +1,97 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Messenger; + +use Symfony\Component\Cache\Adapter\AdapterInterface; +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\DependencyInjection\ReverseContainer; + +/** + * Conveys a cached value that needs to be computed. + */ +final class EarlyExpirationMessage +{ + private $item; + private $pool; + private $callback; + + public static function create(ReverseContainer $reverseContainer, callable $callback, CacheItem $item, AdapterInterface $pool): ?self + { + try { + $item = clone $item; + $item->set(null); + } catch (\Exception $e) { + return null; + } + + $pool = $reverseContainer->getId($pool); + + if (\is_object($callback)) { + if (null === $id = $reverseContainer->getId($callback)) { + return null; + } + + $callback = '@'.$id; + } elseif (!\is_array($callback)) { + $callback = (string) $callback; + } elseif (!\is_object($callback[0])) { + $callback = [(string) $callback[0], (string) $callback[1]]; + } else { + if (null === $id = $reverseContainer->getId($callback[0])) { + return null; + } + + $callback = ['@'.$id, (string) $callback[1]]; + } + + return new self($item, $pool, $callback); + } + + public function getItem(): CacheItem + { + return $this->item; + } + + public function getPool(): string + { + return $this->pool; + } + + public function getCallback() + { + return $this->callback; + } + + public function findPool(ReverseContainer $reverseContainer): AdapterInterface + { + return $reverseContainer->getService($this->pool); + } + + public function findCallback(ReverseContainer $reverseContainer): callable + { + if (\is_string($callback = $this->callback)) { + return '@' === $callback[0] ? $reverseContainer->getService(substr($callback, 1)) : $callback; + } + if ('@' === $callback[0][0]) { + $callback[0] = $reverseContainer->getService(substr($callback[0], 1)); + } + + return $callback; + } + + private function __construct(CacheItem $item, string $pool, $callback) + { + $this->item = $item; + $this->pool = $pool; + $this->callback = $callback; + } +} diff --git a/src/Symfony/Component/Cache/Tests/Adapter/FilesystemAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/FilesystemAdapterTest.php index 54264eeac5b42..74c6ee870477f 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/FilesystemAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/FilesystemAdapterTest.php @@ -13,6 +13,7 @@ use Psr\Cache\CacheItemPoolInterface; use Symfony\Component\Cache\Adapter\FilesystemAdapter; +use Symfony\Component\Filesystem\Filesystem; /** * @group time-sensitive @@ -26,29 +27,7 @@ public function createCachePool(int $defaultLifetime = 0): CacheItemPoolInterfac public static function tearDownAfterClass(): void { - self::rmdir(sys_get_temp_dir().'/symfony-cache'); - } - - public static function rmdir(string $dir) - { - if (!file_exists($dir)) { - return; - } - if (!$dir || 0 !== strpos(\dirname($dir), sys_get_temp_dir())) { - throw new \Exception(__METHOD__."() operates only on subdirs of system's temp dir"); - } - $children = new \RecursiveIteratorIterator( - new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS), - \RecursiveIteratorIterator::CHILD_FIRST - ); - foreach ($children as $child) { - if ($child->isDir()) { - rmdir($child); - } else { - unlink($child); - } - } - rmdir($dir); + (new Filesystem())->remove(sys_get_temp_dir().'/symfony-cache'); } protected function isPruned(CacheItemPoolInterface $cache, string $name): bool diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterTest.php index 69f334656d58e..f1ee0d6c71dff 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterTest.php @@ -16,6 +16,7 @@ use Symfony\Component\Cache\Adapter\FilesystemAdapter; use Symfony\Component\Cache\Adapter\NullAdapter; use Symfony\Component\Cache\Adapter\PhpArrayAdapter; +use Symfony\Component\Filesystem\Filesystem; /** * @group time-sensitive @@ -69,7 +70,7 @@ protected function tearDown(): void $this->createCachePool()->clear(); if (file_exists(sys_get_temp_dir().'/symfony-cache')) { - FilesystemAdapterTest::rmdir(sys_get_temp_dir().'/symfony-cache'); + (new Filesystem())->remove(sys_get_temp_dir().'/symfony-cache'); } } diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterWithFallbackTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterWithFallbackTest.php index a3e998b4b2af2..265b55e5ea392 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterWithFallbackTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterWithFallbackTest.php @@ -14,6 +14,7 @@ use Psr\Cache\CacheItemPoolInterface; use Symfony\Component\Cache\Adapter\FilesystemAdapter; use Symfony\Component\Cache\Adapter\PhpArrayAdapter; +use Symfony\Component\Filesystem\Filesystem; /** * @group time-sensitive @@ -41,7 +42,7 @@ protected function tearDown(): void $this->createCachePool()->clear(); if (file_exists(sys_get_temp_dir().'/symfony-cache')) { - FilesystemAdapterTest::rmdir(sys_get_temp_dir().'/symfony-cache'); + (new Filesystem())->remove(sys_get_temp_dir().'/symfony-cache'); } } diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PhpFilesAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PhpFilesAdapterTest.php index d204ef8d2993b..e084114e48625 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/PhpFilesAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/PhpFilesAdapterTest.php @@ -13,6 +13,7 @@ use Psr\Cache\CacheItemPoolInterface; use Symfony\Component\Cache\Adapter\PhpFilesAdapter; +use Symfony\Component\Filesystem\Filesystem; /** * @group time-sensitive @@ -30,7 +31,7 @@ public function createCachePool(): CacheItemPoolInterface public static function tearDownAfterClass(): void { - FilesystemAdapterTest::rmdir(sys_get_temp_dir().'/symfony-cache'); + (new Filesystem())->remove(sys_get_temp_dir().'/symfony-cache'); } protected function isPruned(CacheItemPoolInterface $cache, string $name): bool diff --git a/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterTest.php index 4d60f4cbd418c..bb47794b86aaf 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterTest.php @@ -20,6 +20,7 @@ use Symfony\Component\Cache\Adapter\TagAwareAdapter; use Symfony\Component\Cache\Tests\Fixtures\PrunableAdapter; use Symfony\Component\Cache\Tests\Traits\TagAwareTestTrait; +use Symfony\Component\Filesystem\Filesystem; /** * @group time-sensitive @@ -35,7 +36,7 @@ public function createCachePool($defaultLifetime = 0): CacheItemPoolInterface public static function tearDownAfterClass(): void { - FilesystemAdapterTest::rmdir(sys_get_temp_dir().'/symfony-cache'); + (new Filesystem())->remove(sys_get_temp_dir().'/symfony-cache'); } /** diff --git a/src/Symfony/Component/Cache/Tests/DependencyInjection/CachePoolPassTest.php b/src/Symfony/Component/Cache/Tests/DependencyInjection/CachePoolPassTest.php index 20701adcb4507..9c230837a5f81 100644 --- a/src/Symfony/Component/Cache/Tests/DependencyInjection/CachePoolPassTest.php +++ b/src/Symfony/Component/Cache/Tests/DependencyInjection/CachePoolPassTest.php @@ -135,7 +135,7 @@ public function testArgsAreReplaced() $this->assertInstanceOf(Reference::class, $cachePool->getArgument(0)); $this->assertSame('foobar', (string) $cachePool->getArgument(0)); - $this->assertSame('tQNhcV-8xa', $cachePool->getArgument(1)); + $this->assertSame('6Ridbw4aMn', $cachePool->getArgument(1)); $this->assertSame(3, $cachePool->getArgument(2)); } @@ -156,7 +156,7 @@ public function testWithNameAttribute() $this->cachePoolPass->process($container); - $this->assertSame('+naTpPa4Sm', $cachePool->getArgument(1)); + $this->assertSame('PeXBWSl6ca', $cachePool->getArgument(1)); } public function testThrowsExceptionWhenCachePoolTagHasUnknownAttributes() diff --git a/src/Symfony/Component/Cache/Tests/Messenger/EarlyExpirationDispatcherTest.php b/src/Symfony/Component/Cache/Tests/Messenger/EarlyExpirationDispatcherTest.php new file mode 100644 index 0000000000000..56c505f4b02af --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Messenger/EarlyExpirationDispatcherTest.php @@ -0,0 +1,134 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Messenger; + +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; +use Psr\Log\Test\TestLogger; +use Symfony\Component\Cache\Adapter\AdapterInterface; +use Symfony\Component\Cache\Adapter\FilesystemAdapter; +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\Messenger\EarlyExpirationDispatcher; +use Symfony\Component\Cache\Messenger\EarlyExpirationMessage; +use Symfony\Component\DependencyInjection\Container; +use Symfony\Component\DependencyInjection\ReverseContainer; +use Symfony\Component\DependencyInjection\ServiceLocator; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\MessageBusInterface; + +/** + * @requires function Symfony\Component\DependencyInjection\ReverseContainer::__construct + */ +class EarlyExpirationDispatcherTest extends TestCase +{ + public static function tearDownAfterClass(): void + { + (new Filesystem())->remove(sys_get_temp_dir().'/symfony-cache'); + } + + public function testFetch() + { + $logger = new TestLogger(); + $pool = new FilesystemAdapter(); + $pool->setLogger($logger); + + $item = $pool->getItem('foo'); + + $computationService = new class() { + public function __invoke(CacheItem $item) + { + return 123; + } + }; + + $container = new Container(); + $container->set('computation_service', $computationService); + $container->set('cache_pool', $pool); + + $reverseContainer = new ReverseContainer($container, new ServiceLocator([])); + + $bus = $this->getMockBuilder(MessageBusInterface::class)->getMock(); + + $dispatcher = new EarlyExpirationDispatcher($bus, $reverseContainer); + + $saveResult = null; + $pool->setCallbackWrapper(function (callable $callback, CacheItem $item, bool &$save, AdapterInterface $pool, \Closure $setMetadata, ?LoggerInterface $logger) use ($dispatcher, &$saveResult) { + try { + return $dispatcher($callback, $item, $save, $pool, $setMetadata, $logger); + } finally { + $saveResult = $save; + } + }); + + $this->assertSame(345, $pool->get('foo', function () { return 345; })); + $this->assertTrue($saveResult); + + $expected = [ + [ + 'level' => 'info', + 'message' => 'Computing item "{key}" online: item is stale', + 'context' => ['key' => 'foo'], + ], + ]; + $this->assertSame($expected, $logger->records); + } + + public function testEarlyExpiration() + { + $logger = new TestLogger(); + $pool = new FilesystemAdapter(); + $pool->setLogger($logger); + + $item = $pool->getItem('foo'); + $pool->save($item->set(789)); + $item = $pool->getItem('foo'); + + $computationService = new class() { + public function __invoke(CacheItem $item) + { + return 123; + } + }; + + $container = new Container(); + $container->set('computation_service', $computationService); + $container->set('cache_pool', $pool); + + $reverseContainer = new ReverseContainer($container, new ServiceLocator([])); + $msg = EarlyExpirationMessage::create($reverseContainer, $computationService, $item, $pool); + + $bus = $this->getMockBuilder(MessageBusInterface::class)->getMock(); + $bus->expects($this->once()) + ->method('dispatch') + ->with($msg) + ->willReturn(new Envelope($msg)); + + $dispatcher = new EarlyExpirationDispatcher($bus, $reverseContainer); + + $saveResult = true; + $setMetadata = function () { + }; + $dispatcher($computationService, $item, $saveResult, $pool, $setMetadata, $logger); + + $this->assertFalse($saveResult); + + $expected = [ + [ + 'level' => 'info', + 'message' => 'Item "{key}" sent for recomputation', + 'context' => ['key' => 'foo'], + ], + ]; + $this->assertSame($expected, $logger->records); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Messenger/EarlyExpirationHandlerTest.php b/src/Symfony/Component/Cache/Tests/Messenger/EarlyExpirationHandlerTest.php new file mode 100644 index 0000000000000..1953d2274e7a0 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Messenger/EarlyExpirationHandlerTest.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Messenger; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Cache\Adapter\FilesystemAdapter; +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\Messenger\EarlyExpirationHandler; +use Symfony\Component\Cache\Messenger\EarlyExpirationMessage; +use Symfony\Component\DependencyInjection\Container; +use Symfony\Component\DependencyInjection\ReverseContainer; +use Symfony\Component\DependencyInjection\ServiceLocator; +use Symfony\Component\Filesystem\Filesystem; + +/** + * @requires function Symfony\Component\DependencyInjection\ReverseContainer::__construct + */ +class EarlyExpirationHandlerTest extends TestCase +{ + public static function tearDownAfterClass(): void + { + (new Filesystem())->remove(sys_get_temp_dir().'/symfony-cache'); + } + + public function testHandle() + { + $pool = new FilesystemAdapter(); + $item = $pool->getItem('foo'); + $item->set(234); + + $computationService = new class() { + public function __invoke(CacheItem $item) + { + usleep(30000); + $item->expiresAfter(3600); + + return 123; + } + }; + + $container = new Container(); + $container->set('computation_service', $computationService); + $container->set('cache_pool', $pool); + + $reverseContainer = new ReverseContainer($container, new ServiceLocator([])); + + $msg = EarlyExpirationMessage::create($reverseContainer, $computationService, $item, $pool); + + $handler = new EarlyExpirationHandler($reverseContainer); + + $handler($msg); + + $this->assertSame(123, $pool->get('foo', [$this, 'fail'], 0.0, $metadata)); + + $this->assertGreaterThan(25, $metadata['ctime']); + $this->assertGreaterThan(time(), $metadata['expiry']); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Messenger/EarlyExpirationMessageTest.php b/src/Symfony/Component/Cache/Tests/Messenger/EarlyExpirationMessageTest.php new file mode 100644 index 0000000000000..038357a499718 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Messenger/EarlyExpirationMessageTest.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Messenger; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Cache\Adapter\ArrayAdapter; +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\Messenger\EarlyExpirationMessage; +use Symfony\Component\DependencyInjection\Container; +use Symfony\Component\DependencyInjection\ReverseContainer; +use Symfony\Component\DependencyInjection\ServiceLocator; + +/** + * @requires function Symfony\Component\DependencyInjection\ReverseContainer::__construct + */ +class EarlyExpirationMessageTest extends TestCase +{ + public function testCreate() + { + $pool = new ArrayAdapter(); + $item = $pool->getItem('foo'); + $item->set(234); + + $computationService = new class() { + public function __invoke(CacheItem $item) + { + return 123; + } + }; + + $container = new Container(); + $container->set('computation_service', $computationService); + $container->set('cache_pool', $pool); + + $reverseContainer = new ReverseContainer($container, new ServiceLocator([])); + + $msg = EarlyExpirationMessage::create($reverseContainer, [$computationService, '__invoke'], $item, $pool); + + $this->assertSame('cache_pool', $msg->getPool()); + $this->assertSame($pool, $msg->findPool($reverseContainer)); + + $this->assertSame('foo', $msg->getItem()->getKey()); + $this->assertNull($msg->getItem()->get()); + $this->assertSame(234, $item->get()); + + $this->assertSame(['@computation_service', '__invoke'], $msg->getCallback()); + $this->assertSame([$computationService, '__invoke'], $msg->findCallback($reverseContainer)); + + $msg = EarlyExpirationMessage::create($reverseContainer, $computationService, $item, $pool); + + $this->assertSame('@computation_service', $msg->getCallback()); + $this->assertSame($computationService, $msg->findCallback($reverseContainer)); + } +} diff --git a/src/Symfony/Component/Cache/Traits/RedisTrait.php b/src/Symfony/Component/Cache/Traits/RedisTrait.php index 7910f95c40c84..f17866985801b 100644 --- a/src/Symfony/Component/Cache/Traits/RedisTrait.php +++ b/src/Symfony/Component/Cache/Traits/RedisTrait.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Cache\Traits; +use Predis\Command\Redis\UNLINK; use Predis\Connection\Aggregate\ClusterInterface; use Predis\Connection\Aggregate\RedisCluster; use Predis\Response\Status; @@ -363,7 +364,8 @@ protected function doClear(string $namespace) // As documented in Redis documentation (http://redis.io/commands/keys) using KEYS // can hang your server when it is executed against large databases (millions of items). // Whenever you hit this scale, you should really consider upgrading to Redis 2.8 or above. - $cleared = $host->eval("local keys=redis.call('KEYS',ARGV[1]..'*') for i=1,#keys,5000 do redis.call('DEL',unpack(keys,i,math.min(i+4999,#keys))) end return 1", $evalArgs[0], $evalArgs[1]) && $cleared; + $unlink = version_compare($info['redis_version'], '4.0', '>=') ? 'UNLINK' : 'DEL'; + $cleared = $host->eval("local keys=redis.call('KEYS',ARGV[1]..'*') for i=1,#keys,5000 do redis.call('$unlink',unpack(keys,i,math.min(i+4999,#keys))) end return 1", $evalArgs[0], $evalArgs[1]) && $cleared; continue; } @@ -393,12 +395,27 @@ protected function doDelete(array $ids) } if ($this->redis instanceof \Predis\ClientInterface && $this->redis->getConnection() instanceof ClusterInterface) { - $this->pipeline(function () use ($ids) { + static $del; + $del = $del ?? (class_exists(UNLINK::class) ? 'unlink' : 'del'); + + $this->pipeline(function () use ($ids, $del) { foreach ($ids as $id) { - yield 'del' => [$id]; + yield $del => [$id]; } })->rewind(); } else { + static $unlink = true; + + if ($unlink) { + try { + $this->redis->unlink($ids); + + return true; + } catch (\Throwable $e) { + $unlink = false; + } + } + $this->redis->del($ids); } diff --git a/src/Symfony/Component/Cache/composer.json b/src/Symfony/Component/Cache/composer.json index 7ced133b4c6dc..c1363a3e3578c 100644 --- a/src/Symfony/Component/Cache/composer.json +++ b/src/Symfony/Component/Cache/composer.json @@ -23,7 +23,7 @@ "require": { "php": ">=7.2.5", "psr/cache": "~1.0", - "psr/log": "~1.0", + "psr/log": "^1.1", "symfony/cache-contracts": "^1.1.7|^2", "symfony/polyfill-php80": "^1.15", "symfony/service-contracts": "^1.1|^2", @@ -32,15 +32,17 @@ "require-dev": { "cache/integration-tests": "dev-master", "doctrine/cache": "^1.6", - "doctrine/dbal": "^2.5|^3.0", + "doctrine/dbal": "^2.10|^3.0", "predis/predis": "^1.1", "psr/simple-cache": "^1.0", "symfony/config": "^4.4|^5.0", "symfony/dependency-injection": "^4.4|^5.0", + "symfony/filesystem": "^4.4|^5.0", + "symfony/messenger": "^4.4|^5.0", "symfony/var-dumper": "^4.4|^5.0" }, "conflict": { - "doctrine/dbal": "<2.5", + "doctrine/dbal": "<2.10", "symfony/dependency-injection": "<4.4", "symfony/http-kernel": "<4.4", "symfony/var-dumper": "<4.4" @@ -54,7 +56,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "5.1-dev" + "dev-master": "5.2-dev" } } } diff --git a/src/Symfony/Component/Config/Definition/ArrayNode.php b/src/Symfony/Component/Config/Definition/ArrayNode.php index 2a828caf78538..1107fff232294 100644 --- a/src/Symfony/Component/Config/Definition/ArrayNode.php +++ b/src/Symfony/Component/Config/Definition/ArrayNode.php @@ -213,7 +213,13 @@ protected function finalizeValue($value) foreach ($this->children as $name => $child) { if (!\array_key_exists($name, $value)) { if ($child->isRequired()) { - $ex = new InvalidConfigurationException(sprintf('The child node "%s" at path "%s" must be configured.', $name, $this->getPath())); + $message = sprintf('The child config "%s" under "%s" must be configured', $name, $this->getPath()); + if ($child->getInfo()) { + $message .= sprintf(": %s", $child->getInfo()); + } else { + $message .= '.'; + } + $ex = new InvalidConfigurationException($message); $ex->setPath($this->getPath()); throw $ex; diff --git a/src/Symfony/Component/Config/composer.json b/src/Symfony/Component/Config/composer.json index 70072cfa2ea7d..00703c93b8a8a 100644 --- a/src/Symfony/Component/Config/composer.json +++ b/src/Symfony/Component/Config/composer.json @@ -44,7 +44,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "5.1-dev" + "dev-master": "5.2-dev" } } } diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index 784af89b3189a..79217013ee5bb 100644 --- a/src/Symfony/Component/Console/Application.php +++ b/src/Symfony/Component/Console/Application.php @@ -14,9 +14,11 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\HelpCommand; use Symfony\Component\Console\Command\ListCommand; +use Symfony\Component\Console\Command\SignalableCommandInterface; use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; use Symfony\Component\Console\Event\ConsoleCommandEvent; use Symfony\Component\Console\Event\ConsoleErrorEvent; +use Symfony\Component\Console\Event\ConsoleSignalEvent; use Symfony\Component\Console\Event\ConsoleTerminateEvent; use Symfony\Component\Console\Exception\CommandNotFoundException; use Symfony\Component\Console\Exception\ExceptionInterface; @@ -39,6 +41,7 @@ use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\SignalRegistry\SignalRegistry; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\ErrorHandler\ErrorHandler; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; @@ -76,6 +79,8 @@ class Application implements ResetInterface private $defaultCommand; private $singleCommand = false; private $initialized; + private $signalRegistry; + private $signalsToDispatchEvent = []; public function __construct(string $name = 'UNKNOWN', string $version = 'UNKNOWN') { @@ -83,6 +88,10 @@ public function __construct(string $name = 'UNKNOWN', string $version = 'UNKNOWN $this->version = $version; $this->terminal = new Terminal(); $this->defaultCommand = 'list'; + $this->signalRegistry = new SignalRegistry(); + if (\defined('SIGINT')) { + $this->signalsToDispatchEvent = [\SIGINT, \SIGTERM, \SIGUSR1, \SIGUSR2]; + } } /** @@ -98,6 +107,16 @@ public function setCommandLoader(CommandLoaderInterface $commandLoader) $this->commandLoader = $commandLoader; } + public function getSignalRegistry(): SignalRegistry + { + return $this->signalRegistry; + } + + public function setSignalsToDispatchEvent(int ...$signalsToDispatchEvent) + { + $this->signalsToDispatchEvent = $signalsToDispatchEvent; + } + /** * Runs the current application. * @@ -262,6 +281,23 @@ public function doRun(InputInterface $input, OutputInterface $output) $command = $this->find($alternative); } + if ($this->dispatcher) { + foreach ($this->signalsToDispatchEvent as $signal) { + $event = new ConsoleSignalEvent($command, $input, $output, $signal); + + $this->signalRegistry->register($signal, function ($signal, $hasNext) use ($event) { + $this->dispatcher->dispatch($event, ConsoleEvents::SIGNAL); + + // No more handlers, we try to simulate PHP default behavior + if (!$hasNext) { + if (!\in_array($signal, [\SIGUSR1, \SIGUSR2], true)) { + exit(0); + } + } + }); + } + } + $this->runningCommand = $command; $exitCode = $this->doRunCommand($command, $input, $output); $this->runningCommand = null; @@ -916,6 +952,12 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI } } + if ($command instanceof SignalableCommandInterface) { + foreach ($command->getSubscribedSignals() as $signal) { + $this->signalRegistry->register($signal, [$command, 'handleSignal']); + } + } + if (null === $this->dispatcher) { return $command->run($input, $output); } @@ -978,8 +1020,7 @@ protected function getDefaultInputDefinition() { return new InputDefinition([ new InputArgument('command', InputArgument::REQUIRED, 'The command to execute'), - - new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display this help message'), + new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display help for the given command. When no command is given display help for the '.$this->defaultCommand.' command'), new InputOption('--quiet', '-q', InputOption::VALUE_NONE, 'Do not output any message'), new InputOption('--verbose', '-v|vv|vvv', InputOption::VALUE_NONE, 'Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug'), new InputOption('--version', '-V', InputOption::VALUE_NONE, 'Display this application version'), diff --git a/src/Symfony/Component/Console/CHANGELOG.md b/src/Symfony/Component/Console/CHANGELOG.md index 788bf4279a40c..c5a69637e1ba8 100644 --- a/src/Symfony/Component/Console/CHANGELOG.md +++ b/src/Symfony/Component/Console/CHANGELOG.md @@ -1,6 +1,19 @@ CHANGELOG ========= +5.2.0 +----- + + * Added `SingleCommandApplication::setAutoExit()` to allow testing via `CommandTester` + * added support for multiline responses to questions through `Question::setMultiline()` + and `Question::isMultiline()` + * Added `SignalRegistry` class to stack signals handlers + * Added support for signals: + * Added `Application::getSignalRegistry()` and `Application::setSignalsToDispatchEvent()` methods + * Added `SignalableCommandInterface` interface + * Added `TableCellStyle` class to customize table cell + * Removed `php ` prefix invocation from help messages. + 5.1.0 ----- diff --git a/src/Symfony/Component/Console/Color.php b/src/Symfony/Component/Console/Color.php new file mode 100644 index 0000000000000..b45f4523b9d25 --- /dev/null +++ b/src/Symfony/Component/Console/Color.php @@ -0,0 +1,165 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console; + +use Symfony\Component\Console\Exception\InvalidArgumentException; + +/** + * @author Fabien Potencier + */ +final class Color +{ + private const COLORS = [ + 'black' => 0, + 'red' => 1, + 'green' => 2, + 'yellow' => 3, + 'blue' => 4, + 'magenta' => 5, + 'cyan' => 6, + 'white' => 7, + 'default' => 9, + ]; + + private const AVAILABLE_OPTIONS = [ + 'bold' => ['set' => 1, 'unset' => 22], + 'underscore' => ['set' => 4, 'unset' => 24], + 'blink' => ['set' => 5, 'unset' => 25], + 'reverse' => ['set' => 7, 'unset' => 27], + 'conceal' => ['set' => 8, 'unset' => 28], + ]; + + private $foreground; + private $background; + private $options = []; + + public function __construct(string $foreground = '', string $background = '', array $options = []) + { + $this->foreground = $this->parseColor($foreground); + $this->background = $this->parseColor($background); + + foreach ($options as $option) { + if (!isset(self::AVAILABLE_OPTIONS[$option])) { + throw new InvalidArgumentException(sprintf('Invalid option specified: "%s". Expected one of (%s).', $option, implode(', ', array_keys(self::AVAILABLE_OPTIONS)))); + } + + $this->options[$option] = self::AVAILABLE_OPTIONS[$option]; + } + } + + public function apply(string $text): string + { + return $this->set().$text.$this->unset(); + } + + public function set(): string + { + $setCodes = []; + if ('' !== $this->foreground) { + $setCodes[] = '3'.$this->foreground; + } + if ('' !== $this->background) { + $setCodes[] = '4'.$this->background; + } + foreach ($this->options as $option) { + $setCodes[] = $option['set']; + } + if (0 === \count($setCodes)) { + return ''; + } + + return sprintf("\033[%sm", implode(';', $setCodes)); + } + + public function unset(): string + { + $unsetCodes = []; + if ('' !== $this->foreground) { + $unsetCodes[] = 39; + } + if ('' !== $this->background) { + $unsetCodes[] = 49; + } + foreach ($this->options as $option) { + $unsetCodes[] = $option['unset']; + } + if (0 === \count($unsetCodes)) { + return ''; + } + + return sprintf("\033[%sm", implode(';', $unsetCodes)); + } + + private function parseColor(string $color): string + { + if ('' === $color) { + return ''; + } + + if ('#' === $color[0]) { + $color = substr($color, 1); + + if (3 === \strlen($color)) { + $color = $color[0].$color[0].$color[1].$color[1].$color[2].$color[2]; + } + + if (6 !== \strlen($color)) { + throw new InvalidArgumentException(sprintf('Invalid "%s" color.', $color)); + } + + return $this->convertHexColorToAnsi(hexdec($color)); + } + + if (!isset(self::COLORS[$color])) { + throw new InvalidArgumentException(sprintf('Invalid "%s" color; expected one of (%s).', $color, implode(', ', array_keys(self::COLORS)))); + } + + return (string) self::COLORS[$color]; + } + + private function convertHexColorToAnsi(int $color): string + { + $r = ($color >> 16) & 255; + $g = ($color >> 8) & 255; + $b = $color & 255; + + // see https://github.com/termstandard/colors/ for more information about true color support + if ('truecolor' !== getenv('COLORTERM')) { + return (string) $this->degradeHexColorToAnsi($r, $g, $b); + } + + return sprintf('8;2;%d;%d;%d', $r, $g, $b); + } + + private function degradeHexColorToAnsi(int $r, int $g, int $b): int + { + if (0 === round($this->getSaturation($r, $g, $b) / 50)) { + return 0; + } + + return (round($b / 255) << 2) | (round($g / 255) << 1) | round($r / 255); + } + + private function getSaturation(int $r, int $g, int $b): int + { + $r = $r / 255; + $g = $g / 255; + $b = $b / 255; + $v = max($r, $g, $b); + + if (0 === $diff = $v - min($r, $g, $b)) { + return 0; + } + + return (int) $diff * 100 / $v; + } +} diff --git a/src/Symfony/Component/Console/Command/Command.php b/src/Symfony/Component/Console/Command/Command.php index bcb86cdfbebbc..e5d4cf18a1f0e 100644 --- a/src/Symfony/Component/Console/Command/Command.php +++ b/src/Symfony/Component/Console/Command/Command.php @@ -45,9 +45,8 @@ class Command private $hidden = false; private $help = ''; private $description = ''; + private $fullDefinition; private $ignoreValidationErrors = false; - private $applicationDefinitionMerged = false; - private $applicationDefinitionMergedWithArgs = false; private $code; private $synopsis = []; private $usages = []; @@ -98,6 +97,8 @@ public function setApplication(Application $application = null) } else { $this->helperSet = null; } + + $this->fullDefinition = null; } public function setHelperSet(HelperSet $helperSet) @@ -205,16 +206,12 @@ protected function initialize(InputInterface $input, OutputInterface $output) */ public function run(InputInterface $input, OutputInterface $output) { - // force the creation of the synopsis before the merge with the app definition - $this->getSynopsis(true); - $this->getSynopsis(false); - // add the application arguments and options $this->mergeApplicationDefinition(); // bind the input against the command specific arguments/options try { - $input->bind($this->definition); + $input->bind($this->getDefinition()); } catch (ExceptionInterface $e) { if (!$this->ignoreValidationErrors) { throw $e; @@ -302,20 +299,19 @@ public function setCode(callable $code) */ public function mergeApplicationDefinition(bool $mergeArgs = true) { - if (null === $this->application || (true === $this->applicationDefinitionMerged && ($this->applicationDefinitionMergedWithArgs || !$mergeArgs))) { + if (null === $this->application) { return; } - $this->definition->addOptions($this->application->getDefinition()->getOptions()); - - $this->applicationDefinitionMerged = true; + $this->fullDefinition = new InputDefinition(); + $this->fullDefinition->setOptions($this->definition->getOptions()); + $this->fullDefinition->addOptions($this->application->getDefinition()->getOptions()); if ($mergeArgs) { - $currentArguments = $this->definition->getArguments(); - $this->definition->setArguments($this->application->getDefinition()->getArguments()); - $this->definition->addArguments($currentArguments); - - $this->applicationDefinitionMergedWithArgs = true; + $this->fullDefinition->setArguments($this->application->getDefinition()->getArguments()); + $this->fullDefinition->addArguments($this->definition->getArguments()); + } else { + $this->fullDefinition->setArguments($this->definition->getArguments()); } } @@ -334,7 +330,7 @@ public function setDefinition($definition) $this->definition->setDefinition($definition); } - $this->applicationDefinitionMerged = false; + $this->fullDefinition = null; return $this; } @@ -346,11 +342,7 @@ public function setDefinition($definition) */ public function getDefinition() { - if (null === $this->definition) { - throw new LogicException(sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', static::class)); - } - - return $this->definition; + return $this->fullDefinition ?? $this->getNativeDefinition(); } /** @@ -365,7 +357,11 @@ public function getDefinition() */ public function getNativeDefinition() { - return $this->getDefinition(); + if (null === $this->definition) { + throw new LogicException(sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', static::class)); + } + + return $this->definition; } /** @@ -381,6 +377,9 @@ public function getNativeDefinition() public function addArgument(string $name, int $mode = null, string $description = '', $default = null) { $this->definition->addArgument(new InputArgument($name, $mode, $description, $default)); + if (null !== $this->fullDefinition) { + $this->fullDefinition->addArgument(new InputArgument($name, $mode, $description, $default)); + } return $this; } @@ -399,6 +398,9 @@ public function addArgument(string $name, int $mode = null, string $description public function addOption(string $name, $shortcut = null, int $mode = null, string $description = '', $default = null) { $this->definition->addOption(new InputOption($name, $shortcut, $mode, $description, $default)); + if (null !== $this->fullDefinition) { + $this->fullDefinition->addOption(new InputOption($name, $shortcut, $mode, $description, $default)); + } return $this; } diff --git a/src/Symfony/Component/Console/Command/HelpCommand.php b/src/Symfony/Component/Console/Command/HelpCommand.php index b32be4c95cce4..e704b9b2a0097 100644 --- a/src/Symfony/Component/Console/Command/HelpCommand.php +++ b/src/Symfony/Component/Console/Command/HelpCommand.php @@ -44,11 +44,11 @@ protected function configure() ->setHelp(<<<'EOF' The %command.name% command displays help for a given command: - php %command.full_name% list + %command.full_name% list You can also output the help in other formats by using the --format option: - php %command.full_name% --format=xml list + %command.full_name% --format=xml list To display the list of available commands, please use the list command. EOF diff --git a/src/Symfony/Component/Console/Command/ListCommand.php b/src/Symfony/Component/Console/Command/ListCommand.php index 8af952652872a..284ddb5fea53a 100644 --- a/src/Symfony/Component/Console/Command/ListCommand.php +++ b/src/Symfony/Component/Console/Command/ListCommand.php @@ -13,7 +13,6 @@ use Symfony\Component\Console\Helper\DescriptorHelper; use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; @@ -32,37 +31,33 @@ protected function configure() { $this ->setName('list') - ->setDefinition($this->createDefinition()) + ->setDefinition([ + new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name'), + new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command list'), + new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt'), + ]) ->setDescription('Lists commands') ->setHelp(<<<'EOF' The %command.name% command lists all commands: - php %command.full_name% + %command.full_name% You can also display the commands for a specific namespace: - php %command.full_name% test + %command.full_name% test You can also output the information in other formats by using the --format option: - php %command.full_name% --format=xml + %command.full_name% --format=xml It's also possible to get raw list of commands (useful for embedding command runner): - php %command.full_name% --raw + %command.full_name% --raw EOF ) ; } - /** - * {@inheritdoc} - */ - public function getNativeDefinition() - { - return $this->createDefinition(); - } - /** * {@inheritdoc} */ @@ -77,13 +72,4 @@ protected function execute(InputInterface $input, OutputInterface $output) return 0; } - - private function createDefinition(): InputDefinition - { - return new InputDefinition([ - new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name'), - new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command list'), - new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt'), - ]); - } } diff --git a/src/Symfony/Component/Console/Command/SignalableCommandInterface.php b/src/Symfony/Component/Console/Command/SignalableCommandInterface.php new file mode 100644 index 0000000000000..d439728b65225 --- /dev/null +++ b/src/Symfony/Component/Console/Command/SignalableCommandInterface.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Command; + +/** + * Interface for command reacting to signal. + * + * @author Grégoire Pineau + */ +interface SignalableCommandInterface +{ + /** + * Returns the list of signals to subscribe. + */ + public function getSubscribedSignals(): array; + + /** + * The method will be called when the application is signaled. + */ + public function handleSignal(int $signal): void; +} diff --git a/src/Symfony/Component/Console/ConsoleEvents.php b/src/Symfony/Component/Console/ConsoleEvents.php index 4975643aedf2b..d63beed94bc7e 100644 --- a/src/Symfony/Component/Console/ConsoleEvents.php +++ b/src/Symfony/Component/Console/ConsoleEvents.php @@ -11,6 +11,11 @@ namespace Symfony\Component\Console; +use Symfony\Component\Console\Event\ConsoleCommandEvent; +use Symfony\Component\Console\Event\ConsoleErrorEvent; +use Symfony\Component\Console\Event\ConsoleSignalEvent; +use Symfony\Component\Console\Event\ConsoleTerminateEvent; + /** * Contains all events dispatched by an Application. * @@ -27,6 +32,14 @@ final class ConsoleEvents */ const COMMAND = 'console.command'; + /** + * The SIGNAL event allows you to perform some actions + * after the command execution was interrupted. + * + * @Event("Symfony\Component\Console\Event\ConsoleSignalEvent") + */ + const SIGNAL = 'console.signal'; + /** * The TERMINATE event allows you to attach listeners after a command is * executed by the console. @@ -44,4 +57,16 @@ final class ConsoleEvents * @Event("Symfony\Component\Console\Event\ConsoleErrorEvent") */ const ERROR = 'console.error'; + + /** + * Event aliases. + * + * These aliases can be consumed by RegisterListenersPass. + */ + const ALIASES = [ + ConsoleCommandEvent::class => self::COMMAND, + ConsoleErrorEvent::class => self::ERROR, + ConsoleSignalEvent::class => self::SIGNAL, + ConsoleTerminateEvent::class => self::TERMINATE, + ]; } diff --git a/src/Symfony/Component/Console/Descriptor/JsonDescriptor.php b/src/Symfony/Component/Console/Descriptor/JsonDescriptor.php index 5ba588ee98a3e..8b2a279489d5b 100644 --- a/src/Symfony/Component/Console/Descriptor/JsonDescriptor.php +++ b/src/Symfony/Component/Console/Descriptor/JsonDescriptor.php @@ -141,7 +141,6 @@ private function getInputDefinitionData(InputDefinition $definition): array private function getCommandData(Command $command): array { - $command->getSynopsis(); $command->mergeApplicationDefinition(false); return [ @@ -149,7 +148,7 @@ private function getCommandData(Command $command): array 'usage' => array_merge([$command->getSynopsis()], $command->getUsages(), $command->getAliases()), 'description' => $command->getDescription(), 'help' => $command->getProcessedHelp(), - 'definition' => $this->getInputDefinitionData($command->getNativeDefinition()), + 'definition' => $this->getInputDefinitionData($command->getDefinition()), 'hidden' => $command->isHidden(), ]; } diff --git a/src/Symfony/Component/Console/Descriptor/MarkdownDescriptor.php b/src/Symfony/Component/Console/Descriptor/MarkdownDescriptor.php index 8b3e182efc358..483a8338dfc1f 100644 --- a/src/Symfony/Component/Console/Descriptor/MarkdownDescriptor.php +++ b/src/Symfony/Component/Console/Descriptor/MarkdownDescriptor.php @@ -118,7 +118,6 @@ protected function describeInputDefinition(InputDefinition $definition, array $o */ protected function describeCommand(Command $command, array $options = []) { - $command->getSynopsis(); $command->mergeApplicationDefinition(false); $this->write( @@ -136,9 +135,10 @@ protected function describeCommand(Command $command, array $options = []) $this->write($help); } - if ($command->getNativeDefinition()) { + $definition = $command->getDefinition(); + if ($definition->getOptions() || $definition->getArguments()) { $this->write("\n\n"); - $this->describeInputDefinition($command->getNativeDefinition()); + $this->describeInputDefinition($definition); } } diff --git a/src/Symfony/Component/Console/Descriptor/TextDescriptor.php b/src/Symfony/Component/Console/Descriptor/TextDescriptor.php index 05a32443d935b..e73b8a99fbedd 100644 --- a/src/Symfony/Component/Console/Descriptor/TextDescriptor.php +++ b/src/Symfony/Component/Console/Descriptor/TextDescriptor.php @@ -136,8 +136,6 @@ protected function describeInputDefinition(InputDefinition $definition, array $o */ protected function describeCommand(Command $command, array $options = []) { - $command->getSynopsis(true); - $command->getSynopsis(false); $command->mergeApplicationDefinition(false); if ($description = $command->getDescription()) { @@ -154,7 +152,7 @@ protected function describeCommand(Command $command, array $options = []) } $this->writeText("\n"); - $definition = $command->getNativeDefinition(); + $definition = $command->getDefinition(); if ($definition->getOptions() || $definition->getArguments()) { $this->writeText("\n"); $this->describeInputDefinition($definition, $options); diff --git a/src/Symfony/Component/Console/Descriptor/XmlDescriptor.php b/src/Symfony/Component/Console/Descriptor/XmlDescriptor.php index 3d5dce1d6aff9..24035f5a3b58b 100644 --- a/src/Symfony/Component/Console/Descriptor/XmlDescriptor.php +++ b/src/Symfony/Component/Console/Descriptor/XmlDescriptor.php @@ -49,7 +49,6 @@ public function getCommandDocument(Command $command): \DOMDocument $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->appendChild($commandXML = $dom->createElement('command')); - $command->getSynopsis(); $command->mergeApplicationDefinition(false); $commandXML->setAttribute('id', $command->getName()); @@ -68,7 +67,7 @@ public function getCommandDocument(Command $command): \DOMDocument $commandXML->appendChild($helpXML = $dom->createElement('help')); $helpXML->appendChild($dom->createTextNode(str_replace("\n", "\n ", $command->getProcessedHelp()))); - $definitionXML = $this->getInputDefinitionDocument($command->getNativeDefinition()); + $definitionXML = $this->getInputDefinitionDocument($command->getDefinition()); $this->appendDocument($commandXML, $definitionXML->getElementsByTagName('definition')->item(0)); return $dom; diff --git a/src/Symfony/Component/Console/Event/ConsoleSignalEvent.php b/src/Symfony/Component/Console/Event/ConsoleSignalEvent.php new file mode 100644 index 0000000000000..ef13ed2f5d0b2 --- /dev/null +++ b/src/Symfony/Component/Console/Event/ConsoleSignalEvent.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Event; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @author marie + */ +final class ConsoleSignalEvent extends ConsoleEvent +{ + private $handlingSignal; + + public function __construct(Command $command, InputInterface $input, OutputInterface $output, int $handlingSignal) + { + parent::__construct($command, $input, $output); + $this->handlingSignal = $handlingSignal; + } + + public function getHandlingSignal(): int + { + return $this->handlingSignal; + } +} diff --git a/src/Symfony/Component/Console/Formatter/OutputFormatterStyle.php b/src/Symfony/Component/Console/Formatter/OutputFormatterStyle.php index b1291cb031667..b2d9fbefb167d 100644 --- a/src/Symfony/Component/Console/Formatter/OutputFormatterStyle.php +++ b/src/Symfony/Component/Console/Formatter/OutputFormatterStyle.php @@ -11,7 +11,7 @@ namespace Symfony\Component\Console\Formatter; -use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Color; /** * Formatter style class for defining styles. @@ -20,40 +20,11 @@ */ class OutputFormatterStyle implements OutputFormatterStyleInterface { - private static $availableForegroundColors = [ - 'black' => ['set' => 30, 'unset' => 39], - 'red' => ['set' => 31, 'unset' => 39], - 'green' => ['set' => 32, 'unset' => 39], - 'yellow' => ['set' => 33, 'unset' => 39], - 'blue' => ['set' => 34, 'unset' => 39], - 'magenta' => ['set' => 35, 'unset' => 39], - 'cyan' => ['set' => 36, 'unset' => 39], - 'white' => ['set' => 37, 'unset' => 39], - 'default' => ['set' => 39, 'unset' => 39], - ]; - private static $availableBackgroundColors = [ - 'black' => ['set' => 40, 'unset' => 49], - 'red' => ['set' => 41, 'unset' => 49], - 'green' => ['set' => 42, 'unset' => 49], - 'yellow' => ['set' => 43, 'unset' => 49], - 'blue' => ['set' => 44, 'unset' => 49], - 'magenta' => ['set' => 45, 'unset' => 49], - 'cyan' => ['set' => 46, 'unset' => 49], - 'white' => ['set' => 47, 'unset' => 49], - 'default' => ['set' => 49, 'unset' => 49], - ]; - private static $availableOptions = [ - 'bold' => ['set' => 1, 'unset' => 22], - 'underscore' => ['set' => 4, 'unset' => 24], - 'blink' => ['set' => 5, 'unset' => 25], - 'reverse' => ['set' => 7, 'unset' => 27], - 'conceal' => ['set' => 8, 'unset' => 28], - ]; - + private $color; private $foreground; private $background; + private $options; private $href; - private $options = []; private $handlesHrefGracefully; /** @@ -64,15 +35,7 @@ class OutputFormatterStyle implements OutputFormatterStyleInterface */ public function __construct(string $foreground = null, string $background = null, array $options = []) { - if (null !== $foreground) { - $this->setForeground($foreground); - } - if (null !== $background) { - $this->setBackground($background); - } - if (\count($options)) { - $this->setOptions($options); - } + $this->color = new Color($this->foreground = $foreground ?: '', $this->background = $background ?: '', $this->options = $options); } /** @@ -80,17 +43,7 @@ public function __construct(string $foreground = null, string $background = null */ public function setForeground(string $color = null) { - if (null === $color) { - $this->foreground = null; - - return; - } - - if (!isset(static::$availableForegroundColors[$color])) { - throw new InvalidArgumentException(sprintf('Invalid foreground color specified: "%s". Expected one of (%s).', $color, implode(', ', array_keys(static::$availableForegroundColors)))); - } - - $this->foreground = static::$availableForegroundColors[$color]; + $this->color = new Color($this->foreground = $color ?: '', $this->background, $this->options); } /** @@ -98,17 +51,7 @@ public function setForeground(string $color = null) */ public function setBackground(string $color = null) { - if (null === $color) { - $this->background = null; - - return; - } - - if (!isset(static::$availableBackgroundColors[$color])) { - throw new InvalidArgumentException(sprintf('Invalid background color specified: "%s". Expected one of (%s).', $color, implode(', ', array_keys(static::$availableBackgroundColors)))); - } - - $this->background = static::$availableBackgroundColors[$color]; + $this->color = new Color($this->foreground, $this->background = $color ?: '', $this->options); } public function setHref(string $url): void @@ -121,13 +64,8 @@ public function setHref(string $url): void */ public function setOption(string $option) { - if (!isset(static::$availableOptions[$option])) { - throw new InvalidArgumentException(sprintf('Invalid option specified: "%s". Expected one of (%s).', $option, implode(', ', array_keys(static::$availableOptions)))); - } - - if (!\in_array(static::$availableOptions[$option], $this->options)) { - $this->options[] = static::$availableOptions[$option]; - } + $this->options[] = $option; + $this->color = new Color($this->foreground, $this->background, $this->options); } /** @@ -135,14 +73,12 @@ public function setOption(string $option) */ public function unsetOption(string $option) { - if (!isset(static::$availableOptions[$option])) { - throw new InvalidArgumentException(sprintf('Invalid option specified: "%s". Expected one of (%s).', $option, implode(', ', array_keys(static::$availableOptions)))); - } - - $pos = array_search(static::$availableOptions[$option], $this->options); + $pos = array_search($option, $this->options); if (false !== $pos) { unset($this->options[$pos]); } + + $this->color = new Color($this->foreground, $this->background, $this->options); } /** @@ -150,11 +86,7 @@ public function unsetOption(string $option) */ public function setOptions(array $options) { - $this->options = []; - - foreach ($options as $option) { - $this->setOption($option); - } + $this->color = new Color($this->foreground, $this->background, $this->options = $options); } /** @@ -162,35 +94,14 @@ public function setOptions(array $options) */ public function apply(string $text) { - $setCodes = []; - $unsetCodes = []; - if (null === $this->handlesHrefGracefully) { $this->handlesHrefGracefully = 'JetBrains-JediTerm' !== getenv('TERMINAL_EMULATOR') && !getenv('KONSOLE_VERSION'); } - if (null !== $this->foreground) { - $setCodes[] = $this->foreground['set']; - $unsetCodes[] = $this->foreground['unset']; - } - if (null !== $this->background) { - $setCodes[] = $this->background['set']; - $unsetCodes[] = $this->background['unset']; - } - - foreach ($this->options as $option) { - $setCodes[] = $option['set']; - $unsetCodes[] = $option['unset']; - } - if (null !== $this->href && $this->handlesHrefGracefully) { $text = "\033]8;;$this->href\033\\$text\033]8;;\033\\"; } - if (0 === \count($setCodes)) { - return $text; - } - - return sprintf("\033[%sm%s\033[%sm", implode(';', $setCodes), $text, implode(';', $unsetCodes)); + return $this->color->apply($text); } } diff --git a/src/Symfony/Component/Console/Helper/ProgressBar.php b/src/Symfony/Component/Console/Helper/ProgressBar.php index 3fce6886e58bc..7b6b99e431859 100644 --- a/src/Symfony/Component/Console/Helper/ProgressBar.php +++ b/src/Symfony/Component/Console/Helper/ProgressBar.php @@ -56,7 +56,7 @@ final class ProgressBar /** * @param int $max Maximum steps (0 if unknown) */ - public function __construct(OutputInterface $output, int $max = 0, float $minSecondsBetweenRedraws = 0.1) + public function __construct(OutputInterface $output, int $max = 0, float $minSecondsBetweenRedraws = 1 / 25) { if ($output instanceof ConsoleOutputInterface) { $output = $output->getErrorOutput(); diff --git a/src/Symfony/Component/Console/Helper/QuestionHelper.php b/src/Symfony/Component/Console/Helper/QuestionHelper.php index 1d3483a3f1fe9..643005f075f28 100644 --- a/src/Symfony/Component/Console/Helper/QuestionHelper.php +++ b/src/Symfony/Component/Console/Helper/QuestionHelper.php @@ -129,7 +129,7 @@ private function doAsk(OutputInterface $output, Question $question) } if (false === $ret) { - $ret = fgets($inputStream, 4096); + $ret = $this->readInput($inputStream, $question); if (false === $ret) { throw new MissingInputException('Aborted.'); } @@ -502,4 +502,68 @@ private function isInteractiveInput($inputStream): bool return self::$stdinIsInteractive = 1 !== $status; } + + /** + * Reads one or more lines of input and returns what is read. + * + * @param resource $inputStream The handler resource + * @param Question $question The question being asked + * + * @return string|bool The input received, false in case input could not be read + */ + private function readInput($inputStream, Question $question) + { + if (!$question->isMultiline()) { + return fgets($inputStream, 4096); + } + + $multiLineStreamReader = $this->cloneInputStream($inputStream); + if (null === $multiLineStreamReader) { + return false; + } + + $ret = ''; + while (false !== ($char = fgetc($multiLineStreamReader))) { + if (\PHP_EOL === "{$ret}{$char}") { + break; + } + $ret .= $char; + } + + return $ret; + } + + /** + * Clones an input stream in order to act on one instance of the same + * stream without affecting the other instance. + * + * @param resource $inputStream The handler resource + * + * @return resource|null The cloned resource, null in case it could not be cloned + */ + private function cloneInputStream($inputStream) + { + $streamMetaData = stream_get_meta_data($inputStream); + $seekable = $streamMetaData['seekable'] ?? false; + $mode = $streamMetaData['mode'] ?? 'rb'; + $uri = $streamMetaData['uri'] ?? null; + + if (null === $uri) { + return null; + } + + $cloneStream = fopen($uri, $mode); + + // For seekable and writable streams, add all the same data to the + // cloned stream and then seek to the same offset. + if (true === $seekable && !\in_array($mode, ['r', 'rb', 'rt'])) { + $offset = ftell($inputStream); + rewind($inputStream); + stream_copy_to_stream($inputStream, $cloneStream); + fseek($inputStream, $offset); + fseek($cloneStream, $offset); + } + + return $cloneStream; + } } diff --git a/src/Symfony/Component/Console/Helper/SymfonyQuestionHelper.php b/src/Symfony/Component/Console/Helper/SymfonyQuestionHelper.php index e4e87b2f99188..8b923bb13c7c3 100644 --- a/src/Symfony/Component/Console/Helper/SymfonyQuestionHelper.php +++ b/src/Symfony/Component/Console/Helper/SymfonyQuestionHelper.php @@ -33,6 +33,10 @@ protected function writePrompt(OutputInterface $output, Question $question) $text = OutputFormatter::escapeTrailingBackslash($question->getQuestion()); $default = $question->getDefault(); + if ($question->isMultiline()) { + $text .= sprintf(' (press %s to continue)', $this->getEofShortcut()); + } + switch (true) { case null === $default: $text = sprintf(' %s:', $text); @@ -93,4 +97,13 @@ protected function writeError(OutputInterface $output, \Exception $error) parent::writeError($output, $error); } + + private function getEofShortcut(): string + { + if (false !== strpos(\PHP_OS, 'WIN')) { + return 'Ctrl+Z then Enter'; + } + + return 'Ctrl+D'; + } } diff --git a/src/Symfony/Component/Console/Helper/Table.php b/src/Symfony/Component/Console/Helper/Table.php index fee5a416b73d9..60896408a6059 100644 --- a/src/Symfony/Component/Console/Helper/Table.php +++ b/src/Symfony/Component/Console/Helper/Table.php @@ -514,7 +514,30 @@ private function renderCell(array $row, int $column, string $cellFormat): string $width += Helper::strlen($cell) - Helper::strlenWithoutDecoration($this->output->getFormatter(), $cell); $content = sprintf($style->getCellRowContentFormat(), $cell); - return sprintf($cellFormat, str_pad($content, $width, $style->getPaddingChar(), $style->getPadType())); + $padType = $style->getPadType(); + if ($cell instanceof TableCell && $cell->getStyle() instanceof TableCellStyle) { + $isNotStyledByTag = !preg_match('/^<(\w+|(\w+=[\w,]+;?)*)>.+<\/(\w+|(\w+=\w+;?)*)?>$/', $cell); + if ($isNotStyledByTag) { + $cellFormat = $cell->getStyle()->getCellFormat(); + if (!\is_string($cellFormat)) { + $tag = http_build_query($cell->getStyle()->getTagOptions(), null, ';'); + $cellFormat = '<'.$tag.'>%s'; + } + + if (strstr($content, '')) { + $content = str_replace('', '', $content); + $width -= 3; + } + if (strstr($content, '')) { + $content = str_replace('', '', $content); + $width -= \strlen(''); + } + } + + $padType = $cell->getStyle()->getPadByAlign(); + } + + return sprintf($cellFormat, str_pad($content, $width, $style->getPaddingChar(), $padType)); } /** @@ -618,7 +641,7 @@ private function fillNextRows(array $rows, int $line): array $lines = explode("\n", str_replace("\n", "\n", $cell)); $nbLines = \count($lines) > $nbLines ? substr_count($cell, "\n") : $nbLines; - $rows[$line][$column] = new TableCell($lines[0], ['colspan' => $cell->getColspan()]); + $rows[$line][$column] = new TableCell($lines[0], ['colspan' => $cell->getColspan(), 'style' => $cell->getStyle()]); unset($lines[0]); } @@ -626,7 +649,7 @@ private function fillNextRows(array $rows, int $line): array $unmergedRows = array_replace_recursive(array_fill($line + 1, $nbLines, []), $unmergedRows); foreach ($unmergedRows as $unmergedRowKey => $unmergedRow) { $value = isset($lines[$unmergedRowKey - $line]) ? $lines[$unmergedRowKey - $line] : ''; - $unmergedRows[$unmergedRowKey][$column] = new TableCell($value, ['colspan' => $cell->getColspan()]); + $unmergedRows[$unmergedRowKey][$column] = new TableCell($value, ['colspan' => $cell->getColspan(), 'style' => $cell->getStyle()]); if ($nbLines === $unmergedRowKey - $line) { break; } diff --git a/src/Symfony/Component/Console/Helper/TableCell.php b/src/Symfony/Component/Console/Helper/TableCell.php index 5b6af4a933b37..1a7bc6ede6647 100644 --- a/src/Symfony/Component/Console/Helper/TableCell.php +++ b/src/Symfony/Component/Console/Helper/TableCell.php @@ -22,6 +22,7 @@ class TableCell private $options = [ 'rowspan' => 1, 'colspan' => 1, + 'style' => null, ]; public function __construct(string $value = '', array $options = []) @@ -33,6 +34,10 @@ public function __construct(string $value = '', array $options = []) throw new InvalidArgumentException(sprintf('The TableCell does not support the following options: \'%s\'.', implode('\', \'', $diff))); } + if (isset($options['style']) && !$options['style'] instanceof TableCellStyle) { + throw new InvalidArgumentException('The style option must be an instance of "TableCellStyle".'); + } + $this->options = array_merge($this->options, $options); } @@ -65,4 +70,9 @@ public function getRowspan() { return (int) $this->options['rowspan']; } + + public function getStyle(): ?TableCellStyle + { + return $this->options['style']; + } } diff --git a/src/Symfony/Component/Console/Helper/TableCellStyle.php b/src/Symfony/Component/Console/Helper/TableCellStyle.php new file mode 100644 index 0000000000000..4766ba3736241 --- /dev/null +++ b/src/Symfony/Component/Console/Helper/TableCellStyle.php @@ -0,0 +1,86 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +use Symfony\Component\Console\Exception\InvalidArgumentException; + +/** + * @author Yewhen Khoptynskyi + */ +class TableCellStyle +{ + const DEFAULT_ALIGN = 'left'; + + private $options = [ + 'fg' => 'default', + 'bg' => 'default', + 'options' => null, + 'align' => self::DEFAULT_ALIGN, + 'cellFormat' => null, + ]; + + private $tagOptions = [ + 'fg', + 'bg', + 'options', + ]; + + private $alignMap = [ + 'left' => \STR_PAD_RIGHT, + 'center' => \STR_PAD_BOTH, + 'right' => \STR_PAD_LEFT, + ]; + + public function __construct(array $options = []) + { + if ($diff = array_diff(array_keys($options), array_keys($this->options))) { + throw new InvalidArgumentException(sprintf('The TableCellStyle does not support the following options: \'%s\'.', implode('\', \'', $diff))); + } + + if (isset($options['align']) && !\array_key_exists($options['align'], $this->alignMap)) { + throw new InvalidArgumentException(sprintf('Wrong align value. Value must be following: \'%s\'.', implode('\', \'', array_keys($this->alignMap)))); + } + + $this->options = array_merge($this->options, $options); + } + + public function getOptions(): array + { + return $this->options; + } + + /** + * Gets options we need for tag for example fg, bg. + * + * @return string[] + */ + public function getTagOptions() + { + return array_filter( + $this->getOptions(), + function ($key) { + return \in_array($key, $this->tagOptions) && isset($this->options[$key]); + }, + \ARRAY_FILTER_USE_KEY + ); + } + + public function getPadByAlign() + { + return $this->alignMap[$this->getOptions()['align']]; + } + + public function getCellFormat(): ?string + { + return $this->getOptions()['cellFormat']; + } +} diff --git a/src/Symfony/Component/Console/Question/ChoiceQuestion.php b/src/Symfony/Component/Console/Question/ChoiceQuestion.php index 24e6604946672..92b6e8663fdc3 100644 --- a/src/Symfony/Component/Console/Question/ChoiceQuestion.php +++ b/src/Symfony/Component/Console/Question/ChoiceQuestion.php @@ -169,7 +169,8 @@ private function getDefaultValidator(): callable throw new InvalidArgumentException(sprintf($errorMessage, $value)); } - $multiselectChoices[] = (string) $result; + // For associative choices, consistently return the key as string: + $multiselectChoices[] = $isAssoc ? (string) $result : $result; } if ($multiselect) { diff --git a/src/Symfony/Component/Console/Question/Question.php b/src/Symfony/Component/Console/Question/Question.php index 17d890d763474..0b5eefd546e54 100644 --- a/src/Symfony/Component/Console/Question/Question.php +++ b/src/Symfony/Component/Console/Question/Question.php @@ -30,6 +30,7 @@ class Question private $default; private $normalizer; private $trimmable = true; + private $multiline = false; /** * @param string $question The question to ask to the user @@ -61,6 +62,26 @@ public function getDefault() return $this->default; } + /** + * Returns whether the user response accepts newline characters. + */ + public function isMultiline(): bool + { + return $this->multiline; + } + + /** + * Sets whether the user response should accept newline characters. + * + * @return $this + */ + public function setMultiline(bool $multiline): self + { + $this->multiline = $multiline; + + return $this; + } + /** * Returns whether the user response must be hidden. * diff --git a/src/Symfony/Component/Console/SignalRegistry/SignalRegistry.php b/src/Symfony/Component/Console/SignalRegistry/SignalRegistry.php new file mode 100644 index 0000000000000..de256376d7cb1 --- /dev/null +++ b/src/Symfony/Component/Console/SignalRegistry/SignalRegistry.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\SignalRegistry; + +final class SignalRegistry +{ + private $signalHandlers = []; + + public function __construct() + { + if (\function_exists('pcntl_async_signals')) { + pcntl_async_signals(true); + } + } + + public function register(int $signal, callable $signalHandler): void + { + if (!isset($this->signalHandlers[$signal])) { + $previousCallback = pcntl_signal_get_handler($signal); + + if (\is_callable($previousCallback)) { + $this->signalHandlers[$signal][] = $previousCallback; + } + } + + $this->signalHandlers[$signal][] = $signalHandler; + + pcntl_signal($signal, [$this, 'handle']); + } + + /** + * @internal + */ + public function handle(int $signal): void + { + $count = \count($this->signalHandlers[$signal]); + + foreach ($this->signalHandlers[$signal] as $i => $signalHandler) { + $hasNext = $i !== $count - 1; + $signalHandler($signal, $hasNext); + } + } +} diff --git a/src/Symfony/Component/Console/SingleCommandApplication.php b/src/Symfony/Component/Console/SingleCommandApplication.php index ffa176fbd0bc8..c1831d1d255c8 100644 --- a/src/Symfony/Component/Console/SingleCommandApplication.php +++ b/src/Symfony/Component/Console/SingleCommandApplication.php @@ -21,6 +21,7 @@ class SingleCommandApplication extends Command { private $version = 'UNKNOWN'; + private $autoExit = true; private $running = false; public function setVersion(string $version): self @@ -30,6 +31,16 @@ public function setVersion(string $version): self return $this; } + /** + * @final + */ + public function setAutoExit(bool $autoExit): self + { + $this->autoExit = $autoExit; + + return $this; + } + public function run(InputInterface $input = null, OutputInterface $output = null): int { if ($this->running) { @@ -38,6 +49,7 @@ public function run(InputInterface $input = null, OutputInterface $output = null // We use the command name as the application name $application = new Application($this->getName() ?: 'UNKNOWN', $this->version); + $application->setAutoExit($this->autoExit); // Fix the usage of the command displayed with "--help" $this->setName($_SERVER['argv'][0]); $application->add($this); diff --git a/src/Symfony/Component/Console/Style/SymfonyStyle.php b/src/Symfony/Component/Console/Style/SymfonyStyle.php index fa4dfa018bd15..ba8626f74b0c1 100644 --- a/src/Symfony/Component/Console/Style/SymfonyStyle.php +++ b/src/Symfony/Component/Console/Style/SymfonyStyle.php @@ -163,6 +163,16 @@ public function note($message) $this->block($message, 'NOTE', 'fg=yellow', ' ! '); } + /** + * Formats an info message. + * + * @param string|array $message + */ + public function info($message) + { + $this->block($message, 'INFO', 'fg=green', ' ', true); + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/Console/Tester/TesterTrait.php b/src/Symfony/Component/Console/Tester/TesterTrait.php index 4a344ca2e91ba..69442b22d8682 100644 --- a/src/Symfony/Component/Console/Tester/TesterTrait.php +++ b/src/Symfony/Component/Console/Tester/TesterTrait.php @@ -29,6 +29,8 @@ trait TesterTrait /** * Gets the display returned by the last execution of the command or application. * + * @throws \RuntimeException If it's called before the execute method + * * @return string The display */ public function getDisplay(bool $normalize = false) @@ -95,10 +97,16 @@ public function getOutput() /** * Gets the status code returned by the last execution of the command or application. * + * @throws \RuntimeException If it's called before the execute method + * * @return int The status code */ public function getStatusCode() { + if (null === $this->statusCode) { + throw new \RuntimeException('Status code not initialized, did you execute the command before requesting the status code?'); + } + return $this->statusCode; } diff --git a/src/Symfony/Component/Console/Tests/ApplicationTest.php b/src/Symfony/Component/Console/Tests/ApplicationTest.php index f275ccac1ab07..88631e0b6ea49 100644 --- a/src/Symfony/Component/Console/Tests/ApplicationTest.php +++ b/src/Symfony/Component/Console/Tests/ApplicationTest.php @@ -1005,6 +1005,12 @@ public function testRun() $tester->run(['command' => 'list', '-vvv' => true]); $this->assertSame(Output::VERBOSITY_DEBUG, $tester->getOutput()->getVerbosity(), '->run() sets the output to verbose if -v is passed'); + $tester->run(['command' => 'help', '--help' => true], ['decorated' => false]); + $this->assertStringEqualsFile(self::$fixturesPath.'/application_run5.txt', $tester->getDisplay(true), '->run() displays the help if --help is passed'); + + $tester->run(['command' => 'help', '-h' => true], ['decorated' => false]); + $this->assertStringEqualsFile(self::$fixturesPath.'/application_run5.txt', $tester->getDisplay(true), '->run() displays the help if -h is passed'); + $application = new Application(); $application->setAutoExit(false); $application->setCatchExceptions(false); diff --git a/src/Symfony/Component/Console/Tests/ColorTest.php b/src/Symfony/Component/Console/Tests/ColorTest.php new file mode 100644 index 0000000000000..7fe008cae1c1d --- /dev/null +++ b/src/Symfony/Component/Console/Tests/ColorTest.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Color; + +class ColorTest extends TestCase +{ + public function testAnsiColors() + { + $color = new Color(); + $this->assertSame(' ', $color->apply(' ')); + + $color = new Color('red', 'yellow'); + $this->assertSame("\033[31;43m \033[39;49m", $color->apply(' ')); + + $color = new Color('red', 'yellow', ['underscore']); + $this->assertSame("\033[31;43;4m \033[39;49;24m", $color->apply(' ')); + } + + public function testTrueColors() + { + if ('truecolor' !== getenv('COLORTERM')) { + $this->markTestSkipped('True color not supported.'); + } + + $color = new Color('#fff', '#000'); + $this->assertSame("\033[38;2;255;255;255;48;2;0;0;0m \033[39;49m", $color->apply(' ')); + + $color = new Color('#ffffff', '#000000'); + $this->assertSame("\033[38;2;255;255;255;48;2;0;0;0m \033[39;49m", $color->apply(' ')); + } +} diff --git a/src/Symfony/Component/Console/Tests/Command/CommandTest.php b/src/Symfony/Component/Console/Tests/Command/CommandTest.php index c15555897be41..e2947dc44e100 100644 --- a/src/Symfony/Component/Console/Tests/Command/CommandTest.php +++ b/src/Symfony/Component/Console/Tests/Command/CommandTest.php @@ -153,7 +153,7 @@ public function testGetSetHelp() public function testGetProcessedHelp() { $command = new \TestCommand(); - $command->setHelp('The %command.name% command does... Example: php %command.full_name%.'); + $command->setHelp('The %command.name% command does... Example: %command.full_name%.'); $this->assertStringContainsString('The namespace:name command does...', $command->getProcessedHelp(), '->getProcessedHelp() replaces %command.name% correctly'); $this->assertStringNotContainsString('%command.full_name%', $command->getProcessedHelp(), '->getProcessedHelp() replaces %command.full_name%'); @@ -162,7 +162,7 @@ public function testGetProcessedHelp() $this->assertStringContainsString('description', $command->getProcessedHelp(), '->getProcessedHelp() falls back to the description'); $command = new \TestCommand(); - $command->setHelp('The %command.name% command does... Example: php %command.full_name%.'); + $command->setHelp('The %command.name% command does... Example: %command.full_name%.'); $application = new Application(); $application->add($command); $application->setDefaultCommand('namespace:name', true); diff --git a/src/Symfony/Component/Console/Tests/Command/ListCommandTest.php b/src/Symfony/Component/Console/Tests/Command/ListCommandTest.php index 3908ca5bb21f2..a5d6653ad8186 100644 --- a/src/Symfony/Component/Console/Tests/Command/ListCommandTest.php +++ b/src/Symfony/Component/Console/Tests/Command/ListCommandTest.php @@ -77,7 +77,7 @@ public function testExecuteListsCommandsOrder() command [options] [arguments] Options: - -h, --help Display this help message + -h, --help Display help for the given command. When no command is given display help for the list command -q, --quiet Do not output any message -V, --version Display this application version --ansi Force ANSI output diff --git a/src/Symfony/Component/Console/Tests/Command/SingleCommandApplicationTest.php b/src/Symfony/Component/Console/Tests/Command/SingleCommandApplicationTest.php new file mode 100644 index 0000000000000..8fae4876ba39f --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Command/SingleCommandApplicationTest.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Tests\Command; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\SingleCommandApplication; +use Symfony\Component\Console\Tester\CommandTester; + +class SingleCommandApplicationTest extends TestCase +{ + public function testRun() + { + $command = new class() extends SingleCommandApplication { + protected function execute(InputInterface $input, OutputInterface $output): int + { + return 0; + } + }; + + $command->setAutoExit(false); + $this->assertSame(0, (new CommandTester($command))->execute([])); + } +} diff --git a/src/Symfony/Component/Console/Tests/ConsoleEventsTest.php b/src/Symfony/Component/Console/Tests/ConsoleEventsTest.php new file mode 100644 index 0000000000000..45eb2220d1743 --- /dev/null +++ b/src/Symfony/Component/Console/Tests/ConsoleEventsTest.php @@ -0,0 +1,86 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\ConsoleEvents; +use Symfony\Component\Console\Event\ConsoleCommandEvent; +use Symfony\Component\Console\Event\ConsoleErrorEvent; +use Symfony\Component\Console\Event\ConsoleTerminateEvent; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Tester\ApplicationTester; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +class ConsoleEventsTest extends TestCase +{ + public function testEventAliases() + { + $container = new ContainerBuilder(); + $container->setParameter('event_dispatcher.event_aliases', ConsoleEvents::ALIASES); + $container->addCompilerPass(new RegisterListenersPass()); + + $container->register('event_dispatcher', EventDispatcher::class); + $container->register('tracer', EventTraceSubscriber::class) + ->setPublic(true) + ->addTag('kernel.event_subscriber'); + $container->register('failing_command', FailingCommand::class); + $container->register('application', Application::class) + ->setPublic(true) + ->addMethodCall('setAutoExit', [false]) + ->addMethodCall('setDispatcher', [new Reference('event_dispatcher')]) + ->addMethodCall('add', [new Reference('failing_command')]) + ; + + $container->compile(); + + $tester = new ApplicationTester($container->get('application')); + $tester->run(['fail']); + + $this->assertSame([ConsoleCommandEvent::class, ConsoleErrorEvent::class, ConsoleTerminateEvent::class], $container->get('tracer')->observedEvents); + } +} + +class EventTraceSubscriber implements EventSubscriberInterface +{ + public $observedEvents = []; + + public static function getSubscribedEvents(): array + { + return [ + ConsoleCommandEvent::class => 'observe', + ConsoleErrorEvent::class => 'observe', + ConsoleTerminateEvent::class => 'observe', + ]; + } + + public function observe(object $event): void + { + $this->observedEvents[] = get_debug_type($event); + } +} + +class FailingCommand extends Command +{ + protected static $defaultName = 'fail'; + + protected function execute(InputInterface $input, OutputInterface $output): int + { + throw new \RuntimeException('I failed. Sorry.'); + } +} diff --git a/src/Symfony/Component/Console/Tests/Fixtures/Style/SymfonyStyle/command/command_2.php b/src/Symfony/Component/Console/Tests/Fixtures/Style/SymfonyStyle/command/command_2.php index 791b626f24f48..a16ad505d2bc4 100644 --- a/src/Symfony/Component/Console/Tests/Fixtures/Style/SymfonyStyle/command/command_2.php +++ b/src/Symfony/Component/Console/Tests/Fixtures/Style/SymfonyStyle/command/command_2.php @@ -12,5 +12,6 @@ $output->error('Error'); $output->success('Success'); $output->note('Note'); + $output->info('Info'); $output->block('Custom block', 'CUSTOM', 'fg=white;bg=green', 'X ', true); }; diff --git a/src/Symfony/Component/Console/Tests/Fixtures/Style/SymfonyStyle/output/output_2.txt b/src/Symfony/Component/Console/Tests/Fixtures/Style/SymfonyStyle/output/output_2.txt index ca609760cc12a..9513b862f7492 100644 --- a/src/Symfony/Component/Console/Tests/Fixtures/Style/SymfonyStyle/output/output_2.txt +++ b/src/Symfony/Component/Console/Tests/Fixtures/Style/SymfonyStyle/output/output_2.txt @@ -9,5 +9,7 @@ ! [NOTE] Note + [INFO] Info + X [CUSTOM] Custom block diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_1.json b/src/Symfony/Component/Console/Tests/Fixtures/application_1.json index 29faa8262dceb..bb1eab28ed4c1 100644 --- a/src/Symfony/Component/Console/Tests/Fixtures/application_1.json +++ b/src/Symfony/Component/Console/Tests/Fixtures/application_1.json @@ -7,7 +7,7 @@ "help [--format FORMAT] [--raw] [--] []" ], "description": "Displays help for a command", - "help": "The help<\/info> command displays help for a given command:\n\n php app\/console help list<\/info>\n\nYou can also output the help in other formats by using the --format<\/comment> option:\n\n php app\/console help --format=xml list<\/info>\n\nTo display the list of available commands, please use the list<\/info> command.", + "help": "The help<\/info> command displays help for a given command:\n\n app\/console help list<\/info>\n\nYou can also output the help in other formats by using the --format<\/comment> option:\n\n app\/console help --format=xml list<\/info>\n\nTo display the list of available commands, please use the list<\/info> command.", "definition": { "arguments": { "command_name": { @@ -43,7 +43,7 @@ "accept_value": false, "is_value_required": false, "is_multiple": false, - "description": "Display this help message", + "description": "Display help for the given command. When no command is given display help for the list command", "default": false }, "quiet": { @@ -110,7 +110,7 @@ "list [--raw] [--format FORMAT] [--] []" ], "description": "Lists commands", - "help": "The list<\/info> command lists all commands:\n\n php app\/console list<\/info>\n\nYou can also display the commands for a specific namespace:\n\n php app\/console list test<\/info>\n\nYou can also output the information in other formats by using the --format<\/comment> option:\n\n php app\/console list --format=xml<\/info>\n\nIt's also possible to get raw list of commands (useful for embedding command runner):\n\n php app\/console list --raw<\/info>", + "help": "The list<\/info> command lists all commands:\n\n app\/console list<\/info>\n\nYou can also display the commands for a specific namespace:\n\n app\/console list test<\/info>\n\nYou can also output the information in other formats by using the --format<\/comment> option:\n\n app\/console list --format=xml<\/info>\n\nIt's also possible to get raw list of commands (useful for embedding command runner):\n\n app\/console list --raw<\/info>", "definition": { "arguments": { "namespace": { @@ -139,6 +139,69 @@ "is_multiple": false, "description": "The output format (txt, xml, json, or md)", "default": "txt" + }, + "help": { + "name": "--help", + "shortcut": "-h", + "accept_value": false, + "is_value_required": false, + "is_multiple": false, + "description": "Display help for the given command. When no command is given display help for the list command", + "default": false + }, + "quiet": { + "name": "--quiet", + "shortcut": "-q", + "accept_value": false, + "is_value_required": false, + "is_multiple": false, + "description": "Do not output any message", + "default": false + }, + "verbose": { + "name": "--verbose", + "shortcut": "-v|-vv|-vvv", + "accept_value": false, + "is_value_required": false, + "is_multiple": false, + "description": "Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug", + "default": false + }, + "version": { + "name": "--version", + "shortcut": "-V", + "accept_value": false, + "is_value_required": false, + "is_multiple": false, + "description": "Display this application version", + "default": false + }, + "ansi": { + "name": "--ansi", + "shortcut": "", + "accept_value": false, + "is_value_required": false, + "is_multiple": false, + "description": "Force ANSI output", + "default": false + }, + "no-ansi": { + "name": "--no-ansi", + "shortcut": "", + "accept_value": false, + "is_value_required": false, + "is_multiple": false, + "description": "Disable ANSI output", + "default": false + }, + "no-interaction": { + "name": "--no-interaction", + "shortcut": "-n", + "accept_value": false, + "is_value_required": false, + "is_multiple": false, + "description": "Do not ask any interactive question", + "default": false } } } diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_1.md b/src/Symfony/Component/Console/Tests/Fixtures/application_1.md index b46c975a79082..38c84e4e232b9 100644 --- a/src/Symfony/Component/Console/Tests/Fixtures/application_1.md +++ b/src/Symfony/Component/Console/Tests/Fixtures/application_1.md @@ -15,11 +15,11 @@ Displays help for a command The help command displays help for a given command: - php app/console help list + app/console help list You can also output the help in other formats by using the --format option: - php app/console help --format=xml list + app/console help --format=xml list To display the list of available commands, please use the list command. @@ -55,7 +55,7 @@ To output raw command help #### `--help|-h` -Display this help message +Display help for the given command. When no command is given display help for the list command * Accept value: no * Is value required: no @@ -127,19 +127,19 @@ Lists commands The list command lists all commands: - php app/console list + app/console list You can also display the commands for a specific namespace: - php app/console list test + app/console list test You can also output the information in other formats by using the --format option: - php app/console list --format=xml + app/console list --format=xml It's also possible to get raw list of commands (useful for embedding command runner): - php app/console list --raw + app/console list --raw ### Arguments @@ -170,3 +170,66 @@ The output format (txt, xml, json, or md) * Is value required: yes * Is multiple: no * Default: `'txt'` + +#### `--help|-h` + +Display help for the given command. When no command is given display help for the list command + +* Accept value: no +* Is value required: no +* Is multiple: no +* Default: `false` + +#### `--quiet|-q` + +Do not output any message + +* Accept value: no +* Is value required: no +* Is multiple: no +* Default: `false` + +#### `--verbose|-v|-vv|-vvv` + +Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug + +* Accept value: no +* Is value required: no +* Is multiple: no +* Default: `false` + +#### `--version|-V` + +Display this application version + +* Accept value: no +* Is value required: no +* Is multiple: no +* Default: `false` + +#### `--ansi` + +Force ANSI output + +* Accept value: no +* Is value required: no +* Is multiple: no +* Default: `false` + +#### `--no-ansi` + +Disable ANSI output + +* Accept value: no +* Is value required: no +* Is multiple: no +* Default: `false` + +#### `--no-interaction|-n` + +Do not ask any interactive question + +* Accept value: no +* Is value required: no +* Is multiple: no +* Default: `false` diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_1.txt b/src/Symfony/Component/Console/Tests/Fixtures/application_1.txt index 8a7b47e0c4b00..cc55447241368 100644 --- a/src/Symfony/Component/Console/Tests/Fixtures/application_1.txt +++ b/src/Symfony/Component/Console/Tests/Fixtures/application_1.txt @@ -4,7 +4,7 @@ Console Tool command [options] [arguments] Options: - -h, --help Display this help message + -h, --help Display help for the given command. When no command is given display help for the list command -q, --quiet Do not output any message -V, --version Display this application version --ansi Force ANSI output diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_1.xml b/src/Symfony/Component/Console/Tests/Fixtures/application_1.xml index a0bd076c5a9f3..5b6a906c5a2a2 100644 --- a/src/Symfony/Component/Console/Tests/Fixtures/application_1.xml +++ b/src/Symfony/Component/Console/Tests/Fixtures/application_1.xml @@ -8,11 +8,11 @@ Displays help for a command The <info>help</info> command displays help for a given command: - <info>php app/console help list</info> + <info>app/console help list</info> You can also output the help in other formats by using the <comment>--format</comment> option: - <info>php app/console help --format=xml list</info> + <info>app/console help --format=xml list</info> To display the list of available commands, please use the <info>list</info> command. @@ -34,7 +34,7 @@ To output raw command help + + + + + + + diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_2.json b/src/Symfony/Component/Console/Tests/Fixtures/application_2.json index 4777a60b52a3e..ef8c99746bd62 100644 --- a/src/Symfony/Component/Console/Tests/Fixtures/application_2.json +++ b/src/Symfony/Component/Console/Tests/Fixtures/application_2.json @@ -11,7 +11,7 @@ "help [--format FORMAT] [--raw] [--] []" ], "description": "Displays help for a command", - "help": "The help<\/info> command displays help for a given command:\n\n php app\/console help list<\/info>\n\nYou can also output the help in other formats by using the --format<\/comment> option:\n\n php app\/console help --format=xml list<\/info>\n\nTo display the list of available commands, please use the list<\/info> command.", + "help": "The help<\/info> command displays help for a given command:\n\n app\/console help list<\/info>\n\nYou can also output the help in other formats by using the --format<\/comment> option:\n\n app\/console help --format=xml list<\/info>\n\nTo display the list of available commands, please use the list<\/info> command.", "definition": { "arguments": { "command_name": { @@ -47,7 +47,7 @@ "accept_value": false, "is_value_required": false, "is_multiple": false, - "description": "Display this help message", + "description": "Display help for the given command. When no command is given display help for the list command", "default": false }, "quiet": { @@ -114,7 +114,7 @@ "list [--raw] [--format FORMAT] [--] []" ], "description": "Lists commands", - "help": "The list<\/info> command lists all commands:\n\n php app\/console list<\/info>\n\nYou can also display the commands for a specific namespace:\n\n php app\/console list test<\/info>\n\nYou can also output the information in other formats by using the --format<\/comment> option:\n\n php app\/console list --format=xml<\/info>\n\nIt's also possible to get raw list of commands (useful for embedding command runner):\n\n php app\/console list --raw<\/info>", + "help": "The list<\/info> command lists all commands:\n\n app\/console list<\/info>\n\nYou can also display the commands for a specific namespace:\n\n app\/console list test<\/info>\n\nYou can also output the information in other formats by using the --format<\/comment> option:\n\n app\/console list --format=xml<\/info>\n\nIt's also possible to get raw list of commands (useful for embedding command runner):\n\n app\/console list --raw<\/info>", "definition": { "arguments": { "namespace": { @@ -143,6 +143,69 @@ "is_multiple": false, "description": "The output format (txt, xml, json, or md)", "default": "txt" + }, + "help": { + "name": "--help", + "shortcut": "-h", + "accept_value": false, + "is_value_required": false, + "is_multiple": false, + "description": "Display help for the given command. When no command is given display help for the list command", + "default": false + }, + "quiet": { + "name": "--quiet", + "shortcut": "-q", + "accept_value": false, + "is_value_required": false, + "is_multiple": false, + "description": "Do not output any message", + "default": false + }, + "verbose": { + "name": "--verbose", + "shortcut": "-v|-vv|-vvv", + "accept_value": false, + "is_value_required": false, + "is_multiple": false, + "description": "Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug", + "default": false + }, + "version": { + "name": "--version", + "shortcut": "-V", + "accept_value": false, + "is_value_required": false, + "is_multiple": false, + "description": "Display this application version", + "default": false + }, + "ansi": { + "name": "--ansi", + "shortcut": "", + "accept_value": false, + "is_value_required": false, + "is_multiple": false, + "description": "Force ANSI output", + "default": false + }, + "no-ansi": { + "name": "--no-ansi", + "shortcut": "", + "accept_value": false, + "is_value_required": false, + "is_multiple": false, + "description": "Disable ANSI output", + "default": false + }, + "no-interaction": { + "name": "--no-interaction", + "shortcut": "-n", + "accept_value": false, + "is_value_required": false, + "is_multiple": false, + "description": "Do not ask any interactive question", + "default": false } } } @@ -166,7 +229,7 @@ "accept_value": false, "is_value_required": false, "is_multiple": false, - "description": "Display this help message", + "description": "Display help for the given command. When no command is given display help for the list command", "default": false }, "quiet": { @@ -262,7 +325,7 @@ "accept_value": false, "is_value_required": false, "is_multiple": false, - "description": "Display this help message", + "description": "Display help for the given command. When no command is given display help for the list command", "default": false }, "quiet": { @@ -339,7 +402,7 @@ "accept_value": false, "is_value_required": false, "is_multiple": false, - "description": "Display this help message", + "description": "Display help for the given command. When no command is given display help for the list command", "default": false }, "quiet": { @@ -418,7 +481,7 @@ "accept_value": false, "is_value_required": false, "is_multiple": false, - "description": "Display this help message", + "description": "Display help for the given command. When no command is given display help for the list command", "default": false }, "quiet": { diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_2.md b/src/Symfony/Component/Console/Tests/Fixtures/application_2.md index 5b4896c0825c7..5f957da376474 100644 --- a/src/Symfony/Component/Console/Tests/Fixtures/application_2.md +++ b/src/Symfony/Component/Console/Tests/Fixtures/application_2.md @@ -28,11 +28,11 @@ Displays help for a command The help command displays help for a given command: - php app/console help list + app/console help list You can also output the help in other formats by using the --format option: - php app/console help --format=xml list + app/console help --format=xml list To display the list of available commands, please use the list command. @@ -68,7 +68,7 @@ To output raw command help #### `--help|-h` -Display this help message +Display help for the given command. When no command is given display help for the list command * Accept value: no * Is value required: no @@ -140,19 +140,19 @@ Lists commands The list command lists all commands: - php app/console list + app/console list You can also display the commands for a specific namespace: - php app/console list test + app/console list test You can also output the information in other formats by using the --format option: - php app/console list --format=xml + app/console list --format=xml It's also possible to get raw list of commands (useful for embedding command runner): - php app/console list --raw + app/console list --raw ### Arguments @@ -184,6 +184,69 @@ The output format (txt, xml, json, or md) * Is multiple: no * Default: `'txt'` +#### `--help|-h` + +Display help for the given command. When no command is given display help for the list command + +* Accept value: no +* Is value required: no +* Is multiple: no +* Default: `false` + +#### `--quiet|-q` + +Do not output any message + +* Accept value: no +* Is value required: no +* Is multiple: no +* Default: `false` + +#### `--verbose|-v|-vv|-vvv` + +Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug + +* Accept value: no +* Is value required: no +* Is multiple: no +* Default: `false` + +#### `--version|-V` + +Display this application version + +* Accept value: no +* Is value required: no +* Is multiple: no +* Default: `false` + +#### `--ansi` + +Force ANSI output + +* Accept value: no +* Is value required: no +* Is multiple: no +* Default: `false` + +#### `--no-ansi` + +Disable ANSI output + +* Accept value: no +* Is value required: no +* Is multiple: no +* Default: `false` + +#### `--no-interaction|-n` + +Do not ask any interactive question + +* Accept value: no +* Is value required: no +* Is multiple: no +* Default: `false` + `descriptor:command1` --------------------- @@ -201,7 +264,7 @@ command 1 help #### `--help|-h` -Display this help message +Display help for the given command. When no command is given display help for the list command * Accept value: no * Is value required: no @@ -294,7 +357,7 @@ command 2 help #### `--help|-h` -Display this help message +Display help for the given command. When no command is given display help for the list command * Accept value: no * Is value required: no @@ -369,7 +432,7 @@ Do not ask any interactive question #### `--help|-h` -Display this help message +Display help for the given command. When no command is given display help for the list command * Accept value: no * Is value required: no diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_2.txt b/src/Symfony/Component/Console/Tests/Fixtures/application_2.txt index d624f19466221..0aab070b08797 100644 --- a/src/Symfony/Component/Console/Tests/Fixtures/application_2.txt +++ b/src/Symfony/Component/Console/Tests/Fixtures/application_2.txt @@ -4,7 +4,7 @@ My Symfony application v1.0 command [options] [arguments] Options: - -h, --help Display this help message + -h, --help Display help for the given command. When no command is given display help for the list command -q, --quiet Do not output any message -V, --version Display this application version --ansi Force ANSI output diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_2.xml b/src/Symfony/Component/Console/Tests/Fixtures/application_2.xml index 5f0f98bd9f15d..4ec76624a7592 100644 --- a/src/Symfony/Component/Console/Tests/Fixtures/application_2.xml +++ b/src/Symfony/Component/Console/Tests/Fixtures/application_2.xml @@ -8,11 +8,11 @@ Displays help for a command The <info>help</info> command displays help for a given command: - <info>php app/console help list</info> + <info>app/console help list</info> You can also output the help in other formats by using the <comment>--format</comment> option: - <info>php app/console help --format=xml list</info> + <info>app/console help --format=xml list</info> To display the list of available commands, please use the <info>list</info> command. @@ -34,7 +34,7 @@ To output raw command help + + + + + + +