diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index c392a1a48ab79..2b2960982140f 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -32,7 +32,7 @@ jobs: mode: low-deps - php: '8.4' - php: '8.5' - #mode: experimental + - php: '8.6' fail-fast: false runs-on: ubuntu-24.04 @@ -179,7 +179,7 @@ jobs: echo -e "\n\\e[32mOK\\e[0m $title\\n\\n::endgroup::" fi - [[ "${{ matrix.mode }}" = experimental ]] || (exit $ok) + exit $ok } export -f _run_tests diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 26cdf7f670422..616d1a71958ce 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -34,7 +34,7 @@ '@PHPUnit9x1Migration:risky' => true, // take version from src/Symfony/Bridge/PhpUnit/phpunit.xml.dist#L4 '@Symfony' => true, '@Symfony:risky' => true, - 'protected_to_private' => false, + 'protected_to_private' => true, 'header_comment' => [ 'header' => implode('', $fileHeaderParts), 'validator' => implode('', [ 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/psalm.xml b/psalm.xml index a3dd6b8d5e191..0b544dd2abdf2 100644 --- a/psalm.xml +++ b/psalm.xml @@ -27,24 +27,10 @@ - - - - - - - - - - - - - - - - - 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/Bridge/Twig/Test/FormLayoutTestCase.php b/src/Symfony/Bridge/Twig/Test/FormLayoutTestCase.php index bd8123a3e0d8a..c9e9d940d0e1e 100644 --- a/src/Symfony/Bridge/Twig/Test/FormLayoutTestCase.php +++ b/src/Symfony/Bridge/Twig/Test/FormLayoutTestCase.php @@ -17,7 +17,7 @@ use Symfony\Component\Form\FormRendererInterface; use Symfony\Component\Form\FormView; use Symfony\Component\Form\Test\FormIntegrationTestCase; -use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; +use Symfony\Component\Security\Csrf\CsrfTokenManager; use Twig\Environment; use Twig\Extension\ExtensionInterface; use Twig\Loader\FilesystemLoader; @@ -45,7 +45,7 @@ protected function setUp(): void } $rendererEngine = new TwigRendererEngine($this->getThemes(), $environment); - $this->renderer = new FormRenderer($rendererEngine, $this->createMock(CsrfTokenManagerInterface::class)); + $this->renderer = new FormRenderer($rendererEngine, new CsrfTokenManager()); $this->registerTwigRuntimeLoader($environment, $this->renderer); } diff --git a/src/Symfony/Bridge/Twig/Test/Traits/RuntimeLoaderProvider.php b/src/Symfony/Bridge/Twig/Test/Traits/RuntimeLoaderProvider.php index 6d8943a9605d4..bd5ad49173999 100644 --- a/src/Symfony/Bridge/Twig/Test/Traits/RuntimeLoaderProvider.php +++ b/src/Symfony/Bridge/Twig/Test/Traits/RuntimeLoaderProvider.php @@ -11,18 +11,17 @@ namespace Symfony\Bridge\Twig\Test\Traits; +use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\Form\FormRenderer; use Twig\Environment; -use Twig\RuntimeLoader\RuntimeLoaderInterface; +use Twig\RuntimeLoader\ContainerRuntimeLoader; trait RuntimeLoaderProvider { protected function registerTwigRuntimeLoader(Environment $environment, FormRenderer $renderer): void { - $loader = $this->createMock(RuntimeLoaderInterface::class); - $loader->expects($this->any())->method('load')->willReturnMap([ - ['Symfony\Component\Form\FormRenderer', $renderer], - ]); - $environment->addRuntimeLoader($loader); + $environment->addRuntimeLoader(new ContainerRuntimeLoader(new ServiceLocator([ + FormRenderer::class => fn () => $renderer, + ]))); } } diff --git a/src/Symfony/Bridge/Twig/Tests/AppVariableTest.php b/src/Symfony/Bridge/Twig/Tests/AppVariableTest.php index 697acffa106ca..771c0af83541c 100644 --- a/src/Symfony/Bridge/Twig/Tests/AppVariableTest.php +++ b/src/Symfony/Bridge/Twig/Tests/AppVariableTest.php @@ -17,11 +17,12 @@ use Symfony\Bridge\Twig\AppVariable; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; -use Symfony\Component\HttpFoundation\Session\Flash\FlashBag; use Symfony\Component\HttpFoundation\Session\Session; -use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; -use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; -use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; +use Symfony\Component\Security\Core\Authentication\Token\NullToken; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; +use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; +use Symfony\Component\Security\Core\User\InMemoryUser; use Symfony\Component\Translation\LocaleSwitcher; class AppVariableTest extends TestCase @@ -59,9 +60,9 @@ public function testEnvironment() #[RunInSeparateProcess] public function testGetSession() { - $request = $this->createMock(Request::class); - $request->method('hasSession')->willReturn(true); - $request->method('getSession')->willReturn($session = new Session()); + $session = new Session(); + $request = new Request(); + $request->setSession($session); $this->setRequestStack($request); @@ -84,28 +85,25 @@ public function testGetRequest() public function testGetToken() { - $tokenStorage = $this->createMock(TokenStorageInterface::class); + $tokenStorage = new TokenStorage(); $this->appVariable->setTokenStorage($tokenStorage); - $token = $this->createMock(TokenInterface::class); - $tokenStorage->method('getToken')->willReturn($token); + $token = new NullToken(); + $tokenStorage->setToken($token); $this->assertEquals($token, $this->appVariable->getToken()); } public function testGetUser() { - $this->setTokenStorage($user = $this->createMock(UserInterface::class)); + $this->setTokenStorage($user = new InMemoryUser('john', 'password')); $this->assertEquals($user, $this->appVariable->getUser()); } public function testGetLocale() { - $localeSwitcher = $this->createMock(LocaleSwitcher::class); - $this->appVariable->setLocaleSwitcher($localeSwitcher); - - $localeSwitcher->method('getLocale')->willReturn('fr'); + $this->appVariable->setLocaleSwitcher(new LocaleSwitcher('fr', [])); self::assertEquals('fr', $this->appVariable->getLocale()); } @@ -119,16 +117,14 @@ public function testGetEnabledLocales() public function testGetTokenWithNoToken() { - $tokenStorage = $this->createMock(TokenStorageInterface::class); - $this->appVariable->setTokenStorage($tokenStorage); + $this->appVariable->setTokenStorage(new TokenStorage()); $this->assertNull($this->appVariable->getToken()); } public function testGetUserWithNoToken() { - $tokenStorage = $this->createMock(TokenStorageInterface::class); - $this->appVariable->setTokenStorage($tokenStorage); + $this->appVariable->setTokenStorage(new TokenStorage()); $this->assertNull($this->appVariable->getUser()); } @@ -298,13 +294,11 @@ protected function setRequestStack(?Request $request) protected function setTokenStorage($user) { - $tokenStorage = $this->createMock(TokenStorageInterface::class); + $tokenStorage = new TokenStorage(); $this->appVariable->setTokenStorage($tokenStorage); - $token = $this->createMock(TokenInterface::class); - $tokenStorage->method('getToken')->willReturn($token); - - $token->method('getUser')->willReturn($user); + $token = new UsernamePasswordToken($user, 'main'); + $tokenStorage->setToken($token); } private function setFlashMessages($sessionHasStarted = true) @@ -314,16 +308,19 @@ private function setFlashMessages($sessionHasStarted = true) 'warning' => ['Warning #1 message'], 'error' => ['Error #1 message', 'Error #2 message'], ]; - $flashBag = new FlashBag(); - $flashBag->initialize($flashMessages); - $session = $this->createMock(Session::class); - $session->method('isStarted')->willReturn($sessionHasStarted); - $session->method('getFlashBag')->willReturn($flashBag); + $storage = new MockArraySessionStorage(); + $storage->setSessionData([ + '_symfony_flashes' => $flashMessages, + ]); + $session = new Session($storage); + + if ($sessionHasStarted) { + $session->start(); + } - $request = $this->createMock(Request::class); - $request->method('hasSession')->willReturn(true); - $request->method('getSession')->willReturn($session); + $request = new Request(); + $request->setSession($session); $this->setRequestStack($request); return $flashMessages; diff --git a/src/Symfony/Bridge/Twig/Tests/ErrorRenderer/TwigErrorRendererTest.php b/src/Symfony/Bridge/Twig/Tests/ErrorRenderer/TwigErrorRendererTest.php index 9febc61e61887..6837a84ca819a 100644 --- a/src/Symfony/Bridge/Twig/Tests/ErrorRenderer/TwigErrorRendererTest.php +++ b/src/Symfony/Bridge/Twig/Tests/ErrorRenderer/TwigErrorRendererTest.php @@ -25,7 +25,6 @@ public function testFallbackToNativeRendererIfDebugOn() { $exception = new \Exception(); - $twig = $this->createMock(Environment::class); $nativeRenderer = $this->createMock(HtmlErrorRenderer::class); $nativeRenderer ->expects($this->once()) @@ -33,7 +32,7 @@ public function testFallbackToNativeRendererIfDebugOn() ->with($exception) ; - (new TwigErrorRenderer($twig, $nativeRenderer, true))->render(new \Exception()); + (new TwigErrorRenderer(new Environment(new ArrayLoader()), $nativeRenderer, true))->render(new \Exception()); } public function testFallbackToNativeRendererIfCustomTemplateNotFound() diff --git a/src/Symfony/Bridge/Twig/Tests/EventListener/TemplateAttributeListenerTest.php b/src/Symfony/Bridge/Twig/Tests/EventListener/TemplateAttributeListenerTest.php index 478f285eba5e6..2b5bda9533fcb 100644 --- a/src/Symfony/Bridge/Twig/Tests/EventListener/TemplateAttributeListenerTest.php +++ b/src/Symfony/Bridge/Twig/Tests/EventListener/TemplateAttributeListenerTest.php @@ -45,7 +45,7 @@ public function testAttribute() ; $request = new Request(); - $kernel = $this->createMock(HttpKernelInterface::class); + $kernel = $this->createStub(HttpKernelInterface::class); $controllerArgumentsEvent = new ControllerArgumentsEvent($kernel, [new TemplateAttributeController(), 'foo'], ['Bar'], $request, null); $listener = new TemplateAttributeListener($twig); @@ -97,9 +97,11 @@ public function testAttributeWithBlock() public function testForm() { $request = new Request(); - $kernel = $this->createMock(HttpKernelInterface::class); + $kernel = $this->createStub(HttpKernelInterface::class); $controllerArgumentsEvent = new ControllerArgumentsEvent($kernel, [new TemplateAttributeController(), 'foo'], [], $request, null); - $listener = new TemplateAttributeListener($this->createMock(Environment::class)); + $listener = new TemplateAttributeListener(new Environment(new ArrayLoader([ + 'templates/foo.html.twig' => '', + ]))); $form = $this->createMock(FormInterface::class); $form->expects($this->once())->method('createView'); diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractDivLayoutTestCase.php b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractDivLayoutTestCase.php index 171d13effbd65..b941d1e1d3fad 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractDivLayoutTestCase.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractDivLayoutTestCase.php @@ -472,7 +472,7 @@ public function testNestedFormError() public function testCsrf() { - $this->csrfTokenManager->expects($this->any()) + $this->csrfTokenManager ->method('getToken') ->willReturn(new CsrfToken('token_id', 'foo&bar')); diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractLayoutTestCase.php b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractLayoutTestCase.php index c7e30f24ae974..95e1fb01823e8 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractLayoutTestCase.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractLayoutTestCase.php @@ -12,7 +12,7 @@ namespace Symfony\Bridge\Twig\Tests\Extension; use PHPUnit\Framework\Attributes\DataProvider; -use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\MockObject\Stub; use Symfony\Bridge\Twig\Test\FormLayoutTestCase; use Symfony\Component\Form\Extension\Core\Type\PercentType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; @@ -27,7 +27,7 @@ abstract class AbstractLayoutTestCase extends FormLayoutTestCase { - protected MockObject&CsrfTokenManagerInterface $csrfTokenManager; + protected Stub&CsrfTokenManagerInterface $csrfTokenManager; protected array $testableFeatures = []; private string $defaultLocale; @@ -41,7 +41,7 @@ protected function setUp(): void $this->defaultLocale = \Locale::getDefault(); \Locale::setDefault('en'); - $this->csrfTokenManager = $this->createMock(CsrfTokenManagerInterface::class); + $this->csrfTokenManager = $this->createStub(CsrfTokenManagerInterface::class); parent::setUp(); } diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractTableLayoutTestCase.php b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractTableLayoutTestCase.php index c3cdb08e547e8..4147a9e3b6cf4 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractTableLayoutTestCase.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractTableLayoutTestCase.php @@ -337,7 +337,7 @@ public function testNestedFormError() public function testCsrf() { - $this->csrfTokenManager->expects($this->any()) + $this->csrfTokenManager ->method('getToken') ->willReturn(new CsrfToken('token_id', 'foo&bar')); diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/DumpExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/DumpExtensionTest.php index b182b59f67aa6..46677e1c8d28a 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/DumpExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/DumpExtensionTest.php @@ -19,7 +19,6 @@ use Symfony\Component\VarDumper\VarDumper; use Twig\Environment; use Twig\Loader\ArrayLoader; -use Twig\Loader\LoaderInterface; class DumpExtensionTest extends TestCase { @@ -65,7 +64,7 @@ public static function getDumpTags() public function testDump($context, $args, $expectedOutput, $debug = true) { $extension = new DumpExtension(new VarCloner()); - $twig = new Environment($this->createMock(LoaderInterface::class), [ + $twig = new Environment(new ArrayLoader(), [ 'debug' => $debug, 'cache' => false, 'optimizations' => 0, @@ -122,7 +121,7 @@ public function testCustomDumper() '' ); $extension = new DumpExtension(new VarCloner(), $dumper); - $twig = new Environment($this->createMock(LoaderInterface::class), [ + $twig = new Environment(new ArrayLoader(), [ 'debug' => true, 'cache' => false, 'optimizations' => 0, diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap3LayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap3LayoutTest.php index b08d3b31ddcea..148d946914d6f 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap3LayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap3LayoutTest.php @@ -16,7 +16,7 @@ use Symfony\Bridge\Twig\Form\TwigRendererEngine; use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubTranslator; use Symfony\Component\Form\FormRenderer; -use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; +use Symfony\Component\Security\Csrf\CsrfTokenManager; use Twig\Environment; use Twig\Loader\FilesystemLoader; @@ -60,7 +60,7 @@ public function testMoneyWidgetInIso() 'bootstrap_3_layout.html.twig', 'custom_widgets.html.twig', ], $environment); - $this->renderer = new FormRenderer($rendererEngine, $this->createMock(CsrfTokenManagerInterface::class)); + $this->renderer = new FormRenderer($rendererEngine, new CsrfTokenManager()); $this->registerTwigRuntimeLoader($environment, $this->renderer); $view = $this->factory diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap4LayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap4LayoutTest.php index 4aabc11ee0241..19fb232efc016 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap4LayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap4LayoutTest.php @@ -16,7 +16,7 @@ use Symfony\Bridge\Twig\Form\TwigRendererEngine; use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubTranslator; use Symfony\Component\Form\FormRenderer; -use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; +use Symfony\Component\Security\Csrf\CsrfTokenManager; use Twig\Environment; use Twig\Loader\FilesystemLoader; @@ -65,7 +65,7 @@ public function testMoneyWidgetInIso() 'bootstrap_4_layout.html.twig', 'custom_widgets.html.twig', ], $environment); - $this->renderer = new FormRenderer($rendererEngine, $this->createMock(CsrfTokenManagerInterface::class)); + $this->renderer = new FormRenderer($rendererEngine, new CsrfTokenManager()); $this->registerTwigRuntimeLoader($environment, $this->renderer); $view = $this->factory diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap5LayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap5LayoutTest.php index 8f1f746fc4594..0b8ddeb114fbb 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap5LayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap5LayoutTest.php @@ -18,7 +18,7 @@ use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\Extension\Core\Type\MoneyType; use Symfony\Component\Form\FormRenderer; -use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; +use Symfony\Component\Security\Csrf\CsrfTokenManager; use Twig\Environment; use Twig\Loader\FilesystemLoader; @@ -67,7 +67,7 @@ public function testMoneyWidgetInIso() 'bootstrap_5_layout.html.twig', 'custom_widgets.html.twig', ], $environment); - $this->renderer = new FormRenderer($rendererEngine, $this->getMockBuilder(CsrfTokenManagerInterface::class)->getMock()); + $this->renderer = new FormRenderer($rendererEngine, new CsrfTokenManager()); $this->registerTwigRuntimeLoader($environment, $this->renderer); $view = $this->factory diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionDivLayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionDivLayoutTest.php index a334b2e02e057..adc926a281751 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionDivLayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionDivLayoutTest.php @@ -19,7 +19,7 @@ use Symfony\Component\Form\ChoiceList\View\ChoiceView; use Symfony\Component\Form\FormRenderer; use Symfony\Component\Form\FormView; -use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; +use Symfony\Component\Security\Csrf\CsrfTokenManager; use Twig\Environment; use Twig\Loader\FilesystemLoader; @@ -145,7 +145,7 @@ public function testMoneyWidgetInIso() 'form_div_layout.html.twig', 'custom_widgets.html.twig', ], $environment); - $this->renderer = new FormRenderer($rendererEngine, $this->createMock(CsrfTokenManagerInterface::class)); + $this->renderer = new FormRenderer($rendererEngine, new CsrfTokenManager()); $this->registerTwigRuntimeLoader($environment, $this->renderer); $view = $this->factory diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php index 3e17a28ca05f7..dd20162753acc 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php @@ -15,6 +15,7 @@ use Symfony\Bridge\Twig\Extension\HttpKernelExtension; use Symfony\Bridge\Twig\Extension\HttpKernelRuntime; use Symfony\Bundle\FrameworkBundle\Controller\TemplateController; +use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; @@ -25,7 +26,7 @@ use Twig\Environment; use Twig\Error\RuntimeError; use Twig\Loader\ArrayLoader; -use Twig\RuntimeLoader\RuntimeLoaderInterface; +use Twig\RuntimeLoader\ContainerRuntimeLoader; class HttpKernelExtensionTest extends TestCase { @@ -77,10 +78,9 @@ public function testGenerateFragmentUri() $twig = new Environment($loader, ['debug' => true, 'cache' => false]); $twig->addExtension(new HttpKernelExtension()); - $loader = $this->createMock(RuntimeLoaderInterface::class); - $loader->expects($this->any())->method('load')->willReturnMap([ - [HttpKernelRuntime::class, $kernelRuntime], - ]); + $loader = new ContainerRuntimeLoader(new ServiceLocator([ + HttpKernelRuntime::class => fn () => $kernelRuntime, + ])); $twig->addRuntimeLoader($loader); $this->assertMatchesRegularExpression('#/_fragment\?_hash=.+&_path=template%3Dfoo.html.twig%26_format%3Dhtml%26_locale%3Den%26_controller%3DSymfony%255CBundle%255CFrameworkBundle%255CController%255CTemplateController%253A%253AtemplateAction$#', $twig->render('index')); @@ -111,10 +111,9 @@ protected function renderTemplate(FragmentHandler $renderer, $template = '{{ ren $twig = new Environment($loader, ['debug' => true, 'cache' => false]); $twig->addExtension(new HttpKernelExtension()); - $loader = $this->createMock(RuntimeLoaderInterface::class); - $loader->expects($this->any())->method('load')->willReturnMap([ - ['Symfony\Bridge\Twig\Extension\HttpKernelRuntime', new HttpKernelRuntime($renderer)], - ]); + $loader = new ContainerRuntimeLoader(new ServiceLocator([ + HttpKernelRuntime::class => fn () => new HttpKernelRuntime($renderer), + ])); $twig->addRuntimeLoader($loader); return $twig->render('index'); diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/ImportMapExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/ImportMapExtensionTest.php index 26a572e1954f5..61a292dc55c27 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/ImportMapExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/ImportMapExtensionTest.php @@ -9,15 +9,16 @@ * file that was distributed with this source code. */ -namespace Extension; +namespace Symfony\Bridge\Twig\Tests\Extension; use PHPUnit\Framework\TestCase; use Symfony\Bridge\Twig\Extension\ImportMapExtension; use Symfony\Bridge\Twig\Extension\ImportMapRuntime; use Symfony\Component\AssetMapper\ImportMap\ImportMapRenderer; +use Symfony\Component\DependencyInjection\ServiceLocator; use Twig\Environment; use Twig\Loader\ArrayLoader; -use Twig\RuntimeLoader\RuntimeLoaderInterface; +use Twig\RuntimeLoader\ContainerRuntimeLoader; class ImportMapExtensionTest extends TestCase { @@ -35,14 +36,10 @@ public function testItRendersTheImportmap() ->willReturn($expected); $runtime = new ImportMapRuntime($importMapRenderer); - $mockRuntimeLoader = $this->createMock(RuntimeLoaderInterface::class); - $mockRuntimeLoader - ->method('load') - ->willReturnMap([ - [ImportMapRuntime::class, $runtime], - ]) - ; - $twig->addRuntimeLoader($mockRuntimeLoader); + $runtimeLoader = new ContainerRuntimeLoader(new ServiceLocator([ + ImportMapRuntime::class => fn () => $runtime, + ])); + $twig->addRuntimeLoader($runtimeLoader); $this->assertSame($expected, $twig->render('template')); } diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/RoutingExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/RoutingExtensionTest.php index def7085ce71fe..13883678886fa 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/RoutingExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/RoutingExtensionTest.php @@ -16,7 +16,7 @@ use Symfony\Bridge\Twig\Extension\RoutingExtension; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Twig\Environment; -use Twig\Loader\LoaderInterface; +use Twig\Loader\ArrayLoader; use Twig\Node\Expression\FilterExpression; use Twig\Source; @@ -25,8 +25,8 @@ class RoutingExtensionTest extends TestCase #[DataProvider('getEscapingTemplates')] public function testEscaping($template, $mustBeEscaped) { - $twig = new Environment($this->createMock(LoaderInterface::class), ['debug' => true, 'cache' => false, 'autoescape' => 'html', 'optimizations' => 0]); - $twig->addExtension(new RoutingExtension($this->createMock(UrlGeneratorInterface::class))); + $twig = new Environment(new ArrayLoader(), ['debug' => true, 'cache' => false, 'autoescape' => 'html', 'optimizations' => 0]); + $twig->addExtension(new RoutingExtension($this->createStub(UrlGeneratorInterface::class))); $nodes = $twig->parse($twig->tokenize(new Source($template, ''))); diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/SerializerExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/SerializerExtensionTest.php index b6e1a030dc4c0..7b3d5bf5fdd08 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/SerializerExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/SerializerExtensionTest.php @@ -16,6 +16,7 @@ use Symfony\Bridge\Twig\Extension\SerializerExtension; use Symfony\Bridge\Twig\Extension\SerializerRuntime; use Symfony\Bridge\Twig\Tests\Extension\Fixtures\SerializerModelFixture; +use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Encoder\YamlEncoder; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; @@ -24,7 +25,7 @@ use Symfony\Component\Serializer\Serializer; use Twig\Environment; use Twig\Loader\ArrayLoader; -use Twig\RuntimeLoader\RuntimeLoaderInterface; +use Twig\RuntimeLoader\ContainerRuntimeLoader; /** * @author Jesse Rushlow @@ -51,17 +52,13 @@ private function getTwig(string $template): Environment $meta = new ClassMetadataFactory(new AttributeLoader()); $runtime = new SerializerRuntime(new Serializer([new ObjectNormalizer($meta)], [new JsonEncoder(), new YamlEncoder()])); - $mockRuntimeLoader = $this->createMock(RuntimeLoaderInterface::class); - $mockRuntimeLoader - ->method('load') - ->willReturnMap([ - ['Symfony\Bridge\Twig\Extension\SerializerRuntime', $runtime], - ]) - ; + $runtimeLoader = new ContainerRuntimeLoader(new ServiceLocator([ + SerializerRuntime::class => fn () => $runtime, + ])); $twig = new Environment(new ArrayLoader(['template' => $template])); $twig->addExtension(new SerializerExtension()); - $twig->addRuntimeLoader($mockRuntimeLoader); + $twig->addRuntimeLoader($runtimeLoader); return $twig; } diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/StopwatchExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/StopwatchExtensionTest.php index 3d3785342b936..c8ee5e07f9d79 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/StopwatchExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/StopwatchExtensionTest.php @@ -77,7 +77,7 @@ protected function getStopwatch($events = []) $expectedName->evaluate($name); $this->assertSame($expectedCategory, $category); - return $this->createMock(StopwatchEvent::class); + return new StopwatchEvent('1.0'); }) ; @@ -88,7 +88,7 @@ protected function getStopwatch($events = []) [$expectedName] = array_shift($expectedStopCalls); $expectedName->evaluate($name); - return $this->createMock(StopwatchEvent::class); + return new StopwatchEvent('1.0'); }) ; diff --git a/src/Symfony/Bridge/Twig/Tests/Node/DumpNodeTest.php b/src/Symfony/Bridge/Twig/Tests/Node/DumpNodeTest.php index 620789bfbca5a..53656241cd18e 100644 --- a/src/Symfony/Bridge/Twig/Tests/Node/DumpNodeTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Node/DumpNodeTest.php @@ -15,7 +15,7 @@ use Symfony\Bridge\Twig\Node\DumpNode; use Twig\Compiler; use Twig\Environment; -use Twig\Loader\LoaderInterface; +use Twig\Loader\ArrayLoader; use Twig\Node\Expression\Variable\ContextVariable; use Twig\Node\Nodes; @@ -25,7 +25,7 @@ public function testNoVar() { $node = new DumpNode('bar', null, 7); - $env = new Environment($this->createMock(LoaderInterface::class)); + $env = new Environment(new ArrayLoader()); $compiler = new Compiler($env); $expected = <<<'EOTXT' @@ -49,7 +49,7 @@ public function testIndented() { $node = new DumpNode('bar', null, 7); - $env = new Environment($this->createMock(LoaderInterface::class)); + $env = new Environment(new ArrayLoader()); $compiler = new Compiler($env); $expected = <<<'EOTXT' @@ -77,7 +77,7 @@ public function testOneVar() $node = new DumpNode('bar', $vars, 7); - $env = new Environment($this->createMock(LoaderInterface::class)); + $env = new Environment(new ArrayLoader()); $compiler = new Compiler($env); $expected = <<<'EOTXT' @@ -101,7 +101,7 @@ public function testMultiVars() ]); $node = new DumpNode('bar', $vars, 7); - $env = new Environment($this->createMock(LoaderInterface::class)); + $env = new Environment(new ArrayLoader()); $compiler = new Compiler($env); $expected = <<<'EOTXT' diff --git a/src/Symfony/Bridge/Twig/Tests/Node/FormThemeTest.php b/src/Symfony/Bridge/Twig/Tests/Node/FormThemeTest.php index e581ff284938e..3bbb272f0cb12 100644 --- a/src/Symfony/Bridge/Twig/Tests/Node/FormThemeTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Node/FormThemeTest.php @@ -18,7 +18,7 @@ use Symfony\Component\Form\FormRendererEngineInterface; use Twig\Compiler; use Twig\Environment; -use Twig\Loader\LoaderInterface; +use Twig\Loader\ArrayLoader; use Twig\Node\Expression\ArrayExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\Variable\ContextVariable; @@ -55,8 +55,8 @@ public function testCompile() $node = new FormThemeNode($form, $resources, 0); - $environment = new Environment($this->createMock(LoaderInterface::class)); - $formRenderer = new FormRenderer($this->createMock(FormRendererEngineInterface::class)); + $environment = new Environment(new ArrayLoader()); + $formRenderer = new FormRenderer($this->createStub(FormRendererEngineInterface::class)); $this->registerTwigRuntimeLoader($environment, $formRenderer); $compiler = new Compiler($environment); diff --git a/src/Symfony/Bridge/Twig/Tests/Node/SearchAndRenderBlockNodeTest.php b/src/Symfony/Bridge/Twig/Tests/Node/SearchAndRenderBlockNodeTest.php index 71e5c0c5264f5..6bd209e042938 100644 --- a/src/Symfony/Bridge/Twig/Tests/Node/SearchAndRenderBlockNodeTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Node/SearchAndRenderBlockNodeTest.php @@ -15,7 +15,7 @@ use Symfony\Bridge\Twig\Node\SearchAndRenderBlockNode; use Twig\Compiler; use Twig\Environment; -use Twig\Loader\LoaderInterface; +use Twig\Loader\ArrayLoader; use Twig\Node\Expression\ArrayExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\Ternary\ConditionalTernary; @@ -33,7 +33,7 @@ public function testCompileWidget() $node = new SearchAndRenderBlockNode(new TwigFunction('form_widget'), $arguments, 0); - $compiler = new Compiler(new Environment($this->createMock(LoaderInterface::class))); + $compiler = new Compiler(new Environment(new ArrayLoader())); $this->assertEquals( \sprintf( @@ -56,7 +56,7 @@ public function testCompileWidgetWithVariables() $node = new SearchAndRenderBlockNode(new TwigFunction('form_widget'), $arguments, 0); - $compiler = new Compiler(new Environment($this->createMock(LoaderInterface::class))); + $compiler = new Compiler(new Environment(new ArrayLoader())); $this->assertEquals( \sprintf( @@ -76,7 +76,7 @@ public function testCompileLabelWithLabel() $node = new SearchAndRenderBlockNode(new TwigFunction('form_label'), $arguments, 0); - $compiler = new Compiler(new Environment($this->createMock(LoaderInterface::class))); + $compiler = new Compiler(new Environment(new ArrayLoader())); $this->assertEquals( \sprintf( @@ -96,7 +96,7 @@ public function testCompileLabelWithNullLabel() $node = new SearchAndRenderBlockNode(new TwigFunction('form_label'), $arguments, 0); - $compiler = new Compiler(new Environment($this->createMock(LoaderInterface::class))); + $compiler = new Compiler(new Environment(new ArrayLoader())); // "label" => null must not be included in the output! // Otherwise the default label is overwritten with null. @@ -118,7 +118,7 @@ public function testCompileLabelWithEmptyStringLabel() $node = new SearchAndRenderBlockNode(new TwigFunction('form_label'), $arguments, 0); - $compiler = new Compiler(new Environment($this->createMock(LoaderInterface::class))); + $compiler = new Compiler(new Environment(new ArrayLoader())); // "label" => null must not be included in the output! // Otherwise the default label is overwritten with null. @@ -139,7 +139,7 @@ public function testCompileLabelWithDefaultLabel() $node = new SearchAndRenderBlockNode(new TwigFunction('form_label'), $arguments, 0); - $compiler = new Compiler(new Environment($this->createMock(LoaderInterface::class))); + $compiler = new Compiler(new Environment(new ArrayLoader())); $this->assertEquals( \sprintf( @@ -163,7 +163,7 @@ public function testCompileLabelWithAttributes() $node = new SearchAndRenderBlockNode(new TwigFunction('form_label'), $arguments, 0); - $compiler = new Compiler(new Environment($this->createMock(LoaderInterface::class))); + $compiler = new Compiler(new Environment(new ArrayLoader())); // "label" => null must not be included in the output! // Otherwise the default label is overwritten with null. @@ -192,7 +192,7 @@ public function testCompileLabelWithLabelAndAttributes() $node = new SearchAndRenderBlockNode(new TwigFunction('form_label'), $arguments, 0); - $compiler = new Compiler(new Environment($this->createMock(LoaderInterface::class))); + $compiler = new Compiler(new Environment(new ArrayLoader())); $this->assertEquals( \sprintf( @@ -219,7 +219,7 @@ public function testCompileLabelWithLabelThatEvaluatesToNull() $node = new SearchAndRenderBlockNode(new TwigFunction('form_label'), $arguments, 0); - $compiler = new Compiler(new Environment($this->createMock(LoaderInterface::class))); + $compiler = new Compiler(new Environment(new ArrayLoader())); // "label" => null must not be included in the output! // Otherwise the default label is overwritten with null. @@ -258,7 +258,7 @@ public function testCompileLabelWithLabelThatEvaluatesToNullAndAttributes() $node = new SearchAndRenderBlockNode(new TwigFunction('form_label'), $arguments, 0); - $compiler = new Compiler(new Environment($this->createMock(LoaderInterface::class))); + $compiler = new Compiler(new Environment(new ArrayLoader())); // "label" => null must not be included in the output! // Otherwise the default label is overwritten with null. diff --git a/src/Symfony/Bridge/Twig/Tests/Node/TransNodeTest.php b/src/Symfony/Bridge/Twig/Tests/Node/TransNodeTest.php index 5a55a0c846bb8..cb82aef864549 100644 --- a/src/Symfony/Bridge/Twig/Tests/Node/TransNodeTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Node/TransNodeTest.php @@ -15,7 +15,7 @@ use Symfony\Bridge\Twig\Node\TransNode; use Twig\Compiler; use Twig\Environment; -use Twig\Loader\LoaderInterface; +use Twig\Loader\ArrayLoader; use Twig\Node\Expression\Variable\ContextVariable; use Twig\Node\TextNode; @@ -30,7 +30,7 @@ public function testCompileStrict() $vars = new ContextVariable('foo', 0); $node = new TransNode($body, null, null, $vars); - $env = new Environment($this->createMock(LoaderInterface::class), ['strict_variables' => true]); + $env = new Environment(new ArrayLoader(), ['strict_variables' => true]); $compiler = new Compiler($env); $this->assertEquals( diff --git a/src/Symfony/Bridge/Twig/Tests/NodeVisitor/TranslationDefaultDomainNodeVisitorTest.php b/src/Symfony/Bridge/Twig/Tests/NodeVisitor/TranslationDefaultDomainNodeVisitorTest.php index 3d3fd6ec25f57..af08ba3edebe8 100644 --- a/src/Symfony/Bridge/Twig/Tests/NodeVisitor/TranslationDefaultDomainNodeVisitorTest.php +++ b/src/Symfony/Bridge/Twig/Tests/NodeVisitor/TranslationDefaultDomainNodeVisitorTest.php @@ -16,7 +16,7 @@ use Symfony\Bridge\Twig\NodeVisitor\TranslationDefaultDomainNodeVisitor; use Symfony\Bridge\Twig\NodeVisitor\TranslationNodeVisitor; use Twig\Environment; -use Twig\Loader\LoaderInterface; +use Twig\Loader\ArrayLoader; use Twig\Node\Expression\ArrayExpression; use Twig\Node\Node; @@ -28,7 +28,7 @@ class TranslationDefaultDomainNodeVisitorTest extends TestCase #[DataProvider('getDefaultDomainAssignmentTestData')] public function testDefaultDomainAssignment(Node $node) { - $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false, 'optimizations' => 0]); + $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false, 'optimizations' => 0]); $visitor = new TranslationDefaultDomainNodeVisitor(); // visit trans_default_domain tag @@ -54,7 +54,7 @@ public function testDefaultDomainAssignment(Node $node) #[DataProvider('getDefaultDomainAssignmentTestData')] public function testNewModuleWithoutDefaultDomainTag(Node $node) { - $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false, 'optimizations' => 0]); + $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false, 'optimizations' => 0]); $visitor = new TranslationDefaultDomainNodeVisitor(); // visit trans_default_domain tag diff --git a/src/Symfony/Bridge/Twig/Tests/NodeVisitor/TranslationNodeVisitorTest.php b/src/Symfony/Bridge/Twig/Tests/NodeVisitor/TranslationNodeVisitorTest.php index 49a00a539bd0a..c4530c82e5a95 100644 --- a/src/Symfony/Bridge/Twig/Tests/NodeVisitor/TranslationNodeVisitorTest.php +++ b/src/Symfony/Bridge/Twig/Tests/NodeVisitor/TranslationNodeVisitorTest.php @@ -15,7 +15,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Bridge\Twig\NodeVisitor\TranslationNodeVisitor; use Twig\Environment; -use Twig\Loader\LoaderInterface; +use Twig\Loader\ArrayLoader; use Twig\Node\Expression\ArrayExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\FilterExpression; @@ -29,7 +29,7 @@ class TranslationNodeVisitorTest extends TestCase #[DataProvider('getMessagesExtractionTestData')] public function testMessagesExtraction(Node $node, array $expectedMessages) { - $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false, 'optimizations' => 0]); + $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false, 'optimizations' => 0]); $visitor = new TranslationNodeVisitor(); $visitor->enable(); $visitor->enterNode($node, $env); diff --git a/src/Symfony/Bridge/Twig/Tests/TokenParser/FormThemeTokenParserTest.php b/src/Symfony/Bridge/Twig/Tests/TokenParser/FormThemeTokenParserTest.php index f4d79ffbab5b8..0634b6883e713 100644 --- a/src/Symfony/Bridge/Twig/Tests/TokenParser/FormThemeTokenParserTest.php +++ b/src/Symfony/Bridge/Twig/Tests/TokenParser/FormThemeTokenParserTest.php @@ -16,7 +16,7 @@ use Symfony\Bridge\Twig\Node\FormThemeNode; use Symfony\Bridge\Twig\TokenParser\FormThemeTokenParser; use Twig\Environment; -use Twig\Loader\LoaderInterface; +use Twig\Loader\ArrayLoader; use Twig\Node\Expression\ArrayExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\Variable\ContextVariable; @@ -28,7 +28,7 @@ class FormThemeTokenParserTest extends TestCase #[DataProvider('getTestsForFormTheme')] public function testCompile($source, $expected) { - $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false, 'optimizations' => 0]); + $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false, 'optimizations' => 0]); $env->addTokenParser(new FormThemeTokenParser()); $source = new Source($source, ''); $stream = $env->tokenize($source); diff --git a/src/Symfony/Bridge/Twig/Tests/Translation/TwigExtractorTest.php b/src/Symfony/Bridge/Twig/Tests/Translation/TwigExtractorTest.php index d649df4ef955e..e0432987a5009 100644 --- a/src/Symfony/Bridge/Twig/Tests/Translation/TwigExtractorTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Translation/TwigExtractorTest.php @@ -15,11 +15,10 @@ use PHPUnit\Framework\TestCase; use Symfony\Bridge\Twig\Extension\TranslationExtension; use Symfony\Bridge\Twig\Translation\TwigExtractor; +use Symfony\Component\Translation\IdentityTranslator; use Symfony\Component\Translation\MessageCatalogue; -use Symfony\Contracts\Translation\TranslatorInterface; use Twig\Environment; use Twig\Loader\ArrayLoader; -use Twig\Loader\LoaderInterface; class TwigExtractorTest extends TestCase { @@ -28,14 +27,13 @@ class TwigExtractorTest extends TestCase #[DataProvider('getExtractData')] public function testExtract($template, $messages) { - $loader = $this->createMock(LoaderInterface::class); - $twig = new Environment($loader, [ + $twig = new Environment(new ArrayLoader(), [ 'strict_variables' => true, 'debug' => true, 'cache' => false, 'autoescape' => false, ]); - $twig->addExtension(new TranslationExtension($this->createMock(TranslatorInterface::class))); + $twig->addExtension(new TranslationExtension(new IdentityTranslator())); $extractor = new TwigExtractor($twig); $extractor->setPrefix('prefix'); @@ -96,8 +94,8 @@ public static function getExtractData() #[DataProvider('resourcesWithSyntaxErrorsProvider')] public function testExtractSyntaxError($resources, array $messages) { - $twig = new Environment($this->createMock(LoaderInterface::class)); - $twig->addExtension(new TranslationExtension($this->createMock(TranslatorInterface::class))); + $twig = new Environment(new ArrayLoader()); + $twig->addExtension(new TranslationExtension(new IdentityTranslator())); $extractor = new TwigExtractor($twig); $catalogue = new MessageCatalogue('en'); @@ -124,7 +122,7 @@ public function testExtractWithFiles($resource) 'cache' => false, 'autoescape' => false, ]); - $twig->addExtension(new TranslationExtension($this->createMock(TranslatorInterface::class))); + $twig->addExtension(new TranslationExtension(new IdentityTranslator())); $extractor = new TwigExtractor($twig); $catalogue = new MessageCatalogue('en'); 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..c148fe59dc7ba 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php @@ -33,7 +33,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/BrowserKit/AbstractBrowser.php b/src/Symfony/Component/BrowserKit/AbstractBrowser.php index 6d26b86987449..559365db89c7d 100644 --- a/src/Symfony/Component/BrowserKit/AbstractBrowser.php +++ b/src/Symfony/Component/BrowserKit/AbstractBrowser.php @@ -673,3 +673,5 @@ private function extractHost(string $uri): ?string return $host; } } + +// @php-cs-fixer-ignore error_suppression This file is explicitly expected to not silence each of trigger_error calls diff --git a/src/Symfony/Component/BrowserKit/Cookie.php b/src/Symfony/Component/BrowserKit/Cookie.php index 7a0cee905d6eb..b487fc5837e20 100644 --- a/src/Symfony/Component/BrowserKit/Cookie.php +++ b/src/Symfony/Component/BrowserKit/Cookie.php @@ -43,20 +43,20 @@ class Cookie /** * Sets a cookie. * - * @param string $name The cookie name - * @param string|null $value The value of the cookie - * @param string|null $expires The time the cookie expires - * @param string|null $path The path on the server in which the cookie will be available on - * @param string $domain The domain that the cookie is available - * @param bool $secure Indicates that the cookie should only be transmitted over a secure HTTPS connection from the client - * @param bool $httponly The cookie httponly flag - * @param bool $encodedValue Whether the value is encoded or not - * @param string|null $samesite The cookie samesite attribute + * @param string $name The cookie name + * @param string|null $value The value of the cookie + * @param string|int|null $expires The time the cookie expires + * @param string|null $path The path on the server in which the cookie will be available on + * @param string $domain The domain that the cookie is available + * @param bool $secure Indicates that the cookie should only be transmitted over a secure HTTPS connection from the client + * @param bool $httponly The cookie httponly flag + * @param bool $encodedValue Whether the value is encoded or not + * @param string|null $samesite The cookie samesite attribute */ public function __construct( private string $name, ?string $value, - ?string $expires = null, + string|int|null $expires = null, ?string $path = null, private string $domain = '', private bool $secure = false, diff --git a/src/Symfony/Component/Cache/CacheItem.php b/src/Symfony/Component/Cache/CacheItem.php index 4ce8e0cd1948a..1aa5bcdced6f0 100644 --- a/src/Symfony/Component/Cache/CacheItem.php +++ b/src/Symfony/Component/Cache/CacheItem.php @@ -197,3 +197,5 @@ private function unpack(): bool return true; } } + +// @php-cs-fixer-ignore protected_to_private Friend-level scope access relies on protected properties diff --git a/src/Symfony/Component/Cache/Traits/Relay/Relay20Trait.php b/src/Symfony/Component/Cache/Traits/Relay/Relay20Trait.php index 47930d5da3706..f8f818282c571 100644 --- a/src/Symfony/Component/Cache/Traits/Relay/Relay20Trait.php +++ b/src/Symfony/Component/Cache/Traits/Relay/Relay20Trait.php @@ -27,7 +27,7 @@ public function delex($key, $options = null): \Relay\Relay|false|int return $this->initializeLazyObject()->delex(...\func_get_args()); } - public function digest($key): \Relay\Relay|false|null|string + public function digest($key): \Relay\Relay|false|string|null { return $this->initializeLazyObject()->digest(...\func_get_args()); } diff --git a/src/Symfony/Component/Cache/Traits/Relay/RelayCluster20Trait.php b/src/Symfony/Component/Cache/Traits/Relay/RelayCluster20Trait.php index a196f103dc55f..cefb8af28a897 100644 --- a/src/Symfony/Component/Cache/Traits/Relay/RelayCluster20Trait.php +++ b/src/Symfony/Component/Cache/Traits/Relay/RelayCluster20Trait.php @@ -27,7 +27,7 @@ public function delex($key, $options = null): \Relay\Cluster|false|int return $this->initializeLazyObject()->delex(...\func_get_args()); } - public function digest($key): \Relay\Cluster|false|null|string + public function digest($key): \Relay\Cluster|false|string|null { return $this->initializeLazyObject()->digest(...\func_get_args()); } diff --git a/src/Symfony/Component/Cache/Traits/ValueWrapper.php b/src/Symfony/Component/Cache/Traits/ValueWrapper.php index 718a23d391efe..a97c8571c2125 100644 --- a/src/Symfony/Component/Cache/Traits/ValueWrapper.php +++ b/src/Symfony/Component/Cache/Traits/ValueWrapper.php @@ -79,3 +79,6 @@ public function __unserialize(array $data): void $this->metadata = $metadata; } } + +// @php-cs-fixer-ignore long_to_shorthand_operator To prevent false positive causing "Cannot use assign-op operators with string offsets" error +// @php-cs-fixer-ignore psr_autoloading This class is explicitly having short, special name 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/Console/DependencyInjection/AddConsoleCommandPass.php b/src/Symfony/Component/Console/DependencyInjection/AddConsoleCommandPass.php index ed401112404c2..18443f8d003c8 100644 --- a/src/Symfony/Component/Console/DependencyInjection/AddConsoleCommandPass.php +++ b/src/Symfony/Component/Console/DependencyInjection/AddConsoleCommandPass.php @@ -126,10 +126,11 @@ public function process(ContainerBuilder $container): void } if ($description ??= $attribute?->description) { - $definition->addMethodCall('setDescription', [str_replace('%', '%%', $description)]); + $escapedDescription = str_replace('%', '%%', $description); + $definition->addMethodCall('setDescription', [$escapedDescription]); $container->register('.'.$id.'.lazy', LazyCommand::class) - ->setArguments([$commandName, $aliases, $description, $isHidden, new ServiceClosureArgument($lazyCommandRefs[$id])]); + ->setArguments([$commandName, $aliases, $escapedDescription, $isHidden, new ServiceClosureArgument($lazyCommandRefs[$id])]); $lazyCommandRefs[$id] = new Reference('.'.$id.'.lazy'); } diff --git a/src/Symfony/Component/Console/Helper/TableStyle.php b/src/Symfony/Component/Console/Helper/TableStyle.php index 74ac589256834..bc941efc29548 100644 --- a/src/Symfony/Component/Console/Helper/TableStyle.php +++ b/src/Symfony/Component/Console/Helper/TableStyle.php @@ -78,10 +78,11 @@ public function getPaddingChar(): string * * * ╔═══════════════╤══════════════════════════╤══════════════════╗ - * 1 ISBN 2 Title │ Author ║ - * ╠═══════════════╪══════════════════════════╪══════════════════╣ + * ║ ISBN │ Title │ Author ║ + * ╠═══════1═══════╪══════════════════════════╪══════════════════╣ * ║ 99921-58-10-7 │ Divine Comedy │ Dante Alighieri ║ * ║ 9971-5-0210-0 │ A Tale of Two Cities │ Charles Dickens ║ + * ╟───────2───────┼──────────────────────────┼──────────────────╢ * ║ 960-425-059-0 │ The Lord of the Rings │ J. R. R. Tolkien ║ * ║ 80-902734-1-6 │ And Then There Were None │ Agatha Christie ║ * ╚═══════════════╧══════════════════════════╧══════════════════╝ @@ -102,11 +103,10 @@ public function setHorizontalBorderChars(string $outside, ?string $inside = null * * * ╔═══════════════╤══════════════════════════╤══════════════════╗ - * ║ ISBN │ Title │ Author ║ - * ╠═══════1═══════╪══════════════════════════╪══════════════════╣ + * 1 ISBN 2 Title │ Author ║ + * ╠═══════════════╪══════════════════════════╪══════════════════╣ * ║ 99921-58-10-7 │ Divine Comedy │ Dante Alighieri ║ * ║ 9971-5-0210-0 │ A Tale of Two Cities │ Charles Dickens ║ - * ╟───────2───────┼──────────────────────────┼──────────────────╢ * ║ 960-425-059-0 │ The Lord of the Rings │ J. R. R. Tolkien ║ * ║ 80-902734-1-6 │ And Then There Were None │ Agatha Christie ║ * ╚═══════════════╧══════════════════════════╧══════════════════╝ diff --git a/src/Symfony/Component/Console/Tests/DependencyInjection/AddConsoleCommandPassTest.php b/src/Symfony/Component/Console/Tests/DependencyInjection/AddConsoleCommandPassTest.php index 953e5843c24b8..790e31ab0a0ad 100644 --- a/src/Symfony/Component/Console/Tests/DependencyInjection/AddConsoleCommandPassTest.php +++ b/src/Symfony/Component/Console/Tests/DependencyInjection/AddConsoleCommandPassTest.php @@ -329,6 +329,25 @@ public function testProcessInvokableCommand() $this->assertStringContainsString('usage1', $command->getUsages()[0]); } + public function testProcessCommandWithDescriptionWithpercentageSigns() + { + $container = new ContainerBuilder(); + $container + ->register( + 'description_with_percentage_signs_command', + DescriptionWithPercentageSignsCommand::class, + ) + ->addTag('console.command') + ; + $pass = new AddConsoleCommandPass(); + $pass->process($container); + + $command = $container->get('console.command_loader')->get('description-percentage-signs'); + + self::assertTrue($container->has('description_with_percentage_signs_command.command')); + self::assertSame('Just testing %percentage-signs%', $command->getDescription()); + } + public function testProcessInvokableSignalableCommand() { $container = new ContainerBuilder(); @@ -386,6 +405,14 @@ public function __invoke(): void } } +#[AsCommand(name: 'description-percentage-signs', description: 'Just testing %percentage-signs%')] +class DescriptionWithPercentageSignsCommand +{ + public function __invoke(): void + { + } +} + #[AsCommand(name: 'invokable-signalable', description: 'Just testing', help: 'The %command.name% help content.')] class InvokableSignalableCommand implements SignalableCommandInterface { diff --git a/src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php b/src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php index 8d492c564a1a4..774ffc7032d69 100644 --- a/src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php +++ b/src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php @@ -970,7 +970,7 @@ public function testExitCommandOnInputSIGINT(string $mode) } $p = new Process( - ['php', dirname(__DIR__).'/Fixtures/application_test_sigint.php', $mode], + ['php', \dirname(__DIR__).'/Fixtures/application_test_sigint.php', $mode], timeout: 2, // the process will auto shutdown if not killed by SIGINT, to prevent blocking ); $p->setPty(true); 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/InlineServiceDefinitionsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php index 9a6c4c530eb8f..660bf510fdc03 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php @@ -31,6 +31,7 @@ class InlineServiceDefinitionsPass extends AbstractRecursivePass private array $notInlinedIds = []; private array $inlinedIds = []; private array $notInlinableIds = []; + private array $autowireInline = []; private ?ServiceReferenceGraph $graph = null; public function __construct( @@ -86,7 +87,9 @@ public function process(ContainerBuilder $container): void $remainingInlinedIds[$id] = $id; } else { $container->removeDefinition($id); - $analyzedContainer->removeDefinition($id); + if (!isset($this->autowireInline[$id])) { + $analyzedContainer->removeDefinition($id); + } } } } while ($this->inlinedIds && $this->analyzingPass); @@ -169,6 +172,8 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed private function isInlineableDefinition(string $id, Definition $definition): bool { if (str_starts_with($id, '.autowire_inline.')) { + $this->autowireInline[$id] = true; + return true; } if ($definition->hasErrors() || $definition->isDeprecated() || $definition->isLazy() || $definition->isSynthetic() || $definition->hasTag('container.do_not_inline')) { 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/Compiler/AutowirePassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php index 6d23d45df4055..49c9caef2296f 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php @@ -12,7 +12,6 @@ namespace Symfony\Component\DependencyInjection\Tests\Compiler; 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/DependencyInjection/Tests/ContainerBuilderTest.php b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php index c801e0e89b637..3b021f7427c6b 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php @@ -16,7 +16,6 @@ require_once __DIR__.'/Fixtures/includes/ProjectExtension.php'; use PHPUnit\Framework\Attributes\DataProvider; -use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\TestCase; use Symfony\Component\Config\FileLocator; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php index 6f1952ce10b98..786453fdb4fb1 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php @@ -53,6 +53,9 @@ use Symfony\Component\DependencyInjection\Tests\Compiler\Foo; use Symfony\Component\DependencyInjection\Tests\Compiler\FooVoid; use Symfony\Component\DependencyInjection\Tests\Compiler\IInterface; +use Symfony\Component\DependencyInjection\Tests\Compiler\Listener1; +use Symfony\Component\DependencyInjection\Tests\Compiler\Listener2; +use Symfony\Component\DependencyInjection\Tests\Compiler\ListenerResolver; use Symfony\Component\DependencyInjection\Tests\Compiler\MyCallable; use Symfony\Component\DependencyInjection\Tests\Compiler\MyFactory; use Symfony\Component\DependencyInjection\Tests\Compiler\MyInlineService; @@ -320,6 +323,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 +1848,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(); @@ -2068,6 +2134,43 @@ public function testInlineAdapterConsumer() $this->assertNotSame($fooService->factoredFromServiceWithParam, $barService->factoredFromServiceWithParam); } + public function testAutowireCallableWithServiceLocator() + { + $containerBuilder = new ContainerBuilder(); + + $containerBuilder->register(MyInlineService::class, MyInlineService::class); + $containerBuilder->register(\stdClass::class, \stdClass::class); + + $containerBuilder->register(Listener1::class, Listener1::class) + ->setAutowired(true); + + $containerBuilder->register(Listener2::class, Listener2::class) + ->setAutowired(true); + + $containerBuilder->register(ListenerResolver::class, ListenerResolver::class) + ->addArgument(new ServiceLocatorArgument([ + Listener1::class => new TypedReference(Listener1::class, Listener1::class), + Listener2::class => new TypedReference(Listener2::class, Listener2::class), + ])) + ->setPublic(true); + + $containerBuilder->compile(); + + $dumper = new PhpDumper($containerBuilder); + + $this->assertStringEqualsFile(self::$fixturesPath.'/php/autowire_callable_with_service_locator.php', $dumper->dump(['class' => 'Symfony_DI_PhpDumper_Test_AutowireCallable_With_ServiceLocator'])); + + require self::$fixturesPath.'/php/autowire_callable_with_service_locator.php'; + + $container = new \Symfony_DI_PhpDumper_Test_AutowireCallable_With_ServiceLocator(); + + $listenerResolver = $container->get(ListenerResolver::class); + $this->assertTrue($listenerResolver->container->has(Listener1::class)); + $this->assertInstanceOf(\Closure::class, $listenerResolver->container->get(Listener1::class)->closure); + $this->assertTrue($listenerResolver->container->has(Listener2::class)); + $this->assertInstanceOf(\Closure::class, $listenerResolver->container->get(Listener2::class)->closure); + } + #[DataProvider('getStripCommentsCodes')] public function testStripComments(string $source, string $expected) { @@ -2260,6 +2363,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/includes/autowiring_classes.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes.php index 73d641f5466f2..72d9050afec0f 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes.php @@ -2,8 +2,10 @@ namespace Symfony\Component\DependencyInjection\Tests\Compiler; +use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\DependencyInjection\Attribute\AutowireCallable; use Symfony\Contracts\Service\Attribute\Required; require __DIR__.'/uniontype_classes.php'; @@ -577,3 +579,29 @@ public function getDependency(): BaseLazyProxyClass return $this->dep; } } + +class Listener1 +{ + public function __construct( + #[AutowireCallable(service: MyInlineService::class, method: 'someMethod1')] + public \Closure $closure, + ) { + } +} + +class Listener2 +{ + public function __construct( + #[AutowireCallable(service: MyInlineService::class, method: 'someMethod2')] + public \Closure $closure, + public \stdClass $someOtherService, + ) { + } +} + +class ListenerResolver +{ + public function __construct(public ContainerInterface $container) + { + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/autowire_callable_with_service_locator.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/autowire_callable_with_service_locator.php new file mode 100644 index 0000000000000..4d82a7f5ef777 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/autowire_callable_with_service_locator.php @@ -0,0 +1,85 @@ +services = $this->privates = []; + $this->methodMap = [ + 'Symfony\\Component\\DependencyInjection\\Tests\\Compiler\\ListenerResolver' => 'getListenerResolverService', + ]; + + $this->aliases = []; + } + + public function compile(): void + { + throw new LogicException('You cannot compile a dumped container that was already compiled.'); + } + + public function isCompiled(): bool + { + return true; + } + + public function getRemovedIds(): array + { + return [ + 'Symfony\\Component\\DependencyInjection\\Tests\\Compiler\\Listener1' => true, + 'Symfony\\Component\\DependencyInjection\\Tests\\Compiler\\Listener2' => true, + 'Symfony\\Component\\DependencyInjection\\Tests\\Compiler\\MyInlineService' => true, + 'stdClass' => true, + ]; + } + + /** + * Gets the public 'Symfony\Component\DependencyInjection\Tests\Compiler\ListenerResolver' shared service. + * + * @return \Symfony\Component\DependencyInjection\Tests\Compiler\ListenerResolver + */ + protected static function getListenerResolverService($container) + { + return $container->services['Symfony\\Component\\DependencyInjection\\Tests\\Compiler\\ListenerResolver'] = new \Symfony\Component\DependencyInjection\Tests\Compiler\ListenerResolver(new \Symfony\Component\DependencyInjection\Argument\ServiceLocator($container->getService ??= $container->getService(...), [ + 'Symfony\\Component\\DependencyInjection\\Tests\\Compiler\\Listener1' => ['privates', 'Symfony\\Component\\DependencyInjection\\Tests\\Compiler\\Listener1', 'getListener1Service', false], + 'Symfony\\Component\\DependencyInjection\\Tests\\Compiler\\Listener2' => ['privates', 'Symfony\\Component\\DependencyInjection\\Tests\\Compiler\\Listener2', 'getListener2Service', false], + ], [ + 'Symfony\\Component\\DependencyInjection\\Tests\\Compiler\\Listener1' => 'Symfony\\Component\\DependencyInjection\\Tests\\Compiler\\Listener1', + 'Symfony\\Component\\DependencyInjection\\Tests\\Compiler\\Listener2' => 'Symfony\\Component\\DependencyInjection\\Tests\\Compiler\\Listener2', + ])); + } + + /** + * Gets the private 'Symfony\Component\DependencyInjection\Tests\Compiler\Listener1' shared autowired service. + * + * @return \Symfony\Component\DependencyInjection\Tests\Compiler\Listener1 + */ + protected static function getListener1Service($container) + { + return $container->privates['Symfony\\Component\\DependencyInjection\\Tests\\Compiler\\Listener1'] = new \Symfony\Component\DependencyInjection\Tests\Compiler\Listener1(($container->privates['Symfony\\Component\\DependencyInjection\\Tests\\Compiler\\MyInlineService'] ??= new \Symfony\Component\DependencyInjection\Tests\Compiler\MyInlineService())->someMethod1(...)); + } + + /** + * Gets the private 'Symfony\Component\DependencyInjection\Tests\Compiler\Listener2' shared autowired service. + * + * @return \Symfony\Component\DependencyInjection\Tests\Compiler\Listener2 + */ + protected static function getListener2Service($container) + { + return $container->privates['Symfony\\Component\\DependencyInjection\\Tests\\Compiler\\Listener2'] = new \Symfony\Component\DependencyInjection\Tests\Compiler\Listener2(($container->privates['Symfony\\Component\\DependencyInjection\\Tests\\Compiler\\MyInlineService'] ??= new \Symfony\Component\DependencyInjection\Tests\Compiler\MyInlineService())->someMethod2(...), new \stdClass()); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/callable_adapter_consumer.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/callable_adapter_consumer.php index ccd8d2e0bf63b..216dca434e489 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/callable_adapter_consumer.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/callable_adapter_consumer.php @@ -50,6 +50,6 @@ public function getRemovedIds(): array */ protected static function getBarService($container) { - return $container->services['bar'] = new \Symfony\Component\DependencyInjection\Tests\Dumper\CallableAdapterConsumer(new class(fn () => new \Symfony\Component\DependencyInjection\Tests\Compiler\Foo()) extends \Symfony\Component\DependencyInjection\Argument\LazyClosure implements \Symfony\Component\DependencyInjection\Tests\Compiler\SingleMethodInterface { public function theMethod() { return $this->service->cloneFoo(...\func_get_args()); } }); + return $container->services['bar'] = new \Symfony\Component\DependencyInjection\Tests\Dumper\CallableAdapterConsumer(new class(fn () => (new \Symfony\Component\DependencyInjection\Tests\Compiler\Foo())) extends \Symfony\Component\DependencyInjection\Argument\LazyClosure implements \Symfony\Component\DependencyInjection\Tests\Compiler\SingleMethodInterface { public function theMethod() { return $this->service->cloneFoo(...\func_get_args()); } }); } } 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/ErrorHandler/Tests/DebugClassLoaderTest.php b/src/Symfony/Component/ErrorHandler/Tests/DebugClassLoaderTest.php index b57530391d9c3..0f391020974e4 100644 --- a/src/Symfony/Component/ErrorHandler/Tests/DebugClassLoaderTest.php +++ b/src/Symfony/Component/ErrorHandler/Tests/DebugClassLoaderTest.php @@ -120,7 +120,7 @@ public function testDeprecatedSuper(string $class, string $super, string $type) { set_error_handler(fn () => false); $e = error_reporting(0); - trigger_error('', E_USER_DEPRECATED); + trigger_error('', \E_USER_DEPRECATED); class_exists('Test\\'.__NAMESPACE__.'\\'.$class, true); @@ -568,3 +568,5 @@ public function ownAbstractBaseMethod() { } } } } + +// @php-cs-fixer-ignore error_suppression This file is explicitly expected to not silence each of trigger_error calls diff --git a/src/Symfony/Component/Form/AbstractType.php b/src/Symfony/Component/Form/AbstractType.php index 3716eb1fb92c1..74548bc6024d4 100644 --- a/src/Symfony/Component/Form/AbstractType.php +++ b/src/Symfony/Component/Form/AbstractType.php @@ -37,8 +37,6 @@ public function buildView(FormView $view, FormInterface $form, array $options): { } - /** - */ public function finishView(FormView $view, FormInterface $form, array $options): void { } 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/EventListener/CacheAttributeListener.php b/src/Symfony/Component/HttpKernel/EventListener/CacheAttributeListener.php index 9dd2abda846d5..0046e4a7be6a7 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/CacheAttributeListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/CacheAttributeListener.php @@ -123,7 +123,7 @@ public function onKernelResponse(ResponseEvent $event): void // Check if the response has a Vary header that should be considered, ignoring cases where // it's only 'Accept-Language' and the request has the '_vary_by_language' attribute $hasVary = ['Accept-Language'] === $response->getVary() ? !$request->attributes->get('_vary_by_language') : $response->hasVary(); - //Check if cache-control directive was set manually in cacheControl (not auto computed) + // Check if cache-control directive was set manually in cacheControl (not auto computed) $hasCacheControlDirective = new class($response->headers) extends HeaderBag { public function __construct(private parent $headerBag) { diff --git a/src/Symfony/Component/HttpKernel/HttpKernelBrowser.php b/src/Symfony/Component/HttpKernel/HttpKernelBrowser.php index 4422bfcdd3e54..06bc420d1c33d 100644 --- a/src/Symfony/Component/HttpKernel/HttpKernelBrowser.php +++ b/src/Symfony/Component/HttpKernel/HttpKernelBrowser.php @@ -184,7 +184,7 @@ protected function filterResponse(object $response): DomResponse ob_start(static function ($chunk) use (&$content) { $content .= $chunk; - return ''; + return ''; }); try { 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/HttpKernel/Tests/HttpCache/StoreTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/StoreTest.php index e8bffba48165a..76af83b0b87d6 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/StoreTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/StoreTest.php @@ -350,7 +350,7 @@ public function testLoadsBodyEval() /** * Basic case when the second header has a different value. - * Both responses should be cached + * Both responses should be cached. */ public function testWriteWithMultipleVaryAndCachedAllResponse() { @@ -376,7 +376,7 @@ public function testWriteWithMultipleVaryAndCachedAllResponse() /** * Basic case when the second header has the same value on both requests. - * The last response should be cached + * The last response should be cached. */ public function testWriteWithMultipleVaryAndCachedLastResponse() { @@ -402,7 +402,7 @@ public function testWriteWithMultipleVaryAndCachedLastResponse() /** * Case when a vary value has been removed. - * Both responses should be cached + * Both responses should be cached. */ public function testWriteWithChangingVary() { @@ -425,11 +425,11 @@ public function testWriteWithChangingVary() /** * Case when a vary value has been removed and headers of the new vary list are the same. - * The last response should be cached + * The last response should be cached. */ public function testWriteWithRemoveVaryAndAllHeadersOnTheList() { - $req1 = Request::create('/foo', 'get', [], [], [], ['HTTP_FOO' => 'foo', 'HTTP_FOOBAR' => 'bar',]); + $req1 = Request::create('/foo', 'get', [], [], [], ['HTTP_FOO' => 'foo', 'HTTP_FOOBAR' => 'bar']); $content = str_repeat('a', 24).'b'.str_repeat('a', 24); $res1 = new Response($content, 200, ['vary' => ['Foo', 'bar', 'foobar'], 'X-Body-Eval' => 'SSI']); $this->store->write($req1, $res1); @@ -448,7 +448,7 @@ public function testWriteWithRemoveVaryAndAllHeadersOnTheList() /** * Case when a vary value has been added and headers of the new vary list are the same. - * The last response should be cached + * The last response should be cached. */ public function testWriteWithAddingVaryAndAllHeadersOnTheList() { diff --git a/src/Symfony/Component/HttpKernel/Tests/KernelTest.php b/src/Symfony/Component/HttpKernel/Tests/KernelTest.php index 60043491cb762..09bdd06f48235 100644 --- a/src/Symfony/Component/HttpKernel/Tests/KernelTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/KernelTest.php @@ -17,8 +17,8 @@ use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; -use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; +use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\Filesystem\Exception\IOException; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpFoundation\Request; 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/JsonStreamer/Read/PhpGenerator.php b/src/Symfony/Component/JsonStreamer/Read/PhpGenerator.php index 8aa6f6ebfd025..aa89d4f0f4c6f 100644 --- a/src/Symfony/Component/JsonStreamer/Read/PhpGenerator.php +++ b/src/Symfony/Component/JsonStreamer/Read/PhpGenerator.php @@ -58,7 +58,7 @@ public function generate(DataModelNodeInterface $dataModel, bool $decodeFromStre .$providers .($this->canBeDecodedWithJsonDecode($dataModel, $decodeFromStream) ? $this->line(' return \\'.Decoder::class.'::decodeStream($stream, 0, null);', $context) - : $this->line(' return $providers[\''.$dataModel->getIdentifier().'\']($stream, 0, null);', $context)) + : $this->line(' return $providers['.$this->quote($dataModel->getIdentifier()).']($stream, 0, null);', $context)) .$this->line('};', $context); } @@ -71,7 +71,7 @@ public function generate(DataModelNodeInterface $dataModel, bool $decodeFromStre .$providers .($this->canBeDecodedWithJsonDecode($dataModel, $decodeFromStream) ? $this->line(' return \\'.Decoder::class.'::decodeString((string) $string);', $context) - : $this->line(' return $providers[\''.$dataModel->getIdentifier().'\'](\\'.Decoder::class.'::decodeString((string) $string));', $context)) + : $this->line(' return $providers['.$this->quote($dataModel->getIdentifier()).'](\\'.Decoder::class.'::decodeString((string) $string));', $context)) .$this->line('};', $context); } @@ -94,7 +94,7 @@ private function generateProviders(DataModelNodeInterface $node, bool $decodeFro $accessor = $decodeFromStream ? '\\'.Decoder::class.'::decodeStream($stream, $offset, $length)' : '$data'; $arguments = $decodeFromStream ? '$stream, $offset, $length' : '$data'; - return $this->line("\$providers['".$node->getIdentifier()."'] = static function ($arguments) {", $context) + return $this->line('$providers['.$this->quote($node->getIdentifier())."] = static function ($arguments) {", $context) .$this->line(' return '.$this->generateValueFormat($node, $accessor).';', $context) .$this->line('};', $context); } @@ -109,20 +109,20 @@ private function generateProviders(DataModelNodeInterface $node, bool $decodeFro $arguments = $decodeFromStream ? '$stream, $offset, $length' : '$data'; - $php .= $this->line("\$providers['".$node->getIdentifier()."'] = static function ($arguments) use (\$options, \$valueTransformers, \$instantiator, &\$providers) {", $context); + $php .= $this->line('$providers['.$this->quote($node->getIdentifier())."] = static function ($arguments) use (\$options, \$valueTransformers, \$instantiator, &\$providers) {", $context); ++$context['indentation_level']; $php .= $decodeFromStream ? $this->line('$data = \\'.Decoder::class.'::decodeStream($stream, $offset, $length);', $context) : ''; foreach ($node->getNodes() as $n) { - $value = $this->canBeDecodedWithJsonDecode($n, $decodeFromStream) ? $this->generateValueFormat($n, '$data') : '$providers[\''.$n->getIdentifier().'\']($data)'; + $value = $this->canBeDecodedWithJsonDecode($n, $decodeFromStream) ? $this->generateValueFormat($n, '$data') : '$providers['.$this->quote($n->getIdentifier()).']($data)'; $php .= $this->line('if ('.$this->generateCompositeNodeItemCondition($n, '$data').') {', $context) .$this->line(" return $value;", $context) .$this->line('}', $context); } - $php .= $this->line('throw new \\'.UnexpectedValueException::class.'(\\sprintf(\'Unexpected "%s" value for "'.$node->getIdentifier().'".\', \\get_debug_type($data)));', $context); + $php .= $this->line('throw new \\'.UnexpectedValueException::class.'(\\sprintf(\'Unexpected "%s" value for "%s".\', \\get_debug_type($data), '.$this->quote($node->getIdentifier()).'));', $context); --$context['indentation_level']; @@ -132,7 +132,7 @@ private function generateProviders(DataModelNodeInterface $node, bool $decodeFro if ($node instanceof CollectionNode) { $arguments = $decodeFromStream ? '$stream, $offset, $length' : '$data'; - $php = $this->line("\$providers['".$node->getIdentifier()."'] = static function ($arguments) use (\$options, \$valueTransformers, \$instantiator, &\$providers) {", $context); + $php = $this->line('$providers['.$this->quote($node->getIdentifier())."] = static function ($arguments) use (\$options, \$valueTransformers, \$instantiator, &\$providers) {", $context); ++$context['indentation_level']; @@ -146,11 +146,11 @@ private function generateProviders(DataModelNodeInterface $node, bool $decodeFro if ($decodeFromStream) { $php .= $this->canBeDecodedWithJsonDecode($node->getItemNode(), $decodeFromStream) ? $this->line(' yield $k => '.$this->generateValueFormat($node->getItemNode(), '\\'.Decoder::class.'::decodeStream($stream, $v[0], $v[1]);'), $context) - : $this->line(' yield $k => $providers[\''.$node->getItemNode()->getIdentifier().'\']($stream, $v[0], $v[1]);', $context); + : $this->line(' yield $k => $providers['.$this->quote($node->getItemNode()->getIdentifier()).']($stream, $v[0], $v[1]);', $context); } else { $php .= $this->canBeDecodedWithJsonDecode($node->getItemNode(), $decodeFromStream) ? $this->line(' yield $k => $v;', $context) - : $this->line(' yield $k => $providers[\''.$node->getItemNode()->getIdentifier().'\']($v);', $context); + : $this->line(' yield $k => $providers['.$this->quote($node->getItemNode()->getIdentifier()).']($v);', $context); } $php .= $this->line(' }', $context) @@ -175,7 +175,7 @@ private function generateProviders(DataModelNodeInterface $node, bool $decodeFro $arguments = $decodeFromStream ? '$stream, $offset, $length' : '$data'; - $php = $this->line("\$providers['".$node->getIdentifier()."'] = static function ($arguments) use (\$options, \$valueTransformers, \$instantiator, &\$providers) {", $context); + $php = $this->line('$providers['.$this->quote($node->getIdentifier())."] = static function ($arguments) use (\$options, \$valueTransformers, \$instantiator, &\$providers) {", $context); ++$context['indentation_level']; @@ -189,7 +189,7 @@ private function generateProviders(DataModelNodeInterface $node, bool $decodeFro foreach ($node->getProperties() as $streamedName => $property) { $propertyValuePhp = $this->canBeDecodedWithJsonDecode($property['value'], $decodeFromStream) ? $this->generateValueFormat($property['value'], '\\'.Decoder::class.'::decodeStream($stream, $v[0], $v[1])') - : '$providers[\''.$property['value']->getIdentifier().'\']($stream, $v[0], $v[1])'; + : '$providers['.$this->quote($property['value']->getIdentifier()).']($stream, $v[0], $v[1])'; $php .= $this->line(" '$streamedName' => \$object->".$property['name'].' = '.$property['accessor']($propertyValuePhp).',', $context); } @@ -204,7 +204,7 @@ private function generateProviders(DataModelNodeInterface $node, bool $decodeFro foreach ($node->getProperties() as $streamedName => $property) { $propertyValuePhp = $this->canBeDecodedWithJsonDecode($property['value'], $decodeFromStream) ? "\$data['$streamedName'] ?? '_symfony_missing_value'" - : "\\array_key_exists('$streamedName', \$data) ? \$providers['".$property['value']->getIdentifier()."'](\$data['$streamedName']) : '_symfony_missing_value'"; + : "\\array_key_exists('$streamedName', \$data) ? \$providers[".$this->quote($property['value']->getIdentifier())."](\$data['$streamedName']) : '_symfony_missing_value'"; $propertiesValuePhp .= "$separator'".$property['name']."' => ".$property['accessor']($propertyValuePhp); $separator = ', '; } @@ -305,6 +305,11 @@ private function line(string $line, array $context): string return str_repeat(' ', $context['indentation_level']).$line."\n"; } + private function quote(string $identifier): string + { + return \sprintf("'%s'", addcslashes($identifier, "'")); + } + /** * Determines if the $node can be decoded using a simple "json_decode". */ diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/array_shape.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/array_shape.php new file mode 100644 index 0000000000000..f0d75ddba23a7 --- /dev/null +++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/array_shape.php @@ -0,0 +1,8 @@ + $v) { + yield $k => \Symfony\Component\JsonStreamer\Read\Decoder::decodeStream($stream, $v[0], $v[1]); + } + }; + return \iterator_to_array($iterable($stream, $data)); + }; + return $providers['array{\'id\': int, \'name\': string}']($stream, 0, null); +}; diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_backed_enum.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_backed_enum.php index fc286791e9f57..1fdf92f9ad238 100644 --- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_backed_enum.php +++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_backed_enum.php @@ -14,7 +14,7 @@ if (null === $data) { return null; } - throw new \Symfony\Component\JsonStreamer\Exception\UnexpectedValueException(\sprintf('Unexpected "%s" value for "Symfony\Component\JsonStreamer\Tests\Fixtures\Enum\DummyBackedEnum|null".', \get_debug_type($data))); + throw new \Symfony\Component\JsonStreamer\Exception\UnexpectedValueException(\sprintf('Unexpected "%s" value for "%s".', \get_debug_type($data), 'Symfony\Component\JsonStreamer\Tests\Fixtures\Enum\DummyBackedEnum|null')); }; return $providers['Symfony\Component\JsonStreamer\Tests\Fixtures\Enum\DummyBackedEnum|null'](\Symfony\Component\JsonStreamer\Read\Decoder::decodeString((string) $string)); }; diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_backed_enum.stream.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_backed_enum.stream.php index 1fe954ebf6fd3..eb7c1950d070f 100644 --- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_backed_enum.stream.php +++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_backed_enum.stream.php @@ -15,7 +15,7 @@ if (null === $data) { return null; } - throw new \Symfony\Component\JsonStreamer\Exception\UnexpectedValueException(\sprintf('Unexpected "%s" value for "Symfony\Component\JsonStreamer\Tests\Fixtures\Enum\DummyBackedEnum|null".', \get_debug_type($data))); + throw new \Symfony\Component\JsonStreamer\Exception\UnexpectedValueException(\sprintf('Unexpected "%s" value for "%s".', \get_debug_type($data), 'Symfony\Component\JsonStreamer\Tests\Fixtures\Enum\DummyBackedEnum|null')); }; return $providers['Symfony\Component\JsonStreamer\Tests\Fixtures\Enum\DummyBackedEnum|null']($stream, 0, null); }; diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_object.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_object.php index 28ff2c0828061..e1f7c50822d1f 100644 --- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_object.php +++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_object.php @@ -16,7 +16,7 @@ if (null === $data) { return null; } - throw new \Symfony\Component\JsonStreamer\Exception\UnexpectedValueException(\sprintf('Unexpected "%s" value for "Symfony\Component\JsonStreamer\Tests\Fixtures\Model\ClassicDummy|null".', \get_debug_type($data))); + throw new \Symfony\Component\JsonStreamer\Exception\UnexpectedValueException(\sprintf('Unexpected "%s" value for "%s".', \get_debug_type($data), 'Symfony\Component\JsonStreamer\Tests\Fixtures\Model\ClassicDummy|null')); }; return $providers['Symfony\Component\JsonStreamer\Tests\Fixtures\Model\ClassicDummy|null'](\Symfony\Component\JsonStreamer\Read\Decoder::decodeString((string) $string)); }; diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_object.stream.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_object.stream.php index ee8a34a2f8b8a..8056586d690fa 100644 --- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_object.stream.php +++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_object.stream.php @@ -24,7 +24,7 @@ if (null === $data) { return null; } - throw new \Symfony\Component\JsonStreamer\Exception\UnexpectedValueException(\sprintf('Unexpected "%s" value for "Symfony\Component\JsonStreamer\Tests\Fixtures\Model\ClassicDummy|null".', \get_debug_type($data))); + throw new \Symfony\Component\JsonStreamer\Exception\UnexpectedValueException(\sprintf('Unexpected "%s" value for "%s".', \get_debug_type($data), 'Symfony\Component\JsonStreamer\Tests\Fixtures\Model\ClassicDummy|null')); }; return $providers['Symfony\Component\JsonStreamer\Tests\Fixtures\Model\ClassicDummy|null']($stream, 0, null); }; diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_object_dict.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_object_dict.php index 83f8387bde755..22a67ce0679c6 100644 --- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_object_dict.php +++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_object_dict.php @@ -24,7 +24,7 @@ if (null === $data) { return null; } - throw new \Symfony\Component\JsonStreamer\Exception\UnexpectedValueException(\sprintf('Unexpected "%s" value for "array|null".', \get_debug_type($data))); + throw new \Symfony\Component\JsonStreamer\Exception\UnexpectedValueException(\sprintf('Unexpected "%s" value for "%s".', \get_debug_type($data), 'array|null')); }; return $providers['array|null'](\Symfony\Component\JsonStreamer\Read\Decoder::decodeString((string) $string)); }; diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_object_dict.stream.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_object_dict.stream.php index c3bf621fed4d9..1b3fe01c609cb 100644 --- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_object_dict.stream.php +++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_object_dict.stream.php @@ -33,7 +33,7 @@ if (null === $data) { return null; } - throw new \Symfony\Component\JsonStreamer\Exception\UnexpectedValueException(\sprintf('Unexpected "%s" value for "array|null".', \get_debug_type($data))); + throw new \Symfony\Component\JsonStreamer\Exception\UnexpectedValueException(\sprintf('Unexpected "%s" value for "%s".', \get_debug_type($data), 'array|null')); }; return $providers['array|null']($stream, 0, null); }; diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_object_list.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_object_list.php index 65cdae459555b..75c5fb7841614 100644 --- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_object_list.php +++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_object_list.php @@ -24,7 +24,7 @@ if (null === $data) { return null; } - throw new \Symfony\Component\JsonStreamer\Exception\UnexpectedValueException(\sprintf('Unexpected "%s" value for "list|null".', \get_debug_type($data))); + throw new \Symfony\Component\JsonStreamer\Exception\UnexpectedValueException(\sprintf('Unexpected "%s" value for "%s".', \get_debug_type($data), 'list|null')); }; return $providers['list|null'](\Symfony\Component\JsonStreamer\Read\Decoder::decodeString((string) $string)); }; diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_object_list.stream.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_object_list.stream.php index 9c035367616a3..b8af34b638acc 100644 --- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_object_list.stream.php +++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/nullable_object_list.stream.php @@ -33,7 +33,7 @@ if (null === $data) { return null; } - throw new \Symfony\Component\JsonStreamer\Exception\UnexpectedValueException(\sprintf('Unexpected "%s" value for "list|null".', \get_debug_type($data))); + throw new \Symfony\Component\JsonStreamer\Exception\UnexpectedValueException(\sprintf('Unexpected "%s" value for "%s".', \get_debug_type($data), 'list|null')); }; return $providers['list|null']($stream, 0, null); }; diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_with_nullable_properties.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_with_nullable_properties.php index 91923525f1d32..0f0f3ab6fd00c 100644 --- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_with_nullable_properties.php +++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_with_nullable_properties.php @@ -19,7 +19,7 @@ if (null === $data) { return null; } - throw new \Symfony\Component\JsonStreamer\Exception\UnexpectedValueException(\sprintf('Unexpected "%s" value for "Symfony\Component\JsonStreamer\Tests\Fixtures\Enum\DummyBackedEnum|null".', \get_debug_type($data))); + throw new \Symfony\Component\JsonStreamer\Exception\UnexpectedValueException(\sprintf('Unexpected "%s" value for "%s".', \get_debug_type($data), 'Symfony\Component\JsonStreamer\Tests\Fixtures\Enum\DummyBackedEnum|null')); }; return $providers['Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithNullableProperties'](\Symfony\Component\JsonStreamer\Read\Decoder::decodeString((string) $string)); }; diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_with_nullable_properties.stream.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_with_nullable_properties.stream.php index c05e0f05d84cf..e7c47d31b5342 100644 --- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_with_nullable_properties.stream.php +++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_with_nullable_properties.stream.php @@ -27,7 +27,7 @@ if (null === $data) { return null; } - throw new \Symfony\Component\JsonStreamer\Exception\UnexpectedValueException(\sprintf('Unexpected "%s" value for "Symfony\Component\JsonStreamer\Tests\Fixtures\Enum\DummyBackedEnum|null".', \get_debug_type($data))); + throw new \Symfony\Component\JsonStreamer\Exception\UnexpectedValueException(\sprintf('Unexpected "%s" value for "%s".', \get_debug_type($data), 'Symfony\Component\JsonStreamer\Tests\Fixtures\Enum\DummyBackedEnum|null')); }; return $providers['Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithNullableProperties']($stream, 0, null); }; diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_with_union.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_with_union.php index 9c64bdb6aa368..7f63358d3e215 100644 --- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_with_union.php +++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_with_union.php @@ -22,7 +22,7 @@ if (\is_string($data)) { return $data; } - throw new \Symfony\Component\JsonStreamer\Exception\UnexpectedValueException(\sprintf('Unexpected "%s" value for "Symfony\Component\JsonStreamer\Tests\Fixtures\Enum\DummyBackedEnum|null|string".', \get_debug_type($data))); + throw new \Symfony\Component\JsonStreamer\Exception\UnexpectedValueException(\sprintf('Unexpected "%s" value for "%s".', \get_debug_type($data), 'Symfony\Component\JsonStreamer\Tests\Fixtures\Enum\DummyBackedEnum|null|string')); }; return $providers['Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithUnionProperties'](\Symfony\Component\JsonStreamer\Read\Decoder::decodeString((string) $string)); }; diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_with_union.stream.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_with_union.stream.php index 1ccf17a7b0bf2..cc6574e1ea17d 100644 --- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_with_union.stream.php +++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/object_with_union.stream.php @@ -29,7 +29,7 @@ if (\is_string($data)) { return $data; } - throw new \Symfony\Component\JsonStreamer\Exception\UnexpectedValueException(\sprintf('Unexpected "%s" value for "Symfony\Component\JsonStreamer\Tests\Fixtures\Enum\DummyBackedEnum|null|string".', \get_debug_type($data))); + throw new \Symfony\Component\JsonStreamer\Exception\UnexpectedValueException(\sprintf('Unexpected "%s" value for "%s".', \get_debug_type($data), 'Symfony\Component\JsonStreamer\Tests\Fixtures\Enum\DummyBackedEnum|null|string')); }; return $providers['Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithUnionProperties']($stream, 0, null); }; diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/union.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/union.php index ad118d3567dd8..09d6599e7ee82 100644 --- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/union.php +++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/union.php @@ -30,7 +30,7 @@ if (\is_int($data)) { return $data; } - throw new \Symfony\Component\JsonStreamer\Exception\UnexpectedValueException(\sprintf('Unexpected "%s" value for "Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithNameAttributes|int|list".', \get_debug_type($data))); + throw new \Symfony\Component\JsonStreamer\Exception\UnexpectedValueException(\sprintf('Unexpected "%s" value for "%s".', \get_debug_type($data), 'Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithNameAttributes|int|list')); }; return $providers['Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithNameAttributes|int|list'](\Symfony\Component\JsonStreamer\Read\Decoder::decodeString((string) $string)); }; diff --git a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/union.stream.php b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/union.stream.php index ebbc18bc2d2c9..7f4a37b7acd86 100644 --- a/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/union.stream.php +++ b/src/Symfony/Component/JsonStreamer/Tests/Fixtures/stream_reader/union.stream.php @@ -39,7 +39,7 @@ if (\is_int($data)) { return $data; } - throw new \Symfony\Component\JsonStreamer\Exception\UnexpectedValueException(\sprintf('Unexpected "%s" value for "Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithNameAttributes|int|list".', \get_debug_type($data))); + throw new \Symfony\Component\JsonStreamer\Exception\UnexpectedValueException(\sprintf('Unexpected "%s" value for "%s".', \get_debug_type($data), 'Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithNameAttributes|int|list')); }; return $providers['Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithNameAttributes|int|list']($stream, 0, null); }; diff --git a/src/Symfony/Component/JsonStreamer/Tests/Read/StreamReaderGeneratorTest.php b/src/Symfony/Component/JsonStreamer/Tests/Read/StreamReaderGeneratorTest.php index 71b29edeabcb1..5bcd4b8c04da3 100644 --- a/src/Symfony/Component/JsonStreamer/Tests/Read/StreamReaderGeneratorTest.php +++ b/src/Symfony/Component/JsonStreamer/Tests/Read/StreamReaderGeneratorTest.php @@ -111,6 +111,8 @@ public static function generatedStreamReaderDataProvider(): iterable yield ['object_list', Type::list(Type::object(ClassicDummy::class))]; yield ['nullable_object_list', Type::nullable(Type::list(Type::object(ClassicDummy::class)))]; + yield ['array_shape', Type::arrayShape(['id' => Type::int(), 'name' => Type::string()])]; + yield ['dict', Type::dict()]; yield ['object_dict', Type::dict(Type::object(ClassicDummy::class))]; yield ['nullable_object_dict', Type::nullable(Type::dict(Type::object(ClassicDummy::class)))]; diff --git a/src/Symfony/Component/JsonStreamer/Tests/Write/StreamWriterGeneratorTest.php b/src/Symfony/Component/JsonStreamer/Tests/Write/StreamWriterGeneratorTest.php index 8fd7fb3f4b412..bbc3a8fa2b3ee 100644 --- a/src/Symfony/Component/JsonStreamer/Tests/Write/StreamWriterGeneratorTest.php +++ b/src/Symfony/Component/JsonStreamer/Tests/Write/StreamWriterGeneratorTest.php @@ -24,8 +24,8 @@ use Symfony\Component\JsonStreamer\Tests\Fixtures\Mapping\SyntheticPropertyMetadataLoader; use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\ClassicDummy; use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithArray; -use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithList; use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithDollarNamedProperties; +use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithList; use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithNameAttributes; use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithNestedArray; use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithNestedDictDummies; 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/Mailer/Tests/MailerTest.php b/src/Symfony/Component/Mailer/Tests/MailerTest.php index 1c537a0693934..76328aab37482 100644 --- a/src/Symfony/Component/Mailer/Tests/MailerTest.php +++ b/src/Symfony/Component/Mailer/Tests/MailerTest.php @@ -21,8 +21,8 @@ use Symfony\Component\Mailer\SentMessage; use Symfony\Component\Mailer\Transport\AbstractTransport; use Symfony\Component\Mailer\Transport\NullTransport; -use Symfony\Component\Mailer\Transport\TransportInterface; use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\MessageBus; use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Messenger\Stamp\StampInterface; use Symfony\Component\Mime\Address; @@ -35,7 +35,7 @@ public function testSendingRawMessages() { $this->expectException(LogicException::class); - $transport = new Mailer($this->createMock(TransportInterface::class), $this->createMock(MessageBusInterface::class), $this->createMock(EventDispatcherInterface::class)); + $transport = new Mailer(new DummyTransport('localhost'), new MessageBus(), new EventDispatcher()); $transport->send(new RawMessage('Some raw email message')); } @@ -54,7 +54,7 @@ public function dispatch($message, array $stamps = []): Envelope } }; - $stamp = $this->createMock(StampInterface::class); + $stamp = new class implements StampInterface {}; $dispatcher = $this->createMock(EventDispatcherInterface::class); $dispatcher->expects($this->once()) diff --git a/src/Symfony/Component/Mailer/Tests/Transport/Smtp/SmtpTransportTest.php b/src/Symfony/Component/Mailer/Tests/Transport/Smtp/SmtpTransportTest.php index 1d9dbcccfd20d..afee7cd21c3a2 100644 --- a/src/Symfony/Component/Mailer/Tests/Transport/Smtp/SmtpTransportTest.php +++ b/src/Symfony/Component/Mailer/Tests/Transport/Smtp/SmtpTransportTest.php @@ -142,8 +142,8 @@ public function testWriteEncodedRecipientAndSenderAddresses() public function testMessageIdFromServerIsEmbeddedInSentMessageEvent() { $calls = 0; - $eventDispatcher = $this->createMock(EventDispatcherInterface::class); - $eventDispatcher->expects($this->any()) + $eventDispatcher = $this->createStub(EventDispatcherInterface::class); + $eventDispatcher ->method('dispatch') ->with($this->callback(static function ($event) use (&$calls): bool { ++$calls; diff --git a/src/Symfony/Component/Mailer/Tests/Transport/TransportsTest.php b/src/Symfony/Component/Mailer/Tests/Transport/TransportsTest.php index e50db434ffc0c..2e725c90ae64f 100644 --- a/src/Symfony/Component/Mailer/Tests/Transport/TransportsTest.php +++ b/src/Symfony/Component/Mailer/Tests/Transport/TransportsTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Mailer\Exception\InvalidArgumentException; +use Symfony\Component\Mailer\Tests\DummyTransport; use Symfony\Component\Mailer\Transport\TransportInterface; use Symfony\Component\Mailer\Transport\Transports; use Symfony\Component\Mime\Header\Headers; @@ -53,8 +54,8 @@ public function testOverrideTransport() public function testTransportDoesNotExist() { $transport = new Transports([ - 'foo' => $this->createMock(TransportInterface::class), - 'bar' => $this->createMock(TransportInterface::class), + 'foo' => new DummyTransport('localhost'), + 'bar' => new DummyTransport('localhost'), ]); $headers = (new Headers())->addTextHeader('X-Transport', 'foobar'); @@ -69,7 +70,7 @@ public function testTransportRestoredAfterFailure() { $exception = new \Exception(); - $fooTransport = $this->createMock(TransportInterface::class); + $fooTransport = $this->createStub(TransportInterface::class); $fooTransport->method('send') ->willThrowException($exception); diff --git a/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/ConnectionTest.php b/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/ConnectionTest.php index a908c234227ea..4cd709dcb46ca 100644 --- a/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/ConnectionTest.php +++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/ConnectionTest.php @@ -667,6 +667,22 @@ public function testConfigureSchema() $connection = new Connection(['table_name' => 'queue_table'], $driverConnection); $connection->configureSchema($schema, $driverConnection, fn () => true); $this->assertTrue($schema->hasTable('queue_table')); + + // Ensure the covering index for the SELECT query exists + $table = $schema->getTable('queue_table'); + $hasCoveringIndex = false; + foreach ($table->getIndexes() as $index) { + // Doctrine DBAL 4+: use getIndexedColumns(); fallback to getColumns() for older versions + $columns = method_exists($index, 'getIndexedColumns') + ? array_map(static fn ($ic) => $ic->getColumnName()->toString(), $index->getIndexedColumns()) + : $index->getColumns(); + + if ($columns === ['queue_name', 'available_at', 'delivered_at', 'id']) { + $hasCoveringIndex = true; + break; + } + } + $this->assertTrue($hasCoveringIndex, 'Expected covering index on [queue_name, available_at, delivered_at, id] not found'); } public function testConfigureSchemaDifferentDbalConnection() diff --git a/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/Connection.php b/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/Connection.php index 24d03bbedc7ca..d6b109e893e8c 100644 --- a/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/Connection.php +++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/Connection.php @@ -12,10 +12,8 @@ namespace Symfony\Component\Messenger\Bridge\Doctrine\Transport; use Doctrine\DBAL\Connection as DBALConnection; -use Doctrine\DBAL\Driver\Exception as DriverException; use Doctrine\DBAL\Exception as DBALException; use Doctrine\DBAL\Exception\TableNotFoundException; -use Doctrine\DBAL\Platforms\AbstractMySQLPlatform; use Doctrine\DBAL\Platforms\OraclePlatform; use Doctrine\DBAL\Platforms\PostgreSQLPlatform; use Doctrine\DBAL\Query\ForUpdate\ConflictResolutionMode; @@ -160,19 +158,6 @@ public function send(string $body, array $headers, int $delay = 0): string public function get(): ?array { - if ($this->doMysqlCleanup && $this->driverConnection->getDatabasePlatform() instanceof AbstractMySQLPlatform) { - try { - $this->driverConnection->delete($this->configuration['table_name'], ['delivered_at' => '9999-12-31 23:59:59']); - $this->doMysqlCleanup = false; - } catch (TableNotFoundException $e) { - if ($this->autoSetup) { - $this->setup(); - } - } catch (DriverException $e) { - // Ignore the exception - } - } - get: $this->driverConnection->beginTransaction(); try { @@ -246,14 +231,6 @@ public function get(): ?array public function ack(string $id): bool { try { - if ($this->driverConnection->getDatabasePlatform() instanceof AbstractMySQLPlatform) { - if ($updated = $this->driverConnection->update($this->configuration['table_name'], ['delivered_at' => '9999-12-31 23:59:59'], ['id' => $id]) > 0) { - $this->doMysqlCleanup = true; - } - - return $updated; - } - return $this->driverConnection->delete($this->configuration['table_name'], ['id' => $id]) > 0; } catch (DBALException $exception) { throw new TransportException($exception->getMessage(), 0, $exception); @@ -263,14 +240,6 @@ public function ack(string $id): bool public function reject(string $id): bool { try { - if ($this->driverConnection->getDatabasePlatform() instanceof AbstractMySQLPlatform) { - if ($updated = $this->driverConnection->update($this->configuration['table_name'], ['delivered_at' => '9999-12-31 23:59:59'], ['id' => $id]) > 0) { - $this->doMysqlCleanup = true; - } - - return $updated; - } - return $this->driverConnection->delete($this->configuration['table_name'], ['id' => $id]) > 0; } catch (DBALException $exception) { throw new TransportException($exception->getMessage(), 0, $exception); @@ -547,9 +516,7 @@ private function addTableToSchema(Schema $schema): void $table->addColumn('delivered_at', Types::DATETIME_IMMUTABLE) ->setNotnull(false); $table->addPrimaryKeyConstraint(new PrimaryKeyConstraint(null, [new UnqualifiedName(Identifier::unquoted('id'))], true)); - $table->addIndex(['queue_name']); - $table->addIndex(['available_at']); - $table->addIndex(['delivered_at']); + $table->addIndex(['queue_name', 'available_at', 'delivered_at', 'id']); // We need to create a sequence for Oracle and set the id column to get the correct nextval if ($this->driverConnection->getDatabasePlatform() instanceof OraclePlatform) { 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/Messenger/Tests/Command/FailedMessagesRetryCommandTest.php b/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesRetryCommandTest.php index cbcdf34706f4b..4392495cf8703 100644 --- a/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesRetryCommandTest.php +++ b/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesRetryCommandTest.php @@ -226,7 +226,7 @@ public function testCompleteIdWithSpecifiedTransport() $this->assertSame(['2ab50dfa1fbf', '78c2da843723'], $suggestions); } - public function testSuccessMessageGoesToStdout() + public function testSuccessMessageGoesToStdout() { $envelope = new Envelope(new \stdClass(), [new TransportMessageIdStamp('some_id')]); $receiver = $this->createMock(ListableReceiverInterface::class); 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..f3e13ebf1d15a 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php @@ -12,9 +12,9 @@ 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; use PHPUnit\Framework\TestCase; use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; use Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummy; @@ -29,7 +29,6 @@ use Symfony\Component\PropertyInfo\Tests\Fixtures\TraitUsage\DummyUsedInTrait; use Symfony\Component\PropertyInfo\Tests\Fixtures\TraitUsage\DummyUsingTrait; use Symfony\Component\TypeInfo\Type; -use Symfony\Component\TypeInfo\Type\NullableType; /** * @author Kévin Dunglas @@ -372,6 +371,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/Generator/UrlGenerator.php b/src/Symfony/Component/Routing/Generator/UrlGenerator.php index 32d57e9c8dd5d..b85b1e1e5391f 100644 --- a/src/Symfony/Component/Routing/Generator/UrlGenerator.php +++ b/src/Symfony/Component/Routing/Generator/UrlGenerator.php @@ -271,7 +271,7 @@ protected function doGenerate(array $variables, array $defaults, array $requirem // add a query string if needed $extra = array_udiff_assoc(array_diff_key($parameters, $variables), $defaults, fn ($a, $b) => $a == $b ? 0 : 1); - $extra = array_merge($extra, $queryParameters); + $extra = array_replace($extra, $queryParameters); array_walk_recursive($extra, $caster = static function (&$v) use (&$caster) { if (\is_object($v)) { diff --git a/src/Symfony/Component/Routing/Tests/Generator/UrlGeneratorTest.php b/src/Symfony/Component/Routing/Tests/Generator/UrlGeneratorTest.php index d7dd9bb6b885d..ddadcf0dc6c98 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; @@ -149,6 +148,7 @@ public static function valuesProvider(): array 'stdClass in nested stdClass' => ['?foo%5Bnested%5D%5Bbaz%5D=bar', 'foo', $nestedStdClass], 'non stringable object' => ['', 'foo', new NonStringableObject()], 'non stringable object but has public property' => ['?foo%5Bfoo%5D=property', 'foo', new NonStringableObjectWithPublicProperty()], + 'numeric key' => ['?123=foo', '123', 'foo'], ]; } diff --git a/src/Symfony/Component/Routing/Tests/RequestContextTest.php b/src/Symfony/Component/Routing/Tests/RequestContextTest.php index f40045f78919d..83c2293ceaf3e 100644 --- a/src/Symfony/Component/Routing/Tests/RequestContextTest.php +++ b/src/Symfony/Component/Routing/Tests/RequestContextTest.php @@ -47,7 +47,7 @@ public function testConstruct() public function testConstructParametersBcLayer() { - $requestContext = new class() extends RequestContext { + $requestContext = new class extends RequestContext { public function __construct() { $this->setParameters(['foo' => 'bar']); diff --git a/src/Symfony/Component/Runtime/Internal/ComposerPlugin.php b/src/Symfony/Component/Runtime/Internal/ComposerPlugin.php index 92b1f3a37c714..b4cd25b8a33f6 100644 --- a/src/Symfony/Component/Runtime/Internal/ComposerPlugin.php +++ b/src/Symfony/Component/Runtime/Internal/ComposerPlugin.php @@ -82,7 +82,7 @@ public function updateAutoloadFile(): void $projectDir = substr($projectDir, 3); } - // the hack about __DIR__ is required because composer pre-processes plugins + // the hack about __DIR__ is required because Composer pre-processes plugins if (!$nestingLevel) { $projectDir = '__'.'DIR__.'.var_export('/'.$projectDir, true); } else { @@ -118,3 +118,5 @@ public static function getSubscribedEvents(): array ]; } } + +// @php-cs-fixer-ignore no_useless_concat_operator Disable to not override hack about __DIR__ and Composer pre-processes plugins diff --git a/src/Symfony/Component/Security/Http/AccessToken/Oidc/OidcTokenGenerator.php b/src/Symfony/Component/Security/Http/AccessToken/Oidc/OidcTokenGenerator.php index eaba079f68350..4f64f9880921d 100644 --- a/src/Symfony/Component/Security/Http/AccessToken/Oidc/OidcTokenGenerator.php +++ b/src/Symfony/Component/Security/Http/AccessToken/Oidc/OidcTokenGenerator.php @@ -44,19 +44,19 @@ public function generate(string $userIdentifier, ?string $algorithmAlias = null, $now = $this->clock->now(); $payload = [ $this->claim => $userIdentifier, - 'iat' => $now->getTimestamp(), # https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6 - 'aud' => $this->audience, # https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3 - 'iss' => $this->getIssuer($issuer), # https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1 + 'iat' => $now->getTimestamp(), // https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6 + 'aud' => $this->audience, // https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3 + 'iss' => $this->getIssuer($issuer), // https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1 ]; if ($ttl) { if (0 > $ttl) { throw new \InvalidArgumentException('Time to live must be a positive integer.'); } - $payload['exp'] = $now->add(new \DateInterval("PT{$ttl}S"))->getTimestamp(); # https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4 + $payload['exp'] = $now->add(new \DateInterval("PT{$ttl}S"))->getTimestamp(); // https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4 } if ($notBefore) { - $payload['nbf'] = $notBefore->getTimestamp(); # https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5 + $payload['nbf'] = $notBefore->getTimestamp(); // https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5 } $jws = $jwsBuilder @@ -74,13 +74,14 @@ private function getAlgorithm(?string $alias): Algorithm { if ($alias) { if (!$this->algorithmManager->has($alias)) { - throw new \InvalidArgumentException(sprintf('"%s" is not a valid algorithm. Available algorithms: "%s".', $alias, implode('", "', $this->algorithmManager->list()))); + throw new \InvalidArgumentException(\sprintf('"%s" is not a valid algorithm. Available algorithms: "%s".', $alias, implode('", "', $this->algorithmManager->list()))); } + return $this->algorithmManager->get($alias); } - if (1 !== count($list = $this->algorithmManager->list())) { - throw new \InvalidArgumentException(sprintf('Please choose an algorithm. Available algorithms: "%s".', implode('", "', $list))); + if (1 !== \count($list = $this->algorithmManager->list())) { + throw new \InvalidArgumentException(\sprintf('Please choose an algorithm. Available algorithms: "%s".', implode('", "', $list))); } return $this->algorithmManager->get($list[0]); @@ -89,15 +90,15 @@ private function getAlgorithm(?string $alias): Algorithm private function getIssuer(?string $issuer): string { if ($issuer) { - if (!in_array($issuer, $this->issuers, true)) { - throw new \InvalidArgumentException(sprintf('"%s" is not a valid issuer. Available issuers: "%s".', $issuer, implode('", "', $this->issuers))); + if (!\in_array($issuer, $this->issuers, true)) { + throw new \InvalidArgumentException(\sprintf('"%s" is not a valid issuer. Available issuers: "%s".', $issuer, implode('", "', $this->issuers))); } return $issuer; } - if (1 !== count($this->issuers)) { - throw new \InvalidArgumentException(sprintf('Please choose an issuer. Available issuers: "%s".', implode('", "', $this->issuers))); + if (1 !== \count($this->issuers)) { + throw new \InvalidArgumentException(\sprintf('Please choose an issuer. Available issuers: "%s".', implode('", "', $this->issuers))); } return $this->issuers[0]; 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/Serializer/Encoder/XmlEncoder.php b/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php index 0b2fb02fb06e4..d9b7a5d152394 100644 --- a/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php +++ b/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php @@ -480,7 +480,7 @@ private function selectNodeType(\DOMNode $node, mixed $val, string $format, arra return $this->selectNodeType($node, $this->serializer->normalize($val, $format, $context), $format, $context); } elseif (is_numeric($val)) { - return $this->appendText($node, (string) $val); + return $this->appendText($node, is_nan($val) ? 'NAN' : (string) $val); } elseif (\is_string($val) && $this->needsCdataWrapping($node->nodeName, $val, $context)) { return $this->appendCData($node, $val); } elseif (\is_string($val)) { diff --git a/src/Symfony/Component/Serializer/Tests/Encoder/XmlEncoderTest.php b/src/Symfony/Component/Serializer/Tests/Encoder/XmlEncoderTest.php index ae6e6b2b875d5..39a8e118ca99d 100644 --- a/src/Symfony/Component/Serializer/Tests/Encoder/XmlEncoderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Encoder/XmlEncoderTest.php @@ -959,6 +959,26 @@ public function testEncodeWithoutComment() $this->assertEquals($expected, $encoder->encode($data, 'xml')); } + public function testEncodeNan() + { + $value = \NAN; + + $expected = ''."\n". + 'NAN'."\n"; + + $this->assertEquals($expected, $this->encoder->encode($value, 'xml')); + } + + public function testEncodeInfinite() + { + $value = \INF; + + $expected = ''."\n". + 'INF'."\n"; + + $this->assertEquals($expected, $this->encoder->encode($value, 'xml')); + } + private function createXmlEncoderWithEnvelopeNormalizer(): XmlEncoder { $normalizers = [ diff --git a/src/Symfony/Component/Translation/Bridge/Lokalise/LokaliseProvider.php b/src/Symfony/Component/Translation/Bridge/Lokalise/LokaliseProvider.php index 1d68494c66740..18ab959238fa0 100644 --- a/src/Symfony/Component/Translation/Bridge/Lokalise/LokaliseProvider.php +++ b/src/Symfony/Component/Translation/Bridge/Lokalise/LokaliseProvider.php @@ -237,7 +237,7 @@ private function exportFilesAsync(array $locales, array $domains): array return $this->getZipContents($extractPath); } finally { - if (is_resource($h)) { + if (\is_resource($h)) { fclose($h); } @unlink($zipFile); diff --git a/src/Symfony/Component/Uid/BinaryUtil.php b/src/Symfony/Component/Uid/BinaryUtil.php index 7d1e524e5e43e..0c16b9414b19f 100644 --- a/src/Symfony/Component/Uid/BinaryUtil.php +++ b/src/Symfony/Component/Uid/BinaryUtil.php @@ -187,3 +187,5 @@ public static function dateTimeToHex(\DateTimeInterface $time): string return bin2hex($time); } } + +// @php-cs-fixer-ignore long_to_shorthand_operator To prevent false positive causing "Cannot use assign-op operators with string offsets" error diff --git a/src/Symfony/Component/Validator/Constraints/UrlValidator.php b/src/Symfony/Component/Validator/Constraints/UrlValidator.php index ad17f5d25c110..eefdfc434f9d0 100644 --- a/src/Symfony/Component/Validator/Constraints/UrlValidator.php +++ b/src/Symfony/Component/Validator/Constraints/UrlValidator.php @@ -83,7 +83,7 @@ public function validate(mixed $value, Constraint $constraint): void } $pattern = $constraint->relativeProtocol ? str_replace('(%s):', '(?:(%s):)?', static::PATTERN) : static::PATTERN; - $pattern = sprintf($pattern, $protocols); + $pattern = \sprintf($pattern, $protocols); if (!preg_match($pattern, $value)) { $this->context->buildViolation($constraint->message) 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/Test/ConstraintValidatorTestCase.php b/src/Symfony/Component/Validator/Test/ConstraintValidatorTestCase.php index d112f86f7fba1..7120b86b66933 100644 --- a/src/Symfony/Component/Validator/Test/ConstraintValidatorTestCase.php +++ b/src/Symfony/Component/Validator/Test/ConstraintValidatorTestCase.php @@ -122,10 +122,10 @@ protected function restoreDefaultTimezone() protected function createContext() { - $translator = $this->createMock(TranslatorInterface::class); - $translator->expects($this->any())->method('trans')->willReturnArgument(0); - $validator = $this->createMock(ValidatorInterface::class); - $validator->expects($this->any()) + $translator = $this->createStub(TranslatorInterface::class); + $translator->method('trans')->willReturnArgument(0); + $validator = $this->createStub(ValidatorInterface::class); + $validator ->method('validate') ->willReturnCallback(fn () => $this->expectedViolations[$this->call++] ?? new ConstraintViolationList()); @@ -134,36 +134,47 @@ protected function createContext() $context->setNode($this->value, $this->object, $this->metadata, $this->propertyPath); $context->setConstraint($this->constraint); - $contextualValidatorMockBuilder = $this->getMockBuilder(AssertingContextualValidator::class) - ->setConstructorArgs([$context]); - $contextualValidatorMethods = [ - 'atPath', - 'validate', - 'validateProperty', - 'validatePropertyValue', - 'getViolations', - ]; - - $contextualValidatorMockBuilder->onlyMethods($contextualValidatorMethods); - $contextualValidator = $contextualValidatorMockBuilder->getMock(); - $contextualValidator->expects($this->any()) + if (method_exists($this, 'getStubBuilder')) { + $contextualValidator = self::getStubBuilder(AssertingContextualValidator::class) + ->setConstructorArgs([$context]) + ->onlyMethods([ + 'atPath', + 'validate', + 'validateProperty', + 'validatePropertyValue', + 'getViolations', + ]) + ->getStub(); + } else { + $contextualValidator = $this->getMockBuilder(AssertingContextualValidator::class) + ->setConstructorArgs([$context]) + ->onlyMethods([ + 'atPath', + 'validate', + 'validateProperty', + 'validatePropertyValue', + 'getViolations', + ]) + ->getMock(); + } + + $contextualValidator ->method('atPath') ->willReturnCallback(fn ($path) => $contextualValidator->doAtPath($path)); - $contextualValidator->expects($this->any()) + $contextualValidator ->method('validate') ->willReturnCallback(fn ($value, $constraints = null, $groups = null) => $contextualValidator->doValidate($value, $constraints, $groups)); - $contextualValidator->expects($this->any()) + $contextualValidator ->method('validateProperty') ->willReturnCallback(fn ($object, $propertyName, $groups = null) => $contextualValidator->validateProperty($object, $propertyName, $groups)); - $contextualValidator->expects($this->any()) + $contextualValidator ->method('validatePropertyValue') ->willReturnCallback(fn ($objectOrClass, $propertyName, $value, $groups = null) => $contextualValidator->doValidatePropertyValue($objectOrClass, $propertyName, $value, $groups)); - $contextualValidator->expects($this->any()) + $contextualValidator ->method('getViolations') ->willReturnCallback(fn () => $contextualValidator->doGetViolations()); - $validator->expects($this->any()) + $validator ->method('inContext') - ->with($context) ->willReturn($contextualValidator); return $context; diff --git a/src/Symfony/Component/Validator/Tests/Command/DebugCommandTest.php b/src/Symfony/Component/Validator/Tests/Command/DebugCommandTest.php index 35f74a0c0ad30..bcc4587dc5c58 100644 --- a/src/Symfony/Component/Validator/Tests/Command/DebugCommandTest.php +++ b/src/Symfony/Component/Validator/Tests/Command/DebugCommandTest.php @@ -15,7 +15,6 @@ use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\Validator\Command\DebugCommand; use Symfony\Component\Validator\Mapping\Factory\LazyLoadingMetadataFactory; -use Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface; use Symfony\Component\Validator\Mapping\Loader\AttributeLoader; use Symfony\Component\Validator\Tests\Dummy\DummyClassOne; @@ -180,9 +179,7 @@ public function testOutputWithPathArgument() public function testOutputWithInvalidClassArgument() { - $validator = $this->createMock(MetadataFactoryInterface::class); - - $command = new DebugCommand($validator); + $command = new DebugCommand(new LazyLoadingMetadataFactory()); $tester = new CommandTester($command); $tester->execute(['class' => 'App\\NotFoundResource'], ['decorated' => false]); diff --git a/src/Symfony/Component/Validator/Tests/Constraints/ExpressionValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/ExpressionValidatorTest.php index 8ac513e1b8fd8..a20602e30a883 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/ExpressionValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/ExpressionValidatorTest.php @@ -257,7 +257,7 @@ public function testExpressionLanguageUsage() { $constraint = new Expression(expression: 'false'); - $expressionLanguage = $this->createMock(ExpressionLanguage::class); + $expressionLanguage = $this->createStub(ExpressionLanguage::class); $used = false; diff --git a/src/Symfony/Component/Validator/Tests/Constraints/NotCompromisedPasswordValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/NotCompromisedPasswordValidatorTest.php index db9ced0e6a25f..25d75f4ac776b 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/NotCompromisedPasswordValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/NotCompromisedPasswordValidatorTest.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Validator\Tests\Constraints; +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\HttpClient\Response\MockResponse; use Symfony\Component\Validator\Constraints\Luhn; use Symfony\Component\Validator\Constraints\NotCompromisedPassword; use Symfony\Component\Validator\Constraints\NotCompromisedPasswordValidator; @@ -18,9 +20,7 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; use Symfony\Contracts\HttpClient\Exception\ExceptionInterface; -use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; -use Symfony\Contracts\HttpClient\ResponseInterface; /** * @author Kévin Dunglas @@ -195,7 +195,6 @@ public function testInvalidValue() public function testApiError() { $this->expectException(ExceptionInterface::class); - $this->expectExceptionMessage('Problem contacting the Have I been Pwned API.'); $this->validator->validate(self::PASSWORD_TRIGGERING_AN_ERROR, new NotCompromisedPassword()); } @@ -208,44 +207,20 @@ public function testApiErrorSkipped() private function createHttpClientStub(?string $returnValue = null): HttpClientInterface { - $httpClientStub = $this->createMock(HttpClientInterface::class); - $httpClientStub->method('request')->willReturnCallback( - function (string $method, string $url) use ($returnValue): ResponseInterface { - if (self::PASSWORD_TRIGGERING_AN_ERROR_RANGE_URL === $url) { - throw new class('Problem contacting the Have I been Pwned API.') extends \Exception implements ServerExceptionInterface { - public function getResponse(): ResponseInterface - { - throw new \RuntimeException('Not implemented'); - } - }; - } - - $responseStub = $this->createMock(ResponseInterface::class); - $responseStub - ->method('getContent') - ->willReturn($returnValue ?? implode("\r\n", self::RETURN)); - - return $responseStub; + return new MockHttpClient(function ($method, $url) use ($returnValue) { + if (self::PASSWORD_TRIGGERING_AN_ERROR_RANGE_URL !== $url) { + return new MockResponse($returnValue ?? implode("\r\n", self::RETURN)); } - ); - - return $httpClientStub; + }); } private function createHttpClientStubCustomEndpoint($expectedEndpoint): HttpClientInterface { - $httpClientStub = $this->createMock(HttpClientInterface::class); - $httpClientStub->method('request')->with('GET', $expectedEndpoint)->willReturnCallback( - function (string $method, string $url): ResponseInterface { - $responseStub = $this->createMock(ResponseInterface::class); - $responseStub - ->method('getContent') - ->willReturn(implode("\r\n", self::RETURN)); - - return $responseStub; - } - ); + return new MockHttpClient(function ($method, $url) use ($expectedEndpoint) { + $this->assertSame('GET', $method); + $this->assertSame($expectedEndpoint, $url); - return $httpClientStub; + return new MockResponse(implode("\r\n", self::RETURN)); + }); } } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/TimezoneValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/TimezoneValidatorTest.php index 879cee69f3706..f7b4d82e03218 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/TimezoneValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/TimezoneValidatorTest.php @@ -260,7 +260,7 @@ public function testDeprecatedTimezonesAreValidWithBC(string $timezone) { // Skip test if the timezone is not available in the current timezone database if (!\in_array($timezone, \DateTimeZone::listIdentifiers(\DateTimeZone::ALL_WITH_BC), true)) { - $this->markTestSkipped(sprintf('Timezone "%s" is not available in the current timezone database', $timezone)); + $this->markTestSkipped(\sprintf('Timezone "%s" is not available in the current timezone database', $timezone)); } $constraint = new Timezone(\DateTimeZone::ALL_WITH_BC); diff --git a/src/Symfony/Component/Validator/Tests/DataCollector/ValidatorDataCollectorTest.php b/src/Symfony/Component/Validator/Tests/DataCollector/ValidatorDataCollectorTest.php index 29fd4b38d151f..5bae9b6db835e 100644 --- a/src/Symfony/Component/Validator/Tests/DataCollector/ValidatorDataCollectorTest.php +++ b/src/Symfony/Component/Validator/Tests/DataCollector/ValidatorDataCollectorTest.php @@ -22,14 +22,14 @@ class ValidatorDataCollectorTest extends TestCase { public function testCollectsValidatorCalls() { - $originalValidator = $this->createMock(ValidatorInterface::class); + $originalValidator = $this->createStub(ValidatorInterface::class); $validator = new TraceableValidator($originalValidator); $collector = new ValidatorDataCollector($validator); $violations = new ConstraintViolationList([ - $this->createMock(ConstraintViolation::class), - $this->createMock(ConstraintViolation::class), + $this->createStub(ConstraintViolation::class), + $this->createStub(ConstraintViolation::class), ]); $originalValidator->method('validate')->willReturn($violations); @@ -52,14 +52,14 @@ public function testCollectsValidatorCalls() public function testReset() { - $originalValidator = $this->createMock(ValidatorInterface::class); + $originalValidator = $this->createStub(ValidatorInterface::class); $validator = new TraceableValidator($originalValidator); $collector = new ValidatorDataCollector($validator); $violations = new ConstraintViolationList([ - $this->createMock(ConstraintViolation::class), - $this->createMock(ConstraintViolation::class), + $this->createStub(ConstraintViolation::class), + $this->createStub(ConstraintViolation::class), ]); $originalValidator->method('validate')->willReturn($violations); diff --git a/src/Symfony/Component/Validator/Tests/Mapping/Factory/LazyLoadingMetadataFactoryTest.php b/src/Symfony/Component/Validator/Tests/Mapping/Factory/LazyLoadingMetadataFactoryTest.php index afd650581a77e..969dac113136e 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/Factory/LazyLoadingMetadataFactoryTest.php +++ b/src/Symfony/Component/Validator/Tests/Mapping/Factory/LazyLoadingMetadataFactoryTest.php @@ -111,13 +111,12 @@ public function testCachedMetadata() public function testNonClassNameStringValues() { $testedValue = 'error@example.com'; - $loader = $this->createMock(LoaderInterface::class); $cache = $this->createMock(CacheItemPoolInterface::class); $cache ->expects($this->never()) ->method('getItem'); - $factory = new LazyLoadingMetadataFactory($loader, $cache); + $factory = new LazyLoadingMetadataFactory(new StaticMethodLoader(), $cache); $this->expectException(NoSuchMetadataException::class); diff --git a/src/Symfony/Component/Validator/Tests/Mapping/Loader/FilesLoaderTest.php b/src/Symfony/Component/Validator/Tests/Mapping/Loader/FilesLoaderTest.php index ffb0dd23bdf3b..4cf4d1c557ad7 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/Loader/FilesLoaderTest.php +++ b/src/Symfony/Component/Validator/Tests/Mapping/Loader/FilesLoaderTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Mapping\Loader\LoaderInterface; +use Symfony\Component\Validator\Mapping\Loader\StaticMethodLoader; use Symfony\Component\Validator\Tests\Fixtures\FilesLoader; use Symfony\Component\Validator\Tests\Fixtures\NestedAttribute\Entity; @@ -21,7 +22,7 @@ class FilesLoaderTest extends TestCase { public function testCallsGetFileLoaderInstanceForeachPath() { - $loader = $this->getFilesLoader($this->createMock(LoaderInterface::class)); + $loader = $this->getFilesLoader(new StaticMethodLoader()); $this->assertEquals(4, $loader->getTimesCalled()); } diff --git a/src/Symfony/Component/Validator/Tests/Mapping/Loader/LoaderChainTest.php b/src/Symfony/Component/Validator/Tests/Mapping/Loader/LoaderChainTest.php index 9ebe33ec1f59e..1d0138ec04e9e 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/Loader/LoaderChainTest.php +++ b/src/Symfony/Component/Validator/Tests/Mapping/Loader/LoaderChainTest.php @@ -44,13 +44,13 @@ public function testReturnsTrueIfAnyLoaderReturnedTrue() { $metadata = new ClassMetadata('\stdClass'); - $loader1 = $this->createMock(LoaderInterface::class); - $loader1->expects($this->any()) + $loader1 = $this->createStub(LoaderInterface::class); + $loader1 ->method('loadClassMetadata') ->willReturn(true); - $loader2 = $this->createMock(LoaderInterface::class); - $loader2->expects($this->any()) + $loader2 = $this->createStub(LoaderInterface::class); + $loader2 ->method('loadClassMetadata') ->willReturn(false); @@ -66,13 +66,13 @@ public function testReturnsFalseIfNoLoaderReturnedTrue() { $metadata = new ClassMetadata('\stdClass'); - $loader1 = $this->createMock(LoaderInterface::class); - $loader1->expects($this->any()) + $loader1 = $this->createStub(LoaderInterface::class); + $loader1 ->method('loadClassMetadata') ->willReturn(false); - $loader2 = $this->createMock(LoaderInterface::class); - $loader2->expects($this->any()) + $loader2 = $this->createStub(LoaderInterface::class); + $loader2 ->method('loadClassMetadata') ->willReturn(false); diff --git a/src/Symfony/Component/Validator/Tests/Mapping/Loader/PropertyInfoLoaderTest.php b/src/Symfony/Component/Validator/Tests/Mapping/Loader/PropertyInfoLoaderTest.php index 19c9045441aaf..bf685a3dbfb34 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/Loader/PropertyInfoLoaderTest.php +++ b/src/Symfony/Component/Validator/Tests/Mapping/Loader/PropertyInfoLoaderTest.php @@ -39,7 +39,7 @@ class PropertyInfoLoaderTest extends TestCase { public function testLoadClassMetadata() { - $propertyListExtractor = $this->createMock(PropertyListExtractorInterface::class); + $propertyListExtractor = $this->createStub(PropertyListExtractorInterface::class); $propertyListExtractor ->method('getProperties') ->willReturn([ @@ -228,7 +228,7 @@ public function getTypes(string $class, string $property, array $context = []): #[DataProvider('regexpProvider')] public function testClassValidator(bool $expected, ?string $classValidatorRegexp = null) { - $propertyListExtractor = $this->createMock(PropertyListExtractorInterface::class); + $propertyListExtractor = $this->createStub(PropertyListExtractorInterface::class); $propertyListExtractor ->method('getProperties') ->willReturn(['string']) @@ -267,7 +267,7 @@ public static function regexpProvider(): array public function testClassNoAutoMapping(?PropertyTypeExtractorInterface $propertyListExtractor = null) { if (null === $propertyListExtractor) { - $propertyListExtractor = $this->createMock(PropertyListExtractorInterface::class); + $propertyListExtractor = $this->createStub(PropertyListExtractorInterface::class); $propertyListExtractor ->method('getProperties') ->willReturn(['string', 'autoMappingExplicitlyEnabled']) 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/Validator/Tests/Validator/TraceableValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/TraceableValidatorTest.php index ae728b19cb6c0..799d4b98a3061 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/TraceableValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/TraceableValidatorTest.php @@ -12,7 +12,7 @@ namespace Symfony\Component\Validator\Tests\Validator; use PHPUnit\Framework\TestCase; -use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Constraints\NotNull; use Symfony\Component\Validator\ConstraintViolation; use Symfony\Component\Validator\ConstraintViolationList; use Symfony\Component\Validator\Context\ExecutionContextInterface; @@ -27,15 +27,15 @@ public function testValidate() { $originalValidator = $this->createMock(ValidatorInterface::class); $violations = new ConstraintViolationList([ - $this->createMock(ConstraintViolation::class), - $this->createMock(ConstraintViolation::class), + $this->createStub(ConstraintViolation::class), + $this->createStub(ConstraintViolation::class), ]); $originalValidator->expects($this->exactly(2))->method('validate')->willReturn($violations); $validator = new TraceableValidator($originalValidator); $object = new \stdClass(); - $constraints = [$this->createMock(Constraint::class)]; + $constraints = [new NotNull()]; $groups = ['Default', 'Create']; $validator->validate($object, $constraints, $groups); @@ -74,16 +74,16 @@ public function testForwardsToOriginalValidator() $expects = fn ($method) => $originalValidator->expects($this->once())->method($method); - $expects('getMetadataFor')->willReturn($expected = $this->createMock(MetadataInterface::class)); + $expects('getMetadataFor')->willReturn($expected = $this->createStub(MetadataInterface::class)); $this->assertSame($expected, $validator->getMetadataFor('value'), 'returns original validator getMetadataFor() result'); $expects('hasMetadataFor')->willReturn($expected = false); $this->assertSame($expected, $validator->hasMetadataFor('value'), 'returns original validator hasMetadataFor() result'); - $expects('inContext')->willReturn($expected = $this->createMock(ContextualValidatorInterface::class)); - $this->assertSame($expected, $validator->inContext($this->createMock(ExecutionContextInterface::class)), 'returns original validator inContext() result'); + $expects('inContext')->willReturn($expected = $this->createStub(ContextualValidatorInterface::class)); + $this->assertSame($expected, $validator->inContext($this->createStub(ExecutionContextInterface::class)), 'returns original validator inContext() result'); - $expects('startContext')->willReturn($expected = $this->createMock(ContextualValidatorInterface::class)); + $expects('startContext')->willReturn($expected = $this->createStub(ContextualValidatorInterface::class)); $this->assertSame($expected, $validator->startContext(), 'returns original validator startContext() result'); $expects('validate')->willReturn($expected = new ConstraintViolationList()); diff --git a/src/Symfony/Component/Validator/Tests/ValidatorBuilderTest.php b/src/Symfony/Component/Validator/Tests/ValidatorBuilderTest.php index ec464b748635f..3c238a1051a4c 100644 --- a/src/Symfony/Component/Validator/Tests/ValidatorBuilderTest.php +++ b/src/Symfony/Component/Validator/Tests/ValidatorBuilderTest.php @@ -13,11 +13,11 @@ use PHPUnit\Framework\TestCase; use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\Translation\IdentityTranslator; use Symfony\Component\Validator\ConstraintValidatorFactoryInterface; use Symfony\Component\Validator\ObjectInitializerInterface; use Symfony\Component\Validator\Validator\RecursiveValidator; use Symfony\Component\Validator\ValidatorBuilder; -use Symfony\Contracts\Translation\TranslatorInterface; class ValidatorBuilderTest extends TestCase { @@ -31,7 +31,7 @@ protected function setUp(): void public function testAddObjectInitializer() { $this->assertSame($this->builder, $this->builder->addObjectInitializer( - $this->createMock(ObjectInitializerInterface::class) + $this->createStub(ObjectInitializerInterface::class) )); } @@ -77,21 +77,19 @@ public function testDisableAttributeMapping() public function testSetMappingCache() { - $this->assertSame($this->builder, $this->builder->setMappingCache($this->createMock(CacheItemPoolInterface::class))); + $this->assertSame($this->builder, $this->builder->setMappingCache($this->createStub(CacheItemPoolInterface::class))); } public function testSetConstraintValidatorFactory() { $this->assertSame($this->builder, $this->builder->setConstraintValidatorFactory( - $this->createMock(ConstraintValidatorFactoryInterface::class)) + $this->createStub(ConstraintValidatorFactoryInterface::class)) ); } public function testSetTranslator() { - $this->assertSame($this->builder, $this->builder->setTranslator( - $this->createMock(TranslatorInterface::class)) - ); + $this->assertSame($this->builder, $this->builder->setTranslator(new IdentityTranslator())); } public function testSetTranslationDomain() diff --git a/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php b/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php index 0f12f14f393b3..dd35bfefa734c 100644 --- a/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php +++ b/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php @@ -651,7 +651,7 @@ public function testReflectionClassConstantWithAttribute() public function testReflectionParameterWithAttribute() { $var = new \ReflectionParameter([LotsOfAttributes::class, 'someMethod'], 'someParameter'); - + $this->assertDumpMatchesFormat(<<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" + } +}