diff --git a/.appveyor.yml b/.appveyor.yml index 889aafe26929b..dfc01ccce97de 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -2,10 +2,6 @@ build: false clone_depth: 2 clone_folder: c:\projects\symfony -cache: - - composer.phar - - .phpunit -> phpunit - init: - SET PATH=c:\php;%PATH% - SET COMPOSER_NO_INTERACTION=1 @@ -47,9 +43,8 @@ install: - echo extension=php_curl.dll >> php.ini-max - copy /Y php.ini-max php.ini - cd c:\projects\symfony - - IF NOT EXIST composer.phar (appveyor DownloadFile https://github.com/composer/composer/releases/download/2.0.0/composer.phar) - - php composer.phar self-update --2 - - copy /Y .github\composer-config.json %APPDATA%\Composer\config.json + - appveyor DownloadFile https://getcomposer.org/download/latest-2.2.x/composer.phar + - mkdir %APPDATA%\Composer && copy /Y .github\composer-config.json %APPDATA%\Composer\config.json - git config --global user.email "" - git config --global user.name "Symfony" - FOR /F "tokens=* USEBACKQ" %%F IN (`bash -c "grep ' VERSION = ' src/Symfony/Component/HttpKernel/Kernel.php | grep -o '[0-9][0-9]*\.[0-9]'"`) DO (SET SYMFONY_VERSION=%%F) diff --git a/.github/psalm/.gitignore b/.github/psalm/.gitignore index d6b7ef32c8478..53021ab087be4 100644 --- a/.github/psalm/.gitignore +++ b/.github/psalm/.gitignore @@ -1,2 +1,4 @@ * !.gitignore +!stubs +!stubs/* diff --git a/.github/psalm/stubs/ForwardCompatTestTrait.php b/.github/psalm/stubs/ForwardCompatTestTrait.php new file mode 100644 index 0000000000000..e3ddf4da3d431 --- /dev/null +++ b/.github/psalm/stubs/ForwardCompatTestTrait.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Test; + +use PHPUnit\Framework\TestCase; + +/** + * @internal + */ +trait ForwardCompatTestTrait +{ + private function doSetUp(): void + { + } + + private function doTearDown(): void + { + } + + protected function setUp(): void + { + $this->doSetUp(); + } + + protected function tearDown(): void + { + $this->doTearDown(); + } +} diff --git a/.github/psalm/stubs/SetUpTearDownTrait.php b/.github/psalm/stubs/SetUpTearDownTrait.php new file mode 100644 index 0000000000000..20dbe6fe73e0f --- /dev/null +++ b/.github/psalm/stubs/SetUpTearDownTrait.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PhpUnit; + +use PHPUnit\Framework\TestCase; + +trait SetUpTearDownTrait +{ + use Legacy\SetUpTearDownTraitForV8; +} diff --git a/.github/workflows/psalm.yml b/.github/workflows/psalm.yml index f607bf8d5eb05..8631d6c6a2b3d 100644 --- a/.github/workflows/psalm.yml +++ b/.github/workflows/psalm.yml @@ -20,7 +20,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.0' + php-version: '8.1' extensions: "json,memcached,mongodb,redis,xsl,ldap,dom" ini-values: "memory_limit=-1" coverage: none @@ -39,18 +39,17 @@ jobs: ([ -d "$COMPOSER_HOME" ] || mkdir "$COMPOSER_HOME") && cp .github/composer-config.json "$COMPOSER_HOME/config.json" export COMPOSER_ROOT_VERSION=$(grep ' VERSION = ' src/Symfony/Component/HttpKernel/Kernel.php | grep -P -o '[0-9]+\.[0-9]+').x-dev composer remove --dev --no-update --no-interaction symfony/phpunit-bridge - composer require --no-update psalm/phar phpunit/phpunit:^9.5 php-http/discovery psr/event-dispatcher mongodb/mongodb - - echo "::group::composer update" - composer update --no-progress --ansi - git checkout composer.json - echo "::endgroup::" - - ./vendor/bin/psalm.phar --version + composer require --no-progress --ansi psalm/phar phpunit/phpunit:^9.5 php-http/discovery psr/event-dispatcher mongodb/mongodb - name: Generate Psalm baseline run: | + git checkout composer.json git checkout -m ${{ github.base_ref }} + cat .github/psalm/stubs/SetUpTearDownTrait.php > src/Symfony/Bridge/PhpUnit/SetUpTearDownTrait.php + cat .github/psalm/stubs/ForwardCompatTestTrait.php | sed 's/Component/Bundle\\FrameworkBundle/' > src/Symfony/Bundle/FrameworkBundle/Test/ForwardCompatTestTrait.php + cat .github/psalm/stubs/ForwardCompatTestTrait.php | sed 's/Component/Component\\Form/' > src/Symfony/Component/Form/Test/ForwardCompatTestTrait.php + cat .github/psalm/stubs/ForwardCompatTestTrait.php | sed 's/Component/Component\\Validator/' > src/Symfony/Component/Validator/Test/ForwardCompatTestTrait.php + ./vendor/bin/psalm.phar --set-baseline=.github/psalm/psalm.baseline.xml --no-progress git checkout -m FETCH_HEAD diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 49806815338d4..6be35796056d5 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -66,7 +66,7 @@ jobs: ([ -d "$COMPOSER_HOME" ] || mkdir "$COMPOSER_HOME") && cp .github/composer-config.json "$COMPOSER_HOME/config.json" echo COLUMNS=120 >> $GITHUB_ENV - echo PHPUNIT="$(pwd)/phpunit --exclude-group tty,benchmark,intl-data" >> $GITHUB_ENV + echo PHPUNIT="$(pwd)/phpunit --exclude-group tty,benchmark,intl-data,integration" >> $GITHUB_ENV echo COMPOSER_UP='composer update --no-progress --ansi' >> $GITHUB_ENV SYMFONY_VERSIONS=$(git ls-remote -q --heads | cut -f2 | grep -o '/[1-9][0-9]*\.[0-9].*' | sort -V) diff --git a/CHANGELOG-4.4.md b/CHANGELOG-4.4.md index 29cc4c6552b90..273282ab8caf0 100644 --- a/CHANGELOG-4.4.md +++ b/CHANGELOG-4.4.md @@ -7,6 +7,31 @@ in 4.4 minor versions. To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v4.4.0...v4.4.1 +* 4.4.40 (2022-04-02) + + * bug #45910 [Messenger] reset connection on worker shutdown (SanderHagen) + * bug #45909 [Form][TwigBundle] reset Twig form theme resources between requests (xabbuh) + * bug #45906 [HttpClient] on redirections don't send content related request headers (xabbuh) + * bug #45714 [Messenger] Fix cannot select FOR UPDATE from view on Oracle (rjd22) + * bug #45888 [Messenger] Add mysql indexes back and work around deadlocks using soft-delete (nicolas-grekas) + * bug #45891 [HttpClient] Fix exporting objects with readonly properties (nicolas-grekas) + * bug #45875 [ExpressionLanguage] Fix matches when the regexp is not valid (fabpot) + * bug #45870 [Validator] Fix File constraint invalid max size exception message (fancyweb) + * bug #45851 [Console] Fix exit status on uncaught exception with negative code (acoulton) + * bug #45838 [Serializer] Fix denormalizing union types (T-bond) + * bug #45816 [Mailer] Preserve case of headers (nicolas-grekas) + * bug #45814 [HttpClient] Let curl handle Content-Length headers (nicolas-grekas) + * bug #45813 [HttpClient] Move Content-Type after Content-Length (nicolas-grekas) + * bug #45737 [Lock] SemaphoreStore catching exception from sem_get (Triplkrypl) + * bug #45690 [Mailer] Use recipients in sendmail transport (HypeMC) + * bug #45720 [PropertyInfo] strip only leading `\` when unknown docType (EmilMassey) + * bug #44915 [Console] Fix compact table style to avoid outputting a leading space (Seldaek) + * bug #45676 [Process] Don't return executable directories in PhpExecutableFinder (fancyweb) + * bug #45702 [Form] Fix the usage of the Valid constraints in array-based forms (stof) + * bug #45677 [DependencyInjection] fix `ServiceSubscriberTrait` bug where parent has `__call()` (kbond) + * bug #45678 [HttpClient] Fix reading proxy settings from dotenv when curl is used (nicolas-grekas) + * bug #45671 [FrameworkBundle] Ensure container is reset between tests (nicolas-grekas) + * 4.4.39 (2022-03-05) * bug #45631 [HttpFoundation] Fix PHP 8.1 deprecation in `Response::isNotModified` (HypeMC) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index e2781d32f4a30..bd30bf752af21 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -102,6 +102,7 @@ The Symfony Connect username in parenthesis allows to get more information - Henrik Westphal (snc) - Dariusz Górecki (canni) - Fran Moreno (franmomu) + - HypeMC (hypemc) - Jérôme Vasseur (jvasseur) - Mathieu Santostefano (welcomattic) - Dariusz Ruminski @@ -111,19 +112,18 @@ The Symfony Connect username in parenthesis allows to get more information - Daniel Holmes (dholmes) - Sebastiaan Stok (sstok) - Alexandre Daubois (alexandre-daubois) - - HypeMC (hypemc) - Toni Uebernickel (havvg) - Bart van den Burg (burgov) - Jordan Alliot (jalliot) - John Wards (johnwards) - Tomas Norkūnas (norkunas) - Julien Falque (julienfalque) + - Vincent Langlet (deviling) - Baptiste Clavié (talus) - Massimiliano Arione (garak) - Mathias Arlaud (mtarld) - Antoine Hérault (herzult) - Paráda József (paradajozsef) - - Vincent Langlet (deviling) - Arnaud Le Blanc (arnaud-lb) - Przemysław Bogusz (przemyslaw-bogusz) - Maxime STEINHAUSSER @@ -153,6 +153,7 @@ The Symfony Connect username in parenthesis allows to get more information - Teoh Han Hui (teohhanhui) - Colin Frei - Javier Spagnoletti (phansys) + - Gary PEGEOT (gary-p) - Ruud Kamphuis (ruudk) - Joshua Thijssen - Daniel Wehner (dawehner) @@ -161,7 +162,6 @@ The Symfony Connect username in parenthesis allows to get more information - Gordon Franke (gimler) - Saif Eddin Gmati (azjezz) - Richard van Laak (rvanlaak) - - Gary PEGEOT (gary-p) - Jesse Rushlow (geeshoe) - Fabien Pennequin (fabienpennequin) - Olivier Dolbeau (odolbeau) @@ -355,6 +355,7 @@ The Symfony Connect username in parenthesis allows to get more information - Sébastien Lavoie (lavoiesl) - Dariusz - Farhad Safarov (safarov) + - Hugo Alliaume (kocal) - BoShurik - Thomas Lallement (raziel057) - Michael Voříšek @@ -418,7 +419,6 @@ The Symfony Connect username in parenthesis allows to get more information - Christopher Davis (chrisguitarguy) - Dmitriy Mamontov (mamontovdmitriy) - Ben Ramsey (ramsey) - - Hugo Alliaume (kocal) - Laurent Masforné (heisenberg) - Sergey (upyx) - Giorgio Premi @@ -441,6 +441,7 @@ The Symfony Connect username in parenthesis allows to get more information - Iker Ibarguren (ikerib) - Bob van de Vijver (bobvandevijver) - Peter Kruithof (pkruithof) + - Antoine Lamirault - Michael Holm (hollo) - Arjen van der Meijden - Markus Fasselt (digilist) @@ -478,6 +479,7 @@ The Symfony Connect username in parenthesis allows to get more information - Andrey Esaulov (andremaha) - Grégoire Passault (gregwar) - Jerzy Zawadzki (jzawadzki) + - Phil Taylor (prazgod) - Ismael Ambrosi (iambrosi) - Craig Duncan (duncan3dc) - Emmanuel BORGES (eborges78) @@ -585,8 +587,6 @@ The Symfony Connect username in parenthesis allows to get more information - Loïc Chardonnet (gnusat) - Marek Kalnik (marekkalnik) - Vyacheslav Salakhutdinov (megazoll) - - Antoine Lamirault - - Phil Taylor (prazgod) - Hassan Amouhzi - Daniel Gorgan - Tamas Szijarto @@ -618,6 +618,7 @@ The Symfony Connect username in parenthesis allows to get more information - Xavier HAUSHERR - Albert Jessurum (ajessu) - Laszlo Korte + - Jonathan Scheiber (jmsche) - Miha Vrhovnik - Alessandro Desantis - hubert lecorche (hlecorche) @@ -688,6 +689,7 @@ The Symfony Connect username in parenthesis allows to get more information - Boris Vujicic (boris.vujicic) - Chris Sedlmayr (catchamonkey) - Indra Gunawan (indragunawan) + - Jérôme Tanghe (deuchnord) - Mathias STRASSER (roukmoute) - simon chrzanowski (simonch) - Kamil Kokot (pamil) @@ -831,7 +833,6 @@ The Symfony Connect username in parenthesis allows to get more information - Stefan Kruppa - jhonnyL - sasezaki - - Jonathan Scheiber (jmsche) - Kristof Van Cauwenbergh (kristofvc) - Dawid Pakuła (zulusx) - Marco Lipparini (liarco) @@ -1156,6 +1157,7 @@ The Symfony Connect username in parenthesis allows to get more information - Dominik Ritter (dritter) - Sebastian Grodzicki (sgrodzicki) - Florian Caron (shalalalala) + - Eric COURTIAL - Jeroen van den Enden (stoefke) - Aurélien Fontaine - Pascal Helfenstein @@ -1220,7 +1222,6 @@ The Symfony Connect username in parenthesis allows to get more information - Jérémy REYNAUD (babeuloula) - Mathieu Rochette (mathroc) - Victor Garcia - - Jérôme Tanghe (deuchnord) - Marek Víger (freezy) - Andrew Hilobok (hilobok) - Noah Heck (myesain) @@ -1342,6 +1343,7 @@ The Symfony Connect username in parenthesis allows to get more information - Matt Robinson (inanimatt) - Aleksey Podskrebyshev - Calin Mihai Pristavu + - Gabrielle Langer - David Marín Carreño (davefx) - Fabien LUCAS (flucas2) - Ondrej Machulda (ondram) @@ -1763,6 +1765,7 @@ The Symfony Connect username in parenthesis allows to get more information - Vladimir Luchaninov (luchaninov) - spdionis - rchoquet + - rvoisin - gitlost - Taras Girnyk - Dmitry Derepko @@ -2342,6 +2345,7 @@ The Symfony Connect username in parenthesis allows to get more information - Romain Dorgueil - Christopher Parotat - Dennis Haarbrink + - Urban Suppiger - me_shaon - 蝦米 - Grayson Koonce (breerly) @@ -2822,6 +2826,7 @@ The Symfony Connect username in parenthesis allows to get more information - Ikko Ashimine - Erwin Dirks - Brad Jones + - Markus Ramšak - Billie Thompson - lol768 - jamogon @@ -2898,7 +2903,6 @@ The Symfony Connect username in parenthesis allows to get more information - bokonet - Arrilot - ampaze - - Gabrielle Langer - Chris McGehee - Markus Staab - Pierre-Louis LAUNAY @@ -2920,6 +2924,7 @@ The Symfony Connect username in parenthesis allows to get more information - Lebnik - nsbx - Shude + - RTUnreal - Richard Hodgson - Sven Fabricius - Ondřej Führer diff --git a/src/Symfony/Bridge/Twig/Mime/NotificationEmail.php b/src/Symfony/Bridge/Twig/Mime/NotificationEmail.php index e6b28c4db43d2..1a58aa5e5e5bc 100644 --- a/src/Symfony/Bridge/Twig/Mime/NotificationEmail.php +++ b/src/Symfony/Bridge/Twig/Mime/NotificationEmail.php @@ -43,7 +43,7 @@ public function __construct(Headers $headers = null, AbstractPart $body = null) { $missingPackages = []; if (!class_exists(CssInlinerExtension::class)) { - $missingPackages['twig/cssinliner-extra'] = ' CSS Inliner'; + $missingPackages['twig/cssinliner-extra'] = 'CSS Inliner'; } if (!class_exists(InkyExtension::class)) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Application.php b/src/Symfony/Bundle/FrameworkBundle/Console/Application.php index dddde43dda4a1..20fb6e05f73de 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Application.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Application.php @@ -148,7 +148,7 @@ public function all($namespace = null) */ public function getLongVersion() { - return parent::getLongVersion().sprintf(' (env: %s, debug: %s)', $this->kernel->getEnvironment(), $this->kernel->isDebug() ? 'true' : 'false'); + return parent::getLongVersion().sprintf(' (env: %s, debug: %s) #StandWithUkraine https://sf.to/ukraine', $this->kernel->getEnvironment(), $this->kernel->isDebug() ? 'true' : 'false'); } public function add(Command $command) diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/ForwardCompatTestTrait.php b/src/Symfony/Bundle/FrameworkBundle/Test/ForwardCompatTestTrait.php index 7dd933858088d..9a0dad403063c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Test/ForwardCompatTestTrait.php +++ b/src/Symfony/Bundle/FrameworkBundle/Test/ForwardCompatTestTrait.php @@ -15,7 +15,7 @@ // Auto-adapt to PHPUnit 8 that added a `void` return-type to the setUp/tearDown methods -if (method_exists(\ReflectionMethod::class, 'hasReturnType') && (new \ReflectionMethod(TestCase::class, 'tearDown'))->hasReturnType()) { +if ((new \ReflectionMethod(TestCase::class, 'tearDown'))->hasReturnType()) { /** * @internal */ diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php b/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php index a65a17b4c53e2..1ec6548be2738 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php +++ b/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpKernel\KernelInterface; +use Symfony\Contracts\Service\ResetInterface; /** * KernelTestCase is the base class for tests needing a Kernel. @@ -131,8 +132,13 @@ protected static function ensureKernelShutdown() { if (null !== static::$kernel) { static::$kernel->boot(); + $container = static::$kernel->getContainer(); static::$kernel->shutdown(); static::$booted = false; + + if ($container instanceof ResetInterface) { + $container->reset(); + } } static::$container = null; diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php index 234a845edadd6..1c0e2ff327409 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php @@ -18,9 +18,11 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\Form\AbstractRendererEngine; use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\Mailer\Mailer; use Symfony\Component\Translation\Translator; +use Symfony\Contracts\Service\ResetInterface; use Twig\Extension\ExtensionInterface; use Twig\Extension\RuntimeExtensionInterface; use Twig\Loader\LoaderInterface; @@ -40,6 +42,12 @@ public function load(array $configs, ContainerBuilder $container) if (class_exists(\Symfony\Component\Form\Form::class)) { $loader->load('form.xml'); + + if (is_subclass_of(AbstractRendererEngine::class, ResetInterface::class)) { + $container->getDefinition('twig.form.engine')->addTag('kernel.reset', [ + 'method' => 'reset', + ]); + } } if (interface_exists(\Symfony\Component\Templating\EngineInterface::class)) { diff --git a/src/Symfony/Component/Asset/VersionStrategy/JsonManifestVersionStrategy.php b/src/Symfony/Component/Asset/VersionStrategy/JsonManifestVersionStrategy.php index 72688a4926444..998c636e6134a 100644 --- a/src/Symfony/Component/Asset/VersionStrategy/JsonManifestVersionStrategy.php +++ b/src/Symfony/Component/Asset/VersionStrategy/JsonManifestVersionStrategy.php @@ -54,7 +54,7 @@ private function getManifestPath(string $path): ?string { if (null === $this->manifestData) { if (!file_exists($this->manifestPath)) { - throw new \RuntimeException(sprintf('Asset manifest file "%s" does not exist.', $this->manifestPath)); + throw new \RuntimeException(sprintf('Asset manifest file "%s" does not exist. Did you forget to build the assets with npm or yarn?', $this->manifestPath)); } $this->manifestData = json_decode(file_get_contents($this->manifestPath), true); diff --git a/src/Symfony/Component/Cache/DoctrineProvider.php b/src/Symfony/Component/Cache/DoctrineProvider.php index e4255f0177b37..f6ac5745dfd12 100644 --- a/src/Symfony/Component/Cache/DoctrineProvider.php +++ b/src/Symfony/Component/Cache/DoctrineProvider.php @@ -15,6 +15,10 @@ use Psr\Cache\CacheItemPoolInterface; use Symfony\Contracts\Service\ResetInterface; +if (!class_exists(CacheProvider::class)) { + return; +} + /** * @author Nicolas Grekas */ diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index 15d537dacb3a6..1021a900f0972 100644 --- a/src/Symfony/Component/Console/Application.php +++ b/src/Symfony/Component/Console/Application.php @@ -157,7 +157,7 @@ public function run(InputInterface $input = null, OutputInterface $output = null $exitCode = $e->getCode(); if (is_numeric($exitCode)) { $exitCode = (int) $exitCode; - if (0 === $exitCode) { + if ($exitCode <= 0) { $exitCode = 1; } } else { diff --git a/src/Symfony/Component/Console/Helper/Table.php b/src/Symfony/Component/Console/Helper/Table.php index 1d0a22baa3245..868374ab3832c 100644 --- a/src/Symfony/Component/Console/Helper/Table.php +++ b/src/Symfony/Component/Console/Helper/Table.php @@ -804,9 +804,9 @@ private static function initStyles(): array $compact = new TableStyle(); $compact ->setHorizontalBorderChars('') - ->setVerticalBorderChars(' ') + ->setVerticalBorderChars('') ->setDefaultCrossingChar('') - ->setCellRowContentFormat('%s') + ->setCellRowContentFormat('%s ') ; $styleGuide = new TableStyle(); diff --git a/src/Symfony/Component/Console/Tests/ApplicationTest.php b/src/Symfony/Component/Console/Tests/ApplicationTest.php index 6e9953dd27a7b..fae8d2dcf743b 100644 --- a/src/Symfony/Component/Console/Tests/ApplicationTest.php +++ b/src/Symfony/Component/Console/Tests/ApplicationTest.php @@ -1175,6 +1175,25 @@ public function testRunDispatchesExitCodeOneForExceptionCodeZero() $this->assertTrue($passedRightValue, '-> exit code 1 was passed in the console.terminate event'); } + /** + * @testWith [-1] + * [-32000] + */ + public function testRunReturnsExitCodeOneForNegativeExceptionCode($exceptionCode) + { + $exception = new \Exception('', $exceptionCode); + + $application = $this->getMockBuilder(Application::class)->setMethods(['doRun'])->getMock(); + $application->setAutoExit(false); + $application->expects($this->once()) + ->method('doRun') + ->willThrowException($exception); + + $exitCode = $application->run(new ArrayInput([]), new NullOutput()); + + $this->assertSame(1, $exitCode, '->run() returns exit code 1 when exception code is '.$exceptionCode); + } + public function testAddingOptionWithDuplicateShortcut() { $this->expectException(\LogicException::class); diff --git a/src/Symfony/Component/Console/Tests/Helper/TableTest.php b/src/Symfony/Component/Console/Tests/Helper/TableTest.php index eed0b166237fe..d69e817422056 100644 --- a/src/Symfony/Component/Console/Tests/Helper/TableTest.php +++ b/src/Symfony/Component/Console/Tests/Helper/TableTest.php @@ -118,11 +118,11 @@ public function renderProvider() $books, 'compact', <<<'TABLE' - ISBN Title Author - 99921-58-10-7 Divine Comedy Dante Alighieri - 9971-5-0210-0 A Tale of Two Cities Charles Dickens - 960-425-059-0 The Lord of the Rings J. R. R. Tolkien - 80-902734-1-6 And Then There Were None Agatha Christie +ISBN Title Author +99921-58-10-7 Divine Comedy Dante Alighieri +9971-5-0210-0 A Tale of Two Cities Charles Dickens +960-425-059-0 The Lord of the Rings J. R. R. Tolkien +80-902734-1-6 And Then There Were None Agatha Christie TABLE ], diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ValidateEnvPlaceholdersPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ValidateEnvPlaceholdersPassTest.php index dd88c71227c52..3258328993650 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ValidateEnvPlaceholdersPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ValidateEnvPlaceholdersPassTest.php @@ -156,19 +156,6 @@ public function testConcatenatedEnvInConfig() $this->assertSame(['scalar_node' => $expected], $container->resolveEnvPlaceholders($ext->getConfig())); } - public function testEnvIsIncompatibleWithEnumNode() - { - $this->expectException(InvalidConfigurationException::class); - $this->expectExceptionMessage('A dynamic value is not compatible with a "Symfony\Component\Config\Definition\EnumNode" node type at path "env_extension.enum_node".'); - $container = new ContainerBuilder(); - $container->registerExtension(new EnvExtension()); - $container->prependExtensionConfig('env_extension', [ - 'enum_node' => '%env(FOO)%', - ]); - - $this->doProcess($container); - } - public function testEnvIsIncompatibleWithArrayNode() { $this->expectException(InvalidConfigurationException::class); diff --git a/src/Symfony/Component/ErrorHandler/Resources/assets/css/exception.css b/src/Symfony/Component/ErrorHandler/Resources/assets/css/exception.css index 952c66d2fc936..3f629a4104fef 100644 --- a/src/Symfony/Component/ErrorHandler/Resources/assets/css/exception.css +++ b/src/Symfony/Component/ErrorHandler/Resources/assets/css/exception.css @@ -184,7 +184,7 @@ header .container { display: flex; justify-content: space-between; } .trace-line + .trace-line { border-top: var(--border); } .trace-line:hover { background: var(--base-1); } .trace-line a { color: var(--base-6); } -.trace-line .icon { opacity: .4; position: absolute; left: 10px; top: 11px; } +.trace-line .icon { opacity: .4; position: absolute; left: 10px; } .trace-line .icon svg { height: 16px; width: 16px; } .trace-line-header { padding-left: 36px; padding-right: 10px; } diff --git a/src/Symfony/Component/ExpressionLanguage/Node/BinaryNode.php b/src/Symfony/Component/ExpressionLanguage/Node/BinaryNode.php index 3820f880e7f31..2f49bd90c6cae 100644 --- a/src/Symfony/Component/ExpressionLanguage/Node/BinaryNode.php +++ b/src/Symfony/Component/ExpressionLanguage/Node/BinaryNode.php @@ -12,6 +12,7 @@ namespace Symfony\Component\ExpressionLanguage\Node; use Symfony\Component\ExpressionLanguage\Compiler; +use Symfony\Component\ExpressionLanguage\SyntaxError; /** * @author Fabien Potencier @@ -46,8 +47,12 @@ public function compile(Compiler $compiler) $operator = $this->attributes['operator']; if ('matches' == $operator) { + if ($this->nodes['right'] instanceof ConstantNode) { + $this->evaluateMatches($this->nodes['right']->evaluate([], []), ''); + } + $compiler - ->raw('preg_match(') + ->raw('(static function ($regexp, $str) { set_error_handler(function ($t, $m) use ($regexp, $str) { throw new \Symfony\Component\ExpressionLanguage\SyntaxError(sprintf(\'Regexp "%s" passed to "matches" is not valid\', $regexp).substr($m, 12)); }); try { return preg_match($regexp, $str); } finally { restore_error_handler(); } })(') ->compile($this->nodes['right']) ->raw(', ') ->compile($this->nodes['left']) @@ -159,7 +164,7 @@ public function evaluate($functions, $values) return $left % $right; case 'matches': - return preg_match($right, $left); + return $this->evaluateMatches($right, $left); } } @@ -167,4 +172,16 @@ public function toArray() { return ['(', $this->nodes['left'], ' '.$this->attributes['operator'].' ', $this->nodes['right'], ')']; } + + private function evaluateMatches(string $regexp, string $str): int + { + set_error_handler(function ($t, $m) use ($regexp) { + throw new SyntaxError(sprintf('Regexp "%s" passed to "matches" is not valid', $regexp).substr($m, 12)); + }); + try { + return preg_match($regexp, $str); + } finally { + restore_error_handler(); + } + } } diff --git a/src/Symfony/Component/ExpressionLanguage/Tests/Node/BinaryNodeTest.php b/src/Symfony/Component/ExpressionLanguage/Tests/Node/BinaryNodeTest.php index b45a1e57b9b17..fccc04abce4b8 100644 --- a/src/Symfony/Component/ExpressionLanguage/Tests/Node/BinaryNodeTest.php +++ b/src/Symfony/Component/ExpressionLanguage/Tests/Node/BinaryNodeTest.php @@ -11,9 +11,12 @@ namespace Symfony\Component\ExpressionLanguage\Tests\Node; +use Symfony\Component\ExpressionLanguage\Compiler; use Symfony\Component\ExpressionLanguage\Node\ArrayNode; use Symfony\Component\ExpressionLanguage\Node\BinaryNode; use Symfony\Component\ExpressionLanguage\Node\ConstantNode; +use Symfony\Component\ExpressionLanguage\Node\NameNode; +use Symfony\Component\ExpressionLanguage\SyntaxError; class BinaryNodeTest extends AbstractNodeTest { @@ -111,7 +114,7 @@ public function getCompileData() ['range(1, 3)', new BinaryNode('..', new ConstantNode(1), new ConstantNode(3))], - ['preg_match("/^[a-z]+/i\$/", "abc")', new BinaryNode('matches', new ConstantNode('abc'), new ConstantNode('/^[a-z]+/i$/'))], + ['(static function ($regexp, $str) { set_error_handler(function ($t, $m) use ($regexp, $str) { throw new \Symfony\Component\ExpressionLanguage\SyntaxError(sprintf(\'Regexp "%s" passed to "matches" is not valid\', $regexp).substr($m, 12)); }); try { return preg_match($regexp, $str); } finally { restore_error_handler(); } })("/^[a-z]+\$/", "abc")', new BinaryNode('matches', new ConstantNode('abc'), new ConstantNode('/^[a-z]+$/'))], ]; } @@ -160,7 +163,42 @@ public function getDumpData() ['(1 .. 3)', new BinaryNode('..', new ConstantNode(1), new ConstantNode(3))], - ['("abc" matches "/^[a-z]+/i$/")', new BinaryNode('matches', new ConstantNode('abc'), new ConstantNode('/^[a-z]+/i$/'))], + ['("abc" matches "/^[a-z]+$/")', new BinaryNode('matches', new ConstantNode('abc'), new ConstantNode('/^[a-z]+$/'))], ]; } + + public function testEvaluateMatchesWithInvalidRegexp() + { + $node = new BinaryNode('matches', new ConstantNode('abc'), new ConstantNode('this is not a regexp')); + + $this->expectExceptionObject(new SyntaxError('Regexp "this is not a regexp" passed to "matches" is not valid: Delimiter must not be alphanumeric or backslash')); + $node->evaluate([], []); + } + + public function testEvaluateMatchesWithInvalidRegexpAsExpression() + { + $node = new BinaryNode('matches', new ConstantNode('abc'), new NameNode('regexp')); + + $this->expectExceptionObject(new SyntaxError('Regexp "this is not a regexp" passed to "matches" is not valid: Delimiter must not be alphanumeric or backslash')); + $node->evaluate([], ['regexp' => 'this is not a regexp']); + } + + public function testCompileMatchesWithInvalidRegexp() + { + $node = new BinaryNode('matches', new ConstantNode('abc'), new ConstantNode('this is not a regexp')); + + $this->expectExceptionObject(new SyntaxError('Regexp "this is not a regexp" passed to "matches" is not valid: Delimiter must not be alphanumeric or backslash')); + $compiler = new Compiler([]); + $node->compile($compiler); + } + + public function testCompileMatchesWithInvalidRegexpAsExpression() + { + $node = new BinaryNode('matches', new ConstantNode('abc'), new NameNode('regexp')); + + $this->expectExceptionObject(new SyntaxError('Regexp "this is not a regexp" passed to "matches" is not valid: Delimiter must not be alphanumeric or backslash')); + $compiler = new Compiler([]); + $node->compile($compiler); + eval('$regexp = "this is not a regexp"; '.$compiler->getSource().';'); + } } diff --git a/src/Symfony/Component/Form/AbstractRendererEngine.php b/src/Symfony/Component/Form/AbstractRendererEngine.php index 116892d415777..7c4822bf14767 100644 --- a/src/Symfony/Component/Form/AbstractRendererEngine.php +++ b/src/Symfony/Component/Form/AbstractRendererEngine.php @@ -11,12 +11,14 @@ namespace Symfony\Component\Form; +use Symfony\Contracts\Service\ResetInterface; + /** * Default implementation of {@link FormRendererEngineInterface}. * * @author Bernhard Schussek */ -abstract class AbstractRendererEngine implements FormRendererEngineInterface +abstract class AbstractRendererEngine implements FormRendererEngineInterface, ResetInterface { /** * The variable in {@link FormView} used as cache key. @@ -197,4 +199,12 @@ private function loadResourceForBlockNameHierarchy(string $cacheKey, FormView $v return false; } + + public function reset(): void + { + $this->themes = []; + $this->useDefaultThemes = []; + $this->resources = []; + $this->resourceHierarchyLevels = []; + } } diff --git a/src/Symfony/Component/Form/Extension/Validator/Constraints/FormValidator.php b/src/Symfony/Component/Form/Extension/Validator/Constraints/FormValidator.php index b0deef7f04e50..fc50af4d01d94 100644 --- a/src/Symfony/Component/Form/Extension/Validator/Constraints/FormValidator.php +++ b/src/Symfony/Component/Form/Extension/Validator/Constraints/FormValidator.php @@ -110,7 +110,7 @@ public function validate($form, Constraint $formConstraint) foreach ($constraints as $constraint) { // For the "Valid" constraint, validate the data in all groups if ($constraint instanceof Valid) { - if (\is_object($data)) { + if (\is_object($data) || \is_array($data)) { $validator->atPath('data')->validate($data, $constraint, $groups); } diff --git a/src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorFunctionalTest.php b/src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorFunctionalTest.php index e73947ada3081..35712707b1aca 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorFunctionalTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorFunctionalTest.php @@ -15,6 +15,7 @@ use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\CallbackTransformer; use Symfony\Component\Form\Exception\TransformationFailedException; +use Symfony\Component\Form\Extension\Core\Type\CollectionType; use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\Extension\Core\Type\IntegerType; @@ -328,6 +329,35 @@ public function testCascadeValidationToChildFormsWithTwoValidConstraints2() $this->assertSame('children[author].data.email', $violations[1]->getPropertyPath()); } + public function testCascadeValidationToArrayChildForm() + { + $form = $this->formFactory->create(FormType::class, null, [ + 'data_class' => Review::class, + ]) + ->add('title') + ->add('customers', CollectionType::class, [ + 'mapped' => false, + 'entry_type' => CustomerType::class, + 'allow_add' => true, + 'constraints' => [new Valid()], + ]); + + $form->submit([ + 'title' => 'Sample Title', + 'customers' => [ + ['email' => null], + ], + ]); + + $violations = $this->validator->validate($form); + + $this->assertCount(2, $violations); + $this->assertSame('This value should not be blank.', $violations[0]->getMessage()); + $this->assertSame('data.rating', $violations[0]->getPropertyPath()); + $this->assertSame('This value should not be blank.', $violations[1]->getMessage()); + $this->assertSame('children[customers].data[0].email', $violations[1]->getPropertyPath()); + } + public function testCascadeValidationToChildFormsUsingPropertyPathsValidatedInSequence() { $form = $this->formFactory->create(FormType::class, null, [ diff --git a/src/Symfony/Component/HttpClient/CurlHttpClient.php b/src/Symfony/Component/HttpClient/CurlHttpClient.php index e77eb86c4c452..3b63addec8865 100644 --- a/src/Symfony/Component/HttpClient/CurlHttpClient.php +++ b/src/Symfony/Component/HttpClient/CurlHttpClient.php @@ -92,6 +92,10 @@ public function request(string $method, string $url, array $options = []): Respo $scheme = $url['scheme']; $authority = $url['authority']; $host = parse_url($authority, \PHP_URL_HOST); + $proxy = $options['proxy'] + ?? ('https:' === $url['scheme'] ? $_SERVER['https_proxy'] ?? $_SERVER['HTTPS_PROXY'] ?? null : null) + // Ignore HTTP_PROXY except on the CLI to work around httpoxy set of vulnerabilities + ?? $_SERVER['http_proxy'] ?? (\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) ? $_SERVER['HTTP_PROXY'] ?? null : null) ?? $_SERVER['all_proxy'] ?? $_SERVER['ALL_PROXY'] ?? null; $url = implode('', $url); if (!isset($options['normalized_headers']['user-agent'])) { @@ -107,7 +111,7 @@ public function request(string $method, string $url, array $options = []): Respo \CURLOPT_MAXREDIRS => 0 < $options['max_redirects'] ? $options['max_redirects'] : 0, \CURLOPT_COOKIEFILE => '', // Keep track of cookies during redirects \CURLOPT_TIMEOUT => 0, - \CURLOPT_PROXY => $options['proxy'], + \CURLOPT_PROXY => $proxy, \CURLOPT_NOPROXY => $options['no_proxy'] ?? $_SERVER['no_proxy'] ?? $_SERVER['NO_PROXY'] ?? '', \CURLOPT_SSL_VERIFYPEER => $options['verify_peer'], \CURLOPT_SSL_VERIFYHOST => $options['verify_host'] ? 2 : 0, @@ -198,7 +202,14 @@ public function request(string $method, string $url, array $options = []): Respo $options['headers'][] = 'Accept-Encoding: gzip'; // Expose only one encoding, some servers mess up when more are provided } - foreach ($options['headers'] as $header) { + $hasContentLength = isset($options['normalized_headers']['content-length'][0]); + + foreach ($options['headers'] as $i => $header) { + if ($hasContentLength && 0 === stripos($header, 'Content-Length:')) { + // Let curl handle Content-Length headers + unset($options['headers'][$i]); + continue; + } if (':' === $header[-2] && \strlen($header) - 2 === strpos($header, ': ')) { // curl requires a special syntax to send empty headers $curlopts[\CURLOPT_HTTPHEADER][] = substr_replace($header, ';', -2); @@ -225,7 +236,7 @@ public function request(string $method, string $url, array $options = []): Respo }; } - if (isset($options['normalized_headers']['content-length'][0])) { + if ($hasContentLength) { $curlopts[\CURLOPT_INFILESIZE] = substr($options['normalized_headers']['content-length'][0], \strlen('Content-Length: ')); } elseif (!isset($options['normalized_headers']['transfer-encoding'])) { $curlopts[\CURLOPT_HTTPHEADER][] = 'Transfer-Encoding: chunked'; // Enable chunked request bodies @@ -233,8 +244,12 @@ public function request(string $method, string $url, array $options = []): Respo if ('POST' !== $method) { $curlopts[\CURLOPT_UPLOAD] = true; + + if (!isset($options['normalized_headers']['content-type'])) { + $curlopts[\CURLOPT_HTTPHEADER][] = 'Content-Type: application/x-www-form-urlencoded'; + } } - } elseif ('' !== $body || 'POST' === $method) { + } elseif ('' !== $body || 'POST' === $method || $hasContentLength) { $curlopts[\CURLOPT_POSTFIELDS] = $body; } @@ -404,8 +419,15 @@ private static function createRedirectResolver(array $options, string $host): \C } $url = self::parseUrl(curl_getinfo($ch, \CURLINFO_EFFECTIVE_URL)); + $url = self::resolveUrl($location, $url); + + curl_setopt($ch, \CURLOPT_PROXY, $options['proxy'] + ?? ('https:' === $url['scheme'] ? $_SERVER['https_proxy'] ?? $_SERVER['HTTPS_PROXY'] ?? null : null) + // Ignore HTTP_PROXY except on the CLI to work around httpoxy set of vulnerabilities + ?? $_SERVER['http_proxy'] ?? (\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) ? $_SERVER['HTTP_PROXY'] ?? null : null) ?? $_SERVER['all_proxy'] ?? $_SERVER['ALL_PROXY'] ?? null + ); - return implode('', self::resolveUrl($location, $url)); + return implode('', $url); }; } } diff --git a/src/Symfony/Component/HttpClient/HttpClientTrait.php b/src/Symfony/Component/HttpClient/HttpClientTrait.php index 57d48aade8db2..e616ca1f9c01b 100644 --- a/src/Symfony/Component/HttpClient/HttpClientTrait.php +++ b/src/Symfony/Component/HttpClient/HttpClientTrait.php @@ -12,6 +12,7 @@ namespace Symfony\Component\HttpClient; use Symfony\Component\HttpClient\Exception\InvalidArgumentException; +use Symfony\Component\HttpClient\Exception\TransportException; /** * Provides the common logic from writing HttpClientInterface implementations. @@ -76,12 +77,12 @@ private static function prepareRequest(?string $method, ?string $url, array $opt unset($options['json']); if (!isset($options['normalized_headers']['content-type'])) { - $options['normalized_headers']['content-type'] = [$options['headers'][] = 'Content-Type: application/json']; + $options['normalized_headers']['content-type'] = ['Content-Type: application/json']; } } if (!isset($options['normalized_headers']['accept'])) { - $options['normalized_headers']['accept'] = [$options['headers'][] = 'Accept: */*']; + $options['normalized_headers']['accept'] = ['Accept: */*']; } if (isset($options['body'])) { @@ -89,10 +90,14 @@ private static function prepareRequest(?string $method, ?string $url, array $opt if (\is_string($options['body']) && (string) \strlen($options['body']) !== substr($h = $options['normalized_headers']['content-length'][0] ?? '', 16) - && ('' !== $h || ('' !== $options['body'] && !isset($options['normalized_headers']['transfer-encoding']))) + && ('' !== $h || '' !== $options['body']) ) { + if (isset($options['normalized_headers']['transfer-encoding'])) { + unset($options['normalized_headers']['transfer-encoding']); + $options['body'] = self::dechunk($options['body']); + } + $options['normalized_headers']['content-length'] = [substr_replace($h ?: 'Content-Length: ', \strlen($options['body']), 16)]; - $options['headers'] = array_merge(...array_values($options['normalized_headers'])); } } @@ -134,11 +139,11 @@ private static function prepareRequest(?string $method, ?string $url, array $opt if (null !== $url) { // Merge auth with headers if (($options['auth_basic'] ?? false) && !($options['normalized_headers']['authorization'] ?? false)) { - $options['normalized_headers']['authorization'] = [$options['headers'][] = 'Authorization: Basic '.base64_encode($options['auth_basic'])]; + $options['normalized_headers']['authorization'] = ['Authorization: Basic '.base64_encode($options['auth_basic'])]; } // Merge bearer with headers if (($options['auth_bearer'] ?? false) && !($options['normalized_headers']['authorization'] ?? false)) { - $options['normalized_headers']['authorization'] = [$options['headers'][] = 'Authorization: Bearer '.$options['auth_bearer']]; + $options['normalized_headers']['authorization'] = ['Authorization: Bearer '.$options['auth_bearer']]; } unset($options['auth_basic'], $options['auth_bearer']); @@ -160,6 +165,7 @@ private static function prepareRequest(?string $method, ?string $url, array $opt } $options['max_duration'] = isset($options['max_duration']) ? (float) $options['max_duration'] : 0; + $options['headers'] = array_merge(...array_values($options['normalized_headers'])); return [$url, $options]; } @@ -329,6 +335,22 @@ private static function normalizeBody($body) return $body; } + private static function dechunk(string $body): string + { + $h = fopen('php://temp', 'w+'); + stream_filter_append($h, 'dechunk', \STREAM_FILTER_WRITE); + fwrite($h, $body); + $body = stream_get_contents($h, -1, 0); + rewind($h); + ftruncate($h, 0); + + if (fwrite($h, '-') && '' !== stream_get_contents($h, -1, 0)) { + throw new TransportException('Request body has broken chunked encoding.'); + } + + return $body; + } + /** * @param string|string[] $fingerprint * diff --git a/src/Symfony/Component/HttpClient/NativeHttpClient.php b/src/Symfony/Component/HttpClient/NativeHttpClient.php index 4394e51400cec..f52d93d5eb7e0 100644 --- a/src/Symfony/Component/HttpClient/NativeHttpClient.php +++ b/src/Symfony/Component/HttpClient/NativeHttpClient.php @@ -80,9 +80,20 @@ public function request(string $method, string $url, array $options = []): Respo } } + $hasContentLength = isset($options['normalized_headers']['content-length']); + $hasBody = '' !== $options['body'] || 'POST' === $method || $hasContentLength; + $options['body'] = self::getBodyAsString($options['body']); - if ('' !== $options['body'] && 'POST' === $method && !isset($options['normalized_headers']['content-type'])) { + if (isset($options['normalized_headers']['transfer-encoding'])) { + unset($options['normalized_headers']['transfer-encoding']); + $options['headers'] = array_merge(...array_values($options['normalized_headers'])); + $options['body'] = self::dechunk($options['body']); + } + if ('' === $options['body'] && $hasBody && !$hasContentLength) { + $options['headers'][] = 'Content-Length: 0'; + } + if ($hasBody && !isset($options['normalized_headers']['content-type'])) { $options['headers'][] = 'Content-Type: application/x-www-form-urlencoded'; } @@ -419,9 +430,12 @@ private static function createRedirectResolver(array $options, string $host, ?ar if ('POST' === $options['method'] || 303 === $info['http_code']) { $info['http_method'] = $options['method'] = 'HEAD' === $options['method'] ? 'HEAD' : 'GET'; $options['content'] = ''; - $options['header'] = array_filter($options['header'], static function ($h) { + $filterContentHeaders = static function ($h) { return 0 !== stripos($h, 'Content-Length:') && 0 !== stripos($h, 'Content-Type:'); - }); + }; + $options['header'] = array_filter($options['header'], $filterContentHeaders); + $redirectHeaders['no_auth'] = array_filter($redirectHeaders['no_auth'], $filterContentHeaders); + $redirectHeaders['with_auth'] = array_filter($redirectHeaders['with_auth'], $filterContentHeaders); stream_context_set_option($context, ['http' => $options]); } diff --git a/src/Symfony/Component/HttpClient/Response/CurlResponse.php b/src/Symfony/Component/HttpClient/Response/CurlResponse.php index ee0d3f655a6fb..e065c4aa17f0e 100644 --- a/src/Symfony/Component/HttpClient/Response/CurlResponse.php +++ b/src/Symfony/Component/HttpClient/Response/CurlResponse.php @@ -363,6 +363,7 @@ private static function parseHeaderLine($ch, string $data, array &$info, array & } elseif (303 === $info['http_code'] || ('POST' === $info['http_method'] && \in_array($info['http_code'], [301, 302], true))) { $info['http_method'] = 'HEAD' === $info['http_method'] ? 'HEAD' : 'GET'; curl_setopt($ch, \CURLOPT_POSTFIELDS, ''); + curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, $info['http_method']); } } diff --git a/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php b/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php index 189788cce8ca9..d36e7f70b72ca 100644 --- a/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php +++ b/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php @@ -194,6 +194,18 @@ public function testFixContentLength() $this->assertSame(['abc' => 'def', 'REQUEST_METHOD' => 'POST'], $body); } + public function testDropContentRelatedHeadersWhenFollowingRequestIsUsingGet() + { + $client = $this->getHttpClient(__FUNCTION__); + + $response = $client->request('POST', 'http://localhost:8057/302', [ + 'body' => 'foo', + 'headers' => ['Content-Length: 3'], + ]); + + $this->assertSame(200, $response->getStatusCode()); + } + public function testNegativeTimeout() { $client = $this->getHttpClient(__FUNCTION__); @@ -203,11 +215,35 @@ public function testNegativeTimeout() ])->getStatusCode()); } + public function testRedirectAfterPost() + { + $client = $this->getHttpClient(__FUNCTION__); + + $response = $client->request('POST', 'http://localhost:8057/302/relative', [ + 'body' => '', + ]); + + $this->assertSame(200, $response->getStatusCode()); + $this->assertStringContainsString("\r\nContent-Length: 0", $response->getInfo('debug')); + } + + public function testEmptyPut() + { + $client = $this->getHttpClient(__FUNCTION__); + + $response = $client->request('PUT', 'http://localhost:8057/post', [ + 'headers' => ['Content-Length' => '0'], + ]); + + $this->assertSame(200, $response->getStatusCode()); + $this->assertStringContainsString("\r\nContent-Length: ", $response->getInfo('debug')); + } + public function testNullBody() { - $httpClient = $this->getHttpClient(__FUNCTION__); + $client = $this->getHttpClient(__FUNCTION__); - $httpClient->request('POST', 'http://localhost:8057/post', [ + $client->request('POST', 'http://localhost:8057/post', [ 'body' => null, ]); diff --git a/src/Symfony/Component/HttpClient/Tests/MockHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/MockHttpClientTest.php index e324dc107be3f..4e8dbf26d7e29 100644 --- a/src/Symfony/Component/HttpClient/Tests/MockHttpClientTest.php +++ b/src/Symfony/Component/HttpClient/Tests/MockHttpClientTest.php @@ -96,12 +96,12 @@ public function testFixContentLength() $this->assertSame(['Content-Length: 7'], $requestOptions['normalized_headers']['content-length']); $response = $client->request('POST', 'http://localhost:8057/post', [ - 'body' => 'abc=def', + 'body' => "8\r\nSymfony \r\n5\r\nis aw\r\n6\r\nesome!\r\n0\r\n\r\n", 'headers' => ['Transfer-Encoding: chunked'], ]); $requestOptions = $response->getRequestOptions(); - $this->assertFalse(isset($requestOptions['normalized_headers']['content-length'])); + $this->assertSame(['Content-Length: 19'], $requestOptions['normalized_headers']['content-length']); $response = $client->request('POST', 'http://localhost:8057/post', [ 'body' => '', diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index e4903bfff60aa..91093eaef7369 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -76,11 +76,11 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl private static $freshCache = []; - public const VERSION = '4.4.39'; - public const VERSION_ID = 40439; + public const VERSION = '4.4.40'; + public const VERSION_ID = 40440; public const MAJOR_VERSION = 4; public const MINOR_VERSION = 4; - public const RELEASE_VERSION = 39; + public const RELEASE_VERSION = 40; public const EXTRA_VERSION = ''; public const END_OF_MAINTENANCE = '11/2022'; diff --git a/src/Symfony/Component/Lock/Store/SemaphoreStore.php b/src/Symfony/Component/Lock/Store/SemaphoreStore.php index 6fd48a4c2807e..b85a63c6b4b38 100644 --- a/src/Symfony/Component/Lock/Store/SemaphoreStore.php +++ b/src/Symfony/Component/Lock/Store/SemaphoreStore.php @@ -64,12 +64,12 @@ private function lock(Key $key, bool $blocking) } $keyId = unpack('i', md5($key, true))[1]; - $resource = sem_get($keyId); - $acquired = @sem_acquire($resource, !$blocking); + $resource = @sem_get($keyId); + $acquired = $resource && @sem_acquire($resource, !$blocking); while ($blocking && !$acquired) { - $resource = sem_get($keyId); - $acquired = @sem_acquire($resource); + $resource = @sem_get($keyId); + $acquired = $resource && @sem_acquire($resource); } if (!$acquired) { diff --git a/src/Symfony/Component/Lock/Tests/Store/CombinedStoreTest.php b/src/Symfony/Component/Lock/Tests/Store/CombinedStoreTest.php index 991208f8b204e..ebb8199382d77 100644 --- a/src/Symfony/Component/Lock/Tests/Store/CombinedStoreTest.php +++ b/src/Symfony/Component/Lock/Tests/Store/CombinedStoreTest.php @@ -23,6 +23,7 @@ /** * @author Jérémy Derussé + * @group integration */ class CombinedStoreTest extends AbstractStoreTest { @@ -41,7 +42,8 @@ protected function getClockDelay() */ public function getStore(): PersistingStoreInterface { - $redis = new \Predis\Client(array_combine(['host', 'port'], explode(':', getenv('REDIS_HOST')) + [1 => null])); + $redis = new \Predis\Client(array_combine(['host', 'port'], explode(':', getenv('REDIS_HOST')) + [1 => 6379])); + try { $redis->connect(); } catch (\Exception $e) { diff --git a/src/Symfony/Component/Lock/Tests/Store/PdoStoreTest.php b/src/Symfony/Component/Lock/Tests/Store/PdoStoreTest.php index 082f50aa75e63..0562800f28cc2 100644 --- a/src/Symfony/Component/Lock/Tests/Store/PdoStoreTest.php +++ b/src/Symfony/Component/Lock/Tests/Store/PdoStoreTest.php @@ -20,6 +20,7 @@ * @author Jérémy Derussé * * @requires extension pdo_sqlite + * @group integration */ class PdoStoreTest extends AbstractStoreTest { diff --git a/src/Symfony/Component/Lock/Tests/Store/ZookeeperStoreTest.php b/src/Symfony/Component/Lock/Tests/Store/ZookeeperStoreTest.php index b3cd685a23160..2e63b08eb5ad3 100644 --- a/src/Symfony/Component/Lock/Tests/Store/ZookeeperStoreTest.php +++ b/src/Symfony/Component/Lock/Tests/Store/ZookeeperStoreTest.php @@ -20,6 +20,7 @@ * @author Ganesh Chandrasekaran * * @requires extension zookeeper + * @group integration */ class ZookeeperStoreTest extends AbstractStoreTest { diff --git a/src/Symfony/Component/Mailer/Bridge/Mailchimp/Transport/MandrillApiTransport.php b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Transport/MandrillApiTransport.php index 068da4d7eb7a0..469354d67bbe7 100644 --- a/src/Symfony/Component/Mailer/Bridge/Mailchimp/Transport/MandrillApiTransport.php +++ b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Transport/MandrillApiTransport.php @@ -121,7 +121,7 @@ private function getPayload(Email $email, Envelope $envelope): array continue; } - $payload['message']['headers'][$name] = $header->getBodyAsString(); + $payload['message']['headers'][$header->getName()] = $header->getBodyAsString(); } return $payload; diff --git a/src/Symfony/Component/Mailer/Bridge/Mailgun/Tests/Transport/MailgunApiTransportTest.php b/src/Symfony/Component/Mailer/Bridge/Mailgun/Tests/Transport/MailgunApiTransportTest.php index 2e4c53b83fc98..5f5b8f32362a1 100644 --- a/src/Symfony/Component/Mailer/Bridge/Mailgun/Tests/Transport/MailgunApiTransportTest.php +++ b/src/Symfony/Component/Mailer/Bridge/Mailgun/Tests/Transport/MailgunApiTransportTest.php @@ -65,8 +65,8 @@ public function testCustomHeader() $method->setAccessible(true); $payload = $method->invoke($transport, $email, $envelope); - $this->assertArrayHasKey('h:x-mailgun-variables', $payload); - $this->assertEquals($json, $payload['h:x-mailgun-variables']); + $this->assertArrayHasKey('h:X-Mailgun-Variables', $payload); + $this->assertEquals($json, $payload['h:X-Mailgun-Variables']); } public function testSend() diff --git a/src/Symfony/Component/Mailer/Bridge/Mailgun/Transport/MailgunApiTransport.php b/src/Symfony/Component/Mailer/Bridge/Mailgun/Transport/MailgunApiTransport.php index 329b6eeaef449..4607a07037d16 100644 --- a/src/Symfony/Component/Mailer/Bridge/Mailgun/Transport/MailgunApiTransport.php +++ b/src/Symfony/Component/Mailer/Bridge/Mailgun/Transport/MailgunApiTransport.php @@ -120,7 +120,7 @@ private function getPayload(Email $email, Envelope $envelope): array continue; } - $payload['h:'.$name] = $header->getBodyAsString(); + $payload['h:'.$header->getName()] = $header->getBodyAsString(); } return $payload; diff --git a/src/Symfony/Component/Mailer/Bridge/Postmark/Transport/PostmarkApiTransport.php b/src/Symfony/Component/Mailer/Bridge/Postmark/Transport/PostmarkApiTransport.php index 4ccf8762b7a85..e1b4cd3dda8d4 100644 --- a/src/Symfony/Component/Mailer/Bridge/Postmark/Transport/PostmarkApiTransport.php +++ b/src/Symfony/Component/Mailer/Bridge/Postmark/Transport/PostmarkApiTransport.php @@ -93,7 +93,7 @@ private function getPayload(Email $email, Envelope $envelope): array } $payload['Headers'][] = [ - 'Name' => $name, + 'Name' => $header->getName(), 'Value' => $header->getBodyAsString(), ]; } diff --git a/src/Symfony/Component/Mailer/Bridge/Sendgrid/Transport/SendgridApiTransport.php b/src/Symfony/Component/Mailer/Bridge/Sendgrid/Transport/SendgridApiTransport.php index 07108f19aab3c..d198be82d7b35 100644 --- a/src/Symfony/Component/Mailer/Bridge/Sendgrid/Transport/SendgridApiTransport.php +++ b/src/Symfony/Component/Mailer/Bridge/Sendgrid/Transport/SendgridApiTransport.php @@ -121,7 +121,7 @@ private function getPayload(Email $email, Envelope $envelope): array continue; } - $payload['headers'][$name] = $header->getBodyAsString(); + $payload['headers'][$header->getName()] = $header->getBodyAsString(); } return $payload; diff --git a/src/Symfony/Component/Mailer/Tests/Transport/Fixtures/fake-sendmail.php b/src/Symfony/Component/Mailer/Tests/Transport/Fixtures/fake-sendmail.php new file mode 100755 index 0000000000000..5a4bafd20f1d1 --- /dev/null +++ b/src/Symfony/Component/Mailer/Tests/Transport/Fixtures/fake-sendmail.php @@ -0,0 +1,5 @@ +#!/usr/bin/env php +argsPath = sys_get_temp_dir().\DIRECTORY_SEPARATOR.'sendmail_args'; + } + + protected function tearDown(): void + { + if (file_exists($this->argsPath)) { + @unlink($this->argsPath); + } + unset($this->argsPath); + } + public function testToString() { $t = new SendmailTransport(); $this->assertEquals('smtp://sendmail', (string) $t); } + + public function testToIsUsedWhenRecipientsAreNotSet() + { + if ('\\' === \DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Windows does not support shebangs nor non-blocking standard streams'); + } + + $mail = new Email(); + $mail + ->from('from@mail.com') + ->to('to@mail.com') + ->subject('Subject') + ->text('Some text') + ; + + $envelope = new DelayedEnvelope($mail); + + $sendmailTransport = new SendmailTransport(self::FAKE_SENDMAIL); + $sendmailTransport->send($mail, $envelope); + + $this->assertStringEqualsFile($this->argsPath, __DIR__.'/Fixtures/fake-sendmail.php -ffrom@mail.com to@mail.com'); + } + + public function testRecipientsAreUsedWhenSet() + { + if ('\\' === \DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Windows does not support shebangs nor non-blocking standard streams'); + } + + $mail = new Email(); + $mail + ->from('from@mail.com') + ->to('to@mail.com') + ->subject('Subject') + ->text('Some text') + ; + + $envelope = new DelayedEnvelope($mail); + $envelope->setRecipients([new Address('recipient@mail.com')]); + + $sendmailTransport = new SendmailTransport(self::FAKE_SENDMAIL); + $sendmailTransport->send($mail, $envelope); + + $this->assertStringEqualsFile($this->argsPath, __DIR__.'/Fixtures/fake-sendmail.php -ffrom@mail.com recipient@mail.com'); + } } diff --git a/src/Symfony/Component/Mailer/Transport/SendmailTransport.php b/src/Symfony/Component/Mailer/Transport/SendmailTransport.php index c53d72f6974d6..df3dd6b43cfe0 100644 --- a/src/Symfony/Component/Mailer/Transport/SendmailTransport.php +++ b/src/Symfony/Component/Mailer/Transport/SendmailTransport.php @@ -86,6 +86,11 @@ protected function doSend(SentMessage $message): void $this->getLogger()->debug(sprintf('Email transport "%s" starting', __CLASS__)); $command = $this->command; + + if ($recipients = $message->getEnvelope()->getRecipients()) { + $command = str_replace(' -t', '', $command); + } + if (!str_contains($command, ' -f')) { $command .= ' -f'.escapeshellarg($message->getEnvelope()->getSender()->getEncodedAddress()); } @@ -96,6 +101,10 @@ protected function doSend(SentMessage $message): void $chunks = AbstractStream::replace("\n.", "\n..", $chunks); } + foreach ($recipients as $recipient) { + $command .= ' '.escapeshellarg($recipient->getEncodedAddress()); + } + $this->stream->setCommand($command); $this->stream->initialize(); foreach ($chunks as $chunk) { diff --git a/src/Symfony/Component/Messenger/Tests/Transport/Doctrine/ConnectionTest.php b/src/Symfony/Component/Messenger/Tests/Transport/Doctrine/ConnectionTest.php index 5bc7dddcd2934..9d9ad3c4d2c4c 100644 --- a/src/Symfony/Component/Messenger/Tests/Transport/Doctrine/ConnectionTest.php +++ b/src/Symfony/Component/Messenger/Tests/Transport/Doctrine/ConnectionTest.php @@ -12,7 +12,6 @@ namespace Symfony\Component\Messenger\Tests\Transport\Doctrine; use Doctrine\DBAL\Abstraction\Result as AbstractionResult; -use Doctrine\DBAL\Configuration; use Doctrine\DBAL\Connection as DBALConnection; use Doctrine\DBAL\DBALException; use Doctrine\DBAL\Driver\Result as DriverResult; @@ -24,11 +23,8 @@ use Doctrine\DBAL\Query\QueryBuilder; use Doctrine\DBAL\Result; use Doctrine\DBAL\Schema\AbstractSchemaManager; -use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\SchemaConfig; -use Doctrine\DBAL\Schema\TableDiff; use Doctrine\DBAL\Statement; -use Doctrine\DBAL\Types\Types; use PHPUnit\Framework\TestCase; use Symfony\Component\Messenger\Exception\InvalidArgumentException; use Symfony\Component\Messenger\Exception\TransportException; @@ -410,58 +406,4 @@ public function providePlatformSql(): iterable 'SELECT m.* FROM messenger_messages m WITH (UPDLOCK, ROWLOCK) WHERE (m.delivered_at is null OR m.delivered_at < ?) AND (m.available_at <= ?) AND (m.queue_name = ?) ORDER BY available_at ASC OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY ', ]; } - - /** - * @dataProvider setupIndicesProvider - */ - public function testSetupIndices(string $platformClass, array $expectedIndices) - { - $driverConnection = $this->createMock(DBALConnection::class); - $driverConnection->method('getConfiguration')->willReturn(new Configuration()); - - $schemaManager = $this->createMock(AbstractSchemaManager::class); - $schema = new Schema(); - $expectedTable = $schema->createTable('messenger_messages'); - $expectedTable->addColumn('id', Types::BIGINT); - $expectedTable->setPrimaryKey(['id']); - // Make sure columns for indices exists so addIndex() will not throw - foreach (array_unique(array_merge(...$expectedIndices)) as $columnName) { - $expectedTable->addColumn($columnName, Types::STRING); - } - foreach ($expectedIndices as $indexColumns) { - $expectedTable->addIndex($indexColumns); - } - $schemaManager->method('createSchema')->willReturn($schema); - if (method_exists(DBALConnection::class, 'createSchemaManager')) { - $driverConnection->method('createSchemaManager')->willReturn($schemaManager); - } else { - $driverConnection->method('getSchemaManager')->willReturn($schemaManager); - } - - $platformMock = $this->createMock($platformClass); - $platformMock - ->expects(self::once()) - ->method('getAlterTableSQL') - ->with(self::callback(static function (TableDiff $tableDiff): bool { - return 0 === \count($tableDiff->addedIndexes) && 0 === \count($tableDiff->changedIndexes) && 0 === \count($tableDiff->removedIndexes); - })) - ->willReturn([]); - $driverConnection->method('getDatabasePlatform')->willReturn($platformMock); - - $connection = new Connection([], $driverConnection); - $connection->setup(); - } - - public function setupIndicesProvider(): iterable - { - yield 'MySQL' => [ - MySQL57Platform::class, - [['delivered_at']], - ]; - - yield 'Other platforms' => [ - AbstractPlatform::class, - [['queue_name'], ['available_at'], ['delivered_at']], - ]; - } } diff --git a/src/Symfony/Component/Messenger/Tests/WorkerTest.php b/src/Symfony/Component/Messenger/Tests/WorkerTest.php index b5406b59ea3f6..24c281c364d39 100644 --- a/src/Symfony/Component/Messenger/Tests/WorkerTest.php +++ b/src/Symfony/Component/Messenger/Tests/WorkerTest.php @@ -29,6 +29,7 @@ use Symfony\Component\Messenger\Transport\Receiver\ReceiverInterface; use Symfony\Component\Messenger\Worker; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; +use Symfony\Contracts\Service\ResetInterface; /** * @group time-sensitive @@ -85,6 +86,19 @@ public function testHandlingErrorCausesReject() $this->assertSame(0, $receiver->getAcknowledgeCount()); } + public function testWorkerResetsConnectionIfReceiverIsResettable() + { + $resettableReceiver = new ResettableDummyReceiver([]); + + $bus = $this->createMock(MessageBusInterface::class); + $dispatcher = new EventDispatcher(); + + $worker = new Worker([$resettableReceiver], $bus, $dispatcher); + $worker->stop(); + $worker->run(); + $this->assertTrue($resettableReceiver->hasBeenReset()); + } + public function testWorkerDoesNotSendNullMessagesToTheBus() { $receiver = new DummyReceiver([ @@ -283,3 +297,18 @@ public function getRejectCount(): int return $this->rejectCount; } } + +class ResettableDummyReceiver extends DummyReceiver implements ResetInterface +{ + private $hasBeenReset = false; + + public function reset() + { + $this->hasBeenReset = true; + } + + public function hasBeenReset(): bool + { + return $this->hasBeenReset; + } +} diff --git a/src/Symfony/Component/Messenger/Transport/Doctrine/Connection.php b/src/Symfony/Component/Messenger/Transport/Doctrine/Connection.php index 48d00c4be5ede..379132c9c98da 100644 --- a/src/Symfony/Component/Messenger/Transport/Doctrine/Connection.php +++ b/src/Symfony/Component/Messenger/Transport/Doctrine/Connection.php @@ -13,11 +13,13 @@ use Doctrine\DBAL\Connection as DBALConnection; use Doctrine\DBAL\DBALException; +use Doctrine\DBAL\Driver\Exception as DriverException; use Doctrine\DBAL\Driver\Result as DriverResult; use Doctrine\DBAL\Exception; use Doctrine\DBAL\Exception\TableNotFoundException; use Doctrine\DBAL\LockMode; -use Doctrine\DBAL\Platforms\MySqlPlatform; +use Doctrine\DBAL\Platforms\MySQLPlatform; +use Doctrine\DBAL\Platforms\OraclePlatform; use Doctrine\DBAL\Query\QueryBuilder; use Doctrine\DBAL\Result; use Doctrine\DBAL\Schema\AbstractSchemaManager; @@ -157,6 +159,14 @@ public function send(string $body, array $headers, int $delay = 0): string public function get(): ?array { + if ($this->driverConnection->getDatabasePlatform() instanceof MySQLPlatform) { + try { + $this->driverConnection->delete($this->configuration['table_name'], ['delivered_at' => '9999-12-31']); + } catch (DriverException $e) { + // Ignore the exception + } + } + get: $this->driverConnection->beginTransaction(); try { @@ -178,6 +188,18 @@ public function get(): ?array ); } + // Wrap the rownum query in a sub-query to allow writelocks without ORA-02014 error + if ($this->driverConnection->getDatabasePlatform() instanceof OraclePlatform) { + $sql = str_replace('SELECT a.* FROM', 'SELECT a.id FROM', $sql); + + $wrappedQuery = $this->driverConnection->createQueryBuilder() + ->select('w.*') + ->from($this->configuration['table_name'], 'w') + ->where('w.id IN('.$sql.')'); + + $sql = $wrappedQuery->getSQL(); + } + // use SELECT ... FOR UPDATE to lock table $stmt = $this->executeQuery( $sql.' '.$this->driverConnection->getDatabasePlatform()->getWriteLockSQL(), @@ -224,6 +246,10 @@ public function get(): ?array public function ack(string $id): bool { try { + if ($this->driverConnection->getDatabasePlatform() instanceof MySQLPlatform) { + return $this->driverConnection->update($this->configuration['table_name'], ['delivered_at' => '9999-12-31'], ['id' => $id]) > 0; + } + return $this->driverConnection->delete($this->configuration['table_name'], ['id' => $id]) > 0; } catch (DBALException|Exception $exception) { throw new TransportException($exception->getMessage(), 0, $exception); @@ -233,6 +259,10 @@ public function ack(string $id): bool public function reject(string $id): bool { try { + if ($this->driverConnection->getDatabasePlatform() instanceof MySQLPlatform) { + return $this->driverConnection->update($this->configuration['table_name'], ['delivered_at' => '9999-12-31'], ['id' => $id]) > 0; + } + return $this->driverConnection->delete($this->configuration['table_name'], ['id' => $id]) > 0; } catch (DBALException|Exception $exception) { throw new TransportException($exception->getMessage(), 0, $exception); @@ -388,6 +418,7 @@ private function getSchema(): Schema $table->addColumn('headers', self::$useDeprecatedConstants ? Type::TEXT : Types::TEXT) ->setNotnull(true); $table->addColumn('queue_name', self::$useDeprecatedConstants ? Type::STRING : Types::STRING) + ->setLength(190) // MySQL 5.6 only supports 191 characters on an indexed column in utf8mb4 mode ->setNotnull(true); $table->addColumn('created_at', self::$useDeprecatedConstants ? Type::DATETIME : Types::DATETIME_MUTABLE) ->setNotnull(true); @@ -396,11 +427,8 @@ private function getSchema(): Schema $table->addColumn('delivered_at', self::$useDeprecatedConstants ? Type::DATETIME : Types::DATETIME_MUTABLE) ->setNotnull(false); $table->setPrimaryKey(['id']); - // No indices on queue_name and available_at on MySQL to prevent deadlock issues when running multiple consumers. - if (!$this->driverConnection->getDatabasePlatform() instanceof MySqlPlatform) { - $table->addIndex(['queue_name']); - $table->addIndex(['available_at']); - } + $table->addIndex(['queue_name']); + $table->addIndex(['available_at']); $table->addIndex(['delivered_at']); return $schema; diff --git a/src/Symfony/Component/Messenger/Worker.php b/src/Symfony/Component/Messenger/Worker.php index 1a41009fefc6a..98156c887bda9 100644 --- a/src/Symfony/Component/Messenger/Worker.php +++ b/src/Symfony/Component/Messenger/Worker.php @@ -25,6 +25,7 @@ use Symfony\Component\Messenger\Stamp\ReceivedStamp; use Symfony\Component\Messenger\Transport\Receiver\ReceiverInterface; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; +use Symfony\Contracts\Service\ResetInterface; /** * @author Samuel Roze @@ -102,6 +103,7 @@ public function run(array $options = []): void } $this->dispatchEvent(new WorkerStoppedEvent($this)); + $this->resetReceiverConnections(); } private function handleMessage(Envelope $envelope, ReceiverInterface $receiver, string $transportName): void @@ -155,6 +157,15 @@ public function stop(): void $this->shouldStop = true; } + private function resetReceiverConnections(): void + { + foreach ($this->receivers as $transportName => $receiver) { + if ($receiver instanceof ResetInterface) { + $receiver->reset(); + } + } + } + private function dispatchEvent($event) { if (null === $this->eventDispatcher) { diff --git a/src/Symfony/Component/Process/PhpExecutableFinder.php b/src/Symfony/Component/Process/PhpExecutableFinder.php index 3d5eabd997cbe..92e0262ad7c95 100644 --- a/src/Symfony/Component/Process/PhpExecutableFinder.php +++ b/src/Symfony/Component/Process/PhpExecutableFinder.php @@ -47,6 +47,10 @@ public function find($includeArgs = true) } } + if (@is_dir($php)) { + return false; + } + return $php; } @@ -59,7 +63,7 @@ public function find($includeArgs = true) } if ($php = getenv('PHP_PATH')) { - if (!@is_executable($php)) { + if (!@is_executable($php) || @is_dir($php)) { return false; } @@ -67,12 +71,12 @@ public function find($includeArgs = true) } if ($php = getenv('PHP_PEAR_PHP_BIN')) { - if (@is_executable($php)) { + if (@is_executable($php) && !@is_dir($php)) { return $php; } } - if (@is_executable($php = \PHP_BINDIR.('\\' === \DIRECTORY_SEPARATOR ? '\\php.exe' : '/php'))) { + if (@is_executable($php = \PHP_BINDIR.('\\' === \DIRECTORY_SEPARATOR ? '\\php.exe' : '/php')) && !@is_dir($php)) { return $php; } diff --git a/src/Symfony/Component/Process/Tests/PhpExecutableFinderTest.php b/src/Symfony/Component/Process/Tests/PhpExecutableFinderTest.php index cf3ffb55efb78..23de6d42eb5fb 100644 --- a/src/Symfony/Component/Process/Tests/PhpExecutableFinderTest.php +++ b/src/Symfony/Component/Process/Tests/PhpExecutableFinderTest.php @@ -50,12 +50,36 @@ public function testFindArguments() public function testNotExitsBinaryFile() { $f = new PhpExecutableFinder(); - $phpBinaryEnv = \PHP_BINARY; - putenv('PHP_BINARY=/usr/local/php/bin/php-invalid'); - $this->assertFalse($f->find(), '::find() returns false because of not exist file'); - $this->assertFalse($f->find(false), '::find(false) returns false because of not exist file'); + $originalPhpBinary = getenv('PHP_BINARY'); - putenv('PHP_BINARY='.$phpBinaryEnv); + try { + putenv('PHP_BINARY=/usr/local/php/bin/php-invalid'); + + $this->assertFalse($f->find(), '::find() returns false because of not exist file'); + $this->assertFalse($f->find(false), '::find(false) returns false because of not exist file'); + } finally { + putenv('PHP_BINARY='.$originalPhpBinary); + } + } + + public function testFindWithExecutableDirectory() + { + if ('\\' === \DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Directories are not executable on Windows'); + } + + $originalPhpBinary = getenv('PHP_BINARY'); + + try { + $executableDirectoryPath = sys_get_temp_dir().'/PhpExecutableFinderTest_testFindWithExecutableDirectory'; + @mkdir($executableDirectoryPath); + $this->assertTrue(is_executable($executableDirectoryPath)); + putenv('PHP_BINARY='.$executableDirectoryPath); + + $this->assertFalse((new PhpExecutableFinder())->find()); + } finally { + putenv('PHP_BINARY='.$originalPhpBinary); + } } } diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index f73e148a89b74..607c8fad571ac 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -384,7 +384,7 @@ private function readIndex(array $zval, $index): array } /** - * Reads the a property from an object. + * Reads the value of a property from an object. * * @throws NoSuchPropertyException If $ignoreInvalidProperty is false and the property does not exist or is not public */ diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php index 4a141642d0009..5737fb02cd5ef 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php @@ -18,6 +18,7 @@ use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; use Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy; +use Symfony\Component\PropertyInfo\Tests\Fixtures\PseudoTypeDummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\TraitUsage\DummyUsedInTrait; use Symfony\Component\PropertyInfo\Tests\Fixtures\TraitUsage\DummyUsingTrait; use Symfony\Component\PropertyInfo\Type; @@ -350,6 +351,11 @@ public function propertiesParentTypeProvider(): array ]; } + public function testUnknownPseudoType() + { + $this->assertEquals([new Type(Type::BUILTIN_TYPE_OBJECT, false, 'scalar')], $this->extractor->getTypes(PseudoTypeDummy::class, 'unknownPseudoType')); + } + protected function isPhpDocumentorV5() { if (class_exists(InvalidTag::class)) { diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/PseudoTypeDummy.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/PseudoTypeDummy.php new file mode 100644 index 0000000000000..71756044fafc9 --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/PseudoTypeDummy.php @@ -0,0 +1,11 @@ + 1; foreach ($types as $type) { if (null === $data && $type->isNullable()) { return null; @@ -455,29 +456,40 @@ private function validateAndDenormalize(string $currentClass, string $attribute, $expectedTypes[Type::BUILTIN_TYPE_OBJECT === $builtinType && $class ? $class : $builtinType] = true; - if (Type::BUILTIN_TYPE_OBJECT === $builtinType) { - if (!$this->serializer instanceof DenormalizerInterface) { - throw new LogicException(sprintf('Cannot denormalize attribute "%s" for class "%s" because injected serializer is not a denormalizer.', $attribute, $class)); + // This try-catch should cover all NotNormalizableValueException (and all return branches after the first + // exception) so we could try denormalizing all types of an union type. If the target type is not an union + // type, we will just re-throw the catched exception. + // In the case of no denormalization succeeds with an union type, it will fall back to the default exception + // with the acceptable types list. + try { + if (Type::BUILTIN_TYPE_OBJECT === $builtinType) { + if (!$this->serializer instanceof DenormalizerInterface) { + throw new LogicException(sprintf('Cannot denormalize attribute "%s" for class "%s" because injected serializer is not a denormalizer.', $attribute, $class)); + } + + $childContext = $this->createChildContext($context, $attribute, $format); + if ($this->serializer->supportsDenormalization($data, $class, $format, $childContext)) { + return $this->serializer->denormalize($data, $class, $format, $childContext); + } } - $childContext = $this->createChildContext($context, $attribute, $format); - if ($this->serializer->supportsDenormalization($data, $class, $format, $childContext)) { - return $this->serializer->denormalize($data, $class, $format, $childContext); + // JSON only has a Number type corresponding to both int and float PHP types. + // PHP's json_encode, JavaScript's JSON.stringify, Go's json.Marshal as well as most other JSON encoders convert + // floating-point numbers like 12.0 to 12 (the decimal part is dropped when possible). + // PHP's json_decode automatically converts Numbers without a decimal part to integers. + // To circumvent this behavior, integers are converted to floats when denormalizing JSON based formats and when + // a float is expected. + if (Type::BUILTIN_TYPE_FLOAT === $builtinType && \is_int($data) && null !== $format && str_contains($format, JsonEncoder::FORMAT)) { + return (float) $data; } - } - - // JSON only has a Number type corresponding to both int and float PHP types. - // PHP's json_encode, JavaScript's JSON.stringify, Go's json.Marshal as well as most other JSON encoders convert - // floating-point numbers like 12.0 to 12 (the decimal part is dropped when possible). - // PHP's json_decode automatically converts Numbers without a decimal part to integers. - // To circumvent this behavior, integers are converted to floats when denormalizing JSON based formats and when - // a float is expected. - if (Type::BUILTIN_TYPE_FLOAT === $builtinType && \is_int($data) && null !== $format && str_contains($format, JsonEncoder::FORMAT)) { - return (float) $data; - } - if (('is_'.$builtinType)($data)) { - return $data; + if (('is_'.$builtinType)($data)) { + return $data; + } + } catch (NotNormalizableValueException $e) { + if (!$isUnionType) { + throw $e; + } } } @@ -639,7 +651,7 @@ private function getCacheKey(?string $format, array $context) 'ignored' => $this->ignoredAttributes, 'camelized' => $this->camelizedAttributes, ])); - } catch (\Exception $exception) { + } catch (\Exception $e) { // The context cannot be serialized, skip the cache return false; } diff --git a/src/Symfony/Component/Serializer/Tests/SerializerTest.php b/src/Symfony/Component/Serializer/Tests/SerializerTest.php index e5a8acbf03543..a95811a46f8d9 100644 --- a/src/Symfony/Component/Serializer/Tests/SerializerTest.php +++ b/src/Symfony/Component/Serializer/Tests/SerializerTest.php @@ -15,6 +15,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; +use Symfony\Component\PropertyInfo\PropertyInfoExtractor; use Symfony\Component\Serializer\Encoder\DecoderInterface; use Symfony\Component\Serializer\Encoder\EncoderInterface; use Symfony\Component\Serializer\Encoder\JsonEncoder; @@ -33,6 +34,7 @@ use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; use Symfony\Component\Serializer\Normalizer\CustomNormalizer; +use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer; @@ -538,6 +540,38 @@ public function testNormalizePreserveEmptyArrayObject() $this->assertEquals('{"foo":{},"bar":["notempty"],"baz":{"nested":{}}}', $serializer->serialize($object, 'json', [AbstractObjectNormalizer::PRESERVE_EMPTY_OBJECTS => true])); } + public function testUnionTypeDeserializable() + { + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $extractor = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]); + $serializer = new Serializer( + [ + new DateTimeNormalizer(), + new ObjectNormalizer($classMetadataFactory, null, null, $extractor, new ClassDiscriminatorFromClassMetadata($classMetadataFactory)), + ], + ['json' => new JsonEncoder()] + ); + + $actual = $serializer->deserialize('{ "changed": null }', DummyUnionType::class, 'json', [ + DateTimeNormalizer::FORMAT_KEY => \DateTime::ISO8601, + ]); + + $this->assertEquals((new DummyUnionType())->setChanged(null), $actual, 'Union type denormalization first case failed.'); + + $actual = $serializer->deserialize('{ "changed": "2022-03-22T16:15:05+0000" }', DummyUnionType::class, 'json', [ + DateTimeNormalizer::FORMAT_KEY => \DateTime::ISO8601, + ]); + + $expectedDateTime = \DateTime::createFromFormat(\DateTime::ISO8601, '2022-03-22T16:15:05+0000'); + $this->assertEquals((new DummyUnionType())->setChanged($expectedDateTime), $actual, 'Union type denormalization second case failed.'); + + $actual = $serializer->deserialize('{ "changed": false }', DummyUnionType::class, 'json', [ + DateTimeNormalizer::FORMAT_KEY => \DateTime::ISO8601, + ]); + + $this->assertEquals(new DummyUnionType(), $actual, 'Union type denormalization third case failed.'); + } + private function serializerWithClassDiscriminator() { $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); @@ -610,6 +644,26 @@ public function __construct($value) } } +class DummyUnionType +{ + /** + * @var \DateTime|bool|null + */ + public $changed = false; + + /** + * @param \DateTime|bool|null + * + * @return $this + */ + public function setChanged($changed): self + { + $this->changed = $changed; + + return $this; + } +} + interface NormalizerAwareNormalizer extends NormalizerInterface, NormalizerAwareInterface { } diff --git a/src/Symfony/Component/Validator/Constraints/File.php b/src/Symfony/Component/Validator/Constraints/File.php index 29844a4c95476..377be1f2ec979 100644 --- a/src/Symfony/Component/Validator/Constraints/File.php +++ b/src/Symfony/Component/Validator/Constraints/File.php @@ -117,7 +117,7 @@ private function normalizeBinaryFormat($maxSize) $this->maxSize = $matches[1] * $factors[$unit = strtolower($matches[2])]; $this->binaryFormat = $this->binaryFormat ?? (2 === \strlen($unit)); } else { - throw new ConstraintDefinitionException(sprintf('"%s" is not a valid maximum size.', $this->maxSize)); + throw new ConstraintDefinitionException(sprintf('"%s" is not a valid maximum size.', $maxSize)); } } } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/FileValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/FileValidatorTest.php index 72bdc950343b9..5e98ef64313a4 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/FileValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/FileValidatorTest.php @@ -479,5 +479,14 @@ public function uploadedFileErrorProvider() return $tests; } + public function testNegativeMaxSize() + { + $this->expectException(ConstraintDefinitionException::class); + $this->expectExceptionMessage('"-1" is not a valid maximum size.'); + + $file = new File(); + $file->maxSize = -1; + } + abstract protected function getFile($filename); } diff --git a/src/Symfony/Component/VarExporter/Internal/Exporter.php b/src/Symfony/Component/VarExporter/Internal/Exporter.php index c46eb50aa988d..7141b4e6d96c1 100644 --- a/src/Symfony/Component/VarExporter/Internal/Exporter.php +++ b/src/Symfony/Component/VarExporter/Internal/Exporter.php @@ -140,7 +140,7 @@ public static function prepare($values, $objectsPool, &$refsPool, &$objectsCount $i = 0; $n = (string) $name; if ('' === $n || "\0" !== $n[0]) { - $c = 'stdClass'; + $c = \PHP_VERSION_ID >= 80100 && $reflector->hasProperty($n) && ($p = $reflector->getProperty($n))->isReadOnly() ? $p->class : 'stdClass'; } elseif ('*' === $n[1]) { $n = substr($n, 3); $c = $reflector->getProperty($n)->class; diff --git a/src/Symfony/Component/VarExporter/Tests/Fixtures/FooReadonly.php b/src/Symfony/Component/VarExporter/Tests/Fixtures/FooReadonly.php new file mode 100644 index 0000000000000..8e41de95958bc --- /dev/null +++ b/src/Symfony/Component/VarExporter/Tests/Fixtures/FooReadonly.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarExporter\Tests\Fixtures; + +class FooReadonly +{ + public function __construct( + public readonly string $name, + public readonly string $value, + ) { + } +} diff --git a/src/Symfony/Component/VarExporter/Tests/Fixtures/datetime-legacy.php b/src/Symfony/Component/VarExporter/Tests/Fixtures/datetime-legacy.php index 7b217c5fb21b0..64c39f75faa8b 100644 --- a/src/Symfony/Component/VarExporter/Tests/Fixtures/datetime-legacy.php +++ b/src/Symfony/Component/VarExporter/Tests/Fixtures/datetime-legacy.php @@ -7,7 +7,7 @@ clone ($p['DateTimeZone'] ?? \Symfony\Component\VarExporter\Internal\Registry::p('DateTimeZone')), clone ($p['DateInterval'] ?? \Symfony\Component\VarExporter\Internal\Registry::p('DateInterval')), ], [ - 4 => 'O:10:"DatePeriod":6:{s:5:"start";O:8:"DateTime":3:{s:4:"date";s:26:"2012-07-01 00:00:00.000000";s:13:"timezone_type";i:1;s:8:"timezone";s:6:"+00:00";}s:7:"current";N;s:3:"end";N;s:8:"interval";O:12:"DateInterval":16:{s:1:"y";i:0;s:1:"m";i:0;s:1:"d";i:7;s:1:"h";i:0;s:1:"i";i:0;s:1:"s";i:0;s:1:"f";d:0;s:7:"weekday";i:0;s:16:"weekday_behavior";i:0;s:17:"first_last_day_of";i:0;s:6:"invert";i:0;s:4:"days";b:0;s:12:"special_type";i:0;s:14:"special_amount";i:0;s:21:"have_weekday_relative";i:0;s:21:"have_special_relative";i:0;}s:11:"recurrences";i:5;s:18:"include_start_date";b:1;}', + 4 => 'O:10:"DatePeriod":6:{s:5:"start";O:8:"DateTime":3:{s:4:"date";s:26:"2009-10-11 00:00:00.000000";s:13:"timezone_type";i:3;s:8:"timezone";s:12:"Europe/Paris";}s:7:"current";N;s:3:"end";N;s:8:"interval";O:12:"DateInterval":16:{s:1:"y";i:0;s:1:"m";i:0;s:1:"d";i:7;s:1:"h";i:0;s:1:"i";i:0;s:1:"s";i:0;s:1:"f";d:0;s:7:"weekday";i:0;s:16:"weekday_behavior";i:0;s:17:"first_last_day_of";i:0;s:6:"invert";i:0;s:4:"days";i:7;s:12:"special_type";i:0;s:14:"special_amount";i:0;s:21:"have_weekday_relative";i:0;s:21:"have_special_relative";i:0;}s:11:"recurrences";i:5;s:18:"include_start_date";b:1;}', ]), null, [ @@ -60,7 +60,7 @@ 3 => 0, ], 'days' => [ - 3 => false, + 3 => 7, ], 'special_type' => [ 3 => 0, diff --git a/src/Symfony/Component/VarExporter/Tests/Fixtures/datetime.php b/src/Symfony/Component/VarExporter/Tests/Fixtures/datetime.php index 1de8fa03f0919..e9f41f9ade34c 100644 --- a/src/Symfony/Component/VarExporter/Tests/Fixtures/datetime.php +++ b/src/Symfony/Component/VarExporter/Tests/Fixtures/datetime.php @@ -5,8 +5,8 @@ 'O:8:"DateTime":3:{s:4:"date";s:26:"1970-01-01 00:00:00.000000";s:13:"timezone_type";i:1;s:8:"timezone";s:6:"+00:00";}', 'O:17:"DateTimeImmutable":3:{s:4:"date";s:26:"1970-01-01 00:00:00.000000";s:13:"timezone_type";i:1;s:8:"timezone";s:6:"+00:00";}', 'O:12:"DateTimeZone":2:{s:13:"timezone_type";i:3;s:8:"timezone";s:12:"Europe/Paris";}', - 'O:12:"DateInterval":16:{s:1:"y";i:0;s:1:"m";i:0;s:1:"d";i:7;s:1:"h";i:0;s:1:"i";i:0;s:1:"s";i:0;s:1:"f";d:0;s:7:"weekday";i:0;s:16:"weekday_behavior";i:0;s:17:"first_last_day_of";i:0;s:6:"invert";i:0;s:4:"days";b:0;s:12:"special_type";i:0;s:14:"special_amount";i:0;s:21:"have_weekday_relative";i:0;s:21:"have_special_relative";i:0;}', - 'O:10:"DatePeriod":6:{s:5:"start";O:8:"DateTime":3:{s:4:"date";s:26:"2012-07-01 00:00:00.000000";s:13:"timezone_type";i:1;s:8:"timezone";s:6:"+00:00";}s:7:"current";N;s:3:"end";N;s:8:"interval";O:12:"DateInterval":16:{s:1:"y";i:0;s:1:"m";i:0;s:1:"d";i:7;s:1:"h";i:0;s:1:"i";i:0;s:1:"s";i:0;s:1:"f";d:0;s:7:"weekday";i:0;s:16:"weekday_behavior";i:0;s:17:"first_last_day_of";i:0;s:6:"invert";i:0;s:4:"days";b:0;s:12:"special_type";i:0;s:14:"special_amount";i:0;s:21:"have_weekday_relative";i:0;s:21:"have_special_relative";i:0;}s:11:"recurrences";i:5;s:18:"include_start_date";b:1;}', + 'O:12:"DateInterval":16:{s:1:"y";i:0;s:1:"m";i:0;s:1:"d";i:7;s:1:"h";i:0;s:1:"i";i:0;s:1:"s";i:0;s:1:"f";d:0;s:7:"weekday";i:0;s:16:"weekday_behavior";i:0;s:17:"first_last_day_of";i:0;s:6:"invert";i:0;s:4:"days";i:7;s:12:"special_type";i:0;s:14:"special_amount";i:0;s:21:"have_weekday_relative";i:0;s:21:"have_special_relative";i:0;}', + 'O:10:"DatePeriod":6:{s:5:"start";O:8:"DateTime":3:{s:4:"date";s:26:"2009-10-11 00:00:00.000000";s:13:"timezone_type";i:3;s:8:"timezone";s:12:"Europe/Paris";}s:7:"current";N;s:3:"end";N;s:8:"interval";O:12:"DateInterval":16:{s:1:"y";i:0;s:1:"m";i:0;s:1:"d";i:7;s:1:"h";i:0;s:1:"i";i:0;s:1:"s";i:0;s:1:"f";d:0;s:7:"weekday";i:0;s:16:"weekday_behavior";i:0;s:17:"first_last_day_of";i:0;s:6:"invert";i:0;s:4:"days";i:7;s:12:"special_type";i:0;s:14:"special_amount";i:0;s:21:"have_weekday_relative";i:0;s:21:"have_special_relative";i:0;}s:11:"recurrences";i:5;s:18:"include_start_date";b:1;}', ]), null, [], diff --git a/src/Symfony/Component/VarExporter/Tests/Fixtures/readonly.php b/src/Symfony/Component/VarExporter/Tests/Fixtures/readonly.php new file mode 100644 index 0000000000000..3b3db27305859 --- /dev/null +++ b/src/Symfony/Component/VarExporter/Tests/Fixtures/readonly.php @@ -0,0 +1,20 @@ + [ + 'name' => [ + 'k', + ], + 'value' => [ + 'v', + ], + ], + ], + $o[0], + [] +); diff --git a/src/Symfony/Component/VarExporter/Tests/VarExporterTest.php b/src/Symfony/Component/VarExporter/Tests/VarExporterTest.php index f87e4e9b01d1e..f90737da2e8cf 100644 --- a/src/Symfony/Component/VarExporter/Tests/VarExporterTest.php +++ b/src/Symfony/Component/VarExporter/Tests/VarExporterTest.php @@ -16,6 +16,7 @@ use Symfony\Component\VarExporter\Exception\ClassNotFoundException; use Symfony\Component\VarExporter\Exception\NotInstantiableTypeException; use Symfony\Component\VarExporter\Internal\Registry; +use Symfony\Component\VarExporter\Tests\Fixtures\FooReadonly; use Symfony\Component\VarExporter\Tests\Fixtures\FooSerializable; use Symfony\Component\VarExporter\Tests\Fixtures\FooUnitEnum; use Symfony\Component\VarExporter\Tests\Fixtures\MySerializable; @@ -132,9 +133,9 @@ public function provideExport() yield ['datetime', [ \DateTime::createFromFormat('U', 0), \DateTimeImmutable::createFromFormat('U', 0), - new \DateTimeZone('Europe/Paris'), - new \DateInterval('P7D'), - new \DatePeriod('R4/2012-07-01T00:00:00Z/P7D'), + $tz = new \DateTimeZone('Europe/Paris'), + $interval = ($start = new \DateTime('2009-10-11', $tz))->diff(new \DateTime('2009-10-18', $tz)), + new \DatePeriod($start, $interval, 4), ]]; $value = \PHP_VERSION_ID >= 70406 ? new ArrayObject() : new \ArrayObject(); @@ -244,9 +245,12 @@ public function provideExport() yield ['php74-serializable', new Php74Serializable()]; - if (\PHP_VERSION_ID >= 80100) { - yield ['unit-enum', [FooUnitEnum::Bar], true]; + if (\PHP_VERSION_ID < 80100) { + return; } + + yield ['unit-enum', [FooUnitEnum::Bar], true]; + yield ['readonly', new FooReadonly('k', 'v')]; } public function testUnicodeDirectionality() diff --git a/src/Symfony/Contracts/HttpClient/Test/HttpClientTestCase.php b/src/Symfony/Contracts/HttpClient/Test/HttpClientTestCase.php index 7ebf055d75701..ddc5324492c86 100644 --- a/src/Symfony/Contracts/HttpClient/Test/HttpClientTestCase.php +++ b/src/Symfony/Contracts/HttpClient/Test/HttpClientTestCase.php @@ -929,6 +929,16 @@ public function testProxy() $body = $response->toArray(); $this->assertSame('Basic Zm9vOmI9YXI=', $body['HTTP_PROXY_AUTHORIZATION']); + + $_SERVER['http_proxy'] = 'http://localhost:8057'; + try { + $response = $client->request('GET', 'http://localhost:8057/'); + $body = $response->toArray(); + $this->assertSame('localhost:8057', $body['HTTP_HOST']); + $this->assertMatchesRegularExpression('#^http://(localhost|127\.0\.0\.1):8057/$#', $body['REQUEST_URI']); + } finally { + unset($_SERVER['http_proxy']); + } } public function testNoProxy() diff --git a/src/Symfony/Contracts/Service/ServiceSubscriberTrait.php b/src/Symfony/Contracts/Service/ServiceSubscriberTrait.php index 0c085303748d9..020887b3f5736 100644 --- a/src/Symfony/Contracts/Service/ServiceSubscriberTrait.php +++ b/src/Symfony/Contracts/Service/ServiceSubscriberTrait.php @@ -35,7 +35,7 @@ public static function getSubscribedServices(): array return $services; } - $services = \is_callable(['parent', __FUNCTION__]) ? parent::getSubscribedServices() : []; + $services = method_exists(get_parent_class(self::class) ?: '', __FUNCTION__) ? parent::getSubscribedServices() : []; foreach ((new \ReflectionClass(self::class))->getMethods() as $method) { if ($method->isStatic() || $method->isAbstract() || $method->isGenerator() || $method->isInternal() || $method->getNumberOfRequiredParameters()) { @@ -69,7 +69,7 @@ public function setContainer(ContainerInterface $container) { $this->container = $container; - if (\is_callable(['parent', __FUNCTION__])) { + if (method_exists(get_parent_class(self::class) ?: '', __FUNCTION__)) { return parent::setContainer($container); } diff --git a/src/Symfony/Contracts/Tests/Service/ServiceSubscriberTraitTest.php b/src/Symfony/Contracts/Tests/Service/ServiceSubscriberTraitTest.php index 17fd108dbd08c..1d95824baf1e1 100644 --- a/src/Symfony/Contracts/Tests/Service/ServiceSubscriberTraitTest.php +++ b/src/Symfony/Contracts/Tests/Service/ServiceSubscriberTraitTest.php @@ -36,6 +36,32 @@ public function testSetContainerIsCalledOnParent() $this->assertSame($container, (new TestService())->setContainer($container)); } + public function testParentNotCalledIfHasMagicCall() + { + $container = new class([]) implements ContainerInterface { + use ServiceLocatorTrait; + }; + $service = new class() extends ParentWithMagicCall { + use ServiceSubscriberTrait; + }; + + $this->assertNull($service->setContainer($container)); + $this->assertSame([], $service::getSubscribedServices()); + } + + public function testParentNotCalledIfNoParent() + { + $container = new class([]) implements ContainerInterface { + use ServiceLocatorTrait; + }; + $service = new class() { + use ServiceSubscriberTrait; + }; + + $this->assertNull($service->setContainer($container)); + $this->assertSame([], $service::getSubscribedServices()); + } + /** * @requires PHP 8 */ @@ -77,3 +103,16 @@ public function aChildService(): Service3 { } } + +class ParentWithMagicCall +{ + public function __call($method, $args) + { + throw new \BadMethodCallException('Should not be called.'); + } + + public static function __callStatic($method, $args) + { + throw new \BadMethodCallException('Should not be called.'); + } +}