Merge branch '4.4'

* 4.4:
  [HttpClient] Fix a bug preventing Server Pushes to be handled properly
  [HttpClient] fix support for 103 Early Hints and other informational status codes
  fix typo
  [DI] fix failure
  [Validator] Add ConstraintValidator::formatValue() tests
  [HttpClient] improve handling of HTTP/2 PUSH
  Fix #33427
  lint all templates from configured Twig paths if no argument was provided
  Nullable message id?
  [Validator] Only handle numeric values in DivisibleBy
  [Validator] Sync string to date behavior and throw a better exception
  Check phpunit configuration for listeners
  registering basic exception handler for late failures
  [DI] fix support for "!tagged_locator foo"
  [Mailer] Add a more precise exception
  [ErrorHandler][Bridge/PhpUnit] display deprecations for not-autoloaded classes
This commit is contained in:
Nicolas Grekas 2019-09-03 23:48:54 +02:00
commit 2ec3e47001
52 changed files with 597 additions and 134 deletions

View File

@ -32,4 +32,4 @@ foreach ($loader->getClassMap() as $class => $file) {
class_exists($class);
}
Symfony\Component\ErrorHandler\DebugClassLoader::disable();
Symfony\Component\ErrorHandler\DebugClassLoader::checkClasses();

View File

@ -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();

View File

@ -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();

View File

@ -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();

View File

@ -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);

View File

@ -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
-----

View File

@ -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);

View File

@ -124,7 +124,7 @@
{{- block('form_widget_simple') -}}
{%- set label_attr = label_attr|merge({ class: (label_attr.class|default('') ~ ' custom-file-label')|trim }) -%}
<label for="{{ form.vars.id }}" {% with { attr: label_attr } %}{{ block('attributes') }}{% endwith %}>
{%- if attr.placeholder is defined -%}
{%- if attr.placeholder is defined and attr.placeholder is not none -%}
{{- translation_domain is same as(false) ? attr.placeholder : attr.placeholder|trans({}, translation_domain) -}}
{%- endif -%}
</label>

View File

@ -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);

View File

@ -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));
}

View File

@ -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) {

View File

@ -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());

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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('<!DOCTYPE html><html><head><meta charset="%s" /><meta name="robots" content="noindex,nofollow" /></head><body>%s</body></html>', $charset, $message);
$response = sprintf('<!DOCTYPE html><html><head><meta charset="%s" /><meta name="robots" content="noindex,nofollow" /></head><body>%s</body></html>', $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;
}
/**

View File

@ -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
*/

View File

@ -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}
*/

View File

@ -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}
*/

View File

@ -0,0 +1,35 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <p@tchwork.com>
*
* @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;
}
}

View File

@ -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.
*/

View File

