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:
commit
2ec3e47001
2
.github/patch-types.php
vendored
2
.github/patch-types.php
vendored
@ -32,4 +32,4 @@ foreach ($loader->getClassMap() as $class => $file) {
|
||||
class_exists($class);
|
||||
}
|
||||
|
||||
Symfony\Component\ErrorHandler\DebugClassLoader::disable();
|
||||
Symfony\Component\ErrorHandler\DebugClassLoader::checkClasses();
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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
|
||||
-----
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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());
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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}
|
||||
*/
|
||||
|
@ -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}
|
||||
*/
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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.
|
||||
*/
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -17,8 +17,6 @@ use Symfony\Contracts\HttpClient\ResponseStreamInterface;
|
||||
|
||||
/**
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class ResponseStream implements ResponseStreamInterface
|
||||
{
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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())
|
||||
|
@ -20,4 +20,9 @@ class NativeHttpClientTest extends HttpClientTestCase
|
||||
{
|
||||
return new NativeHttpClient();
|
||||
}
|
||||
|
||||
public function testInformationalResponseStream()
|
||||
{
|
||||
$this->markTestSkipped('NativeHttpClient doesn\'t support informational status codes.');
|
||||
}
|
||||
}
|
||||
|
@ -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": {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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';
|
||||
|
@ -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.');
|
||||
|
@ -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';
|
||||
|
@ -82,6 +82,11 @@ _MSG_;
|
||||
'{0,number,integer} monkeys on {1,number,integer} trees make {2,number} monkeys per tree',
|
||||
[4560, 123, 4560 / 123],
|
||||
],
|
||||
[
|
||||
'',
|
||||
'',
|
||||
[],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -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', ''],
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -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)) {
|
||||
|
@ -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
|
||||
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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';
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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);
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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);
|
||||
|
Reference in New Issue
Block a user