diff --git a/.github/patch-types.php b/.github/patch-types.php index 7302921df0..31bb7050ee 100644 --- a/.github/patch-types.php +++ b/.github/patch-types.php @@ -32,4 +32,4 @@ foreach ($loader->getClassMap() as $class => $file) { class_exists($class); } -Symfony\Component\ErrorHandler\DebugClassLoader::disable(); +Symfony\Component\ErrorHandler\DebugClassLoader::checkClasses(); diff --git a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php index 732866dc52..a81c95eea6 100644 --- a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php +++ b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php @@ -15,6 +15,7 @@ use PHPUnit\Framework\TestResult; use PHPUnit\Util\ErrorHandler; use Symfony\Bridge\PhpUnit\DeprecationErrorHandler\Configuration; use Symfony\Bridge\PhpUnit\DeprecationErrorHandler\Deprecation; +use Symfony\Component\ErrorHandler\DebugClassLoader; /** * Catch deprecation notices and print a summary report at the end of the test suite. @@ -173,6 +174,9 @@ class DeprecationErrorHandler return; } + if (method_exists(DebugClassLoader::class, 'checkClasses')) { + DebugClassLoader::checkClasses(); + } $currErrorHandler = set_error_handler('var_dump'); restore_error_handler(); diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/CommandForV5.php b/src/Symfony/Bridge/PhpUnit/Legacy/CommandForV5.php index 95dcb1e554..ed4128482f 100644 --- a/src/Symfony/Bridge/PhpUnit/Legacy/CommandForV5.php +++ b/src/Symfony/Bridge/PhpUnit/Legacy/CommandForV5.php @@ -23,8 +23,6 @@ class CommandForV5 extends \PHPUnit_TextUI_Command */ protected function createRunner() { - $listener = new SymfonyTestsListenerForV5(); - $this->arguments['listeners'] = isset($this->arguments['listeners']) ? $this->arguments['listeners'] : array(); $registeredLocally = false; @@ -37,8 +35,21 @@ class CommandForV5 extends \PHPUnit_TextUI_Command } } + if (isset($this->arguments['configuration'])) { + $configuration = $this->arguments['configuration']; + if (!$configuration instanceof \PHPUnit_Util_Configuration) { + $configuration = \PHPUnit_Util_Configuration::getInstance($this->arguments['configuration']); + } + foreach ($configuration->getListenerConfiguration() as $registeredListener) { + if ('Symfony\Bridge\PhpUnit\SymfonyTestsListener' === ltrim($registeredListener['class'], '\\')) { + $registeredLocally = true; + break; + } + } + } + if (!$registeredLocally) { - $this->arguments['listeners'][] = $listener; + $this->arguments['listeners'][] = new SymfonyTestsListenerForV5(); } return parent::createRunner(); diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/CommandForV6.php b/src/Symfony/Bridge/PhpUnit/Legacy/CommandForV6.php index f8f75bb09a..93e1ad975b 100644 --- a/src/Symfony/Bridge/PhpUnit/Legacy/CommandForV6.php +++ b/src/Symfony/Bridge/PhpUnit/Legacy/CommandForV6.php @@ -13,6 +13,7 @@ namespace Symfony\Bridge\PhpUnit\Legacy; use PHPUnit\TextUI\Command as BaseCommand; use PHPUnit\TextUI\TestRunner as BaseRunner; +use PHPUnit\Util\Configuration; use Symfony\Bridge\PhpUnit\SymfonyTestsListener; /** @@ -27,8 +28,6 @@ class CommandForV6 extends BaseCommand */ protected function createRunner(): BaseRunner { - $listener = new SymfonyTestsListener(); - $this->arguments['listeners'] = isset($this->arguments['listeners']) ? $this->arguments['listeners'] : []; $registeredLocally = false; @@ -41,8 +40,21 @@ class CommandForV6 extends BaseCommand } } + if (isset($this->arguments['configuration'])) { + $configuration = $this->arguments['configuration']; + if (!$configuration instanceof Configuration) { + $configuration = Configuration::getInstance($this->arguments['configuration']); + } + foreach ($configuration->getListenerConfiguration() as $registeredListener) { + if ('Symfony\Bridge\PhpUnit\SymfonyTestsListener' === ltrim($registeredListener['class'], '\\')) { + $registeredLocally = true; + break; + } + } + } + if (!$registeredLocally) { - $this->arguments['listeners'][] = $listener; + $this->arguments['listeners'][] = new SymfonyTestsListener(); } return parent::createRunner(); diff --git a/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php b/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php index 92e5caded6..cdcc423031 100644 --- a/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php +++ b/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php @@ -10,7 +10,7 @@ */ // Please update when phpunit needs to be reinstalled with fresh deps: -// Cache-Id: 2019-08-09 13:00 UTC +// Cache-Id: 2019-09-02 16:00 UTC error_reporting(-1); diff --git a/src/Symfony/Bridge/Twig/CHANGELOG.md b/src/Symfony/Bridge/Twig/CHANGELOG.md index d692bf4431..38f3ed5043 100644 --- a/src/Symfony/Bridge/Twig/CHANGELOG.md +++ b/src/Symfony/Bridge/Twig/CHANGELOG.md @@ -14,6 +14,7 @@ CHANGELOG * marked all classes extending twig as `@final` * deprecated to pass `$rootDir` and `$fileLinkFormatter` as 5th and 6th argument respectively to the `DebugCommand::__construct()` method, swap the variables position. + * the `LintCommand` lints all the templates stored in all configured Twig paths if none argument is provided 4.3.0 ----- diff --git a/src/Symfony/Bridge/Twig/Command/LintCommand.php b/src/Symfony/Bridge/Twig/Command/LintCommand.php index 1e482337f5..036362fcc7 100644 --- a/src/Symfony/Bridge/Twig/Command/LintCommand.php +++ b/src/Symfony/Bridge/Twig/Command/LintCommand.php @@ -23,6 +23,7 @@ use Symfony\Component\Finder\Finder; use Twig\Environment; use Twig\Error\Error; use Twig\Loader\ArrayLoader; +use Twig\Loader\FilesystemLoader; use Twig\Source; /** @@ -78,16 +79,27 @@ EOF $filenames = $input->getArgument('filename'); if (0 === \count($filenames)) { - if (0 !== ftell(STDIN)) { + if (0 === ftell(STDIN)) { + $template = ''; + while (!feof(STDIN)) { + $template .= fread(STDIN, 1024); + } + + return $this->display($input, $output, $io, [$this->validate($template, uniqid('sf_', true))]); + } + + $loader = $this->twig->getLoader(); + if ($loader instanceof FilesystemLoader) { + $paths = []; + foreach ($loader->getNamespaces() as $namespace) { + $paths[] = $loader->getPaths($namespace); + } + $filenames = array_merge(...$paths); + } + + if (0 === \count($filenames)) { throw new RuntimeException('Please provide a filename or pipe template content to STDIN.'); } - - $template = ''; - while (!feof(STDIN)) { - $template .= fread(STDIN, 1024); - } - - return $this->display($input, $output, $io, [$this->validate($template, uniqid('sf_', true))]); } $filesInfo = $this->getFilesInfo($filenames); diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_layout.html.twig index 97710c79b5..ef45624c4e 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_layout.html.twig @@ -124,7 +124,7 @@ {{- block('form_widget_simple') -}} {%- set label_attr = label_attr|merge({ class: (label_attr.class|default('') ~ ' custom-file-label')|trim }) -%} diff --git a/src/Symfony/Bridge/Twig/Tests/Command/LintCommandTest.php b/src/Symfony/Bridge/Twig/Tests/Command/LintCommandTest.php index 22f048e6bc..fe3a7231b4 100644 --- a/src/Symfony/Bridge/Twig/Tests/Command/LintCommandTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Command/LintCommandTest.php @@ -66,9 +66,18 @@ class LintCommandTest extends TestCase $this->assertRegExp('/ERROR in \S+ \(line /', trim($tester->getDisplay())); } + public function testLintDefaultPaths() + { + $tester = $this->createCommandTester(); + $ret = $tester->execute([], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE, 'decorated' => false]); + + $this->assertEquals(0, $ret, 'Returns 0 in case of success'); + self::assertStringContainsString('OK in', trim($tester->getDisplay())); + } + private function createCommandTester(): CommandTester { - $command = new LintCommand(new Environment(new FilesystemLoader())); + $command = new LintCommand(new Environment(new FilesystemLoader(\dirname(__DIR__).'/Fixtures/templates/'))); $application = new Application(); $application->add($command); diff --git a/src/Symfony/Component/DependencyInjection/Loader/Configurator/ContainerConfigurator.php b/src/Symfony/Component/DependencyInjection/Loader/Configurator/ContainerConfigurator.php index 2aa9b0eafa..643b252049 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/Configurator/ContainerConfigurator.php +++ b/src/Symfony/Component/DependencyInjection/Loader/Configurator/ContainerConfigurator.php @@ -124,7 +124,7 @@ function tagged_iterator(string $tag, string $indexAttribute = null, string $def /** * Creates a service locator by tag name. */ -function tagged_locator(string $tag, string $indexAttribute, string $defaultIndexMethod = null): ServiceLocatorArgument +function tagged_locator(string $tag, string $indexAttribute = null, string $defaultIndexMethod = null): ServiceLocatorArgument { return new ServiceLocatorArgument(new TaggedIteratorArgument($tag, $indexAttribute, $defaultIndexMethod, true)); } diff --git a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php index 8d4b550c93..d68098b3c9 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php @@ -707,25 +707,23 @@ class YamlFileLoader extends FileLoader if (\in_array($value->getTag(), ['tagged_iterator', 'tagged_locator'], true)) { $forLocator = 'tagged_locator' === $value->getTag(); - if (\is_string($argument) && $argument) { - return new TaggedIteratorArgument($argument, null, null, $forLocator); - } - if (\is_array($argument) && isset($argument['tag']) && $argument['tag']) { if ($diff = array_diff(array_keys($argument), ['tag', 'index_by', 'default_index_method'])) { throw new InvalidArgumentException(sprintf('"!%s" tag contains unsupported key "%s"; supported ones are "tag", "index_by" and "default_index_method".', $value->getTag(), implode('"", "', $diff))); } $argument = new TaggedIteratorArgument($argument['tag'], $argument['index_by'] ?? null, $argument['default_index_method'] ?? null, $forLocator); - - if ($forLocator) { - $argument = new ServiceLocatorArgument($argument); - } - - return $argument; + } elseif (\is_string($argument) && $argument) { + $argument = new TaggedIteratorArgument($argument, null, null, $forLocator); + } else { + throw new InvalidArgumentException(sprintf('"!%s" tags only accept a non empty string or an array with a key "tag" in "%s".', $value->getTag(), $file)); } - throw new InvalidArgumentException(sprintf('"!%s" tags only accept a non empty string or an array with a key "tag" in "%s".', $value->getTag(), $file)); + if ($forLocator) { + $argument = new ServiceLocatorArgument($argument); + } + + return $argument; } if ('service' === $value->getTag()) { if ($isParameter) { diff --git a/src/Symfony/Component/DependencyInjection/Tests/Dumper/YamlDumperTest.php b/src/Symfony/Component/DependencyInjection/Tests/Dumper/YamlDumperTest.php index ca41a5b339..56512e89c1 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Dumper/YamlDumperTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Dumper/YamlDumperTest.php @@ -104,6 +104,7 @@ class YamlDumperTest extends TestCase $container->register('foo_service', 'Foo')->addTag('foo'); $container->register('foo_service_tagged_iterator', 'Bar')->addArgument($taggedIterator); $container->register('foo_service_tagged_locator', 'Bar')->addArgument(new ServiceLocatorArgument($taggedIterator)); + $container->register('bar_service_tagged_locator', 'Bar')->addArgument(new ServiceLocatorArgument(new TaggedIteratorArgument('foo'))); $dumper = new YamlDumper($container); $this->assertStringEqualsFile(self::$fixturesPath.'/yaml/services_with_tagged_argument.yml', $dumper->dump()); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_with_tagged_argument.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_with_tagged_argument.yml index b30aeb7bff..8c2cd59aaf 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_with_tagged_argument.yml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_with_tagged_argument.yml @@ -14,6 +14,9 @@ services: foo_service_tagged_locator: class: Bar arguments: [!tagged_locator { tag: foo, index_by: barfoo, default_index_method: foobar }] + bar_service_tagged_locator: + class: Bar + arguments: [!tagged_locator foo] Psr\Container\ContainerInterface: alias: service_container public: false diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php index 9518d1a512..2758d4de83 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php @@ -282,6 +282,9 @@ class YamlFileLoaderTest extends TestCase $taggedIterator = new TaggedIteratorArgument('foo', 'barfoo', 'foobar', true); $this->assertEquals(new ServiceLocatorArgument($taggedIterator), $container->getDefinition('foo_service_tagged_locator')->getArgument(0)); + + $taggedIterator = new TaggedIteratorArgument('foo', null, null, true); + $this->assertEquals(new ServiceLocatorArgument($taggedIterator), $container->getDefinition('bar_service_tagged_locator')->getArgument(0)); } public function testNameOnlyTagsAreAllowedAsString() diff --git a/src/Symfony/Component/ErrorHandler/DebugClassLoader.php b/src/Symfony/Component/ErrorHandler/DebugClassLoader.php index 1f30cfa9f5..67a6c1500b 100644 --- a/src/Symfony/Component/ErrorHandler/DebugClassLoader.php +++ b/src/Symfony/Component/ErrorHandler/DebugClassLoader.php @@ -11,7 +11,11 @@ namespace Symfony\Component\ErrorHandler; +use Doctrine\Common\Persistence\Proxy; use PHPUnit\Framework\MockObject\Matcher\StatelessInvocation; +use PHPUnit\Framework\MockObject\MockObject; +use Prophecy\Prophecy\ProphecySubjectInterface; +use ProxyManager\Proxy\ProxyInterface; /** * Autoloader checking if the class is really defined in the file found. @@ -230,22 +234,57 @@ class DebugClassLoader spl_autoload_unregister($function); } - $loader = null; - foreach ($functions as $function) { if (\is_array($function) && $function[0] instanceof self) { - $loader = $function[0]; $function = $function[0]->getClassLoader(); } spl_autoload_register($function); } + } - if (null !== $loader) { - foreach (array_merge(get_declared_interfaces(), get_declared_traits(), get_declared_classes()) as $class) { - $loader->checkClass($class); + public static function checkClasses(): bool + { + if (!\is_array($functions = spl_autoload_functions())) { + return false; + } + + $loader = null; + + foreach ($functions as $function) { + if (\is_array($function) && $function[0] instanceof self) { + $loader = $function[0]; + break; } } + + if (null === $loader) { + return false; + } + + static $offsets = [ + 'get_declared_interfaces' => 0, + 'get_declared_traits' => 0, + 'get_declared_classes' => 0, + ]; + + foreach ($offsets as $getSymbols => $i) { + $symbols = $getSymbols(); + + for (; $i < \count($symbols); ++$i) { + if (!is_subclass_of($symbols[$i], MockObject::class) + && !is_subclass_of($symbols[$i], ProphecySubjectInterface::class) + && !is_subclass_of($symbols[$i], Proxy::class) + && !is_subclass_of($symbols[$i], ProxyInterface::class) + ) { + $loader->checkClass($symbols[$i]); + } + } + + $offsets[$getSymbols] = $i; + } + + return true; } public function findFile(string $class): ?string diff --git a/src/Symfony/Component/ErrorHandler/ErrorHandler.php b/src/Symfony/Component/ErrorHandler/ErrorHandler.php index 5493e61a63..2064427f0a 100644 --- a/src/Symfony/Component/ErrorHandler/ErrorHandler.php +++ b/src/Symfony/Component/ErrorHandler/ErrorHandler.php @@ -592,7 +592,11 @@ class ErrorHandler } } $exceptionHandler = $this->exceptionHandler; - $this->exceptionHandler = null; + if ((!\is_array($exceptionHandler) || !$exceptionHandler[0] instanceof self || 'sendPhpResponse' !== $exceptionHandler[1]) && !\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true)) { + $this->exceptionHandler = [$this, 'sendPhpResponse']; + } else { + $this->exceptionHandler = null; + } try { if (null !== $exceptionHandler) { return $exceptionHandler($exception); @@ -696,18 +700,28 @@ class ErrorHandler private function sendPhpResponse(\Throwable $exception) { $charset = ini_get('default_charset') ?: 'UTF-8'; - - if (!headers_sent()) { - header('HTTP/1.0 500'); - header(sprintf('Content-Type: text/html; charset=%s', $charset)); - } + $statusCode = 500; + $headers = []; if (class_exists(HtmlErrorRenderer::class)) { - echo (new HtmlErrorRenderer(true))->render(FlattenException::createFromThrowable($exception)); + $exception = FlattenException::createFromThrowable($exception); + $statusCode = $exception->getStatusCode(); + $headers = $exception->getHeaders(); + $response = (new HtmlErrorRenderer(true))->render($exception); } else { $message = htmlspecialchars($exception->getMessage(), ENT_COMPAT | ENT_SUBSTITUTE, $charset); - echo sprintf('
%s', $charset, $message); + $response = sprintf('%s', $charset, $message); } + + if (!headers_sent()) { + header(sprintf('HTTP/1.0 %s', $statusCode)); + foreach ($headers as $name => $value) { + header($name.': '.$value, false); + } + header('Content-Type: text/html; charset='.$charset); + } + + echo $response; } /** diff --git a/src/Symfony/Component/ErrorHandler/Tests/ErrorHandlerTest.php b/src/Symfony/Component/ErrorHandler/Tests/ErrorHandlerTest.php index 6f8f56ef69..b536627a02 100644 --- a/src/Symfony/Component/ErrorHandler/Tests/ErrorHandlerTest.php +++ b/src/Symfony/Component/ErrorHandler/Tests/ErrorHandlerTest.php @@ -557,6 +557,18 @@ class ErrorHandlerTest extends TestCase $handler->handleException(new \Exception()); } + public function testSendPhpResponse() + { + $handler = new ErrorHandler(); + $handler->setExceptionHandler([$handler, 'sendPhpResponse']); + + ob_start(); + $handler->handleException(new \RuntimeException('Class Foo not found')); + $response = ob_get_clean(); + + self::assertStringContainsString('Class Foo not found', $response); + } + /** * @dataProvider errorHandlerWhenLoggingProvider */ diff --git a/src/Symfony/Component/HttpClient/Chunk/DataChunk.php b/src/Symfony/Component/HttpClient/Chunk/DataChunk.php index 618112834d..37ca848541 100644 --- a/src/Symfony/Component/HttpClient/Chunk/DataChunk.php +++ b/src/Symfony/Component/HttpClient/Chunk/DataChunk.php @@ -20,8 +20,8 @@ use Symfony\Contracts\HttpClient\ChunkInterface; */ class DataChunk implements ChunkInterface { - private $offset; - private $content; + private $offset = 0; + private $content = ''; public function __construct(int $offset = 0, string $content = '') { @@ -53,6 +53,14 @@ class DataChunk implements ChunkInterface return false; } + /** + * {@inheritdoc} + */ + public function getInformationalStatus(): ?array + { + return null; + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/HttpClient/Chunk/ErrorChunk.php b/src/Symfony/Component/HttpClient/Chunk/ErrorChunk.php index d2a69bc38a..3792dccf6d 100644 --- a/src/Symfony/Component/HttpClient/Chunk/ErrorChunk.php +++ b/src/Symfony/Component/HttpClient/Chunk/ErrorChunk.php @@ -65,6 +65,15 @@ class ErrorChunk implements ChunkInterface throw new TransportException($this->errorMessage, 0, $this->error); } + /** + * {@inheritdoc} + */ + public function getInformationalStatus(): ?array + { + $this->didThrow = true; + throw new TransportException($this->errorMessage, 0, $this->error); + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/HttpClient/Chunk/InformationalChunk.php b/src/Symfony/Component/HttpClient/Chunk/InformationalChunk.php new file mode 100644 index 0000000000..c4452f15a0 --- /dev/null +++ b/src/Symfony/Component/HttpClient/Chunk/InformationalChunk.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpClient\Chunk; + +/** + * @author Nicolas Grekas
+ *
+ * @internal
+ */
+class InformationalChunk extends DataChunk
+{
+ private $status;
+
+ public function __construct(int $statusCode, array $headers)
+ {
+ $this->status = [$statusCode, $headers];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getInformationalStatus(): ?array
+ {
+ return $this->status;
+ }
+}
diff --git a/src/Symfony/Component/HttpClient/CurlHttpClient.php b/src/Symfony/Component/HttpClient/CurlHttpClient.php
index 9ecd62b086..c034d869d8 100644
--- a/src/Symfony/Component/HttpClient/CurlHttpClient.php
+++ b/src/Symfony/Component/HttpClient/CurlHttpClient.php
@@ -56,7 +56,7 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface
*
* @see HttpClientInterface::OPTIONS_DEFAULTS for available options
*/
- public function __construct(array $defaultOptions = [], int $maxHostConnections = 6, int $maxPendingPushes = 50)
+ public function __construct(array $defaultOptions = [], int $maxHostConnections = 6, int $maxPendingPushes = 0)
{
if (!\extension_loaded('curl')) {
throw new \LogicException('You cannot use the "Symfony\Component\HttpClient\CurlHttpClient" as the "curl" extension is not installed.');
@@ -106,20 +106,15 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface
$host = parse_url($authority, PHP_URL_HOST);
$url = implode('', $url);
+ if (!isset($options['normalized_headers']['user-agent'])) {
+ $options['normalized_headers']['user-agent'][] = $options['headers'][] = 'User-Agent: Symfony HttpClient/Curl';
+ }
+
if ($pushedResponse = $this->multi->pushedResponses[$url] ?? null) {
unset($this->multi->pushedResponses[$url]);
- // Accept pushed responses only if their headers related to authentication match the request
- $expectedHeaders = ['authorization', 'cookie', 'x-requested-with', 'range'];
- foreach ($expectedHeaders as $k => $v) {
- $expectedHeaders[$k] = null;
- foreach ($options['normalized_headers'][$v] ?? [] as $h) {
- $expectedHeaders[$k][] = substr($h, 2 + \strlen($v));
- }
- }
-
- if ('GET' === $method && $expectedHeaders === $pushedResponse->headers && !$options['body']) {
- $this->logger && $this->logger->debug(sprintf('Connecting request to pushed response: "%s %s"', $method, $url));
+ if (self::acceptPushForRequest($method, $options, $pushedResponse)) {
+ $this->logger && $this->logger->debug(sprintf('Accepting pushed response: "%s %s"', $method, $url));
// Reinitialize the pushed response with request's options
$pushedResponse->response->__construct($this->multi, $url, $options, $this->logger);
@@ -127,14 +122,13 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface
return $pushedResponse->response;
}
- $this->logger && $this->logger->debug(sprintf('Rejecting pushed response for "%s": authorization headers don\'t match the request', $url));
+ $this->logger && $this->logger->debug(sprintf('Rejecting pushed response: "%s".', $url));
}
$this->logger && $this->logger->info(sprintf('Request: "%s %s"', $method, $url));
$curlopts = [
CURLOPT_URL => $url,
- CURLOPT_USERAGENT => 'Symfony HttpClient/Curl',
CURLOPT_TCP_NODELAY => true,
CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
@@ -330,7 +324,7 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface
$active = 0;
while (CURLM_CALL_MULTI_PERFORM === curl_multi_exec($this->multi->handle, $active));
- foreach ($this->multi->openHandles as $ch) {
+ foreach ($this->multi->openHandles as [$ch]) {
curl_setopt($ch, CURLOPT_VERBOSE, false);
}
}
@@ -342,17 +336,17 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface
foreach ($requestHeaders as $h) {
if (false !== $i = strpos($h, ':', 1)) {
- $headers[substr($h, 0, $i)] = substr($h, 1 + $i);
+ $headers[substr($h, 0, $i)][] = substr($h, 1 + $i);
}
}
- if (!isset($headers[':method']) || !isset($headers[':scheme']) || !isset($headers[':authority']) || !isset($headers[':path']) || 'GET' !== $headers[':method'] || isset($headers['range'])) {
+ if (!isset($headers[':method']) || !isset($headers[':scheme']) || !isset($headers[':authority']) || !isset($headers[':path'])) {
$logger && $logger->debug(sprintf('Rejecting pushed response from "%s": pushed headers are invalid', $origin));
return CURL_PUSH_DENY;
}
- $url = $headers[':scheme'].'://'.$headers[':authority'];
+ $url = $headers[':scheme'][0].'://'.$headers[':authority'][0];
if ($maxPendingPushes <= \count($multi->pushedResponses)) {
$logger && $logger->debug(sprintf('Rejecting pushed response from "%s" for "%s": the queue is full', $origin, $url));
@@ -369,22 +363,43 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface
return CURL_PUSH_DENY;
}
- $url .= $headers[':path'];
+ $url .= $headers[':path'][0];
$logger && $logger->debug(sprintf('Queueing pushed response: "%s"', $url));
- $multi->pushedResponses[$url] = new PushedResponse(
- new CurlResponse($multi, $pushed),
- [
- $headers['authorization'] ?? null,
- $headers['cookie'] ?? null,
- $headers['x-requested-with'] ?? null,
- null,
- ]
- );
+ $multi->pushedResponses[$url] = new PushedResponse(new CurlResponse($multi, $pushed), $headers, $multi->openHandles[(int) $parent][1] ?? []);
return CURL_PUSH_OK;
}
+ /**
+ * Accepts pushed responses only if their headers related to authentication match the request.
+ */
+ private static function acceptPushForRequest(string $method, array $options, PushedResponse $pushedResponse): bool
+ {
+ if ($options['body'] || $method !== $pushedResponse->requestHeaders[':method'][0]) {
+ return false;
+ }
+
+ foreach (['proxy', 'no_proxy', 'bindto'] as $k) {
+ if ($options[$k] !== $pushedResponse->parentOptions[$k]) {
+ return false;
+ }
+ }
+
+ foreach (['authorization', 'cookie', 'range', 'proxy-authorization'] as $k) {
+ $normalizedHeaders = $options['normalized_headers'][$k] ?? [];
+ foreach ($normalizedHeaders as $i => $v) {
+ $normalizedHeaders[$i] = substr($v, \strlen($k) + 2);
+ }
+
+ if (($pushedResponse->requestHeaders[$k] ?? []) !== $normalizedHeaders) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
/**
* Wraps the request's body callback to allow it to return strings longer than curl requested.
*/
diff --git a/src/Symfony/Component/HttpClient/Internal/PushedResponse.php b/src/Symfony/Component/HttpClient/Internal/PushedResponse.php
index 632f0c41d0..6f8e8fda3a 100644
--- a/src/Symfony/Component/HttpClient/Internal/PushedResponse.php
+++ b/src/Symfony/Component/HttpClient/Internal/PushedResponse.php
@@ -14,7 +14,7 @@ namespace Symfony\Component\HttpClient\Internal;
use Symfony\Component\HttpClient\Response\CurlResponse;
/**
- * A pushed response with headers.
+ * A pushed response with its request headers.
*
* @author Alexander M. Turek
- *
- * @internal
*/
final class ResponseStream implements ResponseStreamInterface
{
diff --git a/src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php
index 1d1d7c10a5..4fd367fd9d 100644
--- a/src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php
+++ b/src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php
@@ -47,27 +47,22 @@ class CurlHttpClientTest extends HttpClientTestCase
}
};
- $client = new CurlHttpClient();
+ $client = new CurlHttpClient([], 6, 2);
$client->setLogger($logger);
- $index = $client->request('GET', 'https://http2-push.io');
+ $index = $client->request('GET', 'https://http2.akamai.com/');
$index->getContent();
- $css = $client->request('GET', 'https://http2-push.io/css/style.css');
- $js = $client->request('GET', 'https://http2-push.io/js/http2-push.js');
+ $css = $client->request('GET', 'https://http2.akamai.com/resources/push.css');
$css->getHeaders();
- $js->getHeaders();
$expected = [
- 'Request: "GET https://http2-push.io/"',
- 'Queueing pushed response: "https://http2-push.io/css/style.css"',
- 'Queueing pushed response: "https://http2-push.io/js/http2-push.js"',
- 'Response: "200 https://http2-push.io/"',
- 'Connecting request to pushed response: "GET https://http2-push.io/css/style.css"',
- 'Connecting request to pushed response: "GET https://http2-push.io/js/http2-push.js"',
- 'Response: "200 https://http2-push.io/css/style.css"',
- 'Response: "200 https://http2-push.io/js/http2-push.js"',
+ 'Request: "GET https://http2.akamai.com/"',
+ 'Queueing pushed response: "https://http2.akamai.com/resources/push.css"',
+ 'Response: "200 https://http2.akamai.com/"',
+ 'Accepting pushed response: "GET https://http2.akamai.com/resources/push.css"',
+ 'Response: "200 https://http2.akamai.com/resources/push.css"',
];
$this->assertSame($expected, $logger->logs);
}
diff --git a/src/Symfony/Component/HttpClient/Tests/MockHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/MockHttpClientTest.php
index 91fc21873a..542d19333b 100644
--- a/src/Symfony/Component/HttpClient/Tests/MockHttpClientTest.php
+++ b/src/Symfony/Component/HttpClient/Tests/MockHttpClientTest.php
@@ -15,6 +15,8 @@ use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\NativeHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
+use Symfony\Component\HttpClient\Response\ResponseStream;
+use Symfony\Contracts\HttpClient\ChunkInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
@@ -124,6 +126,41 @@ class MockHttpClientTest extends HttpClientTestCase
$responses[] = new MockResponse($body, ['response_headers' => $headers]);
break;
+ case 'testInformationalResponseStream':
+ $client = $this->createMock(HttpClientInterface::class);
+ $response = new MockResponse('Here the body', ['response_headers' => [
+ 'HTTP/1.1 103 ',
+ 'Link: ; rel=preload; as=style',
+ 'HTTP/1.1 200 ',
+ 'Date: foo',
+ 'Content-Length: 13',
+ ]]);
+ $client->method('request')->willReturn($response);
+ $client->method('stream')->willReturn(new ResponseStream((function () use ($response) {
+ $chunk = $this->createMock(ChunkInterface::class);
+ $chunk->method('getInformationalStatus')
+ ->willReturn([103, ['link' => ['; rel=preload; as=style', '; rel=preload; as=script']]]);
+
+ yield $response => $chunk;
+
+ $chunk = $this->createMock(ChunkInterface::class);
+ $chunk->method('isFirst')->willReturn(true);
+
+ yield $response => $chunk;
+
+ $chunk = $this->createMock(ChunkInterface::class);
+ $chunk->method('getContent')->willReturn('Here the body');
+
+ yield $response => $chunk;
+
+ $chunk = $this->createMock(ChunkInterface::class);
+ $chunk->method('isLast')->willReturn(true);
+
+ yield $response => $chunk;
+ })()));
+
+ return $client;
+
case 'testMaxDuration':
$mock = $this->getMockBuilder(ResponseInterface::class)->getMock();
$mock->expects($this->any())
diff --git a/src/Symfony/Component/HttpClient/Tests/NativeHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/NativeHttpClientTest.php
index 2d8b7b8fad..bcfab64bdc 100644
--- a/src/Symfony/Component/HttpClient/Tests/NativeHttpClientTest.php
+++ b/src/Symfony/Component/HttpClient/Tests/NativeHttpClientTest.php
@@ -20,4 +20,9 @@ class NativeHttpClientTest extends HttpClientTestCase
{
return new NativeHttpClient();
}
+
+ public function testInformationalResponseStream()
+ {
+ $this->markTestSkipped('NativeHttpClient doesn\'t support informational status codes.');
+ }
}
diff --git a/src/Symfony/Component/HttpClient/composer.json b/src/Symfony/Component/HttpClient/composer.json
index 9cb320049d..66d80bfb51 100644
--- a/src/Symfony/Component/HttpClient/composer.json
+++ b/src/Symfony/Component/HttpClient/composer.json
@@ -22,7 +22,7 @@
"require": {
"php": "^7.2.9",
"psr/log": "^1.0",
- "symfony/http-client-contracts": "^1.1.6",
+ "symfony/http-client-contracts": "^1.1.7",
"symfony/polyfill-php73": "^1.11"
},
"require-dev": {
diff --git a/src/Symfony/Component/Mailer/Tests/Transport/TransportsTest.php b/src/Symfony/Component/Mailer/Tests/Transport/TransportsTest.php
index 93f6dee9f5..10d46ca846 100644
--- a/src/Symfony/Component/Mailer/Tests/Transport/TransportsTest.php
+++ b/src/Symfony/Component/Mailer/Tests/Transport/TransportsTest.php
@@ -12,6 +12,7 @@
namespace Symfony\Component\Mailer\Tests\Transport;
use PHPUnit\Framework\TestCase;
+use Symfony\Component\Mailer\Exception\InvalidArgumentException;
use Symfony\Component\Mailer\Transport\TransportInterface;
use Symfony\Component\Mailer\Transport\Transports;
use Symfony\Component\Mime\Header\Headers;
@@ -48,4 +49,19 @@ class TransportsTest extends TestCase
$email = new Message($headers, new TextPart('...'));
$transport->send($email);
}
+
+ public function testTransportDoesNotExist()
+ {
+ $transport = new Transports([
+ 'foo' => $this->createMock(TransportInterface::class),
+ 'bar' => $this->createMock(TransportInterface::class),
+ ]);
+
+ $headers = (new Headers())->addTextHeader('X-Transport', 'foobar');
+ $email = new Message($headers, new TextPart('...'));
+
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('The "foobar" transport does not exist (available transports: "foo", "bar").');
+ $transport->send($email);
+ }
}
diff --git a/src/Symfony/Component/Mailer/Transport/Transports.php b/src/Symfony/Component/Mailer/Transport/Transports.php
index 0158d2a1f8..983ad15018 100644
--- a/src/Symfony/Component/Mailer/Transport/Transports.php
+++ b/src/Symfony/Component/Mailer/Transport/Transports.php
@@ -56,7 +56,7 @@ class Transports implements TransportInterface
$headers->remove('X-Transport');
if (!isset($this->transports[$transport])) {
- throw new InvalidArgumentException(sprintf('The "%s" transport does not exist.', $transport));
+ throw new InvalidArgumentException(sprintf('The "%s" transport does not exist (available transports: "%s").', $transport, implode('", "', array_keys($this->transports))));
}
return $this->transports[$transport]->send($message, $envelope);
diff --git a/src/Symfony/Component/Translation/DataCollectorTranslator.php b/src/Symfony/Component/Translation/DataCollectorTranslator.php
index c46e058c9e..9c51b5ad97 100644
--- a/src/Symfony/Component/Translation/DataCollectorTranslator.php
+++ b/src/Symfony/Component/Translation/DataCollectorTranslator.php
@@ -119,7 +119,7 @@ class DataCollectorTranslator implements TranslatorInterface, TranslatorBagInter
return $this->messages;
}
- private function collectMessage(?string $locale, ?string $domain, string $id, string $translation, ?array $parameters = [])
+ private function collectMessage(?string $locale, ?string $domain, ?string $id, string $translation, ?array $parameters = [])
{
if (null === $domain) {
$domain = 'messages';
diff --git a/src/Symfony/Component/Translation/Formatter/IntlFormatter.php b/src/Symfony/Component/Translation/Formatter/IntlFormatter.php
index 1d5f468d46..ad4a45b95b 100644
--- a/src/Symfony/Component/Translation/Formatter/IntlFormatter.php
+++ b/src/Symfony/Component/Translation/Formatter/IntlFormatter.php
@@ -28,6 +28,11 @@ class IntlFormatter implements IntlFormatterInterface
*/
public function formatIntl(string $message, string $locale, array $parameters = []): string
{
+ // MessageFormatter constructor throws an exception if the message is empty
+ if ('' === $message) {
+ return '';
+ }
+
if (!$formatter = $this->cache[$locale][$message] ?? null) {
if (!($this->hasMessageFormatter ?? $this->hasMessageFormatter = class_exists(\MessageFormatter::class))) {
throw new LogicException('Cannot parse message translation: please install the "intl" PHP extension or the "symfony/polyfill-intl-messageformatter" package.');
diff --git a/src/Symfony/Component/Translation/LoggingTranslator.php b/src/Symfony/Component/Translation/LoggingTranslator.php
index 2c9e97c1e9..cac8141cc5 100644
--- a/src/Symfony/Component/Translation/LoggingTranslator.php
+++ b/src/Symfony/Component/Translation/LoggingTranslator.php
@@ -107,7 +107,7 @@ class LoggingTranslator implements TranslatorInterface, TranslatorBagInterface,
/**
* Logs for missing translations.
*/
- private function log(string $id, ?string $domain, ?string $locale)
+ private function log(?string $id, ?string $domain, ?string $locale)
{
if (null === $domain) {
$domain = 'messages';
diff --git a/src/Symfony/Component/Translation/Tests/Formatter/IntlFormatterTest.php b/src/Symfony/Component/Translation/Tests/Formatter/IntlFormatterTest.php
index 45ce6d4f6e..37d982cf29 100644
--- a/src/Symfony/Component/Translation/Tests/Formatter/IntlFormatterTest.php
+++ b/src/Symfony/Component/Translation/Tests/Formatter/IntlFormatterTest.php
@@ -82,6 +82,11 @@ _MSG_;
'{0,number,integer} monkeys on {1,number,integer} trees make {2,number} monkeys per tree',
[4560, 123, 4560 / 123],
],
+ [
+ '',
+ '',
+ [],
+ ],
];
}
diff --git a/src/Symfony/Component/Translation/Tests/TranslatorTest.php b/src/Symfony/Component/Translation/Tests/TranslatorTest.php
index 20c83686fa..9666ac97ea 100644
--- a/src/Symfony/Component/Translation/Tests/TranslatorTest.php
+++ b/src/Symfony/Component/Translation/Tests/TranslatorTest.php
@@ -382,6 +382,19 @@ class TranslatorTest extends TestCase
$this->assertEquals($expected, $translator->trans($id, [], '', 'fr'));
}
+ public function testTransNullId()
+ {
+ $translator = new Translator('en');
+ $translator->addLoader('array', new ArrayLoader());
+ $translator->addResource('array', ['foo' => 'foofoo'], 'en');
+
+ $this->assertSame('', $translator->trans(null));
+
+ (\Closure::bind(function () use ($translator) {
+ $this->assertSame([], $translator->catalogues);
+ }, $this, Translator::class))();
+ }
+
public function getTransFileTests()
{
return [
@@ -403,6 +416,7 @@ class TranslatorTest extends TestCase
['Symfony est super !', 'Symfony is great!', 'Symfony est super !', [], 'fr', ''],
['Symfony est awesome !', 'Symfony is %what%!', 'Symfony est %what% !', ['%what%' => 'awesome'], 'fr', ''],
['Symfony est super !', new StringClass('Symfony is great!'), 'Symfony est super !', [], 'fr', ''],
+ ['', null, '', [], 'fr', ''],
];
}
diff --git a/src/Symfony/Component/Translation/Translator.php b/src/Symfony/Component/Translation/Translator.php
index 0ecfee378b..3e6047d672 100644
--- a/src/Symfony/Component/Translation/Translator.php
+++ b/src/Symfony/Component/Translation/Translator.php
@@ -189,11 +189,14 @@ class Translator implements TranslatorInterface, TranslatorBagInterface, LocaleA
*/
public function trans($id, array $parameters = [], $domain = null, $locale = null)
{
+ if ('' === $id = (string) $id) {
+ return '';
+ }
+
if (null === $domain) {
$domain = 'messages';
}
- $id = (string) $id;
$catalogue = $this->getCatalogue($locale);
$locale = $catalogue->getLocale();
while (!$catalogue->defines($id, $domain)) {
diff --git a/src/Symfony/Component/Validator/ConstraintValidator.php b/src/Symfony/Component/Validator/ConstraintValidator.php
index 704252f402..5bcebb7181 100644
--- a/src/Symfony/Component/Validator/ConstraintValidator.php
+++ b/src/Symfony/Component/Validator/ConstraintValidator.php
@@ -85,12 +85,10 @@ abstract class ConstraintValidator implements ConstraintValidatorInterface
*/
protected function formatValue($value, int $format = 0)
{
- $isDateTime = $value instanceof \DateTimeInterface;
-
- if (($format & self::PRETTY_DATE) && $isDateTime) {
+ if (($format & self::PRETTY_DATE) && $value instanceof \DateTimeInterface) {
if (class_exists('IntlDateFormatter')) {
$locale = \Locale::getDefault();
- $formatter = new \IntlDateFormatter($locale, \IntlDateFormatter::MEDIUM, \IntlDateFormatter::SHORT);
+ $formatter = new \IntlDateFormatter($locale, \IntlDateFormatter::MEDIUM, \IntlDateFormatter::SHORT, $value->getTimezone());
// neither the native nor the stub IntlDateFormatter support
// DateTimeImmutable as of yet
diff --git a/src/Symfony/Component/Validator/Constraints/AbstractComparisonValidator.php b/src/Symfony/Component/Validator/Constraints/AbstractComparisonValidator.php
index a6ade9c542..6c538fddc9 100644
--- a/src/Symfony/Component/Validator/Constraints/AbstractComparisonValidator.php
+++ b/src/Symfony/Component/Validator/Constraints/AbstractComparisonValidator.php
@@ -65,14 +65,14 @@ abstract class AbstractComparisonValidator extends ConstraintValidator
// This allows to compare with any date/time value supported by
// the DateTime constructor:
// https://php.net/datetime.formats
- if (\is_string($comparedValue)) {
- if ($value instanceof \DateTimeImmutable) {
- // If $value is immutable, convert the compared value to a
- // DateTimeImmutable too
- $comparedValue = new \DateTimeImmutable($comparedValue);
- } elseif ($value instanceof \DateTimeInterface) {
- // Otherwise use DateTime
- $comparedValue = new \DateTime($comparedValue);
+ if (\is_string($comparedValue) && $value instanceof \DateTimeInterface) {
+ // If $value is immutable, convert the compared value to a DateTimeImmutable too, otherwise use DateTime
+ $dateTimeClass = $value instanceof \DateTimeImmutable ? \DateTimeImmutable::class : \DateTime::class;
+
+ try {
+ $comparedValue = new $dateTimeClass($comparedValue);
+ } catch (\Exception $e) {
+ throw new ConstraintDefinitionException(sprintf('The compared value "%s" could not be converted to a "%s" instance in the "%s" constraint.', $comparedValue, $dateTimeClass, \get_class($constraint)));
}
}
diff --git a/src/Symfony/Component/Validator/Constraints/DivisibleByValidator.php b/src/Symfony/Component/Validator/Constraints/DivisibleByValidator.php
index a2def7f50c..53b8d38930 100644
--- a/src/Symfony/Component/Validator/Constraints/DivisibleByValidator.php
+++ b/src/Symfony/Component/Validator/Constraints/DivisibleByValidator.php
@@ -11,6 +11,8 @@
namespace Symfony\Component\Validator\Constraints;
+use Symfony\Component\Validator\Exception\UnexpectedValueException;
+
/**
* Validates that values are a multiple of the given number.
*
@@ -23,6 +25,14 @@ class DivisibleByValidator extends AbstractComparisonValidator
*/
protected function compareValues($value1, $value2)
{
+ if (!is_numeric($value1)) {
+ throw new UnexpectedValueException($value1, 'numeric');
+ }
+
+ if (!is_numeric($value2)) {
+ throw new UnexpectedValueException($value2, 'numeric');
+ }
+
if (!$value2 = abs($value2)) {
return false;
}
diff --git a/src/Symfony/Component/Validator/Constraints/RangeValidator.php b/src/Symfony/Component/Validator/Constraints/RangeValidator.php
index 46c5ea8e68..44f54a63c0 100644
--- a/src/Symfony/Component/Validator/Constraints/RangeValidator.php
+++ b/src/Symfony/Component/Validator/Constraints/RangeValidator.php
@@ -61,12 +61,26 @@ class RangeValidator extends ConstraintValidator
// the DateTime constructor:
// https://php.net/datetime.formats
if ($value instanceof \DateTimeInterface) {
+ $dateTimeClass = null;
+
if (\is_string($min)) {
- $min = new \DateTime($min);
+ $dateTimeClass = $value instanceof \DateTimeImmutable ? \DateTimeImmutable::class : \DateTime::class;
+
+ try {
+ $min = new $dateTimeClass($min);
+ } catch (\Exception $e) {
+ throw new ConstraintDefinitionException(sprintf('The min value "%s" could not be converted to a "%s" instance in the "%s" constraint.', $min, $dateTimeClass, \get_class($constraint)));
+ }
}
if (\is_string($max)) {
- $max = new \DateTime($max);
+ $dateTimeClass = $dateTimeClass ?: ($value instanceof \DateTimeImmutable ? \DateTimeImmutable::class : \DateTime::class);
+
+ try {
+ $max = new $dateTimeClass($max);
+ } catch (\Exception $e) {
+ throw new ConstraintDefinitionException(sprintf('The max value "%s" could not be converted to a "%s" instance in the "%s" constraint.', $max, $dateTimeClass, \get_class($constraint)));
+ }
}
}
diff --git a/src/Symfony/Component/Validator/Tests/ConstraintValidatorTest.php b/src/Symfony/Component/Validator/Tests/ConstraintValidatorTest.php
new file mode 100644
index 0000000000..96af6f13eb
--- /dev/null
+++ b/src/Symfony/Component/Validator/Tests/ConstraintValidatorTest.php
@@ -0,0 +1,65 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Validator\Tests;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintValidator;
+
+final class ConstraintValidatorTest extends TestCase
+{
+ /**
+ * @dataProvider formatValueProvider
+ */
+ public function testFormatValue($expected, $value, $format = 0)
+ {
+ $this->assertSame($expected, (new TestFormatValueConstraintValidator())->formatValueProxy($value, $format));
+ }
+
+ public function formatValueProvider()
+ {
+ $data = [
+ ['true', true],
+ ['false', false],
+ ['null', null],
+ ['resource', fopen('php://memory', 'r')],
+ ['"foo"', 'foo'],
+ ['array', []],
+ ['object', $toString = new TestToStringObject()],
+ ['ccc', $toString, ConstraintValidator::OBJECT_TO_STRING],
+ ['object', $dateTime = (new \DateTimeImmutable('@0'))->setTimezone(new \DateTimeZone('UTC'))],
+ [class_exists(\IntlDateFormatter::class) ? 'Jan 1, 1970, 12:00 AM' : '1970-01-01 00:00:00', $dateTime, ConstraintValidator::PRETTY_DATE],
+ ];
+
+ return $data;
+ }
+}
+
+final class TestFormatValueConstraintValidator extends ConstraintValidator
+{
+ public function validate($value, Constraint $constraint)
+ {
+ }
+
+ public function formatValueProxy($value, $format)
+ {
+ return $this->formatValue($value, $format);
+ }
+}
+
+final class TestToStringObject
+{
+ public function __toString()
+ {
+ return 'ccc';
+ }
+}
diff --git a/src/Symfony/Component/Validator/Tests/Constraints/AbstractComparisonValidatorTestCase.php b/src/Symfony/Component/Validator/Tests/Constraints/AbstractComparisonValidatorTestCase.php
index 944195f0f3..51ea8aecd5 100644
--- a/src/Symfony/Component/Validator/Tests/Constraints/AbstractComparisonValidatorTestCase.php
+++ b/src/Symfony/Component/Validator/Tests/Constraints/AbstractComparisonValidatorTestCase.php
@@ -13,6 +13,7 @@ namespace Symfony\Component\Validator\Tests\Constraints;
use Symfony\Component\Intl\Util\IntlTestHelper;
use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\Constraints\AbstractComparison;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
@@ -224,6 +225,31 @@ abstract class AbstractComparisonValidatorTestCase extends ConstraintValidatorTe
->assertRaised();
}
+ /**
+ * @dataProvider throwsOnInvalidStringDatesProvider
+ */
+ public function testThrowsOnInvalidStringDates(AbstractComparison $constraint, $expectedMessage, $value)
+ {
+ $this->expectException(ConstraintDefinitionException::class);
+ $this->expectExceptionMessage($expectedMessage);
+
+ $this->validator->validate($value, $constraint);
+ }
+
+ public function throwsOnInvalidStringDatesProvider(): array
+ {
+ $constraint = $this->createConstraint([
+ 'value' => 'foo',
+ ]);
+
+ $constraintClass = \get_class($constraint);
+
+ return [
+ [$constraint, sprintf('The compared value "foo" could not be converted to a "DateTimeImmutable" instance in the "%s" constraint.', $constraintClass), new \DateTimeImmutable()],
+ [$constraint, sprintf('The compared value "foo" could not be converted to a "DateTime" instance in the "%s" constraint.', $constraintClass), new \DateTime()],
+ ];
+ }
+
public function provideAllInvalidComparisons(): array
{
// The provider runs before setUp(), so we need to manually fix
diff --git a/src/Symfony/Component/Validator/Tests/Constraints/DivisibleByValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/DivisibleByValidatorTest.php
index f11fdce97b..25187bafc8 100644
--- a/src/Symfony/Component/Validator/Tests/Constraints/DivisibleByValidatorTest.php
+++ b/src/Symfony/Component/Validator/Tests/Constraints/DivisibleByValidatorTest.php
@@ -14,6 +14,7 @@ namespace Symfony\Component\Validator\Tests\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\DivisibleBy;
use Symfony\Component\Validator\Constraints\DivisibleByValidator;
+use Symfony\Component\Validator\Exception\UnexpectedValueException;
/**
* @author Colin O'Dell