diff --git a/UPGRADE-8.1.md b/UPGRADE-8.1.md new file mode 100644 index 0000000000000..4aefd06c644b8 --- /dev/null +++ b/UPGRADE-8.1.md @@ -0,0 +1,12 @@ +UPGRADE FROM 8.0 to 8.1 +======================= + +DependencyInjection +------------------- + + * Deprecate configuring options `alias`, `parent`, `synthetic`, `file`, `arguments`, `properties`, `configurator` or `calls` when using `from_callable` + +FrameworkBundle +--------------- + + * Deprecate setting the `framework.profiler.collect_serializer_data` config option diff --git a/splitsh.json b/splitsh.json new file mode 100644 index 0000000000000..232189f349bc0 --- /dev/null +++ b/splitsh.json @@ -0,0 +1,198 @@ +{ + "subtrees": { + "doctrine-bridge": "src/Symfony/Bridge/Doctrine", + "monolog-bridge": "src/Symfony/Bridge/Monolog", + "phpunit-bridge": "src/Symfony/Bridge/PhpUnit", + "psr-http-message-bridge": "src/Symfony/Bridge/PsrHttpMessage", + "twig-bridge": "src/Symfony/Bridge/Twig", + "debug-bundle": "src/Symfony/Bundle/DebugBundle", + "framework-bundle": "src/Symfony/Bundle/FrameworkBundle", + "security-bundle": "src/Symfony/Bundle/SecurityBundle", + "twig-bundle": "src/Symfony/Bundle/TwigBundle", + "web-profiler-bundle": "src/Symfony/Bundle/WebProfilerBundle", + "asset": "src/Symfony/Component/Asset", + "asset-mapper": "src/Symfony/Component/AssetMapper", + "browser-kit": "src/Symfony/Component/BrowserKit", + "cache": "src/Symfony/Component/Cache", + "clock": "src/Symfony/Component/Clock", + "config": "src/Symfony/Component/Config", + "console": "src/Symfony/Component/Console", + "css-selector": "src/Symfony/Component/CssSelector", + "dependency-injection": "src/Symfony/Component/DependencyInjection", + "dom-crawler": "src/Symfony/Component/DomCrawler", + "dotenv": "src/Symfony/Component/Dotenv", + "emoji": "src/Symfony/Component/Emoji", + "error-handler": "src/Symfony/Component/ErrorHandler", + "event-dispatcher": "src/Symfony/Component/EventDispatcher", + "expression-language": "src/Symfony/Component/ExpressionLanguage", + "filesystem": "src/Symfony/Component/Filesystem", + "finder": "src/Symfony/Component/Finder", + "form": "src/Symfony/Component/Form", + "html-sanitizer": "src/Symfony/Component/HtmlSanitizer", + "http-client": "src/Symfony/Component/HttpClient", + "http-foundation": "src/Symfony/Component/HttpFoundation", + "http-kernel": "src/Symfony/Component/HttpKernel", + "intl": "src/Symfony/Component/Intl", + "json-path": "src/Symfony/Component/JsonPath", + "json-streamer": "src/Symfony/Component/JsonStreamer", + "ldap": "src/Symfony/Component/Ldap", + "lock": { + "prefixes": [{ "from": "src/Symfony/Component/Lock", "to": "", "excludes": ["Bridge"] }] + }, + "dynamo-db-lock": "src/Symfony/Component/Lock/Bridge/DynamoDb", + "mailer": { + "prefixes": [{ "from": "src/Symfony/Component/Mailer", "to": "", "excludes": ["Bridge"] }] + }, + "aha-send-mailer": "src/Symfony/Component/Mailer/Bridge/AhaSend", + "amazon-mailer": "src/Symfony/Component/Mailer/Bridge/Amazon", + "azure-mailer": "src/Symfony/Component/Mailer/Bridge/Azure", + "brevo-mailer": "src/Symfony/Component/Mailer/Bridge/Brevo", + "google-mailer": "src/Symfony/Component/Mailer/Bridge/Google", + "infobip-mailer": "src/Symfony/Component/Mailer/Bridge/Infobip", + "mail-pace-mailer": "src/Symfony/Component/Mailer/Bridge/MailPace", + "mailchimp-mailer": "src/Symfony/Component/Mailer/Bridge/Mailchimp", + "mailer-send-mailer": "src/Symfony/Component/Mailer/Bridge/MailerSend", + "mailgun-mailer": "src/Symfony/Component/Mailer/Bridge/Mailgun", + "mailjet-mailer": "src/Symfony/Component/Mailer/Bridge/Mailjet", + "mailomat-mailer": "src/Symfony/Component/Mailer/Bridge/Mailomat", + "mailtrap-mailer": "src/Symfony/Component/Mailer/Bridge/Mailtrap", + "microsoft-graph-mailer": "src/Symfony/Component/Mailer/Bridge/MicrosoftGraph", + "postal-mailer": "src/Symfony/Component/Mailer/Bridge/Postal", + "postmark-mailer": "src/Symfony/Component/Mailer/Bridge/Postmark", + "resend-mailer": "src/Symfony/Component/Mailer/Bridge/Resend", + "scaleway-mailer": "src/Symfony/Component/Mailer/Bridge/Scaleway", + "sendgrid-mailer": "src/Symfony/Component/Mailer/Bridge/Sendgrid", + "sweego-mailer": "src/Symfony/Component/Mailer/Bridge/Sweego", + "messenger": { + "prefixes": [{ "from": "src/Symfony/Component/Messenger", "to": "", "excludes": ["Bridge"] }] + }, + "amazon-sqs-messenger": "src/Symfony/Component/Messenger/Bridge/AmazonSqs", + "amqp-messenger": "src/Symfony/Component/Messenger/Bridge/Amqp", + "beanstalkd-messenger": "src/Symfony/Component/Messenger/Bridge/Beanstalkd", + "doctrine-messenger": "src/Symfony/Component/Messenger/Bridge/Doctrine", + "redis-messenger": "src/Symfony/Component/Messenger/Bridge/Redis", + "mime": "src/Symfony/Component/Mime", + "notifier": { + "prefixes": [{ "from": "src/Symfony/Component/Notifier", "to": "", "excludes": ["Bridge"] }] + }, + "all-my-sms-notifier": "src/Symfony/Component/Notifier/Bridge/AllMySms", + "amazon-sns-notifier": "src/Symfony/Component/Notifier/Bridge/AmazonSns", + "bandwidth-notifier": "src/Symfony/Component/Notifier/Bridge/Bandwidth", + "bluesky-notifier": "src/Symfony/Component/Notifier/Bridge/Bluesky", + "brevo-notifier": "src/Symfony/Component/Notifier/Bridge/Brevo", + "chatwork-notifier": "src/Symfony/Component/Notifier/Bridge/Chatwork", + "click-send-notifier": "src/Symfony/Component/Notifier/Bridge/ClickSend", + "clickatell-notifier": "src/Symfony/Component/Notifier/Bridge/Clickatell", + "contact-everyone-notifier": "src/Symfony/Component/Notifier/Bridge/ContactEveryone", + "discord-notifier": "src/Symfony/Component/Notifier/Bridge/Discord", + "engagespot-notifier": "src/Symfony/Component/Notifier/Bridge/Engagespot", + "esendex-notifier": "src/Symfony/Component/Notifier/Bridge/Esendex", + "expo-notifier": "src/Symfony/Component/Notifier/Bridge/Expo", + "fake-chat-notifier": "src/Symfony/Component/Notifier/Bridge/FakeChat", + "fake-sms-notifier": "src/Symfony/Component/Notifier/Bridge/FakeSms", + "firebase-notifier": "src/Symfony/Component/Notifier/Bridge/Firebase", + "forty-six-elks-notifier": "src/Symfony/Component/Notifier/Bridge/FortySixElks", + "free-mobile-notifier": "src/Symfony/Component/Notifier/Bridge/FreeMobile", + "gateway-api-notifier": "src/Symfony/Component/Notifier/Bridge/GatewayApi", + "go-ip-notifier": "src/Symfony/Component/Notifier/Bridge/GoIp", + "google-chat-notifier": "src/Symfony/Component/Notifier/Bridge/GoogleChat", + "infobip-notifier": "src/Symfony/Component/Notifier/Bridge/Infobip", + "iqsms-notifier": "src/Symfony/Component/Notifier/Bridge/Iqsms", + "isendpro-notifier": "src/Symfony/Component/Notifier/Bridge/Isendpro", + "joli-notif-notifier": "src/Symfony/Component/Notifier/Bridge/JoliNotif", + "kaz-info-teh-notifier": "src/Symfony/Component/Notifier/Bridge/KazInfoTeh", + "light-sms-notifier": "src/Symfony/Component/Notifier/Bridge/LightSms", + "line-bot-notifier": "src/Symfony/Component/Notifier/Bridge/LineBot", + "line-notify-notifier": "src/Symfony/Component/Notifier/Bridge/LineNotify", + "linked-in-notifier": "src/Symfony/Component/Notifier/Bridge/LinkedIn", + "lox24-notifier": "src/Symfony/Component/Notifier/Bridge/Lox24", + "mailjet-notifier": "src/Symfony/Component/Notifier/Bridge/Mailjet", + "mastodon-notifier": "src/Symfony/Component/Notifier/Bridge/Mastodon", + "matrix-notifier": "src/Symfony/Component/Notifier/Bridge/Matrix", + "mattermost-notifier": "src/Symfony/Component/Notifier/Bridge/Mattermost", + "mercure-notifier": "src/Symfony/Component/Notifier/Bridge/Mercure", + "message-bird-notifier": "src/Symfony/Component/Notifier/Bridge/MessageBird", + "message-media-notifier": "src/Symfony/Component/Notifier/Bridge/MessageMedia", + "microsoft-teams-notifier": "src/Symfony/Component/Notifier/Bridge/MicrosoftTeams", + "mobyt-notifier": "src/Symfony/Component/Notifier/Bridge/Mobyt", + "novu-notifier": "src/Symfony/Component/Notifier/Bridge/Novu", + "ntfy-notifier": "src/Symfony/Component/Notifier/Bridge/Ntfy", + "octopush-notifier": "src/Symfony/Component/Notifier/Bridge/Octopush", + "one-signal-notifier": "src/Symfony/Component/Notifier/Bridge/OneSignal", + "orange-sms-notifier": "src/Symfony/Component/Notifier/Bridge/OrangeSms", + "ovh-cloud-notifier": "src/Symfony/Component/Notifier/Bridge/OvhCloud", + "pager-duty-notifier": "src/Symfony/Component/Notifier/Bridge/PagerDuty", + "plivo-notifier": "src/Symfony/Component/Notifier/Bridge/Plivo", + "primotexto-notifier": "src/Symfony/Component/Notifier/Bridge/Primotexto", + "pushover-notifier": "src/Symfony/Component/Notifier/Bridge/Pushover", + "pushy-notifier": "src/Symfony/Component/Notifier/Bridge/Pushy", + "redlink-notifier": "src/Symfony/Component/Notifier/Bridge/Redlink", + "ring-central-notifier": "src/Symfony/Component/Notifier/Bridge/RingCentral", + "rocket-chat-notifier": "src/Symfony/Component/Notifier/Bridge/RocketChat", + "sendberry-notifier": "src/Symfony/Component/Notifier/Bridge/Sendberry", + "sevenio-notifier": "src/Symfony/Component/Notifier/Bridge/Sevenio", + "simple-textin-notifier": "src/Symfony/Component/Notifier/Bridge/SimpleTextin", + "sinch-notifier": "src/Symfony/Component/Notifier/Bridge/Sinch", + "sipgate-notifier": "src/Symfony/Component/Notifier/Bridge/Sipgate", + "slack-notifier": "src/Symfony/Component/Notifier/Bridge/Slack", + "sms-biuras-notifier": "src/Symfony/Component/Notifier/Bridge/SmsBiuras", + "sms-factor-notifier": "src/Symfony/Component/Notifier/Bridge/SmsFactor", + "sms-sluzba-notifier": "src/Symfony/Component/Notifier/Bridge/SmsSluzba", + "smsapi-notifier": "src/Symfony/Component/Notifier/Bridge/Smsapi", + "smsbox-notifier": "src/Symfony/Component/Notifier/Bridge/Smsbox", + "smsc-notifier": "src/Symfony/Component/Notifier/Bridge/Smsc", + "smsense-notifier": "src/Symfony/Component/Notifier/Bridge/Smsense", + "smsmode-notifier": "src/Symfony/Component/Notifier/Bridge/Smsmode", + "spot-hit-notifier": "src/Symfony/Component/Notifier/Bridge/SpotHit", + "sweego-notifier": "src/Symfony/Component/Notifier/Bridge/Sweego", + "telegram-notifier": "src/Symfony/Component/Notifier/Bridge/Telegram", + "telnyx-notifier": "src/Symfony/Component/Notifier/Bridge/Telnyx", + "termii-notifier": "src/Symfony/Component/Notifier/Bridge/Termii", + "turbo-sms-notifier": "src/Symfony/Component/Notifier/Bridge/TurboSms", + "twilio-notifier": "src/Symfony/Component/Notifier/Bridge/Twilio", + "twitter-notifier": "src/Symfony/Component/Notifier/Bridge/Twitter", + "unifonic-notifier": "src/Symfony/Component/Notifier/Bridge/Unifonic", + "vonage-notifier": "src/Symfony/Component/Notifier/Bridge/Vonage", + "yunpian-notifier": "src/Symfony/Component/Notifier/Bridge/Yunpian", + "zendesk-notifier": "src/Symfony/Component/Notifier/Bridge/Zendesk", + "zulip-notifier": "src/Symfony/Component/Notifier/Bridge/Zulip", + "object-mapper": "src/Symfony/Component/ObjectMapper", + "options-resolver": "src/Symfony/Component/OptionsResolver", + "password-hasher": "src/Symfony/Component/PasswordHasher", + "process": "src/Symfony/Component/Process", + "property-access": "src/Symfony/Component/PropertyAccess", + "property-info": "src/Symfony/Component/PropertyInfo", + "rate-limiter": "src/Symfony/Component/RateLimiter", + "remote-event": "src/Symfony/Component/RemoteEvent", + "routing": "src/Symfony/Component/Routing", + "runtime": "src/Symfony/Component/Runtime", + "scheduler": "src/Symfony/Component/Scheduler", + "security-core": "src/Symfony/Component/Security/Core", + "security-csrf": "src/Symfony/Component/Security/Csrf", + "security-http": "src/Symfony/Component/Security/Http", + "semaphore": "src/Symfony/Component/Semaphore", + "serializer": "src/Symfony/Component/Serializer", + "stopwatch": "src/Symfony/Component/Stopwatch", + "string": "src/Symfony/Component/String", + "translation": { + "prefixes": [{ "from": "src/Symfony/Component/Translation", "to": "", "excludes": ["Bridge"] }] + }, + "crowdin-translation-provider": "src/Symfony/Component/Translation/Bridge/Crowdin", + "loco-translation-provider": "src/Symfony/Component/Translation/Bridge/Loco", + "lokalise-translation-provider": "src/Symfony/Component/Translation/Bridge/Lokalise", + "phrase-translation-provider": "src/Symfony/Component/Translation/Bridge/Phrase", + "type-info": "src/Symfony/Component/TypeInfo", + "uid": "src/Symfony/Component/Uid", + "validator": "src/Symfony/Component/Validator", + "var-dumper": "src/Symfony/Component/VarDumper", + "var-exporter": "src/Symfony/Component/VarExporter", + "web-link": "src/Symfony/Component/WebLink", + "webhook": "src/Symfony/Component/Webhook", + "workflow": "src/Symfony/Component/Workflow", + "yaml": "src/Symfony/Component/Yaml", + "contracts": "src/Symfony/Contracts" + }, + "defaults": { + "git_constraint": "<1.8.2" + } +} diff --git a/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php b/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php index f62628667798b..76972606a465b 100644 --- a/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php +++ b/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php @@ -276,6 +276,7 @@ if ($PHPUNIT_REMOVE_RETURN_TYPEHINT) { $alteredCode = preg_replace('/^ ((?:protected|public)(?: static)? function \w+\(\)): void/m', ' $1', $alteredCode); } + file_put_contents($alteredFile, $alteredCode); // Mutate Assert code $alteredCode = file_get_contents($alteredFile = './src/Framework/Assert.php'); diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig index f1856f066e25f..4e2def28cb1ab 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig @@ -285,7 +285,7 @@ '%name%': name, '%id%': id, }) %} - {%- else -%} + {%- elseif 'button' not in block_prefixes or label is not same as(false) -%} {% set label = name|humanize %} {%- endif -%} {%- endif -%} diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index be82a64bda2a2..914ff0d1c051b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +8.1 +--- + + * Deprecate setting the `framework.profiler.collect_serializer_data` config option + 8.0 --- diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 179d288504805..89f2f83e22069 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -361,7 +361,11 @@ private function addProfilerSection(ArrayNodeDefinition $rootNode): void ->booleanNode('only_exceptions')->defaultFalse()->end() ->booleanNode('only_main_requests')->defaultFalse()->end() ->scalarNode('dsn')->defaultValue('file:%kernel.cache_dir%/profiler')->end() - ->enumNode('collect_serializer_data')->values([true])->defaultTrue()->end() // to be @deprecated in Symfony 8.1 + ->enumNode('collect_serializer_data') + ->values([true]) + ->defaultTrue() + ->setDeprecated('symfony/framework-bundle', '8.1', 'Setting the "%path%.%node%" configuration option is deprecated. It will be removed in version 9.0.') + ->end() ->end() ->end() ->end() diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 01d64ee611dfe..5e0d5a5ccaab7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -579,7 +579,7 @@ public function load(array $configs, ContainerBuilder $container): void $this->registerNotifierConfiguration($config['notifier'], $container, $loader, $this->readConfigEnabled('webhook', $container, $config['webhook'])); } - // profiler depends on form, validation, translation, messenger, mailer, http-client, notifier, serializer being registered + // profiler depends on form, validation, translation, messenger, mailer, http-client, notifier, serializer being registered. console is optional $this->registerProfilerConfiguration($config['profiler'], $container, $loader); if ($this->readConfigEnabled('webhook', $container, $config['webhook'])) { @@ -992,7 +992,7 @@ private function registerProfilerConfiguration(array $config, ContainerBuilder $ $container->getDefinition('profiler_listener') ->addArgument($config['collect_parameter']); - if (!$container->getParameter('kernel.debug') || !$container->has('debug.stopwatch')) { + if (!$container->getParameter('kernel.debug') || !$this->hasConsole() || !$container->has('debug.stopwatch')) { $container->removeDefinition('console_profiler_listener'); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/profiler.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/profiler.php index 56cc6a1723fa4..60d0cdf8b54e2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/profiler.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/profiler.php @@ -3,7 +3,6 @@ $container->loadFromExtension('framework', [ 'profiler' => [ 'enabled' => true, - 'collect_serializer_data' => true, ], 'serializer' => [ 'enabled' => true, diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/profiler.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/profiler.yml index 8cc59ab8f46fe..1cfe686b1eb19 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/profiler.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/profiler.yml @@ -1,6 +1,5 @@ framework: profiler: enabled: true - collect_serializer_data: true serializer: enabled: 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 7c0a1b2bcf3cc..17e9cba676fe6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/config/framework.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/config/framework.yml @@ -13,8 +13,6 @@ framework: storage_factory_id: session.storage.factory.mock_file cookie_secure: auto cookie_samesite: lax - profiler: - collect_serializer_data: true services: logger: { class: Psr\Log\NullLogger } diff --git a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md index 73ba9206d744a..a92b1800eb20f 100644 --- a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +8.1 +--- + + * Add support for the `clientHints`, `prefetchCache`, and `prerenderCache` `ClearSite-Data` directives + 8.0 --- diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php index 45476a176f69b..a8e5a6a317429 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php @@ -236,7 +236,7 @@ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $facto ->beforeNormalization()->ifString()->then(static fn ($v) => $v ? array_map('trim', explode(',', $v)) : [])->end() ->enumPrototype() ->values([ - '*', 'cache', 'cookies', 'storage', 'executionContexts', + '*', 'cache', 'cookies', 'storage', 'clientHints', 'executionContexts', 'prefetchCache', 'prerenderCache', ]) ->end() ->end() 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 0601c253a5c1c..14d47a95bb9be 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/FirewallEntryPoint/config.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/FirewallEntryPoint/config.yml @@ -14,7 +14,6 @@ framework: cookie_samesite: lax profiler: only_exceptions: false - collect_serializer_data: true services: logger: { class: Psr\Log\NullLogger } 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 1b16f1f027444..38ea5de3c2659 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/config/framework.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/config/framework.yml @@ -15,7 +15,6 @@ framework: cookie_samesite: lax profiler: only_exceptions: false - collect_serializer_data: true services: logger: { class: Psr\Log\NullLogger } diff --git a/src/Symfony/Bundle/TwigBundle/CHANGELOG.md b/src/Symfony/Bundle/TwigBundle/CHANGELOG.md index ed3776b92b16f..20d7ff6b5ecd0 100644 --- a/src/Symfony/Bundle/TwigBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/TwigBundle/CHANGELOG.md @@ -7,6 +7,11 @@ CHANGELOG * Make `TemplateCacheWarmer` class `final` * Remove the `base_template_class` config option +7.4 +--- + +* Deprecate setting the `exception_controller` config to `null`. This was a legacy opt-out of a deprecation that is a no-op since Symfony 5.0. Remove that setting entirely instead. + 7.3 --- diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php index dc63335624c30..e085d5d7df32a 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php @@ -14,6 +14,7 @@ use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\Mime\HtmlToTextConverter\HtmlToTextConverterInterface; /** @@ -33,7 +34,7 @@ public function getConfigTreeBuilder(): TreeBuilder $rootNode ->docUrl('https://symfony.com/doc/{version:major}.{version:minor}/reference/configuration/twig.html', 'symfony/twig-bundle') - ->end(); + ; $this->addFormThemesSection($rootNode); $this->addGlobalsSection($rootNode); diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/Functional/WebProfilerBundleKernel.php b/src/Symfony/Bundle/WebProfilerBundle/Tests/Functional/WebProfilerBundleKernel.php index 556befe105409..4915fdb176248 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Tests/Functional/WebProfilerBundleKernel.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Tests/Functional/WebProfilerBundleKernel.php @@ -52,7 +52,7 @@ protected function configureContainer(ContainerBuilder $container, LoaderInterfa { $config = [ 'secret' => 'foo-secret', - 'profiler' => ['only_exceptions' => false, 'collect_serializer_data' => true], + 'profiler' => ['only_exceptions' => false], 'session' => ['handler_id' => null, 'storage_factory_id' => 'session.storage.factory.mock_file', 'cookie-secure' => 'auto', 'cookie-samesite' => 'lax'], 'router' => ['utf8' => true], ]; diff --git a/src/Symfony/Component/Config/Definition/Builder/FloatNodeDefinition.php b/src/Symfony/Component/Config/Definition/Builder/FloatNodeDefinition.php index 2548edc320c59..7fffca787bfc1 100644 --- a/src/Symfony/Component/Config/Definition/Builder/FloatNodeDefinition.php +++ b/src/Symfony/Component/Config/Definition/Builder/FloatNodeDefinition.php @@ -16,7 +16,7 @@ /** * This class provides a fluent interface for defining a float node. * - * @template TParent of NodeParentInterface|null + * @template TParent of NodeParentInterface|null = null * * @extends NumericNodeDefinition * diff --git a/src/Symfony/Component/Config/Definition/Builder/IntegerNodeDefinition.php b/src/Symfony/Component/Config/Definition/Builder/IntegerNodeDefinition.php index 0a16859e1491e..da28d5a40a220 100644 --- a/src/Symfony/Component/Config/Definition/Builder/IntegerNodeDefinition.php +++ b/src/Symfony/Component/Config/Definition/Builder/IntegerNodeDefinition.php @@ -16,7 +16,7 @@ /** * This class provides a fluent interface for defining an integer node. * - * @template TParent of NodeParentInterface|null + * @template TParent of NodeParentInterface|null = null * * @extends NumericNodeDefinition * diff --git a/src/Symfony/Component/Config/Definition/Builder/NodeBuilder.php b/src/Symfony/Component/Config/Definition/Builder/NodeBuilder.php index 45cde61b5ceac..c24bc570ac132 100644 --- a/src/Symfony/Component/Config/Definition/Builder/NodeBuilder.php +++ b/src/Symfony/Component/Config/Definition/Builder/NodeBuilder.php @@ -14,7 +14,7 @@ /** * This class provides a fluent interface for building a node. * - * @template TParent of (NodeDefinition&ParentNodeDefinitionInterface)|null + * @template TParent of (NodeDefinition&ParentNodeDefinitionInterface)|null = null * * @author Johannes M. Schmitt */ diff --git a/src/Symfony/Component/Config/Definition/Builder/NodeDefinition.php b/src/Symfony/Component/Config/Definition/Builder/NodeDefinition.php index c6f4d27375e23..ad74d3eab7781 100644 --- a/src/Symfony/Component/Config/Definition/Builder/NodeDefinition.php +++ b/src/Symfony/Component/Config/Definition/Builder/NodeDefinition.php @@ -19,7 +19,7 @@ /** * This class provides a fluent interface for defining a node. * - * @template TParent of NodeParentInterface|null + * @template TParent of NodeParentInterface|null = null * * @author Johannes M. Schmitt */ diff --git a/src/Symfony/Component/Config/Definition/Builder/NumericNodeDefinition.php b/src/Symfony/Component/Config/Definition/Builder/NumericNodeDefinition.php index 47656ea2a96aa..6a8f5b966ee1d 100644 --- a/src/Symfony/Component/Config/Definition/Builder/NumericNodeDefinition.php +++ b/src/Symfony/Component/Config/Definition/Builder/NumericNodeDefinition.php @@ -16,7 +16,7 @@ /** * Abstract class that contains common code of integer and float node definitions. * - * @template TParent of NodeParentInterface|null + * @template TParent of NodeParentInterface|null = null * * @extends ScalarNodeDefinition * diff --git a/src/Symfony/Component/Config/Definition/Builder/ScalarNodeDefinition.php b/src/Symfony/Component/Config/Definition/Builder/ScalarNodeDefinition.php index 27e4caf4c7f6d..6409aa69c5445 100644 --- a/src/Symfony/Component/Config/Definition/Builder/ScalarNodeDefinition.php +++ b/src/Symfony/Component/Config/Definition/Builder/ScalarNodeDefinition.php @@ -16,7 +16,7 @@ /** * This class provides a fluent interface for defining a node. * - * @template TParent of NodeParentInterface|null + * @template TParent of NodeParentInterface|null = null * * @extends VariableNodeDefinition * diff --git a/src/Symfony/Component/Config/Definition/Builder/StringNodeDefinition.php b/src/Symfony/Component/Config/Definition/Builder/StringNodeDefinition.php index 9ff38d88dc3ad..e14f352adda25 100644 --- a/src/Symfony/Component/Config/Definition/Builder/StringNodeDefinition.php +++ b/src/Symfony/Component/Config/Definition/Builder/StringNodeDefinition.php @@ -16,7 +16,7 @@ /** * This class provides a fluent interface for defining a node. * - * @template TParent of NodeParentInterface|null + * @template TParent of NodeParentInterface|null = null * * @extends ScalarNodeDefinition * diff --git a/src/Symfony/Component/Config/Definition/Builder/VariableNodeDefinition.php b/src/Symfony/Component/Config/Definition/Builder/VariableNodeDefinition.php index 6431a70a3a44e..6e6977f6d273b 100644 --- a/src/Symfony/Component/Config/Definition/Builder/VariableNodeDefinition.php +++ b/src/Symfony/Component/Config/Definition/Builder/VariableNodeDefinition.php @@ -17,7 +17,7 @@ /** * This class provides a fluent interface for defining a node. * - * @template TParent of NodeParentInterface|null + * @template TParent of NodeParentInterface|null = null * * @extends NodeDefinition * diff --git a/src/Symfony/Component/DependencyInjection/CHANGELOG.md b/src/Symfony/Component/DependencyInjection/CHANGELOG.md index 343ed02c18226..e2632babb4efd 100644 --- a/src/Symfony/Component/DependencyInjection/CHANGELOG.md +++ b/src/Symfony/Component/DependencyInjection/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +8.1 +--- + + * Deprecate configuring options `alias`, `parent`, `synthetic`, `file`, `arguments`, `properties`, `configurator` or `calls` when using `from_callable` + 8.0 --- diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AnalyzeServiceReferencesPass.php b/src/Symfony/Component/DependencyInjection/Compiler/AnalyzeServiceReferencesPass.php index 02c8cf16347ee..a4348e934aad7 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/AnalyzeServiceReferencesPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/AnalyzeServiceReferencesPass.php @@ -39,6 +39,7 @@ class AnalyzeServiceReferencesPass extends AbstractRecursivePass private bool $lazy; private bool $byConstructor; private bool $byFactory; + private bool $byMultiUseArgument; private array $definitions; private array $aliases; @@ -63,6 +64,7 @@ public function process(ContainerBuilder $container): void $this->lazy = false; $this->byConstructor = false; $this->byFactory = false; + $this->byMultiUseArgument = false; $this->definitions = $container->getDefinitions(); $this->aliases = $container->getAliases(); @@ -85,7 +87,12 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed if ($value instanceof ArgumentInterface) { $this->lazy = !$this->byFactory || !$value instanceof IteratorArgument; + $byMultiUseArgument = $this->byMultiUseArgument; + if ($value instanceof IteratorArgument) { + $this->byMultiUseArgument = true; + } parent::processValue($value->getValues()); + $this->byMultiUseArgument = $byMultiUseArgument; $this->lazy = $lazy; return $value; @@ -102,7 +109,8 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed $value, $this->lazy || ($this->hasProxyDumper && $targetDefinition?->isLazy()), ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE === $value->getInvalidBehavior(), - $this->byConstructor + $this->byConstructor, + $this->byMultiUseArgument ); if ($inExpression) { @@ -113,7 +121,9 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed $targetDefinition, $value, $this->lazy || $targetDefinition?->isLazy(), - true + true, + $this->byConstructor, + $this->byMultiUseArgument ); } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ServiceReferenceGraph.php b/src/Symfony/Component/DependencyInjection/Compiler/ServiceReferenceGraph.php index 2544cde95498e..97f9398e1b6b9 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ServiceReferenceGraph.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ServiceReferenceGraph.php @@ -74,7 +74,7 @@ public function clear(): void /** * Connects 2 nodes together in the Graph. */ - public function connect(?string $sourceId, mixed $sourceValue, ?string $destId, mixed $destValue = null, ?Reference $reference = null, bool $lazy = false, bool $weak = false, bool $byConstructor = false): void + public function connect(?string $sourceId, mixed $sourceValue, ?string $destId, mixed $destValue = null, ?Reference $reference = null, bool $lazy = false, bool $weak = false, bool $byConstructor = false, bool $byMultiUseArgument = false): void { if (null === $sourceId || null === $destId) { return; @@ -82,7 +82,7 @@ public function connect(?string $sourceId, mixed $sourceValue, ?string $destId, $sourceNode = $this->createNode($sourceId, $sourceValue); $destNode = $this->createNode($destId, $destValue); - $edge = new ServiceReferenceGraphEdge($sourceNode, $destNode, $reference, $lazy, $weak, $byConstructor); + $edge = new ServiceReferenceGraphEdge($sourceNode, $destNode, $reference, $lazy, $weak, $byConstructor, $byMultiUseArgument); $sourceNode->addOutEdge($edge); $destNode->addInEdge($edge); diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ServiceReferenceGraphEdge.php b/src/Symfony/Component/DependencyInjection/Compiler/ServiceReferenceGraphEdge.php index 1606ee14ad7f2..bab92e88ed6db 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ServiceReferenceGraphEdge.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ServiceReferenceGraphEdge.php @@ -27,6 +27,7 @@ public function __construct( private bool $lazy = false, private bool $weak = false, private bool $byConstructor = false, + private bool $byMultiUseArgument = false, ) { } @@ -77,4 +78,9 @@ public function isReferencedByConstructor(): bool { return $this->byConstructor; } + + public function isFromMultiUseArgument(): bool + { + return $this->byMultiUseArgument; + } } diff --git a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php index 1bb7e91650239..f9488c324b37a 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php @@ -443,7 +443,7 @@ private function collectCircularReferences(string $sourceId, array $edges, array foreach ($edges as $edge) { $node = $edge->getDestNode(); $id = $node->getId(); - if ($sourceId === $id || !$node->getValue() instanceof Definition || $edge->isWeak()) { + if (($sourceId === $id && !$edge->isLazy()) || !$node->getValue() instanceof Definition || $edge->isWeak()) { continue; } @@ -684,7 +684,6 @@ private function addServiceInstance(string $id, Definition $definition, bool $is $asGhostObject = false; $isProxyCandidate = $this->isProxyCandidate($definition, $asGhostObject, $id); - $instantiation = ''; $lastWitherIndex = null; foreach ($definition->getMethodCalls() as $k => $call) { @@ -693,20 +692,26 @@ private function addServiceInstance(string $id, Definition $definition, bool $is } } - if (!$isProxyCandidate && $definition->isShared() && !isset($this->singleUsePrivateIds[$id]) && null === $lastWitherIndex) { - $instantiation = \sprintf('$container->%s[%s] = %s', $this->container->getDefinition($id)->isPublic() ? 'services' : 'privates', $this->doExport($id), $isSimpleInstance ? '' : '$instance'); - } elseif (!$isSimpleInstance) { - $instantiation = '$instance'; - } + $shouldShareInline = !$isProxyCandidate && $definition->isShared() && !isset($this->singleUsePrivateIds[$id]) && null === $lastWitherIndex; + $serviceAccessor = \sprintf('$container->%s[%s]', $this->container->getDefinition($id)->isPublic() ? 'services' : 'privates', $this->doExport($id)); + $return = match (true) { + $shouldShareInline && !isset($this->circularReferences[$id]) && $isSimpleInstance => 'return '.$serviceAccessor.' = ', + $shouldShareInline && !isset($this->circularReferences[$id]) => $serviceAccessor.' = $instance = ', + $shouldShareInline || !$isSimpleInstance => '$instance = ', + default => 'return ', + }; - $return = ''; - if ($isSimpleInstance) { - $return = 'return '; - } else { - $instantiation .= ' = '; + $code = $this->addNewInstance($definition, ' '.$return, $id, $asGhostObject); + + if ($shouldShareInline && isset($this->circularReferences[$id])) { + $code .= \sprintf( + "\n if (isset(%s)) {\n return %1\$s;\n }\n\n %s%1\$s = \$instance;\n", + $serviceAccessor, + $isSimpleInstance ? 'return ' : '' + ); } - return $this->addNewInstance($definition, ' '.$return.$instantiation, $id, $asGhostObject); + return $code; } private function isTrivialInstance(Definition $definition): bool @@ -1037,7 +1042,7 @@ private function addInlineService(string $id, Definition $definition, ?Definitio $code = ''; if ($isSimpleInstance = $isRootInstance = null === $inlineDef) { - foreach ($this->serviceCalls as $targetId => [$callCount, $behavior, $byConstructor]) { + foreach ($this->serviceCalls as $targetId => [, , $byConstructor]) { if ($byConstructor && isset($this->circularReferences[$id][$targetId]) && !$this->circularReferences[$id][$targetId] && !($this->hasProxyDumper && $definition->isLazy())) { $code .= $this->addInlineReference($id, $definition, $targetId, $forConstructor); } @@ -2200,7 +2205,7 @@ private function isSingleUsePrivateNode(ServiceReferenceGraphNode $node): bool if (!$value = $edge->getSourceNode()->getValue()) { continue; } - if ($edge->isLazy() || !$value instanceof Definition || !$value->isShared()) { + if ($edge->isLazy() || !$value instanceof Definition || !$value->isShared() || $edge->isFromMultiUseArgument()) { return false; } diff --git a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php index a0572b3a86091..785740df6db87 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php @@ -412,6 +412,10 @@ private function parseDefinition(string $id, array|string|null $service, string if (isset($service['factory'])) { throw new InvalidArgumentException(\sprintf('The configuration key "%s" is unsupported for the service "%s" when using "from_callable" in "%s".', $key, $id, $file)); } + + if (isset($service[$key])) { + trigger_deprecation('symfony/dependency-injection', '8.1', 'Configuring the "%s" key for the service "%s" when using "from_callable" is deprecated and will throw an "InvalidArgumentException" in 9.0.', $key, $id); + } } if ('Closure' !== $service['class'] ??= 'Closure') { diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AnalyzeServiceReferencesPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AnalyzeServiceReferencesPassTest.php index a075b51d41ae2..5b9e52e2a5706 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AnalyzeServiceReferencesPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AnalyzeServiceReferencesPassTest.php @@ -17,6 +17,7 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\ExpressionLanguage\Expression; class AnalyzeServiceReferencesPassTest extends TestCase { @@ -204,6 +205,29 @@ public function testProcessDetectsFactoryReferences() $this->assertCount(1, $graph->getNode('foo')->getInEdges()); } + public function testExpressionReferenceKeepsConstructorFlag() + { + $container = new ContainerBuilder(); + $container->register('bar'); + $container + ->register('foo') + ->addArgument(new Expression('service("bar")')); + + $graph = $this->process($container); + + $edges = $graph->getNode('bar')->getInEdges(); + $exprEdge = null; + foreach ($edges as $edge) { + if ('.internal.reference_in_expression' === $edge->getSourceNode()->getId()) { + $exprEdge = $edge; + break; + } + } + + $this->assertNotNull($exprEdge, 'Expression edge should exist.'); + $this->assertTrue($exprEdge->isReferencedByConstructor()); + } + protected function process(ContainerBuilder $container) { $pass = new AnalyzeServiceReferencesPass(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php index 6f1952ce10b98..10627c6e66098 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php @@ -320,6 +320,38 @@ public function testDumpAsFilesWithFactoriesInlinedWithTaggedIterator() $this->assertStringMatchesFormatFile(self::$fixturesPath.'/php/services9_inlined_factories_with_tagged_iterrator.txt', $dump); } + public function testTaggedIteratorServicesRemainSharedWhenUsedByFactory() + { + PhpDumperTest_TaggedIteratorService::reset(); + + $container = new ContainerBuilder(); + $container + ->register('tagged_service', PhpDumperTest_TaggedIteratorService::class) + ->addTag('tag1'); + $container + ->register('wrapper', PhpDumperTest_TaggedIteratorWrapper::class) + ->setFactory([new Reference('wrapper_factory'), 'create']) + ->setPublic(true); + $container + ->register('wrapper_factory', PhpDumperTest_TaggedIteratorWrapperFactory::class) + ->setArguments([new TaggedIteratorArgument('tag1')]); + + $container->compile(); + + $dumper = new PhpDumper($container); + eval('?>'.$dumper->dump(['class' => 'Symfony_DI_PhpDumper_Test_Tagged_Iterator_Factory'])); + + $compiled = new \Symfony_DI_PhpDumper_Test_Tagged_Iterator_Factory(); + $wrapper = $compiled->get('wrapper'); + + $firstIteration = iterator_to_array($wrapper->getTaggedServices(), false); + $secondIteration = iterator_to_array($wrapper->getTaggedServices(), false); + + $this->assertCount(1, $firstIteration); + $this->assertSame($firstIteration, $secondIteration); + $this->assertSame(1, PhpDumperTest_TaggedIteratorService::$constructed); + } + public function testDumpAsFilesWithLazyFactoriesInlined() { $container = new ContainerBuilder(); @@ -1813,6 +1845,37 @@ public function testReferencingDeprecatedPublicService() $this->addToAssertionCount(1); } + public function testDecoratedFactoryServiceKeepsReentrantInstance() + { + ReentrantFactory::reset(); + + $container = new ContainerBuilder(); + $container + ->register('decorated_service', \stdClass::class) + ->setPublic(true); + $container + ->register('decorated_service.reentrant', \stdClass::class) + ->setPublic(true) + ->setDecoratedService('decorated_service') + ->setFactory([ReentrantFactory::class, 'create']) + ->setArguments([ + new ServiceLocatorArgument(['decorated_service' => new Reference('decorated_service')]), + new Reference('decorated_service.reentrant.inner'), + ]); + + $container->compile(); + + $dumper = new PhpDumper($container); + eval('?>'.$dumper->dump(['class' => 'Symfony_DI_PhpDumper_Test_Reentrant_Service'])); + + $compiled = new \Symfony_DI_PhpDumper_Test_Reentrant_Service(); + + $service = $compiled->get('decorated_service'); + + $this->assertInstanceOf(\stdClass::class, ReentrantFactory::$reentrantInstance); + $this->assertSame(ReentrantFactory::$reentrantInstance, $service); + } + public function testExpressionInFactory() { $container = new ContainerBuilder(); @@ -2260,6 +2323,70 @@ public function __construct( } } +class ReentrantFactory +{ + public static ?object $reentrantInstance = null; + private static bool $shouldReenter = true; + + public static function reset(): void + { + self::$reentrantInstance = null; + self::$shouldReenter = true; + } + + public static function create(ServiceLocator $locator, object $inner): \stdClass + { + if (self::$shouldReenter) { + self::$shouldReenter = false; + self::$reentrantInstance = $locator->get('decorated_service'); + self::$shouldReenter = true; + } + + return (object) ['inner' => $inner]; + } +} + +class PhpDumperTest_TaggedIteratorService +{ + public static int $constructed = 0; + + public function __construct() + { + ++self::$constructed; + } + + public static function reset(): void + { + self::$constructed = 0; + } +} + +class PhpDumperTest_TaggedIteratorWrapper +{ + public function __construct( + private iterable $taggedServices, + ) { + } + + public function getTaggedServices(): iterable + { + return $this->taggedServices; + } +} + +class PhpDumperTest_TaggedIteratorWrapperFactory +{ + public function __construct( + private iterable $taggedServices, + ) { + } + + public function create(): PhpDumperTest_TaggedIteratorWrapper + { + return new PhpDumperTest_TaggedIteratorWrapper($this->taggedServices); + } +} + class InlineAdapterConsumer { public function __construct( diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/lazy_autowire_attribute_with_intersection.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/lazy_autowire_attribute_with_intersection.php index 15cab6e1d19de..2f0fb9fff66d4 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/lazy_autowire_attribute_with_intersection.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/lazy_autowire_attribute_with_intersection.php @@ -54,7 +54,13 @@ protected static function getFooService($container) return $container->services['foo']; } - return $container->services['foo'] = new \Symfony\Component\DependencyInjection\Tests\Compiler\AAndIInterfaceConsumer($a); + $instance = new \Symfony\Component\DependencyInjection\Tests\Compiler\AAndIInterfaceConsumer($a); + + if (isset($container->services['foo'])) { + return $container->services['foo']; + } + + return $container->services['foo'] = $instance; } /** diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_as_files.txt b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_as_files.txt index 7c2345f1510c4..ba685d66ada8b 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_as_files.txt +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_as_files.txt @@ -117,7 +117,13 @@ class getBazService extends ProjectServiceContainer */ public static function do($container, $lazyLoad = true) { - $container->services['baz'] = $instance = new \Baz(); + $instance = new \Baz(); + + if (isset($container->services['baz'])) { + return $container->services['baz']; + } + + $container->services['baz'] = $instance; $instance->setFoo(($container->services['foo_with_inline'] ?? $container->load('getFooWithInlineService'))); @@ -339,7 +345,13 @@ class getFooWithInlineService extends ProjectServiceContainer */ public static function do($container, $lazyLoad = true) { - $container->services['foo_with_inline'] = $instance = new \Foo(); + $instance = new \Foo(); + + if (isset($container->services['foo_with_inline'])) { + return $container->services['foo_with_inline']; + } + + $container->services['foo_with_inline'] = $instance; $a = new \Bar(); $a->pub = 'pub'; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_compiled.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_compiled.php index e56d83936623c..949b5ff826980 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_compiled.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_compiled.php @@ -162,7 +162,13 @@ protected static function getBar22Service($container) */ protected static function getBazService($container) { - $container->services['baz'] = $instance = new \Baz(); + $instance = new \Baz(); + + if (isset($container->services['baz'])) { + return $container->services['baz']; + } + + $container->services['baz'] = $instance; $instance->setFoo(($container->services['foo_with_inline'] ?? self::getFooWithInlineService($container))); @@ -310,7 +316,13 @@ protected static function getFooBarService($container) */ protected static function getFooWithInlineService($container) { - $container->services['foo_with_inline'] = $instance = new \Foo(); + $instance = new \Foo(); + + if (isset($container->services['foo_with_inline'])) { + return $container->services['foo_with_inline']; + } + + $container->services['foo_with_inline'] = $instance; $a = new \Bar(); $a->pub = 'pub'; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_inlined_factories.txt b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_inlined_factories.txt index eb1d7cd7846b0..6b159b5b4e224 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_inlined_factories.txt +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_inlined_factories.txt @@ -181,7 +181,13 @@ class ProjectServiceContainer extends Container */ protected static function getBazService($container) { - $container->services['baz'] = $instance = new \Baz(); + $instance = new \Baz(); + + if (isset($container->services['baz'])) { + return $container->services['baz']; + } + + $container->services['baz'] = $instance; $instance->setFoo(($container->services['foo_with_inline'] ?? self::getFooWithInlineService($container))); @@ -331,7 +337,13 @@ class ProjectServiceContainer extends Container */ protected static function getFooWithInlineService($container) { - $container->services['foo_with_inline'] = $instance = new \Foo(); + $instance = new \Foo(); + + if (isset($container->services['foo_with_inline'])) { + return $container->services['foo_with_inline']; + } + + $container->services['foo_with_inline'] = $instance; $a = new \Bar(); $a->pub = 'pub'; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_adawson.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_adawson.php index ae05d67a54999..7ac7a8e58eac9 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_adawson.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_adawson.php @@ -78,7 +78,13 @@ protected static function getBusService($container) */ protected static function getDbService($container) { - $container->services['App\\Db'] = $instance = new \App\Db(); + $instance = new \App\Db(); + + if (isset($container->services['App\\Db'])) { + return $container->services['App\\Db']; + } + + $container->services['App\\Db'] = $instance; $instance->schema = ($container->privates['App\\Schema'] ?? self::getSchemaService($container)); @@ -98,6 +104,12 @@ protected static function getSchemaService($container) return $container->privates['App\\Schema']; } - return $container->privates['App\\Schema'] = new \App\Schema($a); + $instance = new \App\Schema($a); + + if (isset($container->privates['App\\Schema'])) { + return $container->privates['App\\Schema']; + } + + return $container->privates['App\\Schema'] = $instance; } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_almost_circular_private.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_almost_circular_private.php index 0c234ac3934c3..986be4bf95da8 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_almost_circular_private.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_almost_circular_private.php @@ -106,7 +106,13 @@ public function getRemovedIds(): array */ protected static function getBar2Service($container) { - $container->services['bar2'] = $instance = new \BarCircular(); + $instance = new \BarCircular(); + + if (isset($container->services['bar2'])) { + return $container->services['bar2']; + } + + $container->services['bar2'] = $instance; $instance->addFoobar(new \FoobarCircular(($container->services['foo2'] ?? self::getFoo2Service($container)))); @@ -154,7 +160,13 @@ protected static function getConnectionService($container) $b = new \stdClass(); - $container->services['connection'] = $instance = new \stdClass($a, $b); + $instance = new \stdClass($a, $b); + + if (isset($container->services['connection'])) { + return $container->services['connection']; + } + + $container->services['connection'] = $instance; $b->logger = ($container->services['logger'] ?? self::getLoggerService($container)); @@ -174,7 +186,13 @@ protected static function getConnection2Service($container) $b = new \stdClass(); - $container->services['connection2'] = $instance = new \stdClass($a, $b); + $instance = new \stdClass($a, $b); + + if (isset($container->services['connection2'])) { + return $container->services['connection2']; + } + + $container->services['connection2'] = $instance; $c = new \stdClass($instance); @@ -202,7 +220,13 @@ protected static function getDoctrine_EntityManagerService($container) }, 1)); $a->flag = 'ok'; - return $container->services['doctrine.entity_manager'] = \FactoryChecker::create($a); + $instance = \FactoryChecker::create($a); + + if (isset($container->services['doctrine.entity_manager'])) { + return $container->services['doctrine.entity_manager']; + } + + return $container->services['doctrine.entity_manager'] = $instance; } /** @@ -234,7 +258,13 @@ protected static function getFoo2Service($container) return $container->services['foo2']; } - return $container->services['foo2'] = new \FooCircular($a); + $instance = new \FooCircular($a); + + if (isset($container->services['foo2'])) { + return $container->services['foo2']; + } + + return $container->services['foo2'] = $instance; } /** @@ -261,7 +291,13 @@ protected static function getFoo5Service($container) */ protected static function getFoo6Service($container) { - $container->services['foo6'] = $instance = new \stdClass(); + $instance = new \stdClass(); + + if (isset($container->services['foo6'])) { + return $container->services['foo6']; + } + + $container->services['foo6'] = $instance; $instance->bar6 = ($container->privates['bar6'] ?? self::getBar6Service($container)); @@ -291,7 +327,13 @@ protected static function getFoobar4Service($container) */ protected static function getListener3Service($container) { - $container->services['listener3'] = $instance = new \stdClass(); + $instance = new \stdClass(); + + if (isset($container->services['listener3'])) { + return $container->services['listener3']; + } + + $container->services['listener3'] = $instance; $instance->manager = ($container->services['manager3'] ?? self::getManager3Service($container)); @@ -311,7 +353,13 @@ protected static function getListener4Service($container) return $container->services['listener4']; } - return $container->services['listener4'] = new \stdClass($a); + $instance = new \stdClass($a); + + if (isset($container->services['listener4'])) { + return $container->services['listener4']; + } + + return $container->services['listener4'] = $instance; } /** @@ -327,7 +375,13 @@ protected static function getLoggerService($container) return $container->services['logger']; } - $container->services['logger'] = $instance = new \stdClass($a); + $instance = new \stdClass($a); + + if (isset($container->services['logger'])) { + return $container->services['logger']; + } + + $container->services['logger'] = $instance; $instance->handler = new \stdClass(($container->services['manager'] ?? self::getManagerService($container))); @@ -347,7 +401,13 @@ protected static function getManagerService($container) return $container->services['manager']; } - return $container->services['manager'] = new \stdClass($a); + $instance = new \stdClass($a); + + if (isset($container->services['manager'])) { + return $container->services['manager']; + } + + return $container->services['manager'] = $instance; } /** @@ -363,7 +423,13 @@ protected static function getManager2Service($container) return $container->services['manager2']; } - return $container->services['manager2'] = new \stdClass($a); + $instance = new \stdClass($a); + + if (isset($container->services['manager2'])) { + return $container->services['manager2']; + } + + return $container->services['manager2'] = $instance; } /** @@ -379,7 +445,13 @@ protected static function getManager3Service($container, $lazyLoad = true) return $container->services['manager3']; } - return $container->services['manager3'] = new \stdClass($a); + $instance = new \stdClass($a); + + if (isset($container->services['manager3'])) { + return $container->services['manager3']; + } + + return $container->services['manager3'] = $instance; } /** @@ -424,7 +496,13 @@ protected static function getPAService($container) } $b = new \stdClass(); - $container->services['pA'] = $instance = new \stdClass($b, $a); + $instance = new \stdClass($b, $a); + + if (isset($container->services['pA'])) { + return $container->services['pA']; + } + + $container->services['pA'] = $instance; $b->d = ($container->privates['pD'] ?? self::getPDService($container)); @@ -460,7 +538,13 @@ protected static function getSubscriberService($container) return $container->services['subscriber']; } - return $container->services['subscriber'] = new \stdClass($a); + $instance = new \stdClass($a); + + if (isset($container->services['subscriber'])) { + return $container->services['subscriber']; + } + + return $container->services['subscriber'] = $instance; } /** @@ -476,7 +560,13 @@ protected static function getBar6Service($container) return $container->privates['bar6']; } - return $container->privates['bar6'] = new \stdClass($a); + $instance = new \stdClass($a); + + if (isset($container->privates['bar6'])) { + return $container->privates['bar6']; + } + + return $container->privates['bar6'] = $instance; } /** @@ -486,7 +576,13 @@ protected static function getBar6Service($container) */ protected static function getConnection3Service($container) { - $container->privates['connection3'] = $instance = new \stdClass(); + $instance = new \stdClass(); + + if (isset($container->privates['connection3'])) { + return $container->privates['connection3']; + } + + $container->privates['connection3'] = $instance; $instance->listener = [($container->services['listener3'] ?? self::getListener3Service($container))]; @@ -500,7 +596,13 @@ protected static function getConnection3Service($container) */ protected static function getConnection4Service($container) { - $container->privates['connection4'] = $instance = new \stdClass(); + $instance = new \stdClass(); + + if (isset($container->privates['connection4'])) { + return $container->privates['connection4']; + } + + $container->privates['connection4'] = $instance; $instance->listener = [($container->services['listener4'] ?? self::getListener4Service($container))]; @@ -520,7 +622,13 @@ protected static function getDoctrine_ListenerService($container) return $container->privates['doctrine.listener']; } - return $container->privates['doctrine.listener'] = new \stdClass($a); + $instance = new \stdClass($a); + + if (isset($container->privates['doctrine.listener'])) { + return $container->privates['doctrine.listener']; + } + + return $container->privates['doctrine.listener'] = $instance; } /** @@ -546,10 +654,16 @@ protected static function getLevel5Service($container) */ protected static function getMailer_TransportService($container) { - return $container->privates['mailer.transport'] = (new \FactoryCircular(new RewindableGenerator(function () use ($container) { + $instance = (new \FactoryCircular(new RewindableGenerator(function () use ($container) { yield 0 => ($container->privates['mailer.transport_factory.amazon'] ?? self::getMailer_TransportFactory_AmazonService($container)); - yield 1 => self::getMailerInline_TransportFactory_AmazonService($container); + yield 1 => ($container->privates['mailer_inline.transport_factory.amazon'] ?? self::getMailerInline_TransportFactory_AmazonService($container)); }, 2)))->create(); + + if (isset($container->privates['mailer.transport'])) { + return $container->privates['mailer.transport']; + } + + return $container->privates['mailer.transport'] = $instance; } /** @@ -561,7 +675,13 @@ protected static function getMailer_TransportFactory_AmazonService($container) { $a = new \stdClass(); - $container->privates['mailer.transport_factory.amazon'] = $instance = new \stdClass($a); + $instance = new \stdClass($a); + + if (isset($container->privates['mailer.transport_factory.amazon'])) { + return $container->privates['mailer.transport_factory.amazon']; + } + + $container->privates['mailer.transport_factory.amazon'] = $instance; $a->handler = ($container->privates['mailer.transport'] ?? self::getMailer_TransportService($container)); @@ -588,7 +708,7 @@ protected static function getMailerInline_TransportFactory_AmazonService($contai $a = new \stdClass(); $a->handler = ($container->privates['mailer_inline.mailer'] ?? self::getMailerInline_MailerService($container)); - return new \stdClass($a); + return $container->privates['mailer_inline.transport_factory.amazon'] = new \stdClass($a); } /** @@ -604,7 +724,13 @@ protected static function getManager4Service($container, $lazyLoad = true) return $container->privates['manager4']; } - return $container->privates['manager4'] = new \stdClass($a); + $instance = new \stdClass($a); + + if (isset($container->privates['manager4'])) { + return $container->privates['manager4']; + } + + return $container->privates['manager4'] = $instance; } /** @@ -614,7 +740,13 @@ protected static function getManager4Service($container, $lazyLoad = true) */ protected static function getPCService($container, $lazyLoad = true) { - $container->privates['pC'] = $instance = new \stdClass(); + $instance = new \stdClass(); + + if (isset($container->privates['pC'])) { + return $container->privates['pC']; + } + + $container->privates['pC'] = $instance; $instance->d = ($container->privates['pD'] ?? self::getPDService($container)); @@ -634,6 +766,12 @@ protected static function getPDService($container) return $container->privates['pD']; } - return $container->privates['pD'] = new \stdClass($a); + $instance = new \stdClass($a); + + if (isset($container->privates['pD'])) { + return $container->privates['pD']; + } + + return $container->privates['pD'] = $instance; } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_almost_circular_public.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_almost_circular_public.php index ae283e556a0da..edb2da14c3bea 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_almost_circular_public.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_almost_circular_public.php @@ -106,7 +106,13 @@ public function getRemovedIds(): array */ protected static function getBarService($container) { - $container->services['bar'] = $instance = new \BarCircular(); + $instance = new \BarCircular(); + + if (isset($container->services['bar'])) { + return $container->services['bar']; + } + + $container->services['bar'] = $instance; $instance->addFoobar(($container->services['foobar'] ?? self::getFoobarService($container))); @@ -142,7 +148,13 @@ protected static function getBar5Service($container) return $container->services['bar5']; } - $container->services['bar5'] = $instance = new \stdClass($a); + $instance = new \stdClass($a); + + if (isset($container->services['bar5'])) { + return $container->services['bar5']; + } + + $container->services['bar5'] = $instance; $instance->foo = $a; @@ -177,7 +189,13 @@ protected static function getConnectionService($container) } $b = new \stdClass(); - $container->services['connection'] = $instance = new \stdClass($a, $b); + $instance = new \stdClass($a, $b); + + if (isset($container->services['connection'])) { + return $container->services['connection']; + } + + $container->services['connection'] = $instance; $b->logger = ($container->services['logger'] ?? self::getLoggerService($container)); @@ -198,7 +216,13 @@ protected static function getConnection2Service($container) } $b = new \stdClass(); - $container->services['connection2'] = $instance = new \stdClass($a, $b); + $instance = new \stdClass($a, $b); + + if (isset($container->services['connection2'])) { + return $container->services['connection2']; + } + + $container->services['connection2'] = $instance; $c = new \stdClass($instance); $c->handler2 = new \stdClass(($container->services['manager2'] ?? self::getManager2Service($container))); @@ -215,7 +239,13 @@ protected static function getConnection2Service($container) */ protected static function getConnection3Service($container) { - $container->services['connection3'] = $instance = new \stdClass(); + $instance = new \stdClass(); + + if (isset($container->services['connection3'])) { + return $container->services['connection3']; + } + + $container->services['connection3'] = $instance; $instance->listener = [($container->services['listener3'] ?? self::getListener3Service($container))]; @@ -229,7 +259,13 @@ protected static function getConnection3Service($container) */ protected static function getConnection4Service($container) { - $container->services['connection4'] = $instance = new \stdClass(); + $instance = new \stdClass(); + + if (isset($container->services['connection4'])) { + return $container->services['connection4']; + } + + $container->services['connection4'] = $instance; $instance->listener = [($container->services['listener4'] ?? self::getListener4Service($container))]; @@ -243,7 +279,13 @@ protected static function getConnection4Service($container) */ protected static function getDispatcherService($container, $lazyLoad = true) { - $container->services['dispatcher'] = $instance = new \stdClass(); + $instance = new \stdClass(); + + if (isset($container->services['dispatcher'])) { + return $container->services['dispatcher']; + } + + $container->services['dispatcher'] = $instance; $instance->subscriber = ($container->services['subscriber'] ?? self::getSubscriberService($container)); @@ -257,7 +299,13 @@ protected static function getDispatcherService($container, $lazyLoad = true) */ protected static function getDispatcher2Service($container, $lazyLoad = true) { - $container->services['dispatcher2'] = $instance = new \stdClass(); + $instance = new \stdClass(); + + if (isset($container->services['dispatcher2'])) { + return $container->services['dispatcher2']; + } + + $container->services['dispatcher2'] = $instance; $instance->subscriber2 = ($container->privates['subscriber2'] ?? self::getSubscriber2Service($container)); @@ -271,9 +319,15 @@ protected static function getDispatcher2Service($container, $lazyLoad = true) */ protected static function getDoctrine_EntityListenerResolverService($container) { - return $container->services['doctrine.entity_listener_resolver'] = new \stdClass(new RewindableGenerator(function () use ($container) { + $instance = new \stdClass(new RewindableGenerator(function () use ($container) { yield 0 => ($container->services['doctrine.listener'] ?? self::getDoctrine_ListenerService($container)); }, 1)); + + if (isset($container->services['doctrine.entity_listener_resolver'])) { + return $container->services['doctrine.entity_listener_resolver']; + } + + return $container->services['doctrine.entity_listener_resolver'] = $instance; } /** @@ -292,7 +346,13 @@ protected static function getDoctrine_EntityManagerService($container) $b->resolver = $a; $b->flag = 'ok'; - return $container->services['doctrine.entity_manager'] = \FactoryChecker::create($b); + $instance = \FactoryChecker::create($b); + + if (isset($container->services['doctrine.entity_manager'])) { + return $container->services['doctrine.entity_manager']; + } + + return $container->services['doctrine.entity_manager'] = $instance; } /** @@ -308,7 +368,13 @@ protected static function getDoctrine_ListenerService($container) return $container->services['doctrine.listener']; } - return $container->services['doctrine.listener'] = new \stdClass($a); + $instance = new \stdClass($a); + + if (isset($container->services['doctrine.listener'])) { + return $container->services['doctrine.listener']; + } + + return $container->services['doctrine.listener'] = $instance; } /** @@ -324,7 +390,13 @@ protected static function getFooService($container) return $container->services['foo']; } - return $container->services['foo'] = new \FooCircular($a); + $instance = new \FooCircular($a); + + if (isset($container->services['foo'])) { + return $container->services['foo']; + } + + return $container->services['foo'] = $instance; } /** @@ -336,7 +408,13 @@ protected static function getFoo2Service($container) { $a = new \BarCircular(); - $container->services['foo2'] = $instance = new \FooCircular($a); + $instance = new \FooCircular($a); + + if (isset($container->services['foo2'])) { + return $container->services['foo2']; + } + + $container->services['foo2'] = $instance; $a->addFoobar(($container->services['foobar2'] ?? self::getFoobar2Service($container))); @@ -368,7 +446,13 @@ protected static function getFoo4Service($container) */ protected static function getFoo5Service($container) { - $container->services['foo5'] = $instance = new \stdClass(); + $instance = new \stdClass(); + + if (isset($container->services['foo5'])) { + return $container->services['foo5']; + } + + $container->services['foo5'] = $instance; $instance->bar = ($container->services['bar5'] ?? self::getBar5Service($container)); @@ -382,7 +466,13 @@ protected static function getFoo5Service($container) */ protected static function getFoo6Service($container) { - $container->services['foo6'] = $instance = new \stdClass(); + $instance = new \stdClass(); + + if (isset($container->services['foo6'])) { + return $container->services['foo6']; + } + + $container->services['foo6'] = $instance; $instance->bar6 = ($container->privates['bar6'] ?? self::getBar6Service($container)); @@ -402,7 +492,13 @@ protected static function getFoobarService($container) return $container->services['foobar']; } - return $container->services['foobar'] = new \FoobarCircular($a); + $instance = new \FoobarCircular($a); + + if (isset($container->services['foobar'])) { + return $container->services['foobar']; + } + + return $container->services['foobar'] = $instance; } /** @@ -418,7 +514,13 @@ protected static function getFoobar2Service($container) return $container->services['foobar2']; } - return $container->services['foobar2'] = new \FoobarCircular($a); + $instance = new \FoobarCircular($a); + + if (isset($container->services['foobar2'])) { + return $container->services['foobar2']; + } + + return $container->services['foobar2'] = $instance; } /** @@ -454,7 +556,13 @@ protected static function getFoobar4Service($container) */ protected static function getListener3Service($container) { - $container->services['listener3'] = $instance = new \stdClass(); + $instance = new \stdClass(); + + if (isset($container->services['listener3'])) { + return $container->services['listener3']; + } + + $container->services['listener3'] = $instance; $instance->manager = ($container->services['manager3'] ?? self::getManager3Service($container)); @@ -474,7 +582,13 @@ protected static function getListener4Service($container) return $container->services['listener4']; } - return $container->services['listener4'] = new \stdClass($a); + $instance = new \stdClass($a); + + if (isset($container->services['listener4'])) { + return $container->services['listener4']; + } + + return $container->services['listener4'] = $instance; } /** @@ -490,7 +604,13 @@ protected static function getLoggerService($container) return $container->services['logger']; } - $container->services['logger'] = $instance = new \stdClass($a); + $instance = new \stdClass($a); + + if (isset($container->services['logger'])) { + return $container->services['logger']; + } + + $container->services['logger'] = $instance; $instance->handler = new \stdClass(($container->services['manager'] ?? self::getManagerService($container))); @@ -510,7 +630,13 @@ protected static function getMailer_TransportService($container) return $container->services['mailer.transport']; } - return $container->services['mailer.transport'] = $a->create(); + $instance = $a->create(); + + if (isset($container->services['mailer.transport'])) { + return $container->services['mailer.transport']; + } + + return $container->services['mailer.transport'] = $instance; } /** @@ -520,10 +646,16 @@ protected static function getMailer_TransportService($container) */ protected static function getMailer_TransportFactoryService($container) { - return $container->services['mailer.transport_factory'] = new \FactoryCircular(new RewindableGenerator(function () use ($container) { + $instance = new \FactoryCircular(new RewindableGenerator(function () use ($container) { yield 0 => ($container->services['mailer.transport_factory.amazon'] ?? self::getMailer_TransportFactory_AmazonService($container)); yield 1 => ($container->services['mailer_inline.transport_factory.amazon'] ?? self::getMailerInline_TransportFactory_AmazonService($container)); }, 2)); + + if (isset($container->services['mailer.transport_factory'])) { + return $container->services['mailer.transport_factory']; + } + + return $container->services['mailer.transport_factory'] = $instance; } /** @@ -539,7 +671,13 @@ protected static function getMailer_TransportFactory_AmazonService($container) return $container->services['mailer.transport_factory.amazon']; } - return $container->services['mailer.transport_factory.amazon'] = new \stdClass($a); + $instance = new \stdClass($a); + + if (isset($container->services['mailer.transport_factory.amazon'])) { + return $container->services['mailer.transport_factory.amazon']; + } + + return $container->services['mailer.transport_factory.amazon'] = $instance; } /** @@ -575,7 +713,13 @@ protected static function getManagerService($container) return $container->services['manager']; } - return $container->services['manager'] = new \stdClass($a); + $instance = new \stdClass($a); + + if (isset($container->services['manager'])) { + return $container->services['manager']; + } + + return $container->services['manager'] = $instance; } /** @@ -591,7 +735,13 @@ protected static function getManager2Service($container) return $container->services['manager2']; } - return $container->services['manager2'] = new \stdClass($a); + $instance = new \stdClass($a); + + if (isset($container->services['manager2'])) { + return $container->services['manager2']; + } + + return $container->services['manager2'] = $instance; } /** @@ -607,7 +757,13 @@ protected static function getManager3Service($container, $lazyLoad = true) return $container->services['manager3']; } - return $container->services['manager3'] = new \stdClass($a); + $instance = new \stdClass($a); + + if (isset($container->services['manager3'])) { + return $container->services['manager3']; + } + + return $container->services['manager3'] = $instance; } /** @@ -631,7 +787,13 @@ protected static function getMonolog_LoggerService($container) */ protected static function getMonolog_Logger2Service($container) { - $container->services['monolog.logger_2'] = $instance = new \stdClass(); + $instance = new \stdClass(); + + if (isset($container->services['monolog.logger_2'])) { + return $container->services['monolog.logger_2']; + } + + $container->services['monolog.logger_2'] = $instance; $instance->handler = ($container->services['mailer.transport'] ?? self::getMailer_TransportService($container)); @@ -684,7 +846,13 @@ protected static function getPAService($container) return $container->services['pA']; } - return $container->services['pA'] = new \stdClass($a, $b); + $instance = new \stdClass($a, $b); + + if (isset($container->services['pA'])) { + return $container->services['pA']; + } + + return $container->services['pA'] = $instance; } /** @@ -694,7 +862,13 @@ protected static function getPAService($container) */ protected static function getPBService($container) { - $container->services['pB'] = $instance = new \stdClass(); + $instance = new \stdClass(); + + if (isset($container->services['pB'])) { + return $container->services['pB']; + } + + $container->services['pB'] = $instance; $instance->d = ($container->services['pD'] ?? self::getPDService($container)); @@ -708,7 +882,13 @@ protected static function getPBService($container) */ protected static function getPCService($container, $lazyLoad = true) { - $container->services['pC'] = $instance = new \stdClass(); + $instance = new \stdClass(); + + if (isset($container->services['pC'])) { + return $container->services['pC']; + } + + $container->services['pC'] = $instance; $instance->d = ($container->services['pD'] ?? self::getPDService($container)); @@ -728,7 +908,13 @@ protected static function getPDService($container) return $container->services['pD']; } - return $container->services['pD'] = new \stdClass($a); + $instance = new \stdClass($a); + + if (isset($container->services['pD'])) { + return $container->services['pD']; + } + + return $container->services['pD'] = $instance; } /** @@ -760,7 +946,13 @@ protected static function getSubscriberService($container) return $container->services['subscriber']; } - return $container->services['subscriber'] = new \stdClass($a); + $instance = new \stdClass($a); + + if (isset($container->services['subscriber'])) { + return $container->services['subscriber']; + } + + return $container->services['subscriber'] = $instance; } /** @@ -776,7 +968,13 @@ protected static function getBar6Service($container) return $container->privates['bar6']; } - return $container->privates['bar6'] = new \stdClass($a); + $instance = new \stdClass($a); + + if (isset($container->privates['bar6'])) { + return $container->privates['bar6']; + } + + return $container->privates['bar6'] = $instance; } /** @@ -818,7 +1016,13 @@ protected static function getManager4Service($container, $lazyLoad = true) return $container->privates['manager4']; } - return $container->privates['manager4'] = new \stdClass($a); + $instance = new \stdClass($a); + + if (isset($container->privates['manager4'])) { + return $container->privates['manager4']; + } + + return $container->privates['manager4'] = $instance; } /** @@ -834,6 +1038,12 @@ protected static function getSubscriber2Service($container) return $container->privates['subscriber2']; } - return $container->privates['subscriber2'] = new \stdClass($a); + $instance = new \stdClass($a); + + if (isset($container->privates['subscriber2'])) { + return $container->privates['subscriber2']; + } + + return $container->privates['subscriber2'] = $instance; } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_deep_graph.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_deep_graph.php index 982366cc0687c..a0f70aa0e0d9f 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_deep_graph.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_deep_graph.php @@ -44,7 +44,13 @@ public function isCompiled(): bool */ protected static function getBarService($container) { - $container->services['bar'] = $instance = new \stdClass(); + $instance = new \stdClass(); + + if (isset($container->services['bar'])) { + return $container->services['bar']; + } + + $container->services['bar'] = $instance; $instance->p5 = new \stdClass(($container->services['foo'] ?? self::getFooService($container))); @@ -70,6 +76,12 @@ protected static function getFooService($container) $b->p2 = $c; - return $container->services['foo'] = new \Symfony\Component\DependencyInjection\Tests\Dumper\FooForDeepGraph($a, $b); + $instance = new \Symfony\Component\DependencyInjection\Tests\Dumper\FooForDeepGraph($a, $b); + + if (isset($container->services['foo'])) { + return $container->services['foo']; + } + + return $container->services['foo'] = $instance; } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_errored_definition.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_errored_definition.php index 9c330aedbc410..17d7814805aaa 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_errored_definition.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_errored_definition.php @@ -162,7 +162,13 @@ protected static function getBar22Service($container) */ protected static function getBazService($container) { - $container->services['baz'] = $instance = new \Baz(); + $instance = new \Baz(); + + if (isset($container->services['baz'])) { + return $container->services['baz']; + } + + $container->services['baz'] = $instance; $instance->setFoo(($container->services['foo_with_inline'] ?? self::getFooWithInlineService($container))); @@ -310,7 +316,13 @@ protected static function getFooBarService($container) */ protected static function getFooWithInlineService($container) { - $container->services['foo_with_inline'] = $instance = new \Foo(); + $instance = new \Foo(); + + if (isset($container->services['foo_with_inline'])) { + return $container->services['foo_with_inline']; + } + + $container->services['foo_with_inline'] = $instance; $a = new \Bar(); $a->pub = 'pub'; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php index 35527e9bc34dd..9e9f3b911e4c9 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php @@ -12,6 +12,8 @@ namespace Symfony\Component\DependencyInjection\Tests\Loader; use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\TestCase; use Symfony\Component\Config\Exception\FileLocatorFileNotFoundException; use Symfony\Component\Config\Exception\LoaderLoadException; @@ -1217,4 +1219,28 @@ public function testStaticConstructor() $definition = $container->getDefinition('static_constructor'); $this->assertEquals((new Definition('stdClass'))->setFactory([null, 'create']), $definition); } + + #[IgnoreDeprecations] + #[Group('legacy')] + #[DataProvider('provideForbiddenKeys')] + public function testFromCallableTriggersDeprecationOnForbiddenKeys(string $key, mixed $value) + { + $this->expectUserDeprecationMessage(\sprintf('Since symfony/dependency-injection 8.1: Configuring the "%s" key for the service "my_service" when using "from_callable" is deprecated and will throw an "InvalidArgumentException" in 9.0.', $key)); + + $loader = new YamlFileLoader(new ContainerBuilder(), new FileLocator()); + + $reflectionMethod = new \ReflectionMethod($loader, 'parseDefinition'); + $reflectionMethod->invoke($loader, 'my_service', ['from_callable' => 'strlen', $key => $value], 'config/services.yaml', []); + } + + public static function provideForbiddenKeys(): iterable + { + yield 'parent' => ['parent', 'App\\SomeParent']; + yield 'synthetic' => ['synthetic', true]; + yield 'file' => ['file', 'some_file.php']; + yield 'arguments' => ['arguments', []]; + yield 'properties' => ['properties', ['foo' => 'bar']]; + yield 'configurator' => ['configurator', 'some_configurator']; + yield 'calls' => ['calls', [['method' => 'setFoo', 'arguments' => ['bar']]]]; + } } diff --git a/src/Symfony/Component/Form/CHANGELOG.md b/src/Symfony/Component/Form/CHANGELOG.md index 7ac95ccc8db1e..238f7ca83e9ea 100644 --- a/src/Symfony/Component/Form/CHANGELOG.md +++ b/src/Symfony/Component/Form/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +8.1 +--- + + * Add `ResetFlowType` button in `NavigatorFlowType` that you can display with `with_reset` option + 8.0 --- diff --git a/src/Symfony/Component/Form/Flow/Type/NavigatorFlowType.php b/src/Symfony/Component/Form/Flow/Type/NavigatorFlowType.php index 7dab23c35a4e0..3510c44da2ec6 100644 --- a/src/Symfony/Component/Form/Flow/Type/NavigatorFlowType.php +++ b/src/Symfony/Component/Form/Flow/Type/NavigatorFlowType.php @@ -27,6 +27,10 @@ public function buildForm(FormBuilderInterface $builder, array $options): void $builder->add('previous', PreviousFlowType::class); $builder->add('next', NextFlowType::class); $builder->add('finish', FinishFlowType::class); + + if ($options['with_reset']) { + $builder->add('reset', ResetFlowType::class); + } } public function configureOptions(OptionsResolver $resolver): void @@ -36,5 +40,10 @@ public function configureOptions(OptionsResolver $resolver): void 'mapped' => false, 'priority' => -100, ]); + + $resolver->define('with_reset') + ->allowedTypes('bool') + ->default(false) + ->info('Whether to add a reset button to restart the flow from the first step'); } } diff --git a/src/Symfony/Component/Form/Tests/Fixtures/Flow/LastStepSkippedType.php b/src/Symfony/Component/Form/Tests/Fixtures/Flow/LastStepSkippedType.php index a4194e755bc4f..eb7e786cfff77 100644 --- a/src/Symfony/Component/Form/Tests/Fixtures/Flow/LastStepSkippedType.php +++ b/src/Symfony/Component/Form/Tests/Fixtures/Flow/LastStepSkippedType.php @@ -24,7 +24,9 @@ public function buildFormFlow(FormFlowBuilderInterface $builder, array $options) $builder->addStep('step1', TextType::class); $builder->addStep('step2', skip: static fn () => true); - $builder->add('navigator', NavigatorFlowType::class); + $builder->add('navigator', NavigatorFlowType::class, [ + 'with_reset' => true, + ]); } public function configureOptions(OptionsResolver $resolver): void diff --git a/src/Symfony/Component/Form/Tests/Fixtures/Flow/UserSignUpNavigatorType.php b/src/Symfony/Component/Form/Tests/Fixtures/Flow/UserSignUpNavigatorType.php index ed1bc31d96590..2341423947992 100644 --- a/src/Symfony/Component/Form/Tests/Fixtures/Flow/UserSignUpNavigatorType.php +++ b/src/Symfony/Component/Form/Tests/Fixtures/Flow/UserSignUpNavigatorType.php @@ -14,8 +14,8 @@ use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Flow\Type\NavigatorFlowType; use Symfony\Component\Form\Flow\Type\NextFlowType; -use Symfony\Component\Form\Flow\Type\ResetFlowType; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; class UserSignUpNavigatorType extends AbstractType { @@ -25,8 +25,13 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'clear_submission' => true, 'include_if' => ['professional'], ]); + } - $builder->add('reset', ResetFlowType::class); + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'with_reset' => true, + ]); } public function getParent(): string diff --git a/src/Symfony/Component/Form/Tests/Flow/FormFlowTest.php b/src/Symfony/Component/Form/Tests/Flow/FormFlowTest.php index 9d21b8d034d02..97d3c253e0af5 100644 --- a/src/Symfony/Component/Form/Tests/Flow/FormFlowTest.php +++ b/src/Symfony/Component/Form/Tests/Flow/FormFlowTest.php @@ -945,8 +945,9 @@ public function testLastStepSkippedMarkFlowAsFinished() self::assertTrue($flow->has('navigator')); $navigatorForm = $flow->get('navigator'); - self::assertCount(1, $navigatorForm->all()); + self::assertCount(2, $navigatorForm->all()); self::assertTrue($navigatorForm->has('next')); + self::assertTrue($navigatorForm->has('reset')); $flow->submit([ 'step1' => 'foo', diff --git a/src/Symfony/Component/Form/Tests/Flow/Type/NavigatorFlowTypeTest.php b/src/Symfony/Component/Form/Tests/Flow/Type/NavigatorFlowTypeTest.php new file mode 100644 index 0000000000000..6d919b8ebeb75 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Flow/Type/NavigatorFlowTypeTest.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\Flow\Type; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Form\Flow\Type\NavigatorFlowType; +use Symfony\Component\Form\FormFactoryInterface; +use Symfony\Component\Form\Forms; + +class NavigatorFlowTypeTest extends TestCase +{ + private FormFactoryInterface $factory; + + protected function setUp(): void + { + $this->factory = Forms::createFormFactoryBuilder()->getFormFactory(); + } + + public function testDefaultOptionsDoNotIncludeReset() + { + $form = $this->factory->create(NavigatorFlowType::class); + + self::assertTrue($form->has('previous')); + self::assertTrue($form->has('next')); + self::assertTrue($form->has('finish')); + self::assertFalse($form->has('reset')); + } + + public function testWithResetOptionAddsResetButton() + { + $form = $this->factory->create(NavigatorFlowType::class, null, [ + 'with_reset' => true, + ]); + + self::assertTrue($form->has('previous')); + self::assertTrue($form->has('next')); + self::assertTrue($form->has('finish')); + self::assertTrue($form->has('reset')); + } +} diff --git a/src/Symfony/Component/HttpClient/CachingHttpClient.php b/src/Symfony/Component/HttpClient/CachingHttpClient.php index d0ec3c66fb544..b530bcc1906aa 100644 --- a/src/Symfony/Component/HttpClient/CachingHttpClient.php +++ b/src/Symfony/Component/HttpClient/CachingHttpClient.php @@ -486,7 +486,7 @@ private function evaluateCacheFreshness(array $data): Freshness $now = time(); $expires = $data['expires_at']; - if (null !== $expires && $now <= $expires) { + if (null !== $expires && $now < $expires) { return Freshness::Fresh; } diff --git a/src/Symfony/Component/HttpClient/DataCollector/HttpClientDataCollector.php b/src/Symfony/Component/HttpClient/DataCollector/HttpClientDataCollector.php index aee15e68b94db..8890a2b318913 100644 --- a/src/Symfony/Component/HttpClient/DataCollector/HttpClientDataCollector.php +++ b/src/Symfony/Component/HttpClient/DataCollector/HttpClientDataCollector.php @@ -203,11 +203,14 @@ private function getCurlCommand(array $trace): ?string $dataArg[] = '--data-raw '.$this->escapePayload($body); } elseif (\is_array($body)) { try { - $body = explode('&', self::normalizeBody($body)); + $body = self::normalizeBody($body); } catch (TransportException) { return null; } - foreach ($body as $value) { + if (!\is_string($body)) { + return null; + } + foreach (explode('&', $body) as $value) { $dataArg[] = '--data-raw '.$this->escapePayload(urldecode($value)); } } else { diff --git a/src/Symfony/Component/HttpClient/HttpClientTrait.php b/src/Symfony/Component/HttpClient/HttpClientTrait.php index 4e0247521476e..1d2d826e25d9c 100644 --- a/src/Symfony/Component/HttpClient/HttpClientTrait.php +++ b/src/Symfony/Component/HttpClient/HttpClientTrait.php @@ -169,13 +169,14 @@ private static function prepareRequest(?string $method, ?string $url, array $opt unset($options['auth_basic'], $options['auth_bearer']); // Parse base URI - if (\is_string($options['base_uri'])) { - $options['base_uri'] = self::parseUrl($options['base_uri']); + if (\is_string($baseUri = $options['base_uri'] ?? null)) { + $baseUri = self::parseUrl($baseUri); } + unset($options['base_uri']); // Validate and resolve URL $url = self::parseUrl($url, $options['query']); - $url = self::resolveUrl($url, $options['base_uri'], $defaultOptions['query'] ?? []); + $url = self::resolveUrl($url, $baseUri, $defaultOptions['query'] ?? []); } // Finalize normalization of options diff --git a/src/Symfony/Component/HttpClient/RetryableHttpClient.php b/src/Symfony/Component/HttpClient/RetryableHttpClient.php index e4536dc18f02e..e8c3ccb089ee4 100644 --- a/src/Symfony/Component/HttpClient/RetryableHttpClient.php +++ b/src/Symfony/Component/HttpClient/RetryableHttpClient.php @@ -201,6 +201,8 @@ private static function shiftBaseUri(array $options, array &$baseUris): array if ($baseUris) { $baseUri = 1 < \count($baseUris) ? array_shift($baseUris) : current($baseUris); $options['base_uri'] = \is_array($baseUri) ? $baseUri[array_rand($baseUri)] : $baseUri; + } elseif (\is_array($options['base_uri'] ?? null)) { + unset($options['base_uri']); } return $options; diff --git a/src/Symfony/Component/HttpClient/ScopingHttpClient.php b/src/Symfony/Component/HttpClient/ScopingHttpClient.php index 739de0163e1a1..9caa803d2c208 100644 --- a/src/Symfony/Component/HttpClient/ScopingHttpClient.php +++ b/src/Symfony/Component/HttpClient/ScopingHttpClient.php @@ -49,9 +49,11 @@ public function request(string $method, string $url, array $options = []): Respo { $e = null; $url = self::parseUrl($url, $options['query'] ?? []); + $resolved = false; if (\is_string($options['base_uri'] ?? null)) { $options['base_uri'] = self::parseUrl($options['base_uri']); + $resolved = true; } try { @@ -65,10 +67,15 @@ public function request(string $method, string $url, array $options = []): Respo $options = self::mergeDefaultOptions($options, $defaultOptions, true); if (\is_string($options['base_uri'] ?? null)) { $options['base_uri'] = self::parseUrl($options['base_uri']); + $resolved = true; } $url = implode('', self::resolveUrl($url, $options['base_uri'] ?? null, $defaultOptions['query'] ?? [])); } + if ($resolved) { + unset($options['base_uri']); + } + foreach ($this->defaultOptionsByRegexp as $regexp => $defaultOptions) { if (preg_match("{{$regexp}}A", $url)) { if (null === $e || $regexp !== $this->defaultRegexp) { diff --git a/src/Symfony/Component/HttpClient/Tests/CachingHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/CachingHttpClientTest.php index 064737abb9b3c..acb785a8dba63 100644 --- a/src/Symfony/Component/HttpClient/Tests/CachingHttpClientTest.php +++ b/src/Symfony/Component/HttpClient/Tests/CachingHttpClientTest.php @@ -172,9 +172,9 @@ public function testItDoesntServeAStaleResponse() self::assertSame(200, $response->getStatusCode()); self::assertSame('foo', $response->getContent()); - sleep(5); + sleep(4); - // After 5 seconds, the cached response is still considered valid. + // After 4 seconds, the cached response is still considered valid. $response = $client->request('GET', 'http://example.com/foo-bar'); self::assertSame(200, $response->getStatusCode()); self::assertSame('foo', $response->getContent()); @@ -954,14 +954,14 @@ public function testHeuristicFreshnessWithLastModified() self::assertSame('foo', $response->getContent()); // Heuristic: 10% of 3600s = 360s; should be fresh within this time - sleep(360); // 5 minutes + sleep(359); // 5 minutes $response = $client->request('GET', 'http://example.com/heuristic'); self::assertSame(200, $response->getStatusCode()); self::assertSame('foo', $response->getContent()); // After heuristic expires - sleep(1); // Total 361s, past 360s heuristic + sleep(2); // Total 361s, past 360s heuristic $response = $client->request('GET', 'http://example.com/heuristic'); self::assertSame(200, $response->getStatusCode()); diff --git a/src/Symfony/Component/HttpClient/Tests/DataCollector/HttpClientDataCollectorTest.php b/src/Symfony/Component/HttpClient/Tests/DataCollector/HttpClientDataCollectorTest.php index 42cee882e9bae..f0964e9b1046e 100644 --- a/src/Symfony/Component/HttpClient/Tests/DataCollector/HttpClientDataCollectorTest.php +++ b/src/Symfony/Component/HttpClient/Tests/DataCollector/HttpClientDataCollectorTest.php @@ -12,8 +12,11 @@ namespace Symfony\Component\HttpClient\Tests\DataCollector; use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpClient\CurlHttpClient; use Symfony\Component\HttpClient\DataCollector\HttpClientDataCollector; +use Symfony\Component\HttpClient\Exception\TransportException; use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\NativeHttpClient; use Symfony\Component\HttpClient\TraceableHttpClient; @@ -426,6 +429,25 @@ public function testItDoesNotGeneratesCurlCommandsForUploadedFiles() self::assertNull($curlCommand); } + #[RequiresPhpExtension('curl')] + public function testGeneratingCurlCommandForArraysWithResourcesAndUnreachableHost() + { + $httpClient = new TraceableHttpClient(new CurlHttpClient()); + try { + $httpClient->request('POST', 'http://localhast:8057/', [ + 'body' => ['file' => fopen('data://text/plain,', 'r')], + ]); + } catch (TransportException) { + } + $sut = new HttpClientDataCollector(); + $sut->registerClient('http_client', $httpClient); + $sut->lateCollect(); + $collectedData = $sut->getClients(); + self::assertCount(1, $collectedData['http_client']['traces']); + $curlCommand = $collectedData['http_client']['traces'][0]['curlCommand']; + self::assertNull($curlCommand); + } + private function httpClientThatHasTracedRequests($tracedRequests): TraceableHttpClient { $httpClient = new TraceableHttpClient(new NativeHttpClient()); diff --git a/src/Symfony/Component/HttpClient/Tests/ScopingHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/ScopingHttpClientTest.php index 96ad3d7250a86..c0338cc5cbe87 100644 --- a/src/Symfony/Component/HttpClient/Tests/ScopingHttpClientTest.php +++ b/src/Symfony/Component/HttpClient/Tests/ScopingHttpClientTest.php @@ -15,6 +15,9 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\HttpClient\Exception\InvalidArgumentException; use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\HttpClient\Response\MockResponse; +use Symfony\Component\HttpClient\Retry\GenericRetryStrategy; +use Symfony\Component\HttpClient\RetryableHttpClient; use Symfony\Component\HttpClient\ScopingHttpClient; class ScopingHttpClientTest extends TestCase @@ -93,10 +96,31 @@ public function testForBaseUri() $client = ScopingHttpClient::forBaseUri(new MockHttpClient(null, null), 'http://example.com/foo'); $response = $client->request('GET', '/bar'); - $this->assertSame('http://example.com/foo', implode('', $response->getRequestOptions()['base_uri'])); $this->assertSame('http://example.com/bar', $response->getInfo('url')); $response = $client->request('GET', 'http://foo.bar/'); - $this->assertNull($response->getRequestOptions()['base_uri']); + $this->assertSame('http://foo.bar/', $response->getInfo('url')); + } + + public function testRetryableHttpClientIntegration() + { + $responses = [ + new MockResponse(info: ['http_code' => 503]), + new MockResponse(info: ['http_code' => 503]), + new MockResponse(info: ['http_code' => 503]), + new MockResponse(), + ]; + + $client = ScopingHttpClient::forBaseUri( + new RetryableHttpClient( + new MockHttpClient($responses), + new GenericRetryStrategy(delayMs: 0) + ), + 'https://foo.example.com/app/', + ); + + $response = $client->request('GET', 'santysisi'); + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame('https://foo.example.com/app/santysisi', $response->getInfo('url')); } } diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index c60056404ecdd..6d5f80c329351 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -72,15 +72,15 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl */ private static array $freshCache = []; - public const VERSION = '8.0.2'; - public const VERSION_ID = 80002; + public const VERSION = '8.1.0-DEV'; + public const VERSION_ID = 80100; public const MAJOR_VERSION = 8; - public const MINOR_VERSION = 0; - public const RELEASE_VERSION = 2; - public const EXTRA_VERSION = ''; + public const MINOR_VERSION = 1; + public const RELEASE_VERSION = 0; + public const EXTRA_VERSION = 'DEV'; - public const END_OF_MAINTENANCE = '07/2026'; - public const END_OF_LIFE = '07/2026'; + public const END_OF_MAINTENANCE = '01/2027'; + public const END_OF_LIFE = '01/2027'; public function __construct( protected string $environment, diff --git a/src/Symfony/Component/JsonPath/JsonCrawler.php b/src/Symfony/Component/JsonPath/JsonCrawler.php index faa87bf555425..e34479f34c348 100644 --- a/src/Symfony/Component/JsonPath/JsonCrawler.php +++ b/src/Symfony/Component/JsonPath/JsonCrawler.php @@ -38,6 +38,8 @@ final class JsonCrawler implements JsonCrawlerInterface 'value' => true, ]; + private const SINGULAR_ARGUMENT_FUNCTIONS = ['length', 'match', 'search']; + /** * Comparison operators and their corresponding lengths. */ @@ -776,6 +778,10 @@ private function evaluateFunction(string $name, string $args, mixed $context): m $value = $argList[0] ?? null; $nodelistSize = $nodelistSizes[0] ?? 0; + if ($nodelistSize > 1 && \in_array($name, self::SINGULAR_ARGUMENT_FUNCTIONS, true)) { + throw new JsonCrawlerException($args, \sprintf('non-singular query is not allowed as argument to "%s" function', $name)); + } + return match ($name) { 'length' => match (true) { \is_string($value) => mb_strlen($value), @@ -1029,11 +1035,31 @@ private function validateFilterExpression(string $expr): void throw new JsonCrawlerException($left, 'non-singular query is not comparable'); } + $this->validateFunctionArguments($left); + $this->validateFunctionArguments($right); + return; } } } + private function validateFunctionArguments(string $expr): void + { + // is there a function call? + if (!preg_match('/^(\w+)\((.*)\)$/', trim($expr), $matches)) { + return; + } + + if (!\in_array($functionName = $matches[1], self::SINGULAR_ARGUMENT_FUNCTIONS, true)) { + return; + } + + $arg = trim($matches[2]); + if (str_starts_with($arg, '@') && $this->isNonSingularRelativeQuery($arg)) { + throw new JsonCrawlerException($arg, \sprintf('non-singular query is not allowed as argument to "%s" function', $functionName)); + } + } + /** * Transforms JSONPath regex patterns to comply with RFC 9485. * diff --git a/src/Symfony/Component/JsonPath/Tests/Fixtures/Makefile b/src/Symfony/Component/JsonPath/Tests/Fixtures/Makefile index d9b4c353f4a76..420896e9d1513 100644 --- a/src/Symfony/Component/JsonPath/Tests/Fixtures/Makefile +++ b/src/Symfony/Component/JsonPath/Tests/Fixtures/Makefile @@ -1,4 +1,4 @@ -override hash := 05f6cac786bf0cce95437e6f1adedc3186d54a71 +override hash := b9d7153e58711ad38bb8e35ece69c13f4b2f7d63 .PHONY: cts.json cts.json: diff --git a/src/Symfony/Component/JsonPath/Tests/Fixtures/cts.json b/src/Symfony/Component/JsonPath/Tests/Fixtures/cts.json index eedcdbaad55d6..1785527ed760a 100644 --- a/src/Symfony/Component/JsonPath/Tests/Fixtures/cts.json +++ b/src/Symfony/Component/JsonPath/Tests/Fixtures/cts.json @@ -565,6 +565,57 @@ "$['o'][1]['a']" ] }, + { + "name": "basic, name shorthand, true", + "selector": "$.true", + "document": { + "true": "A", + "_foo": "B" + }, + "result": [ + "A" + ], + "result_paths": [ + "$['true']" + ], + "tags": [ + "boundary" + ] + }, + { + "name": "basic, name shorthand, false", + "selector": "$.false", + "document": { + "false": "A", + "_foo": "B" + }, + "result": [ + "A" + ], + "result_paths": [ + "$['false']" + ], + "tags": [ + "boundary" + ] + }, + { + "name": "basic, name shorthand, null", + "selector": "$.null", + "document": { + "null": "A", + "_foo": "B" + }, + "result": [ + "A" + ], + "result_paths": [ + "$['null']" + ], + "tags": [ + "boundary" + ] + }, { "name": "basic, descendant segment, wildcard shorthand, array data", "selector": "$..*", @@ -3980,6 +4031,272 @@ ] ] }, + { + "name": "filter, two consecutive ands", + "selector": "$[?@.a && @.b && @.c]", + "document": [ + { + "a": 1, + "b": 2 + }, + { + "a": 1, + "c": 3 + }, + { + "b": 2, + "c": 3 + }, + { + "a": 1, + "b": 2, + "c": 3 + } + ], + "result": [ + { + "a": 1, + "b": 2, + "c": 3 + } + ], + "result_paths": [ + "$[3]" + ] + }, + { + "name": "filter, two consecutive ors", + "selector": "$[?@.a || @.b || @.c]", + "document": [ + { + "a": 1, + "b": 2 + }, + { + "a": 1, + "c": 3 + }, + { + "b": 2, + "c": 3 + }, + { + "a": 1, + "b": 2, + "c": 3 + } + ], + "result": [ + { + "a": 1, + "b": 2 + }, + { + "a": 1, + "c": 3 + }, + { + "b": 2, + "c": 3 + }, + { + "a": 1, + "b": 2, + "c": 3 + } + ], + "result_paths": [ + "$[0]", + "$[1]", + "$[2]", + "$[3]" + ] + }, + { + "name": "filter, multiple consecutive ands", + "selector": "$[?@.a && @.b && @.c && @.d && @.e]", + "document": [ + { + "a": 1, + "b": 2, + "c": 3, + "d": 4 + }, + { + "b": 2, + "c": 3, + "d": 4, + "e": 5 + }, + { + "a": 1, + "c": 3, + "e": 5 + }, + { + "a": 1, + "b": 2, + "c": 3, + "d": 4, + "e": 5 + } + ], + "result": [ + { + "a": 1, + "b": 2, + "c": 3, + "d": 4, + "e": 5 + } + ], + "result_paths": [ + "$[3]" + ] + }, + { + "name": "filter, multiple consecutive ors", + "selector": "$[?@.a || @.b || @.c || @.d || @.e]", + "document": [ + { + "a": 1, + "b": 2, + "c": 3, + "d": 4 + }, + { + "b": 2, + "c": 3, + "d": 4, + "e": 5 + }, + { + "a": 1, + "c": 3, + "e": 5 + }, + { + "a": 1, + "b": 2, + "c": 3, + "d": 4, + "e": 5 + } + ], + "result": [ + { + "a": 1, + "b": 2, + "c": 3, + "d": 4 + }, + { + "b": 2, + "c": 3, + "d": 4, + "e": 5 + }, + { + "a": 1, + "c": 3, + "e": 5 + }, + { + "a": 1, + "b": 2, + "c": 3, + "d": 4, + "e": 5 + } + ], + "result_paths": [ + "$[0]", + "$[1]", + "$[2]", + "$[3]" + ] + }, + { + "name": "filter, multiple consecutive ors and ands", + "selector": "$[?@.a && @.b && @.c || @.d || @.e]", + "document": [ + { + "a": 1 + }, + { + "e": 5 + }, + { + "a": 1, + "b": 2 + }, + { + "d": 4, + "e": 5 + }, + { + "a": 1, + "b": 2, + "c": 3 + }, + { + "c": 3, + "d": 4, + "e": 5 + }, + { + "a": 1, + "c": 3, + "e": 5 + }, + { + "a": 1, + "b": 2, + "c": 3, + "d": 4, + "e": 5 + } + ], + "result": [ + { + "e": 5 + }, + { + "d": 4, + "e": 5 + }, + { + "a": 1, + "b": 2, + "c": 3 + }, + { + "c": 3, + "d": 4, + "e": 5 + }, + { + "a": 1, + "c": 3, + "e": 5 + }, + { + "a": 1, + "b": 2, + "c": 3, + "d": 4, + "e": 5 + } + ], + "result_paths": [ + "$[1]", + "$[3]", + "$[4]", + "$[5]", + "$[6]", + "$[7]" + ] + }, { "name": "filter, and binds more tightly than or", "selector": "$[?@.a || @.b && @.c]", @@ -7733,6 +8050,24 @@ "length" ] }, + { + "name": "functions, length, non-singular query arg, multiple index selectors", + "selector": "$[?length(@[1, 2])<3]", + "invalid_selector": true, + "tags": [ + "function", + "length" + ] + }, + { + "name": "functions, length, non-singular query arg, multiple name selectors", + "selector": "$[?length(@['a', 'b'])<3]", + "invalid_selector": true, + "tags": [ + "function", + "length" + ] + }, { "name": "functions, match, found match", "selector": "$[?match(@.a, 'a.*')]", diff --git a/src/Symfony/Component/Mailer/Bridge/Infobip/CHANGELOG.md b/src/Symfony/Component/Mailer/Bridge/Infobip/CHANGELOG.md index 403fbe93c4bd8..aeecb8624d328 100644 --- a/src/Symfony/Component/Mailer/Bridge/Infobip/CHANGELOG.md +++ b/src/Symfony/Component/Mailer/Bridge/Infobip/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +8.1 +--- + + * Add support of `ipPoolId` option + 7.2 --- diff --git a/src/Symfony/Component/Mailer/Bridge/Infobip/README.md b/src/Symfony/Component/Mailer/Bridge/Infobip/README.md index b260c10a74fa2..edfb5e2bd3a8e 100644 --- a/src/Symfony/Component/Mailer/Bridge/Infobip/README.md +++ b/src/Symfony/Component/Mailer/Bridge/Infobip/README.md @@ -25,8 +25,9 @@ This transport supports the following custom headers: | `X-Infobip-MessageId` | string | The ID that uniquely identifies the message sent to a recipient. | | `X-Infobip-Track` | boolean | Enable or disable open and click tracking. | | `X-Infobip-TrackingUrl` | string | The URL on your callback server on which the open and click notifications will be sent. | -| `X-Infobip-TrackClicks` | boolean | Enable or disable track click feature.. | +| `X-Infobip-TrackClicks` | boolean | Enable or disable track click feature. | | `X-Infobip-TrackOpens` | boolean | Enable or disable open click feature. | +| `X-Infobip-IpPoolId` | string | The ID of the dedicated IP pool that will be used to deliver the message. | Resources --------- diff --git a/src/Symfony/Component/Mailer/Bridge/Infobip/Tests/Transport/InfobipApiTransportTest.php b/src/Symfony/Component/Mailer/Bridge/Infobip/Tests/Transport/InfobipApiTransportTest.php index aa417558c6a53..1cd48855c92a1 100644 --- a/src/Symfony/Component/Mailer/Bridge/Infobip/Tests/Transport/InfobipApiTransportTest.php +++ b/src/Symfony/Component/Mailer/Bridge/Infobip/Tests/Transport/InfobipApiTransportTest.php @@ -252,7 +252,8 @@ public function testSendEmailWithHeadersShouldCalledInfobipWithTheRightParameter ->addTextHeader('X-Infobip-Track', 'false') ->addTextHeader('X-Infobip-TrackingUrl', 'https://bar.foo') ->addTextHeader('X-Infobip-TrackClicks', 'true') - ->addTextHeader('X-Infobip-TrackOpens', 'true'); + ->addTextHeader('X-Infobip-TrackOpens', 'true') + ->addTextHeader('X-Infobip-IpPoolId', 'pool-123'); $this->transport->send($email); @@ -308,6 +309,12 @@ public function testSendEmailWithHeadersShouldCalledInfobipWithTheRightParameter Content-Disposition: form-data; name="trackOpens" true + --%s + Content-Type: text/plain; charset=utf-8 + Content-Transfer-Encoding: 8bit + Content-Disposition: form-data; name="ipPoolId" + + pool-123 --%s-- TXT, $options['body'] @@ -441,7 +448,8 @@ public function testSendEmailWithHeadersWithSuccess() ->addTextHeader('X-Infobip-Track', 'false') ->addTextHeader('X-Infobip-TrackingUrl', 'https://bar.foo') ->addTextHeader('X-Infobip-TrackClicks', 'true') - ->addTextHeader('X-Infobip-TrackOpens', 'true'); + ->addTextHeader('X-Infobip-TrackOpens', 'true') + ->addTextHeader('X-Infobip-IpPoolId', 'pool-123'); $sentMessage = $this->transport->send($email); @@ -457,6 +465,7 @@ public function testSendEmailWithHeadersWithSuccess() X-Infobip-TrackingUrl: https://bar.foo X-Infobip-TrackClicks: true X-Infobip-TrackOpens: true + X-Infobip-IpPoolId: pool-123 %a TXT, $sentMessage->toString() diff --git a/src/Symfony/Component/Mailer/Bridge/Infobip/Transport/InfobipApiTransport.php b/src/Symfony/Component/Mailer/Bridge/Infobip/Transport/InfobipApiTransport.php index b2bfcc6961098..98a64f4a65f97 100644 --- a/src/Symfony/Component/Mailer/Bridge/Infobip/Transport/InfobipApiTransport.php +++ b/src/Symfony/Component/Mailer/Bridge/Infobip/Transport/InfobipApiTransport.php @@ -42,6 +42,7 @@ final class InfobipApiTransport extends AbstractApiTransport 'X-Infobip-TrackingUrl' => 'trackingUrl', 'X-Infobip-TrackClicks' => 'trackClicks', 'X-Infobip-TrackOpens' => 'trackOpens', + 'X-Infobip-IpPoolId' => 'ipPoolId', ]; public function __construct( diff --git a/src/Symfony/Component/Mailer/Bridge/Sendgrid/CHANGELOG.md b/src/Symfony/Component/Mailer/Bridge/Sendgrid/CHANGELOG.md index 9bcfdb33600fd..7a025a0c6b198 100644 --- a/src/Symfony/Component/Mailer/Bridge/Sendgrid/CHANGELOG.md +++ b/src/Symfony/Component/Mailer/Bridge/Sendgrid/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +8.1 +--- + + * Add support for scheduling delivery with the `send_at` API parameter via a `Send-At` date-header + 7.4 --- diff --git a/src/Symfony/Component/Mailer/Bridge/Sendgrid/README.md b/src/Symfony/Component/Mailer/Bridge/Sendgrid/README.md index 33ee90ef65b3b..bf3f1ece7f81d 100644 --- a/src/Symfony/Component/Mailer/Bridge/Sendgrid/README.md +++ b/src/Symfony/Component/Mailer/Bridge/Sendgrid/README.md @@ -60,6 +60,20 @@ where: - `GROUP_ID` is your Sendgrid suppression group ID - `GROUPS_TO_DISPLAY_ID` is an array of the Sendgrid suppression group IDs presented to the user +Scheduling +---------- + +When using the **API transport** (with a `sendgrid+api` DSN), you can schedule +your emails by providing a `\DateTimeInterface` object in a +`Symfony\Component\Mime\Header\DateHeader` named `Send-At`. + +```php +$email = new \Symfony\Component\Mime\Email(); +$email->getHeaders()->addDateHeader('Send-At', new \DateTimeImmutable('+3 hours')); +``` +It will be mapped to the `send_at` parameter of the `[POST] /mail/send` +[API endpoint](https://www.twilio.com/docs/sendgrid/api-reference/mail-send/mail-send#request-body) + Resources --------- diff --git a/src/Symfony/Component/Mailer/Bridge/Sendgrid/Tests/Transport/SendgridApiTransportTest.php b/src/Symfony/Component/Mailer/Bridge/Sendgrid/Tests/Transport/SendgridApiTransportTest.php index 3f3814df7ba90..b5a624579f31d 100644 --- a/src/Symfony/Component/Mailer/Bridge/Sendgrid/Tests/Transport/SendgridApiTransportTest.php +++ b/src/Symfony/Component/Mailer/Bridge/Sendgrid/Tests/Transport/SendgridApiTransportTest.php @@ -313,4 +313,18 @@ public function testWithSuppressionGroup() $this->assertSame([1, 2, 3, 4, 5], $payload['asm']['groups_to_display']); } + + public function testSendAtHeader() + { + $email = new Email(); + $email->getHeaders()->addDateHeader('Send-At', new \DateTime('2025-05-07 16:00:00', new \DateTimeZone('Europe/Paris'))); + $envelope = new Envelope(new Address('alice@system.com'), [new Address('bob@system.com')]); + + $transport = new SendgridApiTransport('ACCESS_KEY'); + $method = new \ReflectionMethod(SendgridApiTransport::class, 'getPayload'); + $payload = $method->invoke($transport, $email, $envelope); + + $this->assertArrayHasKey('send_at', $payload); + $this->assertSame(1746626400, $payload['send_at']); + } } diff --git a/src/Symfony/Component/Mailer/Bridge/Sendgrid/Transport/SendgridApiTransport.php b/src/Symfony/Component/Mailer/Bridge/Sendgrid/Transport/SendgridApiTransport.php index 5492d026870fc..39028b2d53c89 100644 --- a/src/Symfony/Component/Mailer/Bridge/Sendgrid/Transport/SendgridApiTransport.php +++ b/src/Symfony/Component/Mailer/Bridge/Sendgrid/Transport/SendgridApiTransport.php @@ -23,6 +23,7 @@ use Symfony\Component\Mailer\Transport\AbstractApiTransport; use Symfony\Component\Mime\Address; use Symfony\Component\Mime\Email; +use Symfony\Component\Mime\Header\DateHeader; use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface; use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; @@ -126,7 +127,12 @@ private function getPayload(Email $email, Envelope $envelope): array continue; } - if ($header instanceof TagHeader) { + if ('send-at' === $name) { + if (!$header instanceof DateHeader) { + throw new TransportException(\sprintf('The "Send-At" header must be a "%s" instance.', DateHeader::class)); + } + $payload['send_at'] = $header->getDateTime()->getTimestamp(); + } elseif ($header instanceof TagHeader) { if (10 === \count($categories)) { throw new TransportException(\sprintf('Too many "%s" instances present in the email headers. Sendgrid does not accept more than 10 categories on an email.', TagHeader::class)); } @@ -145,11 +151,11 @@ private function getPayload(Email $email, Envelope $envelope): array } } - if (\count($categories) > 0) { + if ($categories) { $payload['categories'] = $categories; } - if (\count($customArguments) > 0) { + if ($customArguments) { $personalization['custom_args'] = $customArguments; } diff --git a/src/Symfony/Component/Messenger/Stamp/DelayStamp.php b/src/Symfony/Component/Messenger/Stamp/DelayStamp.php index f75a2f85e40c7..1164007d15f3a 100644 --- a/src/Symfony/Component/Messenger/Stamp/DelayStamp.php +++ b/src/Symfony/Component/Messenger/Stamp/DelayStamp.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Messenger\Stamp; +use Symfony\Component\Clock\Clock; + /** * Apply this stamp to delay delivery of your message on a transport. */ @@ -31,7 +33,7 @@ public function getDelay(): int public static function delayFor(\DateInterval $interval): self { - $now = new \DateTimeImmutable('now', new \DateTimeZone('UTC')); + $now = Clock::get()->withTimeZone(new \DateTimeZone('UTC'))->now(); $end = $now->add($interval); return new self(($end->getTimestamp() - $now->getTimestamp()) * 1000); @@ -39,6 +41,6 @@ public static function delayFor(\DateInterval $interval): self public static function delayUntil(\DateTimeInterface $dateTime): self { - return new self(($dateTime->getTimestamp() - time()) * 1000); + return new self(($dateTime->getTimestamp() - Clock::get()->now()->getTimestamp()) * 1000); } } diff --git a/src/Symfony/Component/Messenger/Stamp/RedeliveryStamp.php b/src/Symfony/Component/Messenger/Stamp/RedeliveryStamp.php index 08077e73cde5b..bd5963c3b1a5c 100644 --- a/src/Symfony/Component/Messenger/Stamp/RedeliveryStamp.php +++ b/src/Symfony/Component/Messenger/Stamp/RedeliveryStamp.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Messenger\Stamp; +use Symfony\Component\Clock\Clock; use Symfony\Component\Messenger\Envelope; /** @@ -24,7 +25,7 @@ public function __construct( private int $retryCount, ?\DateTimeInterface $redeliveredAt = null, ) { - $this->redeliveredAt = $redeliveredAt ?? new \DateTimeImmutable(); + $this->redeliveredAt = $redeliveredAt ?? Clock::get()->now(); } public static function getRetryCountFromEnvelope(Envelope $envelope): int diff --git a/src/Symfony/Component/PropertyInfo/Extractor/PhpStanExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/PhpStanExtractor.php index 06f06570465bd..6ca1ea8a9f3b0 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/PhpStanExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/PhpStanExtractor.php @@ -140,7 +140,7 @@ public function getTypeFromConstructor(string $class, string $property): ?Type { $declaringClass = $class; if (!$tagDocNode = $this->getDocBlockFromConstructor($declaringClass, $property)) { - return null; + return $this->getType($class, $property); } $typeContext = $this->typeContextFactory->createFromClassName($class, $declaringClass); diff --git a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php index 983ebfed680c3..ba03f89a6ed09 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php @@ -363,6 +363,7 @@ public function getWriteInfo(string $class, string $property, array $context = [ $allowAdderRemover = $context['enable_adder_remover_extraction'] ?? true; $camelized = $this->camelize($property); + $nonCamelized = ucfirst($property); $constructor = $reflClass->getConstructor(); $singulars = $this->inflector->singularize($camelized); $errors = []; @@ -405,7 +406,26 @@ public function getWriteInfo(string $class, string $property, array $context = [ } } + if ($camelized !== $nonCamelized) { + foreach ($this->mutatorPrefixes as $mutatorPrefix) { + $methodName = $mutatorPrefix.$nonCamelized; + + [$accessible, $methodAccessibleErrors] = $this->isMethodAccessible($reflClass, $methodName, 1); + if (!$accessible) { + $errors[] = $methodAccessibleErrors; + continue; + } + + $method = $reflClass->getMethod($methodName); + + if (!\in_array($mutatorPrefix, $this->arrayMutatorPrefixes, true)) { + return new PropertyWriteInfo(PropertyWriteInfo::TYPE_METHOD, $methodName, $this->getWriteVisibilityForMethod($method), $method->isStatic()); + } + } + } + $getsetter = lcfirst($camelized); + $getsetterNonCamelized = lcfirst($nonCamelized); if ($allowGetterSetter) { [$accessible, $methodAccessibleErrors] = $this->isMethodAccessible($reflClass, $getsetter, 1); @@ -416,6 +436,16 @@ public function getWriteInfo(string $class, string $property, array $context = [ } $errors[] = $methodAccessibleErrors; + + if ($getsetter !== $getsetterNonCamelized) { + [$accessible, $methodAccessibleErrors] = $this->isMethodAccessible($reflClass, $getsetterNonCamelized, 1); + if ($accessible) { + $method = $reflClass->getMethod($getsetterNonCamelized); + + return new PropertyWriteInfo(PropertyWriteInfo::TYPE_METHOD, $getsetterNonCamelized, $this->getWriteVisibilityForMethod($method), $method->isStatic()); + } + $errors[] = $methodAccessibleErrors; + } } if ($reflClass->hasProperty($property) && ($reflClass->getProperty($property)->getModifiers() & $this->propertyReflectionFlags)) { diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php index 47e061cf57172..3c76f7a9bf9a4 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php @@ -12,6 +12,8 @@ namespace Symfony\Component\PropertyInfo\Tests\Extractor; use phpDocumentor\Reflection\DocBlock; +use phpDocumentor\Reflection\PseudoTypes\IntMask; +use phpDocumentor\Reflection\PseudoTypes\IntMaskOf; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\IgnoreDeprecations; @@ -372,6 +374,17 @@ public static function pseudoTypeProvider(): iterable yield ['numericString', Type::string()]; yield ['traitString', Type::string()]; yield ['positiveInt', Type::int()]; + yield ['true', Type::true()]; + yield ['false', Type::false()]; + yield ['valueOfStrings', null]; + yield ['valueOfIntegers', null]; + yield ['keyOfStrings', null]; + yield ['keyOfIntegers', null]; + yield ['arrayKey', null]; + yield ['intMask', class_exists(IntMask::class) ? Type::int() : null]; + yield ['intMaskOf', class_exists(IntMaskOf::class) ? Type::int() : null]; + yield ['conditional', null]; + yield ['offsetAccess', null]; } #[DataProvider('promotedPropertyProvider')] diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php index 48f4c5781c5f3..f4fa28e28e01f 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php @@ -18,6 +18,7 @@ use Symfony\Component\PropertyInfo\Tests\Fixtures\Clazz; use Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummyWithoutDocBlock; +use Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummyWithVarTagsDocBlock; use Symfony\Component\PropertyInfo\Tests\Fixtures\DefaultValue; use Symfony\Component\PropertyInfo\Tests\Fixtures\DockBlockFallback; use Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy; @@ -307,10 +308,29 @@ public static function constructorTypesProvider(): iterable yield ['date', Type::int()]; yield ['timezone', Type::object(\DateTimeZone::class)]; yield ['dateObject', Type::object(\DateTimeInterface::class)]; - yield ['dateTime', null]; + yield ['dateTime', Type::int()]; yield ['ddd', null]; } + #[DataProvider('constructorTypesWithOnlyVarTagsProvider')] + public function testExtractConstructorTypesWithOnlyVarTags(string $property, ?Type $type) + { + $this->assertEquals($type, $this->extractor->getTypeFromConstructor(ConstructorDummyWithVarTagsDocBlock::class, $property)); + } + + /** + * @return iterable + */ + public static function constructorTypesWithOnlyVarTagsProvider(): iterable + { + yield ['date', Type::int()]; + yield ['dateObject', Type::object(\DateTimeInterface::class)]; + yield ['objectsArray', Type::array(Type::object(ConstructorDummy::class))]; + yield ['dateTime', null]; + yield ['mixed', null]; + yield ['timezone', null]; + } + #[DataProvider('constructorTypesOfParentClassProvider')] public function testExtractTypeFromConstructorOfParentClass(string $class, string $property, Type $type) { diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php index b44469e87bea6..c1e27e19264fb 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php @@ -454,6 +454,9 @@ public static function writeMutatorProvider(): array [Php71DummyExtended2::class, 'string', false, false, '', '', null, null, PropertyWriteInfo::VISIBILITY_PUBLIC, false], [Php71DummyExtended2::class, 'string', true, false, '', '', null, null, PropertyWriteInfo::VISIBILITY_PUBLIC, false], [Php71DummyExtended2::class, 'baz', false, true, PropertyWriteInfo::TYPE_ADDER_AND_REMOVER, null, 'addBaz', 'removeBaz', PropertyWriteInfo::VISIBILITY_PUBLIC, false], + [SnakeCaseDummy::class, 'snake_property', false, true, PropertyWriteInfo::TYPE_METHOD, 'setSnakeProperty', null, null, PropertyWriteInfo::VISIBILITY_PUBLIC, false], + [SnakeCaseDummy::class, 'snake_method', false, true, PropertyWriteInfo::TYPE_METHOD, 'setSnake_method', null, null, PropertyWriteInfo::VISIBILITY_PUBLIC, false], + [SnakeCaseDummy::class, 'snake_readonly', false, false, PropertyWriteInfo::TYPE_NONE, null, null, null, null, null], ]; } diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/ConstructorDummyWithVarTagsDocBlock.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/ConstructorDummyWithVarTagsDocBlock.php new file mode 100644 index 0000000000000..57e50f4ea2f97 --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/ConstructorDummyWithVarTagsDocBlock.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\PropertyInfo\Tests\Fixtures; + + +class ConstructorDummyWithVarTagsDocBlock +{ + public function __construct( + public \DateTimeZone $timezone, + /** @var int */ + public $date, + /** @var \DateTimeInterface */ + public $dateObject, + public \DateTimeImmutable $dateTime, + public $mixed, + /** @var ConstructorDummy[] */ + public array $objectsArray, + ) + { + } +} diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/PseudoTypesDummy.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/PseudoTypesDummy.php index 48574a1efe43e..8000f79adf984 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/PseudoTypesDummy.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/PseudoTypesDummy.php @@ -16,6 +16,9 @@ */ class PseudoTypesDummy { + public const STRINGS = ['A' => 'A', 'B' => 'B']; + public const INTEGERS = [1, 2]; + /** @var class-string */ public $classString; @@ -45,4 +48,37 @@ class PseudoTypesDummy /** @var literal-string */ public $literalString; + + /** @var true */ + public $true; + + /** @var false */ + public $false; + + /** @var value-of */ + public $valueOfStrings; + + /** @var value-of */ + public $valueOfIntegers; + + /** @var key-of */ + public $keyOfStrings; + + /** @var key-of */ + public $keyOfIntegers; + + /** @var array-key */ + public $arrayKey; + + /** @var int-mask<1,2,4> */ + public $intMask; + + /** @var int-mask-of<1|2|4> */ + public $intMaskOf; + + /** @var (T is int ? string : int) */ + public $conditional; + + /** @var self::STRINGS['A'] */ + public $offsetAccess; } diff --git a/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php b/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php index 3c6abc57d2e67..ff0aceec29bf7 100644 --- a/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php +++ b/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php @@ -157,14 +157,6 @@ private function createType(DocType $docType): ?Type return Type::array($collectionValueType, $collectionKeyType); } - if ($docType instanceof PseudoType) { - if ($docType->underlyingType() instanceof Integer) { - return Type::int(); - } elseif ($docType->underlyingType() instanceof String_) { - return Type::string(); - } - } - $docTypeString = match ($docTypeString) { 'integer' => 'int', 'boolean' => 'bool', @@ -181,7 +173,22 @@ private function createType(DocType $docType): ?Type return Type::array(); } - return null !== $class ? Type::object($class) : Type::builtin($phpType); + if (null === $class) { + return Type::builtin($phpType); + } + + if ($docType instanceof PseudoType) { + if ($docType->underlyingType() instanceof Integer) { + return Type::int(); + } elseif ($docType->underlyingType() instanceof String_) { + return Type::string(); + } else { + // It's safer to fall back to other extractors here, as resolving pseudo types correctly is not easy at the moment + return null; + } + } + + return Type::object($class); } private function getPhpTypeAndClass(string $docType): array diff --git a/src/Symfony/Component/Routing/Tests/Generator/UrlGeneratorTest.php b/src/Symfony/Component/Routing/Tests/Generator/UrlGeneratorTest.php index d7dd9bb6b885d..588632a332476 100644 --- a/src/Symfony/Component/Routing/Tests/Generator/UrlGeneratorTest.php +++ b/src/Symfony/Component/Routing/Tests/Generator/UrlGeneratorTest.php @@ -12,7 +12,6 @@ namespace Symfony\Component\Routing\Tests\Generator; use PHPUnit\Framework\Attributes\DataProvider; -use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; diff --git a/src/Symfony/Component/Security/Http/CHANGELOG.md b/src/Symfony/Component/Security/Http/CHANGELOG.md index 7ed486f313342..1b3b9835c9bdd 100644 --- a/src/Symfony/Component/Security/Http/CHANGELOG.md +++ b/src/Symfony/Component/Security/Http/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +8.1 +--- + + * Add support for the `clientHints`, `prefetchCache`, and `prerenderCache` `ClearSite-Data` directives + 8.0 --- diff --git a/src/Symfony/Component/Security/Http/EventListener/ClearSiteDataLogoutListener.php b/src/Symfony/Component/Security/Http/EventListener/ClearSiteDataLogoutListener.php index 77ca07a642835..0c72a214bc435 100644 --- a/src/Symfony/Component/Security/Http/EventListener/ClearSiteDataLogoutListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/ClearSiteDataLogoutListener.php @@ -27,7 +27,7 @@ class ClearSiteDataLogoutListener implements EventSubscriberInterface /** * @param string[] $cookieValue The value for the Clear-Site-Data header. - * Can be '*' or a subset of 'cache', 'cookies', 'storage', 'executionContexts'. + * Can be '*' or a subset of 'cache', 'cookies', 'storage', 'clientHints', 'executionContexts', 'prefetchCache', 'prerenderCache'. */ public function __construct(private readonly array $cookieValue) { diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/ClearSiteDataLogoutListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/ClearSiteDataLogoutListenerTest.php index 42ceff76444da..9cf21aa36ae26 100644 --- a/src/Symfony/Component/Security/Http/Tests/EventListener/ClearSiteDataLogoutListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/ClearSiteDataLogoutListenerTest.php @@ -43,5 +43,6 @@ public static function provideClearSiteDataConfig(): iterable { yield [['*'], '"*"']; yield [['cache', 'cookies', 'storage', 'executionContexts'], '"cache", "cookies", "storage", "executionContexts"']; + yield [['clientHints', 'executionContexts', 'prefetchCache', 'prerenderCache'], '"clientHints", "executionContexts", "prefetchCache", "prerenderCache"']; } } diff --git a/src/Symfony/Component/Validator/Constraints/Video.php b/src/Symfony/Component/Validator/Constraints/Video.php index 796dfb94e0dca..adc4cfedb72bd 100644 --- a/src/Symfony/Component/Validator/Constraints/Video.php +++ b/src/Symfony/Component/Validator/Constraints/Video.php @@ -13,7 +13,6 @@ use Symfony\Component\Process\ExecutableFinder; use Symfony\Component\Process\Process; -use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Exception\LogicException; /** @@ -131,7 +130,6 @@ class Video extends File * * @see https://www.iana.org/assignments/media-types/media-types.xhtml Existing media types */ - #[HasNamedArguments] public function __construct( int|string|null $maxSize = null, ?bool $binaryFormat = null, diff --git a/src/Symfony/Component/Validator/Tests/Mapping/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/Validator/Tests/Mapping/Loader/XmlFileLoaderTest.php index 162603b3a174a..a98d6be403df9 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/Validator/Tests/Mapping/Loader/XmlFileLoaderTest.php @@ -16,10 +16,14 @@ use Symfony\Component\Validator\Constraints\Callback; use Symfony\Component\Validator\Constraints\Choice; use Symfony\Component\Validator\Constraints\Collection; +use Symfony\Component\Validator\Constraints\Email; use Symfony\Component\Validator\Constraints\IsTrue; +use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\NotNull; +use Symfony\Component\Validator\Constraints\Optional; use Symfony\Component\Validator\Constraints\Range; use Symfony\Component\Validator\Constraints\Regex; +use Symfony\Component\Validator\Constraints\Required; use Symfony\Component\Validator\Constraints\Traverse; use Symfony\Component\Validator\Exception\MappingException; use Symfony\Component\Validator\Mapping\ClassMetadata; @@ -77,6 +81,8 @@ public function testLoadClassMetadata() $expected->addPropertyConstraint('firstName', new Collection(fields: [ 'foo' => [new NotNull(), new Range(min: 3)], 'bar' => [new Range(min: 5)], + 'baz' => new Required([new Email()]), + 'qux' => new Optional([new NotBlank()]), ])); $expected->addPropertyConstraint('firstName', new Choice( message: 'Must be one of %choices%', diff --git a/src/Symfony/Component/Validator/Tests/Mapping/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/Validator/Tests/Mapping/Loader/YamlFileLoaderTest.php index e076e814ac492..97c13e3723408 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/Validator/Tests/Mapping/Loader/YamlFileLoaderTest.php @@ -17,9 +17,13 @@ use Symfony\Component\Validator\Constraints\Callback; use Symfony\Component\Validator\Constraints\Choice; use Symfony\Component\Validator\Constraints\Collection; +use Symfony\Component\Validator\Constraints\Email; use Symfony\Component\Validator\Constraints\IsTrue; +use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\NotNull; +use Symfony\Component\Validator\Constraints\Optional; use Symfony\Component\Validator\Constraints\Range; +use Symfony\Component\Validator\Constraints\Required; use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Mapping\Loader\YamlFileLoader; use Symfony\Component\Validator\Tests\Dummy\DummyGroupProvider; @@ -120,6 +124,8 @@ public function testLoadClassMetadata() $expected->addPropertyConstraint('firstName', new Collection(fields: [ 'foo' => [new NotNull(), new Range(min: 3)], 'bar' => [new Range(min: 5)], + 'baz' => new Required([new Email()]), + 'qux' => new Optional([new NotBlank()]), ])); $expected->addPropertyConstraint('firstName', new Choice( message: 'Must be one of %choices%', diff --git a/src/Symfony/Component/Validator/Tests/Mapping/Loader/constraint-mapping.xml b/src/Symfony/Component/Validator/Tests/Mapping/Loader/constraint-mapping.xml index 588833208db0a..4ece3aec548a0 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/Loader/constraint-mapping.xml +++ b/src/Symfony/Component/Validator/Tests/Mapping/Loader/constraint-mapping.xml @@ -97,6 +97,20 @@ + + + + + + + + + + diff --git a/src/Symfony/Component/Validator/Tests/Mapping/Loader/constraint-mapping.yml b/src/Symfony/Component/Validator/Tests/Mapping/Loader/constraint-mapping.yml index e31a64189ecff..b40bfc02ad87a 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/Loader/constraint-mapping.yml +++ b/src/Symfony/Component/Validator/Tests/Mapping/Loader/constraint-mapping.yml @@ -53,6 +53,14 @@ Symfony\Component\Validator\Tests\Fixtures\NestedAttribute\Entity: bar: - Range: min: 5 + baz: + - Required: + constraints: + - Email: ~ + qux: + - Optional: + constraints: + - NotBlank: ~ # Constraint with options - Choice: { choices: [A, B], message: Must be one of %choices% } dummy: diff --git a/src/Symfony/Component/Workflow/Arc.php b/src/Symfony/Component/Workflow/Arc.php index 07b2b9236cfa1..073466a4c1e50 100644 --- a/src/Symfony/Component/Workflow/Arc.php +++ b/src/Symfony/Component/Workflow/Arc.php @@ -23,7 +23,7 @@ public function __construct( if ($weight < 1) { throw new \InvalidArgumentException(\sprintf('The weight must be greater than 0, %d given.', $weight)); } - if (!$place) { + if ('' === $place) { throw new \InvalidArgumentException('The place name cannot be empty.'); } } diff --git a/src/Symfony/Component/Workflow/Tests/ArcTest.php b/src/Symfony/Component/Workflow/Tests/ArcTest.php index 11e6b1b40187b..801c02a2725b9 100644 --- a/src/Symfony/Component/Workflow/Tests/ArcTest.php +++ b/src/Symfony/Component/Workflow/Tests/ArcTest.php @@ -31,4 +31,10 @@ public function testConstructorWithInvalidWeight() new Arc('not empty', 0); } + + public function testConstructorWithZeroPlaceName() + { + $arc = new Arc('0', 1); + $this->assertEquals('0', $arc->place); + } } diff --git a/src/Symfony/Contracts/Translation/Test/TranslatorTest.php b/src/Symfony/Contracts/Translation/Test/TranslatorTest.php index 5342f5b82c341..c86efe17d1bb5 100644 --- a/src/Symfony/Contracts/Translation/Test/TranslatorTest.php +++ b/src/Symfony/Contracts/Translation/Test/TranslatorTest.php @@ -27,7 +27,7 @@ * * As mentioned by chx http://drupal.org/node/1273968 we can cover all by testing number from 0 to 199 * - * The goal to cover all languages is to far fetched so this test case is smaller. + * The goal to cover all languages is too far fetched so this test case is smaller. * * @author Clemens Tolboom clemens@build2be.nl */ @@ -349,7 +349,7 @@ public static function successLangcodes(): array * This both depends on a complete list trying to add above as understanding * the plural rules of the current failing languages. * - * @return array with nplural together with langcodes + * @return array With nplural together with langcodes */ public static function failingLangcodes(): array { diff --git a/src/Symfony/Contracts/splitsh.json b/src/Symfony/Contracts/splitsh.json new file mode 100644 index 0000000000000..1b1a16353f239 --- /dev/null +++ b/src/Symfony/Contracts/splitsh.json @@ -0,0 +1,13 @@ +{ + "subtrees": { + "cache-contracts": "Cache", + "deprecation-contracts": "Deprecation", + "event-dispatcher-contracts": "EventDispatcher", + "http-client-contracts": "HttpClient", + "service-contracts": "Service", + "translation-contracts": "Translation" + }, + "defaults": { + "git_constraint": "<1.8.2" + } +}