diff --git a/phpunit b/phpunit index 2b4412dc4d..e1b1aea0e4 100755 --- a/phpunit +++ b/phpunit @@ -12,10 +12,10 @@ if (!getenv('SYMFONY_PHPUNIT_VERSION')) { if (false === getenv('SYMFONY_PHPUNIT_REMOVE_RETURN_TYPEHINT') && false !== strpos(@file_get_contents(__DIR__.'/src/Symfony/Component/HttpKernel/Kernel.php'), 'const MAJOR_VERSION = 3;')) { putenv('SYMFONY_PHPUNIT_REMOVE_RETURN_TYPEHINT=1'); } - if (\PHP_VERSION_ID >= 80000) { - putenv('SYMFONY_PHPUNIT_VERSION=9.3'); + if (\PHP_VERSION_ID < 70300) { + putenv('SYMFONY_PHPUNIT_VERSION=8.5'); } else { - putenv('SYMFONY_PHPUNIT_VERSION=8.3'); + putenv('SYMFONY_PHPUNIT_VERSION=9.3'); } } elseif (\PHP_VERSION_ID >= 70000) { putenv('SYMFONY_PHPUNIT_VERSION=6.5'); diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/CoverageListenerTrait.php b/src/Symfony/Bridge/PhpUnit/Legacy/CoverageListenerTrait.php index bd8a452777..c1b276cea2 100644 --- a/src/Symfony/Bridge/PhpUnit/Legacy/CoverageListenerTrait.php +++ b/src/Symfony/Bridge/PhpUnit/Legacy/CoverageListenerTrait.php @@ -109,7 +109,7 @@ class CoverageListenerTrait // Exclude internal classes; PHPUnit 9.1+ is picky about tests covering, say, a \RuntimeException $covers = array_filter($covers, function ($class) { - $reflector = new ReflectionClass($class); + $reflector = new \ReflectionClass($class); return $reflector->isUserDefined(); }); diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index eab898b882..0070353a71 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -1197,7 +1197,7 @@ class Configuration implements ConfigurationInterface return $middleware; } if (1 < \count($middleware)) { - throw new \InvalidArgumentException(sprintf('Invalid middleware at path "framework.messenger": a map with a single factory id as key and its arguments as value was expected, %s given.', json_encode($middleware))); + throw new \InvalidArgumentException('Invalid middleware at path "framework.messenger": a map with a single factory id as key and its arguments as value was expected, '.json_encode($middleware).' given.'); } return [ diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd index e2bc23f936..815efd8870 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd @@ -331,7 +331,7 @@ - + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflows.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflows.php index 7c7f7ed0b4..d0abe507e5 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflows.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflows.php @@ -10,6 +10,10 @@ $container->loadFromExtension('framework', [ FrameworkExtensionTest::class, ], 'initial_marking' => ['draft'], + 'metadata' => [ + 'title' => 'article workflow', + 'description' => 'workflow for articles' + ], 'places' => [ 'draft', 'wait_for_journalist', diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflows.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflows.xml index 0c6a638df4..290ab50e7d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflows.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflows.xml @@ -35,6 +35,10 @@ approved_by_spellchecker published + + article workflow + workflow for articles + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflows.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflows.yml index 225106383d..e4ac9c0189 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflows.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflows.yml @@ -5,6 +5,9 @@ framework: supports: - Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest initial_marking: [draft] + metadata: + title: article workflow + description: workflow for articles places: # simple format - draft diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index fd0b154198..c6f158eb8c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -54,6 +54,7 @@ use Symfony\Component\Translation\DependencyInjection\TranslatorPass; use Symfony\Component\Validator\DependencyInjection\AddConstraintValidatorsPass; use Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader; use Symfony\Component\Workflow; +use Symfony\Component\Workflow\Metadata\InMemoryMetadataStore; abstract class FrameworkExtensionTest extends TestCase { @@ -231,6 +232,12 @@ abstract class FrameworkExtensionTest extends TestCase ); $this->assertCount(4, $workflowDefinition->getArgument(1)); $this->assertSame(['draft'], $workflowDefinition->getArgument(2)); + $metadataStoreDefinition = $workflowDefinition->getArgument(3); + $this->assertSame(InMemoryMetadataStore::class, $metadataStoreDefinition->getClass()); + $this->assertSame([ + 'title' => 'article workflow', + 'description' => 'workflow for articles', + ], $metadataStoreDefinition->getArgument(0)); $this->assertTrue($container->hasDefinition('state_machine.pull_request'), 'State machine is registered as a service'); $this->assertSame('state_machine.abstract', $container->getDefinition('state_machine.pull_request')->getParent()); @@ -255,7 +262,7 @@ abstract class FrameworkExtensionTest extends TestCase $metadataStoreDefinition = $stateMachineDefinition->getArgument(3); $this->assertInstanceOf(Definition::class, $metadataStoreDefinition); - $this->assertSame(Workflow\Metadata\InMemoryMetadataStore::class, $metadataStoreDefinition->getClass()); + $this->assertSame(InMemoryMetadataStore::class, $metadataStoreDefinition->getClass()); $workflowMetadata = $metadataStoreDefinition->getArgument(0); $this->assertSame(['title' => 'workflow title'], $workflowMetadata); diff --git a/src/Symfony/Component/Cache/CacheItem.php b/src/Symfony/Component/Cache/CacheItem.php index 3f121766fe..deb23ad343 100644 --- a/src/Symfony/Component/Cache/CacheItem.php +++ b/src/Symfony/Component/Cache/CacheItem.php @@ -119,9 +119,10 @@ final class CacheItem implements ItemInterface $tags = [$tags]; } foreach ($tags as $tag) { - if (!\is_string($tag)) { - throw new InvalidArgumentException(sprintf('Cache tag must be string, "%s" given.', get_debug_type($tag))); + if (!\is_string($tag) && !(\is_object($tag) && method_exists($tag, '__toString'))) { + throw new InvalidArgumentException(sprintf('Cache tag must be string or object that implements __toString(), "%s" given.', \is_object($tag) ? \get_class($tag) : \gettype($tag))); } + $tag = (string) $tag; if (isset($this->newMetadata[self::METADATA_TAGS][$tag])) { continue; } diff --git a/src/Symfony/Component/Cache/Tests/CacheItemTest.php b/src/Symfony/Component/Cache/Tests/CacheItemTest.php index 3b756f571f..01914e4a36 100644 --- a/src/Symfony/Component/Cache/Tests/CacheItemTest.php +++ b/src/Symfony/Component/Cache/Tests/CacheItemTest.php @@ -13,6 +13,7 @@ namespace Symfony\Component\Cache\Tests; use PHPUnit\Framework\TestCase; use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\Tests\Fixtures\StringableTag; class CacheItemTest extends TestCase { @@ -61,9 +62,11 @@ class CacheItemTest extends TestCase $this->assertSame($item, $item->tag('foo')); $this->assertSame($item, $item->tag(['bar', 'baz'])); + $this->assertSame($item, $item->tag(new StringableTag('qux'))); + $this->assertSame($item, $item->tag([new StringableTag('quux'), new StringableTag('quuux')])); (\Closure::bind(function () use ($item) { - $this->assertSame(['foo' => 'foo', 'bar' => 'bar', 'baz' => 'baz'], $item->newMetadata[CacheItem::METADATA_TAGS]); + $this->assertSame(['foo' => 'foo', 'bar' => 'bar', 'baz' => 'baz', 'qux' => 'qux', 'quux' => 'quux', 'quuux' => 'quuux'], $item->newMetadata[CacheItem::METADATA_TAGS]); }, $this, CacheItem::class))(); } diff --git a/src/Symfony/Component/Cache/Tests/Fixtures/StringableTag.php b/src/Symfony/Component/Cache/Tests/Fixtures/StringableTag.php new file mode 100644 index 0000000000..caaf55c026 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Fixtures/StringableTag.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\Cache\Tests\Fixtures; + +class StringableTag +{ + /** + * @var string + */ + private $tag; + + public function __construct(string $tag) + { + $this->tag = $tag; + } + + public function __toString(): string + { + return $this->tag; + } +} diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index c42609d8c7..784af89b31 100644 --- a/src/Symfony/Component/Console/Application.php +++ b/src/Symfony/Component/Console/Application.php @@ -107,8 +107,10 @@ class Application implements ResetInterface */ public function run(InputInterface $input = null, OutputInterface $output = null) { - putenv('LINES='.$this->terminal->getHeight()); - putenv('COLUMNS='.$this->terminal->getWidth()); + if (\function_exists('putenv')) { + @putenv('LINES='.$this->terminal->getHeight()); + @putenv('COLUMNS='.$this->terminal->getWidth()); + } if (null === $input) { $input = new ArgvInput(); @@ -891,7 +893,9 @@ class Application implements ResetInterface $input->setInteractive(false); } - putenv('SHELL_VERBOSITY='.$shellVerbosity); + if (\function_exists('putenv')) { + @putenv('SHELL_VERBOSITY='.$shellVerbosity); + } $_ENV['SHELL_VERBOSITY'] = $shellVerbosity; $_SERVER['SHELL_VERBOSITY'] = $shellVerbosity; } diff --git a/src/Symfony/Component/EventDispatcher/Tests/Debug/TraceableEventDispatcherTest.php b/src/Symfony/Component/EventDispatcher/Tests/Debug/TraceableEventDispatcherTest.php index 99627a71a9..bcb14d34a6 100644 --- a/src/Symfony/Component/EventDispatcher/Tests/Debug/TraceableEventDispatcherTest.php +++ b/src/Symfony/Component/EventDispatcher/Tests/Debug/TraceableEventDispatcherTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\EventDispatcher\Tests\Debug; use PHPUnit\Framework\TestCase; +use Symfony\Component\ErrorHandler\BufferingLogger; use Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -192,41 +193,57 @@ class TraceableEventDispatcherTest extends TestCase public function testLogger() { - $logger = $this->getMockBuilder('Psr\Log\LoggerInterface')->getMock(); + $logger = new BufferingLogger(); $dispatcher = new EventDispatcher(); $tdispatcher = new TraceableEventDispatcher($dispatcher, new Stopwatch(), $logger); $tdispatcher->addListener('foo', $listener1 = function () {}); $tdispatcher->addListener('foo', $listener2 = function () {}); - $logger->expects($this->exactly(2)) - ->method('debug') - ->withConsecutive( - ['Notified event "{event}" to listener "{listener}".', ['event' => 'foo', 'listener' => 'closure']], - ['Notified event "{event}" to listener "{listener}".', ['event' => 'foo', 'listener' => 'closure']] - ); - $tdispatcher->dispatch(new Event(), 'foo'); + + $this->assertSame([ + [ + 'debug', + 'Notified event "{event}" to listener "{listener}".', + ['event' => 'foo', 'listener' => 'closure'], + ], + [ + 'debug', + 'Notified event "{event}" to listener "{listener}".', + ['event' => 'foo', 'listener' => 'closure'], + ], + ], $logger->cleanLogs()); } public function testLoggerWithStoppedEvent() { - $logger = $this->getMockBuilder('Psr\Log\LoggerInterface')->getMock(); + $logger = new BufferingLogger(); $dispatcher = new EventDispatcher(); $tdispatcher = new TraceableEventDispatcher($dispatcher, new Stopwatch(), $logger); $tdispatcher->addListener('foo', $listener1 = function (Event $event) { $event->stopPropagation(); }); $tdispatcher->addListener('foo', $listener2 = function () {}); - $logger->expects($this->exactly(3)) - ->method('debug') - ->withConsecutive( - ['Notified event "{event}" to listener "{listener}".', ['event' => 'foo', 'listener' => 'closure']], - ['Listener "{listener}" stopped propagation of the event "{event}".', ['event' => 'foo', 'listener' => 'closure']], - ['Listener "{listener}" was not called for event "{event}".', ['event' => 'foo', 'listener' => 'closure']] - ); - $tdispatcher->dispatch(new Event(), 'foo'); + + $this->assertSame([ + [ + 'debug', + 'Notified event "{event}" to listener "{listener}".', + ['event' => 'foo', 'listener' => 'closure'], + ], + [ + 'debug', + 'Listener "{listener}" stopped propagation of the event "{event}".', + ['event' => 'foo', 'listener' => 'closure'], + ], + [ + 'debug', + 'Listener "{listener}" was not called for event "{event}".', + ['event' => 'foo', 'listener' => 'closure'], + ], + ], $logger->cleanLogs()); } public function testDispatchCallListeners() diff --git a/src/Symfony/Component/EventDispatcher/composer.json b/src/Symfony/Component/EventDispatcher/composer.json index a67f66a42a..7fb244fa6a 100644 --- a/src/Symfony/Component/EventDispatcher/composer.json +++ b/src/Symfony/Component/EventDispatcher/composer.json @@ -25,6 +25,7 @@ "symfony/dependency-injection": "^4.4|^5.0", "symfony/expression-language": "^4.4|^5.0", "symfony/config": "^4.4|^5.0", + "symfony/error-handler": "^4.4|^5.0", "symfony/http-foundation": "^4.4|^5.0", "symfony/service-contracts": "^1.1|^2", "symfony/stopwatch": "^4.4|^5.0", diff --git a/src/Symfony/Component/Form/Extension/Core/Type/BaseType.php b/src/Symfony/Component/Form/Extension/Core/Type/BaseType.php index ac371c61c5..3b5a3a01b5 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/BaseType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/BaseType.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Form\Extension\Core\Type; +use Symfony\Component\Form\AbstractRendererEngine; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormInterface; @@ -112,7 +113,7 @@ abstract class BaseType extends AbstractType // collection form have different types (dynamically), they should // be rendered differently. // https://github.com/symfony/symfony/issues/5038 - 'cache_key' => $uniqueBlockPrefix.'_'.$form->getConfig()->getType()->getBlockPrefix(), + AbstractRendererEngine::CACHE_KEY_VAR => $uniqueBlockPrefix.'_'.$form->getConfig()->getType()->getBlockPrefix(), ]); } diff --git a/src/Symfony/Component/HttpClient/CurlHttpClient.php b/src/Symfony/Component/HttpClient/CurlHttpClient.php index b7f814ed14..5174ad053a 100644 --- a/src/Symfony/Component/HttpClient/CurlHttpClient.php +++ b/src/Symfony/Component/HttpClient/CurlHttpClient.php @@ -267,7 +267,14 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface, } if ($options['bindto']) { - $curlopts[file_exists($options['bindto']) ? \CURLOPT_UNIX_SOCKET_PATH : \CURLOPT_INTERFACE] = $options['bindto']; + if (file_exists($options['bindto'])) { + $curlopts[\CURLOPT_UNIX_SOCKET_PATH] = $options['bindto']; + } elseif (preg_match('/^(.*):(\d+)$/', $options['bindto'], $matches)) { + $curlopts[\CURLOPT_INTERFACE] = $matches[1]; + $curlopts[\CURLOPT_LOCALPORT] = $matches[2]; + } else { + $curlopts[\CURLOPT_INTERFACE] = $options['bindto']; + } } if (0 < $options['max_duration']) { diff --git a/src/Symfony/Component/HttpClient/HttpClientTrait.php b/src/Symfony/Component/HttpClient/HttpClientTrait.php index b001f28c18..d9037c832a 100644 --- a/src/Symfony/Component/HttpClient/HttpClientTrait.php +++ b/src/Symfony/Component/HttpClient/HttpClientTrait.php @@ -198,6 +198,16 @@ trait HttpClientTrait continue; } + if ('auth_ntlm' === $name) { + if (!\extension_loaded('curl')) { + $msg = 'try installing the "curl" extension to use "%s" instead.'; + } else { + $msg = 'try using "%s" instead.'; + } + + throw new InvalidArgumentException(sprintf('Option "auth_ntlm" is not supported by "%s", '.$msg, __CLASS__, CurlHttpClient::class)); + } + $alternatives = []; foreach ($defaultOptions as $key => $v) { @@ -206,10 +216,6 @@ trait HttpClientTrait } } - if ('auth_ntlm' === $name) { - throw new InvalidArgumentException(sprintf('Option "auth_ntlm" is not supported by "%s", try using CurlHttpClient instead.', __CLASS__)); - } - throw new InvalidArgumentException(sprintf('Unsupported option "%s" passed to "%s", did you mean "%s"?', $name, __CLASS__, implode('", "', $alternatives ?: array_keys($defaultOptions)))); } diff --git a/src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php index e0a12a49a5..3f51b73603 100644 --- a/src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php +++ b/src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php @@ -34,6 +34,74 @@ class CurlHttpClientTest extends HttpClientTestCase return new CurlHttpClient(['verify_peer' => false, 'verify_host' => false]); } + public function testBindToPort() + { + $client = $this->getHttpClient(__FUNCTION__); + $response = $client->request('GET', 'http://localhost:8057', ['bindto' => '127.0.0.1:9876']); + $response->getStatusCode(); + + $r = new \ReflectionProperty($response, 'handle'); + $r->setAccessible(true); + + $curlInfo = curl_getinfo($r->getValue($response)); + + self::assertSame('127.0.0.1', $curlInfo['local_ip']); + self::assertSame(9876, $curlInfo['local_port']); + } + + /** + * @requires PHP 7.2.17 + */ + public function testHttp2PushVulcain() + { + $client = $this->getVulcainClient(); + $logger = new TestLogger(); + $client->setLogger($logger); + + $responseAsArray = $client->request('GET', 'https://127.0.0.1:3000/json', [ + 'headers' => [ + 'Preload' => '/documents/*/id', + ], + ])->toArray(); + + foreach ($responseAsArray['documents'] as $document) { + $client->request('GET', 'https://127.0.0.1:3000'.$document['id'])->toArray(); + } + + $client->reset(); + + $expected = [ + 'Request: "GET https://127.0.0.1:3000/json"', + 'Queueing pushed response: "https://127.0.0.1:3000/json/1"', + 'Queueing pushed response: "https://127.0.0.1:3000/json/2"', + 'Queueing pushed response: "https://127.0.0.1:3000/json/3"', + 'Response: "200 https://127.0.0.1:3000/json"', + 'Accepting pushed response: "GET https://127.0.0.1:3000/json/1"', + 'Response: "200 https://127.0.0.1:3000/json/1"', + 'Accepting pushed response: "GET https://127.0.0.1:3000/json/2"', + 'Response: "200 https://127.0.0.1:3000/json/2"', + 'Accepting pushed response: "GET https://127.0.0.1:3000/json/3"', + 'Response: "200 https://127.0.0.1:3000/json/3"', + ]; + $this->assertSame($expected, $logger->logs); + } + + /** + * @requires PHP 7.2.17 + */ + public function testHttp2PushVulcainWithUnusedResponse() + { + $client = $this->getVulcainClient(); + $logger = new TestLogger(); + $client->setLogger($logger); + + $responseAsArray = $client->request('GET', 'https://127.0.0.1:3000/json', [ + 'headers' => [ + 'Preload' => '/documents/*/id', + ], + ])->toArray(); + } + public function testTimeoutIsNotAFatalError() { if ('\\' === \DIRECTORY_SEPARATOR) { diff --git a/src/Symfony/Component/HttpKernel/HttpClientKernel.php b/src/Symfony/Component/HttpKernel/HttpClientKernel.php index a5e207d546..818082f439 100644 --- a/src/Symfony/Component/HttpKernel/HttpClientKernel.php +++ b/src/Symfony/Component/HttpKernel/HttpClientKernel.php @@ -35,7 +35,7 @@ final class HttpClientKernel implements HttpKernelInterface public function __construct(HttpClientInterface $client = null) { - if (!class_exists(HttpClient::class)) { + if (null === $client && !class_exists(HttpClient::class)) { throw new \LogicException(sprintf('You cannot use "%s" as the HttpClient component is not installed. Try running "composer require symfony/http-client".', __CLASS__)); } @@ -53,7 +53,6 @@ final class HttpClientKernel implements HttpKernelInterface $response = $this->client->request($request->getMethod(), $request->getUri(), [ 'headers' => $headers, 'body' => $body, - 'max_redirects' => 0, ] + $request->attributes->get('http_client_options', [])); $response = new Response($response->getContent(!$catch), $response->getStatusCode(), $response->getHeaders(!$catch)); diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpClientKernelTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpClientKernelTest.php new file mode 100644 index 0000000000..2b904bf9a2 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/HttpClientKernelTest.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\HttpClientKernel; +use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; + +class HttpClientKernelTest extends TestCase +{ + public function testHandlePassesMaxRedirectsHttpClientOption() + { + $request = new Request(); + $request->attributes->set('http_client_options', ['max_redirects' => 50]); + + $response = $this->getMockBuilder(ResponseInterface::class)->getMock(); + $response->expects($this->once())->method('getStatusCode')->willReturn(200); + + $client = $this->getMockBuilder(HttpClientInterface::class)->getMock(); + $client + ->expects($this->once()) + ->method('request') + ->willReturnCallback(function (string $method, string $uri, array $options) use ($request, $response) { + $this->assertSame($request->getMethod(), $method); + $this->assertSame($request->getUri(), $uri); + $this->assertArrayHasKey('max_redirects', $options); + $this->assertSame(50, $options['max_redirects']); + + return $response; + }); + + $kernel = new HttpClientKernel($client); + $kernel->handle($request); + } +} diff --git a/src/Symfony/Component/HttpKernel/composer.json b/src/Symfony/Component/HttpKernel/composer.json index c016af2369..d29435754f 100644 --- a/src/Symfony/Component/HttpKernel/composer.json +++ b/src/Symfony/Component/HttpKernel/composer.json @@ -20,6 +20,7 @@ "symfony/deprecation-contracts": "^2.1", "symfony/error-handler": "^4.4|^5.0", "symfony/event-dispatcher": "^5.0", + "symfony/http-client-contracts": "^1.1|^2", "symfony/http-foundation": "^4.4|^5.0", "symfony/polyfill-ctype": "^1.8", "symfony/polyfill-php73": "^1.9", diff --git a/src/Symfony/Component/Translation/Tests/TranslatorTest.php b/src/Symfony/Component/Translation/Tests/TranslatorTest.php index 72a4f9055a..ed1bb9dba5 100644 --- a/src/Symfony/Component/Translation/Tests/TranslatorTest.php +++ b/src/Symfony/Component/Translation/Tests/TranslatorTest.php @@ -19,6 +19,19 @@ use Symfony\Component\Translation\Translator; class TranslatorTest extends TestCase { + private $defaultLocale; + + protected function setUp(): void + { + $this->defaultLocale = \Locale::getDefault(); + \Locale::setDefault('en'); + } + + protected function tearDown(): void + { + \Locale::setDefault($this->defaultLocale); + } + /** * @dataProvider getInvalidLocalesTests */ diff --git a/src/Symfony/Component/Translation/Translator.php b/src/Symfony/Component/Translation/Translator.php index c92abf1383..b6799b7daf 100644 --- a/src/Symfony/Component/Translation/Translator.php +++ b/src/Symfony/Component/Translation/Translator.php @@ -158,7 +158,7 @@ class Translator implements TranslatorInterface, TranslatorBagInterface, LocaleA */ public function getLocale() { - return $this->locale; + return $this->locale ?? \Locale::getDefault(); } /** diff --git a/src/Symfony/Component/Validator/Constraints/TimezoneValidator.php b/src/Symfony/Component/Validator/Constraints/TimezoneValidator.php index cf52239d0c..ab6b4eed62 100644 --- a/src/Symfony/Component/Validator/Constraints/TimezoneValidator.php +++ b/src/Symfony/Component/Validator/Constraints/TimezoneValidator.php @@ -78,7 +78,11 @@ class TimezoneValidator extends ConstraintValidator private static function getPhpTimezones(int $zone, string $countryCode = null): array { if (null !== $countryCode) { - return @\DateTimeZone::listIdentifiers($zone, $countryCode) ?: []; + try { + return @\DateTimeZone::listIdentifiers($zone, $countryCode) ?: []; + } catch (\ValueError $e) { + return []; + } } return \DateTimeZone::listIdentifiers($zone); diff --git a/src/Symfony/Component/VarDumper/Caster/SplCaster.php b/src/Symfony/Component/VarDumper/Caster/SplCaster.php index f1854a50ad..49cca4fd0c 100644 --- a/src/Symfony/Component/VarDumper/Caster/SplCaster.php +++ b/src/Symfony/Component/VarDumper/Caster/SplCaster.php @@ -110,6 +110,14 @@ class SplCaster $a[$prefix.'⚠'] = 'The parent constructor was not called: the object is in an invalid state'; + return $a; + } catch (\Error $e) { + if ('Object not initialized' !== $e->getMessage()) { + throw $e; + } + + $a[$prefix.'⚠'] = 'The parent constructor was not called: the object is in an invalid state'; + return $a; } } diff --git a/src/Symfony/Component/Yaml/Inline.php b/src/Symfony/Component/Yaml/Inline.php index c862d8c798..3086ba0bf4 100644 --- a/src/Symfony/Component/Yaml/Inline.php +++ b/src/Symfony/Component/Yaml/Inline.php @@ -90,7 +90,7 @@ class Inline } // some comments are allowed at the end - if (preg_replace('/\s+#.*$/A', '', substr($value, $i))) { + if (preg_replace('/\s*#.*$/A', '', substr($value, $i))) { throw new ParseException(sprintf('Unexpected characters near "%s".', substr($value, $i)), self::$parsedLineNumber + 1, $value, self::$parsedFilename); } diff --git a/src/Symfony/Component/Yaml/Parser.php b/src/Symfony/Component/Yaml/Parser.php index ff6ffa3100..690c202983 100644 --- a/src/Symfony/Component/Yaml/Parser.php +++ b/src/Symfony/Component/Yaml/Parser.php @@ -104,6 +104,7 @@ class Parser $this->refs = []; $this->skippedLineNumbers = []; $this->locallySkippedLineNumbers = []; + $this->totalNumberOfLines = null; } return $data; @@ -728,7 +729,7 @@ class Parser if (\in_array($value[0], ['!', '|', '>'], true) && self::preg_match('/^(?:'.self::TAG_PATTERN.' +)?'.self::BLOCK_SCALAR_HEADER_PATTERN.'$/', $value, $matches)) { $modifiers = isset($matches['modifiers']) ? $matches['modifiers'] : ''; - $data = $this->parseBlockScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), (int) abs((int) $modifiers)); + $data = $this->parseBlockScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), abs((int) $modifiers)); if ('' !== $matches['tag'] && '!' !== $matches['tag']) { if ('!!binary' === $matches['tag']) { diff --git a/src/Symfony/Component/Yaml/Tests/InlineTest.php b/src/Symfony/Component/Yaml/Tests/InlineTest.php index 84cc2c4846..9e7d072934 100644 --- a/src/Symfony/Component/Yaml/Tests/InlineTest.php +++ b/src/Symfony/Component/Yaml/Tests/InlineTest.php @@ -837,6 +837,16 @@ class InlineTest extends TestCase self::assertSame('-0123456789', Inline::parse('-0123456789')); } + public function testParseCommentNotPrefixedBySpaces() + { + self::assertSame('foo', Inline::parse('"foo"#comment')); + } + + public function testParseUnquotedStringContainingHashTagNotPrefixedBySpace() + { + self::assertSame('foo#nocomment', Inline::parse('foo#nocomment')); + } + /** * @dataProvider unquotedExclamationMarkThrowsProvider */ diff --git a/src/Symfony/Component/Yaml/Tests/ParserTest.php b/src/Symfony/Component/Yaml/Tests/ParserTest.php index 4dfc5b58ce..fcc56fb134 100644 --- a/src/Symfony/Component/Yaml/Tests/ParserTest.php +++ b/src/Symfony/Component/Yaml/Tests/ParserTest.php @@ -2437,6 +2437,39 @@ YAML; $this->parser->parse($yaml) ); } + + /** + * This is a regression test for a bug where a YAML block with a nested multiline string using | was parsed without + * a trailing \n when a shorter YAML document was parsed before. + * + * When a shorter document was parsed before, the nested string did not have a \n at the end of the string, because + * the Parser thought it was the end of the file, even though it is not. + */ + public function testParsingMultipleDocuments() + { + $shortDocument = 'foo: bar'; + $longDocument = << ['b' => "row\nrow2\n"], 'c' => 'd']; + + // The parser was not used before, so there is a new line after row2 + $this->assertSame($expected, $this->parser->parse($longDocument)); + + $parser = new Parser(); + // The first parsing set and fixed the totalNumberOfLines in the Parser before, so parsing the short document here + // to reproduce the issue. If the issue would not have been fixed, the next assertion will fail + $parser->parse($shortDocument); + + // After the total number of lines has been rset the result will be the same as if a new parser was used + // (before, there was no \n after row2) + $this->assertSame($expected, $parser->parse($longDocument)); + } } class B