@ -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 <me@derrabus.de>
*
@ -22,15 +22,17 @@ use Symfony\Component\HttpClient\Response\CurlResponse;
*/
final class PushedResponse
{
/** @var CurlResponse */
public $response;
/** @var string[] */
public $headers;
public $requestHeaders;
public function __construct(CurlResponse $response, array $headers)
public $parentOptions = [];
public function __construct(CurlResponse $response, array $requestHeaders, array $parentOptions)
{
$this->response = $response;
$this->headers = $headers;
$this->requestHeaders = $requestHeaders;
$this->parentOptions = $parentOptions;
}
}

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\HttpClient\Response;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpClient\Chunk\FirstChunk;
use Symfony\Component\HttpClient\Chunk\InformationalChunk;
use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Component\HttpClient\Internal\CurlClientState;
use Symfony\Contracts\HttpClient\ResponseInterface;
@ -120,9 +121,6 @@ final class CurlResponse implements ResponseInterface
if (\in_array($waitFor, ['headers', 'destruct'], true)) {
try {
if (\defined('CURLOPT_STREAM_WEIGHT')) {
curl_setopt($ch, CURLOPT_STREAM_WEIGHT, 32);
}
self::stream([$response])->current();
} catch (\Throwable $e) {
// Persist timeouts thrown during initialization
@ -140,7 +138,7 @@ final class CurlResponse implements ResponseInterface
};
// Schedule the request in a non-blocking way
$multi->openHandles[$id] = $ch;
$multi->openHandles[$id] = [$ch, $options];
curl_multi_add_handle($multi->handle, $ch);
self::perform($multi);
}
@ -314,8 +312,11 @@ final class CurlResponse implements ResponseInterface
return \strlen($data);
}
// End of headers: handle redirects and add to the activity list
// End of headers: handle informational responses, redirects, etc.
if (200 > $statusCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE)) {
$multi->handlesActivity[$id][] = new InformationalChunk($statusCode, $headers);
return \strlen($data);
}
@ -342,7 +343,7 @@ final class CurlResponse implements ResponseInterface
if ($statusCode < 300 || 400 <= $statusCode || curl_getinfo($ch, CURLINFO_REDIRECT_COUNT) === $options['max_redirects']) {
// Headers and redirects completed, time to get the response's body
$multi->handlesActivity[$id] = [new FirstChunk()];
$multi->handlesActivity[$id][] = new FirstChunk();
if ('destruct' === $waitFor) {
return 0;

View File

@ -45,7 +45,7 @@ class MockResponse implements ResponseInterface
public function __construct($body = '', array $info = [])
{
$this->body = is_iterable($body) ? $body : (string) $body;
$this->info = $info + $this->info;
$this->info = $info + ['http_code' => 200] + $this->info;
if (!isset($info['response_headers'])) {
return;
@ -59,7 +59,8 @@ class MockResponse implements ResponseInterface
}
}
$this->info['response_headers'] = $responseHeaders;
$this->info['response_headers'] = [];
self::addResponseHeaders($responseHeaders, $this->info, $this->headers);
}
/**

View File

@ -17,8 +17,6 @@ use Symfony\Contracts\HttpClient\ResponseStreamInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
final class ResponseStream implements ResponseStreamInterface
{

View File

@ -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);
}

View File

@ -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: </style.css>; 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' => ['</style.css>; rel=preload; as=style', '</script.js>; 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())

View File

@ -20,4 +20,9 @@ class NativeHttpClientTest extends HttpClientTestCase
{
return new NativeHttpClient();
}
public function testInformationalResponseStream()
{
$this->markTestSkipped('NativeHttpClient doesn\'t support informational status codes.');
}
}

View File

@ -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": {

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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';

View File

@ -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.');

View File

@ -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';

View File

@ -82,6 +82,11 @@ _MSG_;
'{0,number,integer} monkeys on {1,number,integer} trees make {2,number} monkeys per tree',
[4560, 123, 4560 / 123],
],
[
'',
'',
[],
],
];
}

View File

@ -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', ''],
];
}

View File

@ -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)) {

View File

@ -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

View File

@ -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)));
}
}

View File

@ -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;
}

View File

@ -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)));
}
}
}

View File

@ -0,0 +1,65 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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';
}
}

View File

@ -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

View File

@ -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 <colinodell@gmail.com>
@ -76,4 +77,25 @@ class DivisibleByValidatorTest extends AbstractComparisonValidatorTestCase
['22', '"22"', '10', '"10"', 'string'],
];
}
/**
* @dataProvider throwsOnNonNumericValuesProvider
*/
public function testThrowsOnNonNumericValues(string $expectedGivenType, $value, $comparedValue)
{
$this->expectException(UnexpectedValueException::class);
$this->expectExceptionMessage(sprintf('Expected argument of type "numeric", "%s" given', $expectedGivenType));
$this->validator->validate($value, $this->createConstraint([
'value' => $comparedValue,
]));
}
public function throwsOnNonNumericValuesProvider()
{
return [
[\stdClass::class, 2, new \stdClass()],
[\ArrayIterator::class, new \ArrayIterator(), 12],
];
}
}

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Validator\Tests\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\AbstractComparison;
use Symfony\Component\Validator\Constraints\PositiveOrZero;
/**
@ -99,11 +100,11 @@ class GreaterThanOrEqualValidatorWithPositiveOrZeroConstraintTest extends Greate
}
/**
* @dataProvider provideValidComparisonsToPropertyPath
* @dataProvider throwsOnInvalidStringDatesProvider
*/
public function testValidComparisonToPropertyPathOnArray($comparedValue)
public function testThrowsOnInvalidStringDates(AbstractComparison $constraint, $expectedMessage, $value)
{
$this->markTestSkipped('PropertyPath option is not used in Positive constraint');
$this->markTestSkipped('The compared value cannot be an invalid string date because it is hardcoded to 0.');
}
public function testInvalidComparisonToPropertyPathAddsPathAsParameter()

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Validator\Tests\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\AbstractComparison;
use Symfony\Component\Validator\Constraints\Positive;
/**
@ -102,11 +103,11 @@ class GreaterThanValidatorWithPositiveConstraintTest extends GreaterThanValidato
}
/**
* @dataProvider provideValidComparisonsToPropertyPath
* @dataProvider throwsOnInvalidStringDatesProvider
*/
public function testValidComparisonToPropertyPathOnArray($comparedValue)
public function testThrowsOnInvalidStringDates(AbstractComparison $constraint, $expectedMessage, $value)
{
$this->markTestSkipped('PropertyPath option is not used in Positive constraint');
$this->markTestSkipped('The compared value cannot be an invalid string date because it is hardcoded to 0.');
}
public function testInvalidComparisonToPropertyPathAddsPathAsParameter()

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Validator\Tests\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\AbstractComparison;
use Symfony\Component\Validator\Constraints\NegativeOrZero;
/**
@ -102,11 +103,11 @@ class LessThanOrEqualValidatorWithNegativeOrZeroConstraintTest extends LessThanO
}
/**
* @dataProvider provideValidComparisonsToPropertyPath
* @dataProvider throwsOnInvalidStringDatesProvider
*/
public function testValidComparisonToPropertyPathOnArray($comparedValue)
public function testThrowsOnInvalidStringDates(AbstractComparison $constraint, $expectedMessage, $value)
{
$this->markTestSkipped('PropertyPath option is not used in NegativeOrZero constraint');
$this->markTestSkipped('The compared value cannot be an invalid string date because it is hardcoded to 0.');
}
public function testInvalidComparisonToPropertyPathAddsPathAsParameter()

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Validator\Tests\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\AbstractComparison;
use Symfony\Component\Validator\Constraints\Negative;
/**
@ -102,11 +103,11 @@ class LessThanValidatorWithNegativeConstraintTest extends LessThanValidatorTest
}
/**
* @dataProvider provideValidComparisonsToPropertyPath
* @dataProvider throwsOnInvalidStringDatesProvider
*/
public function testValidComparisonToPropertyPathOnArray($comparedValue)
public function testThrowsOnInvalidStringDates(AbstractComparison $constraint, $expectedMessage, $value)
{
$this->markTestSkipped('PropertyPath option is not used in Positive constraint');
$this->markTestSkipped('The compared value cannot be an invalid string date because it is hardcoded to 0.');
}
public function testInvalidComparisonToPropertyPathAddsPathAsParameter()

View File

@ -14,6 +14,7 @@ namespace Symfony\Component\Validator\Tests\Constraints;
use Symfony\Component\Intl\Util\IntlTestHelper;
use Symfony\Component\Validator\Constraints\Range;
use Symfony\Component\Validator\Constraints\RangeValidator;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
class RangeValidatorTest extends ConstraintValidatorTestCase
@ -390,6 +391,31 @@ class RangeValidatorTest extends ConstraintValidatorTestCase
->assertRaised();
}
/**
* @dataProvider throwsOnInvalidStringDatesProvider
*/
public function testThrowsOnInvalidStringDates($expectedMessage, $value, $min, $max)
{
$this->expectException(ConstraintDefinitionException::class);
$this->expectExceptionMessage($expectedMessage);
$this->validator->validate($value, new Range([
'min' => $min,
'max' => $max,
]));
}
public function throwsOnInvalidStringDatesProvider(): array
{
return [
['The min value "foo" could not be converted to a "DateTimeImmutable" instance in the "Symfony\Component\Validator\Constraints\Range" constraint.', new \DateTimeImmutable(), 'foo', null],
['The min value "foo" could not be converted to a "DateTime" instance in the "Symfony\Component\Validator\Constraints\Range" constraint.', new \DateTime(), 'foo', null],
['The max value "foo" could not be converted to a "DateTimeImmutable" instance in the "Symfony\Component\Validator\Constraints\Range" constraint.', new \DateTimeImmutable(), null, 'foo'],
['The max value "foo" could not be converted to a "DateTime" instance in the "Symfony\Component\Validator\Constraints\Range" constraint.', new \DateTime(), null, 'foo'],
['The min value "bar" could not be converted to a "DateTimeImmutable" instance in the "Symfony\Component\Validator\Constraints\Range" constraint.', new \DateTimeImmutable(), 'bar', 'ccc'],
];
}
public function testNoViolationOnNullObjectWithPropertyPaths()
{
$this->setObject(null);

View File

@ -47,6 +47,13 @@ interface ChunkInterface
*/
public function isLast(): bool;
/**
* Returns a [status code, headers] tuple when a 1xx status code was just received.
*
* @throws TransportExceptionInterface on a network error or when the idle timeout is reached
*/
public function getInformationalStatus(): ?array;
/**
* Returns the content of the response chunk.
*

View File

@ -754,6 +754,27 @@ abstract class HttpClientTestCase extends TestCase
$this->assertSame(200, $response->getStatusCode());
}
public function testInformationalResponseStream()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('GET', 'http://localhost:8057/103');
$chunks = [];
foreach ($client->stream($response) as $chunk) {
$chunks[] = $chunk;
}
$this->assertSame(103, $chunks[0]->getInformationalStatus()[0]);
$this->assertSame(['</style.css>; rel=preload; as=style', '</script.js>; rel=preload; as=script'], $chunks[0]->getInformationalStatus()[1]['link']);
$this->assertTrue($chunks[1]->isFirst());
$this->assertSame('Here the body', $chunks[2]->getContent());
$this->assertTrue($chunks[3]->isLast());
$this->assertNull($chunks[3]->getInformationalStatus());
$this->assertSame(['date', 'content-length'], array_keys($response->getHeaders()));
$this->assertContains('Link: </style.css>; rel=preload; as=style', $response->getInfo('response_headers'));
}
/**
* @requires extension zlib
*/

View File

@ -43,7 +43,9 @@ trait TranslatorTrait
*/
public function trans($id, array $parameters = [], $domain = null, $locale = null)
{
$id = (string) $id;
if ('' === $id = (string) $id) {
return '';
}
if (!isset($parameters['%count%']) || !is_numeric($parameters['%count%'])) {
return strtr($id, $parameters);