Merge branch '4.4' into 5.0

* 4.4: (38 commits)
  reset the kernel cache after each test
  [HttpKernel] Ability to define multiple kernel.reset tags
  [Routing] Continue supporting single colon in object route loaders
  [FWBundle] Remove unused parameter
  [Intl] [Workflow] fixes English grammar typos
  [Filesystem] [Serializer] fixes English grammar typo
  mailer: mailchimp bridge is throwing undefined index _id when setting message id in mandrill http transport
  has_roles should be is_granted in upgrade files
  [HttpClient] Fix early cleanup of pushed HTTP/2 responses
  skip test on incompatible PHP versions
  [HttpKernel] Don't cache "not-fresh" state
  [FrameworkBundle][Cache] Don't deep-merge cache pools configuration
  [Messenger] Adding exception to amqp transport in case amqp ext is not installed
  [SecurityBundle] Don't require a user provider for the anonymous listener
  [Monolog Bridge] Fixed accessing static property as non static.
  Improve Symfony description
  [Mailer] Add UPGRADE entries about Envelope and MessageEvent
  [FrameworkBundle] fix leftover mentioning "secret:" processor
  Add DateTimeZoneNormalizer into Dependency Injection
  [Messenger] Error when specified default bus is not among the configured
  ...
This commit is contained in:
Robin Chalas 2019-11-27 00:25:11 +01:00
commit e5f0e60a44
63 changed files with 687 additions and 144 deletions

View File

@ -141,6 +141,12 @@ before_install:
(cd php-$MIN_PHP && ./configure --enable-sigchild --enable-pcntl && make -j2)
fi
- |
# Install vulcain
wget https://github.com/symfony/binary-utils/releases/download/v0.1/vulcain_0.1.3_Linux_x86_64.tar.gz -O - | tar xz
sudo mv vulcain /usr/local/bin
docker pull php:7.3-alpine
- |
# php.ini configuration
for PHP in $TRAVIS_PHP_VERSION $php_extra; do
@ -306,8 +312,14 @@ install:
PHPUNIT_X="$PHPUNIT_X,legacy"
fi
if [[ $PHP = ${MIN_PHP%.*} ]]; then
tfold src/Symfony/Component/HttpClient.h2push docker run -it --rm -v $(pwd):/app -v /usr/local/bin/vulcain:/usr/local/bin/vulcain -w /app php:7.3-alpine ./phpunit src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php --filter testHttp2Push
fi
echo "$COMPONENTS" | parallel --gnu "tfold {} $PHPUNIT_X {}"
tfold src/Symfony/Component/Console.tty $PHPUNIT --group tty
if [[ $PHP = ${MIN_PHP%.*} ]]; then
export PHP=$MIN_PHP
tfold src/Symfony/Component/Process.sigchild SYMFONY_DEPRECATIONS_HELPER=weak php-$MIN_PHP/sapi/cli/php ./phpunit --colors=always src/Symfony/Component/Process/

View File

@ -7,6 +7,13 @@ in 4.4 minor versions.
To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash
To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v4.4.0...v4.4.1
* 4.4.0 (2019-11-21)
* bug #34464 [Form] group constraints when calling the validator (nicolas-grekas)
* bug #34451 [DependencyInjection] Fix dumping multiple deprecated aliases (shyim)
* bug #34448 [Form] allow button names to start with uppercase letter (xabbuh)
* bug #34428 [Security] Fix best encoder not wired using migrate_from (chalasr)
* 4.4.0-RC1 (2019-11-17)
* bug #34419 [Cache] Disable igbinary on PHP >= 7.4 (nicolas-grekas)

View File

@ -2,7 +2,7 @@
<img src="https://symfony.com/logos/symfony_black_02.svg">
</a></p>
[Symfony][1] is a **PHP framework** for web applications and a set of reusable
[Symfony][1] is a **PHP framework** for web and console applications and a set of reusable
**PHP components**. Symfony is used by thousands of web applications (including
BlaBlaCar.com and Spotify.com) and most of the [popular PHP projects][2] (including
Drupal and Magento).

View File

@ -168,6 +168,8 @@ Mailer
------
* [BC BREAK] Changed the DSN to use for disabling delivery (using the `NullTransport`) from `smtp://null` to `null://null` (host doesn't matter).
* [BC BREAK] Renamed class `SmtpEnvelope` to `Envelope` and `DelayedSmtpEnvelope` to `DelayedEnvelope`.
* [BC BREAK] Added a required `string $transport` argument to `MessageEvent::__construct`.
Messenger
---------
@ -228,7 +230,7 @@ Security
**After**
```php
if ($this->authorizationChecker->isGranted(new Expression("has_role('ROLE_USER') or has_role('ROLE_ADMIN')"))) {}
if ($this->authorizationChecker->isGranted(new Expression("is_granted('ROLE_USER') or is_granted('ROLE_ADMIN')"))) {}
// or:
if ($this->authorizationChecker->isGranted('ROLE_USER')

View File

@ -410,7 +410,7 @@ Security
**After**
```php
if ($this->authorizationChecker->isGranted(new Expression("has_role('ROLE_USER') or has_role('ROLE_ADMIN')"))) {}
if ($this->authorizationChecker->isGranted(new Expression("is_granted('ROLE_USER') or is_granted('ROLE_ADMIN')"))) {}
// or:
if ($this->authorizationChecker->isGranted('ROLE_USER')
@ -473,6 +473,7 @@ Security
* Classes implementing the `TokenInterface` must implement the two new methods
`__serialize` and `__unserialize`
* Implementations of `Guard\AuthenticatorInterface::checkCredentials()` must return a boolean value now. Please explicitly return `false` to indicate invalid credentials.
* Removed the `has_role()` function from security expressions, use `is_granted()` instead.
SecurityBundle
--------------

View File

@ -41,7 +41,7 @@ class ChromePhpHandler extends BaseChromePhpHandler
}
if (!preg_match(static::USER_AGENT_REGEX, $event->getRequest()->headers->get('User-Agent'))) {
$this->sendHeaders = false;
self::$sendHeaders = false;
$this->headers = [];
return;
@ -59,7 +59,7 @@ class ChromePhpHandler extends BaseChromePhpHandler
*/
protected function sendHeader($header, $content): void
{
if (!$this->sendHeaders) {
if (!self::$sendHeaders) {
return;
}

View File

@ -39,7 +39,7 @@ CHANGELOG
* Added new `error_controller` configuration to handle system exceptions
* Added sort option for `translation:update` command.
* [BC Break] The `framework.messenger.routing.senders` config key is not deeply merged anymore.
* Added `secrets:*` commands and `%env(secret:...)%` processor to deal with secrets seamlessly.
* Added `secrets:*` commands to deal with secrets seamlessly.
* Made `framework.session.handler_id` accept a DSN
* Marked the `RouterDataCollector` class as `@final`.
* [BC Break] The `framework.messenger.buses.<name>.middleware` config key is not deeply merged anymore.

View File

@ -70,7 +70,6 @@ final class ContainerLintCommand extends Command
if (!$kernel->isDebug() || !(new ConfigCache($kernel->getContainer()->getParameter('debug.container.dump'), true))->isFresh()) {
$buildContainer = \Closure::bind(function () { return $this->buildContainer(); }, $kernel, \get_class($kernel));
$container = $buildContainer();
$container->getCompilerPassConfig()->setRemovingPasses([]);
} else {
(new XmlFileLoader($container = new ContainerBuilder(), new FileLocator()))->load($kernel->getContainer()->getParameter('debug.container.dump'));
}

View File

@ -64,7 +64,7 @@ EOF
{
$io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output);
$io->comment('Use <info>"%env(secret:<name>)%"</info> to reference a secret in a config file.');
$io->comment('Use <info>"%env(<name>)%"</info> to reference a secret in a config file.');
if (!$reveal = $input->getOption('reveal')) {
$io->comment(sprintf('To reveal the secrets run <info>php %s %s --reveal</info>', $_SERVER['PHP_SELF'], $this->getName()));

View File

@ -56,7 +56,7 @@ The <info>%command.name%</info> command stores a secret in the vault.
<info>%command.full_name% <name></info>
To reference secrets in services.yaml or any other config
files, use <info>"%env(secret:<name>)%"</info>.
files, use <info>"%env(<name>)%"</info>.
By default, the secret value should be entered interactively.
Alternatively, provide a file where to read the secret from:

View File

@ -19,6 +19,7 @@ use Symfony\Component\Asset\Package;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\Form\Form;
use Symfony\Component\HttpClient\HttpClient;
@ -877,6 +878,7 @@ class Configuration implements ConfigurationInterface
->end()
->children()
->arrayNode('adapters')
->performNoDeepMerging()
->info('One or more adapters to chain for creating the pool, defaults to "cache.app".')
->beforeNormalization()
->always()->then(function ($values) {
@ -1035,6 +1037,10 @@ class Configuration implements ConfigurationInterface
->ifTrue(function ($v) { return isset($v['buses']) && \count($v['buses']) > 1 && null === $v['default_bus']; })
->thenInvalid('You must specify the "default_bus" if you define more than one bus.')
->end()
->validate()
->ifTrue(static function ($v): bool { return isset($v['buses']) && null !== $v['default_bus'] && !isset($v['buses'][$v['default_bus']]); })
->then(static function (array $v): void { throw new InvalidConfigurationException(sprintf('The specified default bus "%s" is not configured. Available buses are "%s".', $v['default_bus'], implode('", "', array_keys($v['buses'])))); })
->end()
->children()
->arrayNode('routing')
->normalizeKeys(false)

View File

@ -302,7 +302,7 @@ class FrameworkExtension extends Extension
}
if ($this->messengerConfigEnabled = $this->isConfigEnabled($container, $config['messenger'])) {
$this->registerMessengerConfiguration($config['messenger'], $container, $loader, $config['serializer'], $config['validation']);
$this->registerMessengerConfiguration($config['messenger'], $container, $loader, $config['validation']);
} else {
$container->removeDefinition('console.command.messenger_consume_messages');
$container->removeDefinition('console.command.messenger_debug');
@ -1558,7 +1558,7 @@ class FrameworkExtension extends Extension
}
}
private function registerMessengerConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader, array $serializerConfig, array $validationConfig)
private function registerMessengerConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader, array $validationConfig)
{
if (!interface_exists(MessageBusInterface::class)) {
throw new LogicException('Messenger support cannot be enabled as the Messenger component is not installed. Try running "composer require symfony/messenger".');

View File

@ -40,6 +40,11 @@
<tag name="serializer.normalizer" priority="-915" />
</service>
<service id="serializer.normalizer.datetimezone" class="Symfony\Component\Serializer\Normalizer\DateTimeZoneNormalizer">
<!-- Run before serializer.normalizer.object -->
<tag name="serializer.normalizer" priority="-915" />
</service>
<service id="serializer.normalizer.dateinterval" class="Symfony\Component\Serializer\Normalizer\DateIntervalNormalizer">
<!-- Run before serializer.normalizer.object -->
<tag name="serializer.normalizer" priority="-915" />

View File

@ -314,6 +314,27 @@ class ConfigurationTest extends TestCase
);
}
public function testItErrorsWhenDefaultBusDoesNotExist()
{
$processor = new Processor();
$configuration = new Configuration(true);
$this->expectException(InvalidConfigurationException::class);
$this->expectExceptionMessage('The specified default bus "foo" is not configured. Available buses are "bar", "baz".');
$processor->processConfiguration($configuration, [
[
'messenger' => [
'default_bus' => 'foo',
'buses' => [
'bar' => null,
'baz' => null,
],
],
],
]);
}
protected static function getBundleDefaultConfig()
{
return [

View File

@ -459,8 +459,8 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
throw new InvalidConfigurationException(sprintf('Invalid firewall "%s": user provider "%s" not found.', $id, $firewall[$key]['provider']));
}
$userProvider = $providerIds[$normalizedName];
} elseif ('remember_me' === $key) {
// RememberMeFactory will use the firewall secret when created
} elseif ('remember_me' === $key || 'anonymous' === $key) {
// RememberMeFactory will use the firewall secret when created, AnonymousAuthenticationListener does not load users.
$userProvider = null;
} elseif ($defaultProvider) {
$userProvider = $defaultProvider;

View File

@ -210,7 +210,7 @@ class SecurityExtensionTest extends TestCase
$container->compile();
}
public function testPerListenerProviderWithRememberMe()
public function testPerListenerProviderWithRememberMeAndAnonymous()
{
$container = $this->getRawContainer();
$container->loadFromExtension('security', [
@ -223,6 +223,7 @@ class SecurityExtensionTest extends TestCase
'default' => [
'form_login' => ['provider' => 'second'],
'remember_me' => ['secret' => 'baz'],
'anonymous' => true,
],
],
]);

View File

@ -0,0 +1,163 @@
<?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\Bundle\WebServerBundle\Command;
use Monolog\Formatter\FormatterInterface;
use Monolog\Logger;
use Symfony\Bridge\Monolog\Formatter\ConsoleFormatter;
use Symfony\Bridge\Monolog\Handler\ConsoleHandler;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
/**
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*
* @deprecated since Symfony 4.4, to be removed in 5.0; the new Symfony local server has more features, you can use it instead.
*/
class ServerLogCommand extends Command
{
private static $bgColor = ['black', 'blue', 'cyan', 'green', 'magenta', 'red', 'white', 'yellow'];
private $el;
private $handler;
protected static $defaultName = 'server:log';
public function isEnabled()
{
if (!class_exists(ConsoleFormatter::class)) {
return false;
}
// based on a symfony/symfony package, it crashes due a missing FormatterInterface from monolog/monolog
if (!interface_exists(FormatterInterface::class)) {
return false;
}
return parent::isEnabled();
}
protected function configure()
{
if (!class_exists(ConsoleFormatter::class)) {
return;
}
$this
->addOption('host', null, InputOption::VALUE_REQUIRED, 'The server host', '0.0.0.0:9911')
->addOption('format', null, InputOption::VALUE_REQUIRED, 'The line format', ConsoleFormatter::SIMPLE_FORMAT)
->addOption('date-format', null, InputOption::VALUE_REQUIRED, 'The date format', ConsoleFormatter::SIMPLE_DATE)
->addOption('filter', null, InputOption::VALUE_REQUIRED, 'An expression to filter log. Example: "level > 200 or channel in [\'app\', \'doctrine\']"')
->setDescription('Starts a log server that displays logs in real time')
->setHelp(<<<'EOF'
<info>%command.name%</info> starts a log server to display in real time the log
messages generated by your application:
<info>php %command.full_name%</info>
To get the information as a machine readable format, use the
<comment>--filter</> option:
<info>php %command.full_name% --filter=port</info>
EOF
)
;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
@trigger_error('Using the WebserverBundle is deprecated since Symfony 4.4. The new Symfony local server has more features, you can use it instead.', E_USER_DEPRECATED);
$filter = $input->getOption('filter');
if ($filter) {
if (!class_exists(ExpressionLanguage::class)) {
throw new LogicException('Package "symfony/expression-language" is required to use the "filter" option.');
}
$this->el = new ExpressionLanguage();
}
$this->handler = new ConsoleHandler($output, true, [
OutputInterface::VERBOSITY_NORMAL => Logger::DEBUG,
]);
$this->handler->setFormatter(new ConsoleFormatter([
'format' => str_replace('\n', "\n", $input->getOption('format')),
'date_format' => $input->getOption('date-format'),
'colors' => $output->isDecorated(),
'multiline' => OutputInterface::VERBOSITY_DEBUG <= $output->getVerbosity(),
]));
if (false === strpos($host = $input->getOption('host'), '://')) {
$host = 'tcp://'.$host;
}
if (!$socket = stream_socket_server($host, $errno, $errstr)) {
throw new RuntimeException(sprintf('Server start failed on "%s": %s %s.', $host, $errstr, $errno));
}
foreach ($this->getLogs($socket) as $clientId => $message) {
$record = unserialize(base64_decode($message));
// Impossible to decode the message, give up.
if (false === $record) {
continue;
}
if ($filter && !$this->el->evaluate($filter, $record)) {
continue;
}
$this->displayLog($output, $clientId, $record);
}
return 0;
}
private function getLogs($socket): iterable
{
$sockets = [(int) $socket => $socket];
$write = [];
while (true) {
$read = $sockets;
stream_select($read, $write, $write, null);
foreach ($read as $stream) {
if ($socket === $stream) {
$stream = stream_socket_accept($socket);
$sockets[(int) $stream] = $stream;
} elseif (feof($stream)) {
unset($sockets[(int) $stream]);
fclose($stream);
} else {
yield (int) $stream => fgets($stream);
}
}
}
}
private function displayLog(OutputInterface $output, int $clientId, array $record)
{
if (isset($record['log_id'])) {
$clientId = unpack('H*', $record['log_id'])[1];
}
$logBlock = sprintf('<bg=%s> </>', self::$bgColor[$clientId % 8]);
$output->write($logBlock);
$this->handler->handle($record);
}
}

View File

@ -21,6 +21,7 @@ class TaggedIteratorArgument extends IteratorArgument
private $tag;
private $indexAttribute;
private $defaultIndexMethod;
private $defaultPriorityMethod;
private $needsIndexes = false;
/**

View File

@ -152,6 +152,10 @@ final class CheckTypeDeclarationsPass extends AbstractRecursivePass
return;
}
if ('object' === $type) {
return;
}
if (is_a($class, $type, true)) {
return;
}

View File

@ -15,12 +15,14 @@ use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Compiler\CheckTypeDeclarationsPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\Bar;
use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\BarMethodCall;
use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\BarOptionalArgument;
use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\BarOptionalArgumentNotNull;
use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\Foo;
use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\FooObject;
/**
* @author Nicolas Grekas <p@tchwork.com>
@ -390,6 +392,21 @@ class CheckTypeDeclarationsPassTest extends TestCase
$this->addToAssertionCount(1);
}
/**
* @requires PHP 7.2
*/
public function testProcessSuccessWhenPassingDefinitionForObjectType()
{
$container = new ContainerBuilder();
$container->register('foo_object', FooObject::class)
->addArgument(new Definition(Foo::class));
(new CheckTypeDeclarationsPass(true))->process($container);
$this->addToAssertionCount(1);
}
public function testProcessFactory()
{
$container = new ContainerBuilder();

View File

@ -0,0 +1,10 @@
<?php
namespace Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass;
class FooObject
{
public function __construct(object $foo)
{
}
}

View File

@ -461,7 +461,7 @@ final class Dotenv
$value = '';
}
if ('' === $value && isset($matches['default_value'])) {
if ('' === $value && isset($matches['default_value']) && '' !== $matches['default_value']) {
$unsupportedChars = strpbrk($matches['default_value'], '\'"{$');
if (false !== $unsupportedChars) {
throw $this->createFormatException(sprintf('Unsupported character "%s" found in the default value of variable "$%s".', $unsupportedChars[0], $name));

View File

@ -172,6 +172,7 @@ class DotenvTest extends TestCase
["FOO=BAR\nBAR=\${NOTDEFINED:=TEST}", ['FOO' => 'BAR', 'NOTDEFINED' => 'TEST', 'BAR' => 'TEST']],
["FOO=\nBAR=\${FOO:=TEST}", ['FOO' => 'TEST', 'BAR' => 'TEST']],
["FOO=\nBAR=\$FOO:=TEST}", ['FOO' => 'TEST', 'BAR' => 'TEST}']],
["FOO=foo\nFOOBAR=\${FOO}\${BAR}", ['FOO' => 'foo', 'FOOBAR' => 'foo']],
];
if ('\\' !== \DIRECTORY_SEPARATOR) {

View File

@ -783,7 +783,7 @@ class FilesystemTest extends FilesystemTestCase
$file = $this->workspace.\DIRECTORY_SEPARATOR.'file';
$link = $this->workspace.\DIRECTORY_SEPARATOR.'link';
// $file does not exists right now: creating "broken" links is a wanted feature
// $file does not exist right now: creating "broken" links is a wanted feature
$this->filesystem->symlink($file, $link);
$this->assertTrue(is_link($link));

View File

@ -23,6 +23,7 @@ use Symfony\Component\HttpClient\Response\ResponseStream;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
use Symfony\Contracts\Service\ResetInterface;
/**
* A performant implementation of the HttpClientInterface contracts based on the curl extension.
@ -32,7 +33,7 @@ use Symfony\Contracts\HttpClient\ResponseStreamInterface;
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface
final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface, ResetInterface
{
use HttpClientTrait;
use LoggerAwareTrait;
@ -324,9 +325,17 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface
return new ResponseStream(CurlResponse::stream($responses, $timeout));
}
public function __destruct()
public function reset()
{
if ($this->logger) {
foreach ($this->multi->pushedResponses as $url => $response) {
$this->logger->debug(sprintf('Unused pushed response: "%s"', $url));
}
}
$this->multi->pushedResponses = [];
$this->multi->dnsCache->evictions = $this->multi->dnsCache->evictions ?: $this->multi->dnsCache->removals;
$this->multi->dnsCache->removals = $this->multi->dnsCache->hostnames = [];
if (\is_resource($this->multi->handle)) {
if (\defined('CURLMOPT_PUSHFUNCTION')) {
@ -344,6 +353,11 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface
}
}
public function __destruct()
{
$this->reset();
}
private static function handlePush($parent, $pushed, array $requestHeaders, CurlClientState $multi, int $maxPendingPushes, ?LoggerInterface $logger): int
{
$headers = [];
@ -363,12 +377,6 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface
$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));
return CURL_PUSH_DENY;
}
// curl before 7.65 doesn't validate the pushed ":authority" header,
// but this is a MUST in the HTTP/2 RFC; let's restrict pushes to the original host,
// ignoring domains mentioned as alt-name in the certificate for now (same as curl).
@ -378,6 +386,12 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface
return CURL_PUSH_DENY;
}
if ($maxPendingPushes <= \count($multi->pushedResponses)) {
$fifoUrl = key($multi->pushedResponses);
unset($multi->pushedResponses[$fifoUrl]);
$logger && $logger->debug(sprintf('Evicting oldest pushed response: "%s"', $fifoUrl));
}
$url .= $headers[':path'][0];
$logger && $logger->debug(sprintf('Queueing pushed response: "%s"', $url));

View File

@ -15,11 +15,12 @@ use Symfony\Component\HttpClient\TraceableHttpClient;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
/**
* @author Jérémy Romey <jeremy@free-agent.fr>
*/
final class HttpClientDataCollector extends DataCollector
final class HttpClientDataCollector extends DataCollector implements LateDataCollectorInterface
{
/**
* @var TraceableHttpClient[]
@ -36,7 +37,7 @@ final class HttpClientDataCollector extends DataCollector
*/
public function collect(Request $request, Response $response, \Throwable $exception = null)
{
$this->initData();
$this->reset();
foreach ($this->clients as $name => $client) {
[$errorCount, $traces] = $this->collectOnClient($client);
@ -51,6 +52,13 @@ final class HttpClientDataCollector extends DataCollector
}
}
public function lateCollect()
{
foreach ($this->clients as $client) {
$client->reset();
}
}
public function getClients(): array
{
return $this->data['clients'] ?? [];
@ -66,17 +74,6 @@ final class HttpClientDataCollector extends DataCollector
return $this->data['error_count'] ?? 0;
}
/**
* {@inheritdoc}
*/
public function reset()
{
$this->initData();
foreach ($this->clients as $client) {
$client->reset();
}
}
/**
* {@inheritdoc}
*/
@ -85,7 +82,7 @@ final class HttpClientDataCollector extends DataCollector
return 'http_client';
}
private function initData()
public function reset()
{
$this->data = [
'clients' => [],

View File

@ -228,15 +228,7 @@ final class CurlResponse implements ResponseInterface
} finally {
$this->close();
// Clear local caches when the only remaining handles are about pushed responses
if (!$this->multi->openHandles) {
if ($this->logger) {
foreach ($this->multi->pushedResponses as $url => $response) {
$this->logger->debug(sprintf('Unused pushed response: "%s"', $url));
}
}
$this->multi->pushedResponses = [];
// Schedule DNS cache eviction for the next request
$this->multi->dnsCache->evictions = $this->multi->dnsCache->evictions ?: $this->multi->dnsCache->removals;
$this->multi->dnsCache->removals = $this->multi->dnsCache->hostnames = [];

View File

@ -15,13 +15,14 @@ use Symfony\Component\HttpClient\Exception\InvalidArgumentException;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
use Symfony\Contracts\Service\ResetInterface;
/**
* Auto-configure the default options based on the requested URL.
*
* @author Anthony Martin <anthony.martin@sensiolabs.com>
*/
class ScopingHttpClient implements HttpClientInterface
class ScopingHttpClient implements HttpClientInterface, ResetInterface
{
use HttpClientTrait;
@ -90,4 +91,11 @@ class ScopingHttpClient implements HttpClientInterface
{
return $this->client->stream($responses, $timeout);
}
public function reset()
{
if ($this->client instanceof ResetInterface) {
$this->client->reset();
}
}
}

View File

@ -13,13 +13,23 @@ namespace Symfony\Component\HttpClient\Tests;
use Psr\Log\AbstractLogger;
use Symfony\Component\HttpClient\CurlHttpClient;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;
use Symfony\Contracts\HttpClient\HttpClientInterface;
/*
Tests for HTTP2 Push need a recent version of both PHP and curl. This docker command should run them:
docker run -it --rm -v $(pwd):/app -v /path/to/vulcain:/usr/local/bin/vulcain -w /app php:7.3-alpine ./phpunit src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php --filter testHttp2Push
The vulcain binary can be found at https://github.com/symfony/binary-utils/releases/download/v0.1/vulcain_0.1.3_Linux_x86_64.tar.gz - see https://github.com/dunglas/vulcain for source
*/
/**
* @requires extension curl
*/
class CurlHttpClientTest extends HttpClientTestCase
{
private static $vulcainStarted = false;
protected function getHttpClient(string $testCase): HttpClientInterface
{
return new CurlHttpClient();
@ -28,7 +38,81 @@ class CurlHttpClientTest extends HttpClientTestCase
/**
* @requires PHP 7.2.17
*/
public function testHttp2Push()
public function testHttp2PushVulcain()
{
$client = $this->getVulcainClient();
$logger = new TestLogger();
$client->setLogger($logger);
$responseAsArray = $client->request('GET', 'https://127.0.0.1:3000/json', [
'headers' => [
'Preload' => '/documents/*/id',
],
])->toArray();
foreach ($responseAsArray['documents'] as $document) {
$client->request('GET', 'https://127.0.0.1:3000'.$document['id'])->toArray();
}
$client->reset();
$expected = [
'Request: "GET https://127.0.0.1:3000/json"',
'Queueing pushed response: "https://127.0.0.1:3000/json/1"',
'Queueing pushed response: "https://127.0.0.1:3000/json/2"',
'Queueing pushed response: "https://127.0.0.1:3000/json/3"',
'Response: "200 https://127.0.0.1:3000/json"',
'Accepting pushed response: "GET https://127.0.0.1:3000/json/1"',
'Response: "200 https://127.0.0.1:3000/json/1"',
'Accepting pushed response: "GET https://127.0.0.1:3000/json/2"',
'Response: "200 https://127.0.0.1:3000/json/2"',
'Accepting pushed response: "GET https://127.0.0.1:3000/json/3"',
'Response: "200 https://127.0.0.1:3000/json/3"',
];
$this->assertSame($expected, $logger->logs);
}
/**
* @requires PHP 7.2.17
*/
public function testHttp2PushVulcainWithUnusedResponse()
{
$client = $this->getVulcainClient();
$logger = new TestLogger();
$client->setLogger($logger);
$responseAsArray = $client->request('GET', 'https://127.0.0.1:3000/json', [
'headers' => [
'Preload' => '/documents/*/id',
],
])->toArray();
$i = 0;
foreach ($responseAsArray['documents'] as $document) {
$client->request('GET', 'https://127.0.0.1:3000'.$document['id'])->toArray();
if (++$i >= 2) {
break;
}
}
$client->reset();
$expected = [
'Request: "GET https://127.0.0.1:3000/json"',
'Queueing pushed response: "https://127.0.0.1:3000/json/1"',
'Queueing pushed response: "https://127.0.0.1:3000/json/2"',
'Queueing pushed response: "https://127.0.0.1:3000/json/3"',
'Response: "200 https://127.0.0.1:3000/json"',
'Accepting pushed response: "GET https://127.0.0.1:3000/json/1"',
'Response: "200 https://127.0.0.1:3000/json/1"',
'Accepting pushed response: "GET https://127.0.0.1:3000/json/2"',
'Response: "200 https://127.0.0.1:3000/json/2"',
'Unused pushed response: "https://127.0.0.1:3000/json/3"',
];
$this->assertSame($expected, $logger->logs);
}
private function getVulcainClient(): CurlHttpClient
{
if (\PHP_VERSION_ID >= 70300 && \PHP_VERSION_ID < 70304) {
$this->markTestSkipped('PHP 7.3.0 to 7.3.3 don\'t support HTTP/2 PUSH');
@ -38,32 +122,44 @@ class CurlHttpClientTest extends HttpClientTestCase
$this->markTestSkipped('curl <7.61 is used or it is not compiled with support for HTTP/2 PUSH');
}
$logger = new class() extends AbstractLogger {
public $logs = [];
$client = new CurlHttpClient(['verify_peer' => false, 'verify_host' => false]);
public function log($level, $message, array $context = []): void
{
$this->logs[] = $message;
}
};
if (static::$vulcainStarted) {
return $client;
}
$client = new CurlHttpClient([], 6, 2);
$client->setLogger($logger);
if (200 !== $client->request('GET', 'http://127.0.0.1:8057/json')->getStatusCode()) {
$this->markTestSkipped('symfony/http-client-contracts >= 2.0.1 required');
}
$index = $client->request('GET', 'https://http2.akamai.com/');
$index->getContent();
$process = new Process(['vulcain'], null, [
'DEBUG' => 1,
'UPSTREAM' => 'http://127.0.0.1:8057',
'ADDR' => ':3000',
'KEY_FILE' => __DIR__.'/Fixtures/tls/server.key',
'CERT_FILE' => __DIR__.'/Fixtures/tls/server.crt',
]);
$process->start();
$css = $client->request('GET', 'https://http2.akamai.com/resources/push.css');
register_shutdown_function([$process, 'stop']);
sleep('\\' === \DIRECTORY_SEPARATOR ? 10 : 1);
$css->getHeaders();
if (!$process->isRunning()) {
throw new ProcessFailedException($process);
}
$expected = [
'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);
static::$vulcainStarted = true;
return $client;
}
}
class TestLogger extends AbstractLogger
{
public $logs = [];
public function log($level, $message, array $context = []): void
{
$this->logs[] = $message;
}
}

View File

@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDPjCCAiYCCQDpVvfmCZt2GzANBgkqhkiG9w0BAQsFADBhMQswCQYDVQQGEwJV
UzEUMBIGA1UEBwwLR290aGFtIENpdHkxEjAQBgNVBAMMCWxvY2FsaG9zdDEoMCYG
CSqGSIb3DQEJARYZZHVuZ2xhcyttZXJjdXJlQGdtYWlsLmNvbTAeFw0xOTAxMjMx
NTUzMzlaFw0yOTAxMjAxNTUzMzlaMGExCzAJBgNVBAYTAlVTMRQwEgYDVQQHDAtH
b3RoYW0gQ2l0eTESMBAGA1UEAwwJbG9jYWxob3N0MSgwJgYJKoZIhvcNAQkBFhlk
dW5nbGFzK21lcmN1cmVAZ21haWwuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEAuKnXkBSJwOwkKfR58wP/yLYW9QFX2THoqN8iffangRmZwc5KLE6F
1S8jYMv3JGiJ95Ij3MezAfuBCdgPqqP8JrR1XwjR1RFZMOL/4U9R9OuMVng04PLw
L6TzKoEtZuExHUWFP0+5AYblgno2hoN/HVuox8m6zQrBNcbhTgDIjP5Hn491d9od
MtS3OxksDLr1UIOUGUWF7MQMN7lsN7rgT5qxoCkcAGAB4GPOA23HMt2zt4afDiI7
lAmuv8MKkTmBCcFe+q+U7o6wMxkjGstzAWRibtwzR4ejPwdO7se23MXCWGPvF16Z
tu1ip+e+waRus9o5UnyGaVPFAw8iCTC/KwIDAQABMA0GCSqGSIb3DQEBCwUAA4IB
AQB42AW7E57yOky8GpsKLoa9u7okwvvg8CQJ117X8a2MElBGnmMd9tjLa/pXAx2I
bN7jSTSadXiPNYCx4ueiJa4Dwy+C8YkwUbhRf3+mc7Chnz0SXouTjh7OUeeA06jS
W2VAR2pKB0pdJtAkXxIy21Juu8KF5uZqVq1oimgKw2lRUIMdKaqsrVwESk6u5Ojj
3DS40q9DzFnwKGCuZpspvMdWYLscotzLrCbnHp+guWDigEHS3CKzKbNo327nVg6X
7UjqqtPZ2mCsnUx3QTDJsr3gcSqhzmB+Q6I/0Q2Nx/aMmbsNegu+LC3GjFtL59Bv
B8pB/MxID0j47SwPKQghZvb3
-----END CERTIFICATE-----

View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAuKnXkBSJwOwkKfR58wP/yLYW9QFX2THoqN8iffangRmZwc5K
LE6F1S8jYMv3JGiJ95Ij3MezAfuBCdgPqqP8JrR1XwjR1RFZMOL/4U9R9OuMVng0
4PLwL6TzKoEtZuExHUWFP0+5AYblgno2hoN/HVuox8m6zQrBNcbhTgDIjP5Hn491
d9odMtS3OxksDLr1UIOUGUWF7MQMN7lsN7rgT5qxoCkcAGAB4GPOA23HMt2zt4af
DiI7lAmuv8MKkTmBCcFe+q+U7o6wMxkjGstzAWRibtwzR4ejPwdO7se23MXCWGPv
F16Ztu1ip+e+waRus9o5UnyGaVPFAw8iCTC/KwIDAQABAoIBAQCczVNGe7oRADMh
EP/wM4ghhUTvHAndWrzFkFs4fJX1UKi34ZQoFTEdOZ6f1fHwj3f/qa8cDNJar5X9
puJ+siotL3Suks2iT83dbhN63SCpiM2sqvuzu3Xp7vWwNOo5fqR2x46CmQ5uVn5S
EbZ09/mbEza5FvmwnB49rLepxY6F8P+vK5ZnCZYS2SHpOxv3U9wG8gmcHRI9ejbC
X9rwuu3oT23bfbJ0tn6Qh8O3R1kXZUUXqnxsn554cZZrXg5+ygbt4HfDVWMLpqy/
5wG0FCpU8QvjF4L8qErP7TZRrWGFtti1RtACbu9LrWvO/74v54td5V28U6kqlDJR
ff4Mi4whAoGBAOBzReQIxGwoYApPyhF+ohvF39JEEXYfkzk94t6hbgyBFBFvqdFY
shT59im2P9LyDvTd5DnCIo52Sj7vM9H80tRjAA0A8okGOczk31ABbH8aZ2orU/0G
EJe4PV4r3bpLO6DKTYsicgRpXI3aHHLvYFXOVNrQKfrKCQ+GFMVuhDdRAoGBANKe
3Dn3XOq7EW42GZey1xUxrfQRJp491KXHvjYt7z7zSiUzqN+mqIqz6ngCjJWbyQsl
Ud9N9U+4rNfYYLHQ0resjxGQRtmooOHlLhT6pEplXDgQb2SmCg2u22SKkkXA7zOV
OFbNryXgkYThsA6ih8LiKM8aFn7zttRSEeTpfye7AoGBALhIzRyiuiuXpuswgdeF
YrJs8A1jB/c1i5qXHlvurT2lCYYbaZHSQj0I0r2CvrqDNhaEzStDIz5XDzTHD4Qd
EjmBo3wJyBkLPI/nZxb4ZE2jrz8znf0EasE3a2OTnrSjqqylDa/sMzM+EtkBORSB
SFaLV45lFeKs2W2eiBVmXTZRAoGAJoA7qaz6Iz6G9SqWixB6GLm4HsFz2cFbueJF
dwn2jf9TMnG7EQcaECDLX5y3rjGIEq2DxdouWaBcmChJpLeTjVfR31gMW4Vjw2dt
gRBAMAlPTkBS3Ictl0q7eCmMi4u1Liy828FFnxrp/uxyjnpPbuSAqTsPma1bYnyO
INY+FDkCgYAe9e39/vXe7Un3ysjqDUW+0OMM+kg4ulhiopzKY+QbHiSWmUUDtvcN
asqrYiX1d59e2ZNiqrlBn86I8549St81bWSrRMNf7R+WVb79RApsABeUaEoyo3lq
0UgOBM8Nt558kaja/YfJf/jwNC1DPuu5x5t38ZcqAkqrZ/HEPkFdGQ==
-----END RSA PRIVATE KEY-----

View File

@ -14,11 +14,12 @@ namespace Symfony\Component\HttpClient;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
use Symfony\Contracts\Service\ResetInterface;
/**
* @author Jérémy Romey <jeremy@free-agent.fr>
*/
final class TraceableHttpClient implements HttpClientInterface
final class TraceableHttpClient implements HttpClientInterface, ResetInterface
{
private $client;
private $tracedRequests = [];
@ -68,6 +69,10 @@ final class TraceableHttpClient implements HttpClientInterface
public function reset()
{
if ($this->client instanceof ResetInterface) {
$this->client->reset();
}
$this->tracedRequests = [];
}
}

View File

@ -91,7 +91,10 @@ class MemoryDataCollector extends DataCollector implements LateDataCollectorInte
return 'memory';
}
private function convertToBytes(string $memoryLimit): int
/**
* @return int|float
*/
private function convertToBytes(string $memoryLimit)
{
if ('-1' === $memoryLimit) {
return -1;

View File

@ -43,16 +43,21 @@ class ResettableServicePass implements CompilerPassInterface
foreach ($container->findTaggedServiceIds($this->tagName, true) as $id => $tags) {
$services[$id] = new Reference($id, ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE);
$attributes = $tags[0];
if (!isset($attributes['method'])) {
throw new RuntimeException(sprintf('Tag %s requires the "method" attribute to be set.', $this->tagName));
foreach ($tags as $attributes) {
if (!isset($attributes['method'])) {
throw new RuntimeException(sprintf('Tag "%s" requires the "method" attribute to be set.', $this->tagName));
}
if (!isset($methods[$id])) {
$methods[$id] = [];
}
$methods[$id][] = $attributes['method'];
}
$methods[$id] = $attributes['method'];
}
if (empty($services)) {
if (!$services) {
$container->removeAlias('services_resetter');
$container->removeDefinition('services_resetter');

View File

@ -35,7 +35,9 @@ class ServicesResetter implements ResetInterface
public function reset()
{
foreach ($this->resettableServices as $id => $service) {
$service->{$this->resetMethods[$id]}();
foreach ((array) $this->resetMethods[$id] as $resetMethod) {
$service->$resetMethod();
}
}
}
}

View File

@ -377,7 +377,9 @@ class HttpCache implements HttpKernelInterface, TerminableInterface
}
// add our cached last-modified validator
$subRequest->headers->set('if_modified_since', $entry->headers->get('Last-Modified'));
if ($entry->headers->has('Last-Modified')) {
$subRequest->headers->set('if_modified_since', $entry->headers->get('Last-Modified'));
}
// Add our cached etag validator to the environment.
// We keep the etags from the client to handle the case when the client

View File

@ -433,8 +433,9 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl
try {
if (file_exists($cachePath) && \is_object($this->container = include $cachePath)
&& (!$this->debug || (self::$freshCache[$k = $cachePath.'.'.$this->environment] ?? self::$freshCache[$k] = $cache->isFresh()))
&& (!$this->debug || (self::$freshCache[$cachePath] ?? $cache->isFresh()))
) {
self::$freshCache[$cachePath] = true;
$this->container->set('kernel', $this);
error_reporting($errorLevel);

View File

@ -10,6 +10,7 @@ use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpKernel\DependencyInjection\ResettableServicePass;
use Symfony\Component\HttpKernel\DependencyInjection\ServicesResetter;
use Symfony\Component\HttpKernel\Tests\Fixtures\ClearableService;
use Symfony\Component\HttpKernel\Tests\Fixtures\MultiResettableService;
use Symfony\Component\HttpKernel\Tests\Fixtures\ResettableService;
class ResettableServicePassTest extends TestCase
@ -23,6 +24,10 @@ class ResettableServicePassTest extends TestCase
$container->register('two', ClearableService::class)
->setPublic(true)
->addTag('kernel.reset', ['method' => 'clear']);
$container->register('three', MultiResettableService::class)
->setPublic(true)
->addTag('kernel.reset', ['method' => 'resetFirst'])
->addTag('kernel.reset', ['method' => 'resetSecond']);
$container->register('services_resetter', ServicesResetter::class)
->setPublic(true)
@ -38,10 +43,12 @@ class ResettableServicePassTest extends TestCase
new IteratorArgument([
'one' => new Reference('one', ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE),
'two' => new Reference('two', ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE),
'three' => new Reference('three', ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE),
]),
[
'one' => 'reset',
'two' => 'clear',
'one' => ['reset'],
'two' => ['clear'],
'three' => ['resetFirst', 'resetSecond'],
],
],
$definition->getArguments()
@ -51,7 +58,7 @@ class ResettableServicePassTest extends TestCase
public function testMissingMethod()
{
$this->expectException('Symfony\Component\DependencyInjection\Exception\RuntimeException');
$this->expectExceptionMessage('Tag kernel.reset requires the "method" attribute to be set.');
$this->expectExceptionMessage('Tag "kernel.reset" requires the "method" attribute to be set.');
$container = new ContainerBuilder();
$container->register(ResettableService::class)
->addTag('kernel.reset');

View File

@ -14,6 +14,7 @@ namespace Symfony\Component\HttpKernel\Tests\DependencyInjection;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpKernel\DependencyInjection\ServicesResetter;
use Symfony\Component\HttpKernel\Tests\Fixtures\ClearableService;
use Symfony\Component\HttpKernel\Tests\Fixtures\MultiResettableService;
use Symfony\Component\HttpKernel\Tests\Fixtures\ResettableService;
class ServicesResetterTest extends TestCase
@ -22,6 +23,8 @@ class ServicesResetterTest extends TestCase
{
ResettableService::$counter = 0;
ClearableService::$counter = 0;
MultiResettableService::$resetFirstCounter = 0;
MultiResettableService::$resetSecondCounter = 0;
}
public function testResetServices()
@ -29,14 +32,18 @@ class ServicesResetterTest extends TestCase
$resetter = new ServicesResetter(new \ArrayIterator([
'id1' => new ResettableService(),
'id2' => new ClearableService(),
'id3' => new MultiResettableService(),
]), [
'id1' => 'reset',
'id2' => 'clear',
'id1' => ['reset'],
'id2' => ['clear'],
'id3' => ['resetFirst', 'resetSecond'],
]);
$resetter->reset();
$this->assertEquals(1, ResettableService::$counter);
$this->assertEquals(1, ClearableService::$counter);
$this->assertSame(1, ResettableService::$counter);
$this->assertSame(1, ClearableService::$counter);
$this->assertSame(1, MultiResettableService::$resetFirstCounter);
$this->assertSame(1, MultiResettableService::$resetSecondCounter);
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace Symfony\Component\HttpKernel\Tests\Fixtures;
class MultiResettableService
{
public static $resetFirstCounter = 0;
public static $resetSecondCounter = 0;
public function resetFirst()
{
++self::$resetFirstCounter;
}
public function resetSecond()
{
++self::$resetSecondCounter;
}
}

View File

@ -857,6 +857,7 @@ class HttpCacheTest extends HttpCacheTestCase
public function testValidatesCachedResponsesWithETagAndNoFreshnessInformation()
{
$this->setNextResponse(200, [], 'Hello World', function ($request, $response) {
$this->assertFalse($request->headers->has('If-Modified-Since'));
$response->headers->set('Cache-Control', 'public');
$response->headers->set('ETag', '"12345"');
if ($response->getETag() == $request->headers->get('IF_NONE_MATCH')) {

View File

@ -29,7 +29,7 @@ use Symfony\Component\HttpKernel\Tests\Fixtures\ResettableService;
class KernelTest extends TestCase
{
public static function tearDownAfterClass(): void
protected function tearDown(): void
{
$fs = new Filesystem();
$fs->remove(__DIR__.'/Fixtures/var');

View File

@ -46,7 +46,7 @@ final class Currencies extends ResourceBundle
}
/**
* @throws MissingResourceException if the currency code does not exists
* @throws MissingResourceException if the currency code does not exist
*/
public static function getName(string $currency, string $displayLocale = null): string
{
@ -78,7 +78,7 @@ final class Currencies extends ResourceBundle
}
/**
* @throws MissingResourceException if the currency code does not exists
* @throws MissingResourceException if the currency code does not exist
*/
public static function getSymbol(string $currency, string $displayLocale = null): string
{
@ -115,7 +115,7 @@ final class Currencies extends ResourceBundle
}
/**
* @throws MissingResourceException if the numeric code does not exists
* @throws MissingResourceException if the numeric code does not exist
*/
public static function forNumericCode(int $numericCode): array
{

View File

@ -49,7 +49,7 @@ final class Locales extends ResourceBundle
}
/**
* @throws MissingResourceException if the locale does not exists
* @throws MissingResourceException if the locale does not exist
*/
public static function getName(string $locale, string $displayLocale = null): string
{

View File

@ -41,7 +41,7 @@ final class Scripts extends ResourceBundle
}
/**
* @throws MissingResourceException if the script code does not exists
* @throws MissingResourceException if the script code does not exist
*/
public static function getName(string $script, string $displayLocale = null): string
{

View File

@ -60,7 +60,7 @@ class MandrillHttpTransport extends AbstractHttpTransport
throw new HttpTransportException(sprintf('Unable to send an email (code %s).', $result['code']), $response);
}
$message->setMessageId($result['_id']);
$message->setMessageId($result[0]['_id']);
return $response;
}

View File

@ -28,6 +28,9 @@ class AmqpTransportFactoryTest extends TestCase
$this->assertFalse($factory->supports('invalid-dsn', []));
}
/**
* @requires extension amqp
*/
public function testItCreatesTheTransport()
{
$factory = new AmqpTransportFactory();

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\Messenger\Tests\Transport;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Tests\Fixtures\AnEnvelopeStamp;
use Symfony\Component\Messenger\Transport\InMemoryTransport;
/**
@ -50,6 +51,19 @@ class InMemoryTransportTest extends TestCase
$this->assertSame([], $this->transport->get());
}
public function testAcknowledgeSameMessageWithDifferentStamps()
{
$envelope1 = new Envelope(new \stdClass(), [new AnEnvelopeStamp()]);
$this->transport->send($envelope1);
$envelope2 = new Envelope(new \stdClass(), [new AnEnvelopeStamp()]);
$this->transport->send($envelope2);
$this->assertSame([$envelope1, $envelope2], $this->transport->get());
$this->transport->ack($envelope1->with(new AnEnvelopeStamp()));
$this->assertSame([$envelope2], $this->transport->get());
$this->transport->reject($envelope2->with(new AnEnvelopeStamp()));
$this->assertSame([], $this->transport->get());
}
public function testAck()
{
$envelope = new Envelope(new \stdClass());

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Messenger\Transport\AmqpExt;
use Symfony\Component\Messenger\Exception\InvalidArgumentException;
use Symfony\Component\Messenger\Exception\LogicException;
/**
* An AMQP connection.
@ -58,6 +59,10 @@ class Connection
public function __construct(array $connectionOptions, array $exchangeOptions, array $queuesOptions, AmqpFactory $amqpFactory = null)
{
if (!\extension_loaded('amqp')) {
throw new LogicException(sprintf('You cannot use the "%s" as the "amqp" extension is not installed.', __CLASS__));
}
$this->connectionOptions = array_replace_recursive([
'delay' => [
'exchange_name' => 'delays',

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Messenger\Transport\Doctrine;
use Doctrine\Common\Persistence\ConnectionRegistry;
use Symfony\Bridge\Doctrine\RegistryInterface;
use Symfony\Component\Messenger\Exception\TransportException;
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
use Symfony\Component\Messenger\Transport\TransportFactoryInterface;
@ -24,8 +25,12 @@ class DoctrineTransportFactory implements TransportFactoryInterface
{
private $registry;
public function __construct(ConnectionRegistry $registry)
public function __construct($registry)
{
if (!$registry instanceof RegistryInterface && !$registry instanceof ConnectionRegistry) {
throw new \TypeError(sprintf('Expected an instance of %s or %s, but got %s.', RegistryInterface::class, ConnectionRegistry::class, \is_object($registry) ? \get_class($registry) : \gettype($registry)));
}
$this->registry = $registry;
}

View File

@ -55,7 +55,7 @@ class InMemoryTransport implements TransportInterface, ResetInterface
public function ack(Envelope $envelope): void
{
$this->acknowledged[] = $envelope;
$id = spl_object_hash($envelope);
$id = spl_object_hash($envelope->getMessage());
unset($this->queue[$id]);
}
@ -65,7 +65,7 @@ class InMemoryTransport implements TransportInterface, ResetInterface
public function reject(Envelope $envelope): void
{
$this->rejected[] = $envelope;
$id = spl_object_hash($envelope);
$id = spl_object_hash($envelope->getMessage());
unset($this->queue[$id]);
}
@ -75,7 +75,7 @@ class InMemoryTransport implements TransportInterface, ResetInterface
public function send(Envelope $envelope): Envelope
{
$this->sent[] = $envelope;
$id = spl_object_hash($envelope);
$id = spl_object_hash($envelope->getMessage());
$this->queue[$id] = $envelope;
return $envelope;

View File

@ -42,10 +42,15 @@ abstract class ObjectLoader extends Loader
*/
public function load($resource, string $type = null)
{
if (!preg_match('/^[^\:]+(?:::(?:[^\:]+))?$/', $resource)) {
if (!preg_match('/^[^\:]+(?:::?(?:[^\:]+))?$/', $resource)) {
throw new \InvalidArgumentException(sprintf('Invalid resource "%s" passed to the %s route loader: use the format "object_id::method" or "object_id" if your object class has an "__invoke" method.', $resource, \is_string($type) ? '"'.$type.'"' : 'object'));
}
if (1 === substr_count($resource, ':')) {
$resource = str_replace(':', '::', $resource);
@trigger_error(sprintf('Referencing object route loaders with a single colon is deprecated since Symfony 4.1. Use %s instead.', $resource), E_USER_DEPRECATED);
}
$parts = explode('::', $resource);
$method = $parts[1] ?? '__invoke';

View File

@ -144,7 +144,6 @@ class SwitchUserListener
try {
$this->provider->loadUserByUsername($nonExistentUsername);
throw new \LogicException('AuthenticationException expected');
} catch (AuthenticationException $e) {
}
} catch (AuthenticationException $e) {

View File

@ -27,7 +27,7 @@ trait ClassResolverTrait
*
* @param object|string $value
*
* @throws InvalidArgumentException If the class does not exists
* @throws InvalidArgumentException If the class does not exist
*/
private function getClass($value): string
{

View File

@ -287,7 +287,7 @@ abstract class Constraint
*
* @internal
*/
public function __sleep(): array
public function __sleep()
{
// Initialize "groups" option if it is not set
$this->groups;

View File

@ -362,6 +362,10 @@
<source>This password has been leaked in a data breach, it must not be used. Please use another password.</source>
<target>このパスワードは漏洩している為使用できません。</target>
</trans-unit>
<trans-unit id="94">
<source>This value should be between {{ min }} and {{ max }}.</source>
<target>{{ min }}以上{{ max }}以下でなければなりません。</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@ -177,7 +177,8 @@ abstract class ConstraintValidatorTestCase extends TestCase
->willReturn($validator);
$validator->expects($this->at(2 * $i + 1))
->method('validate')
->with($value, $this->logicalOr(null, [], $this->isInstanceOf('\Symfony\Component\Validator\Constraints\Valid')), $group);
->with($value, $this->logicalOr(null, [], $this->isInstanceOf('\Symfony\Component\Validator\Constraints\Valid')), $group)
->willReturn($validator);
}
protected function expectValidateValueAt($i, $propertyPath, $value, $constraints, $group = null)
@ -189,7 +190,8 @@ abstract class ConstraintValidatorTestCase extends TestCase
->willReturn($contextualValidator);
$contextualValidator->expects($this->at(2 * $i + 1))
->method('validate')
->with($value, $constraints, $group);
->with($value, $constraints, $group)
->willReturn($contextualValidator);
}
protected function assertNoViolation()

View File

@ -51,7 +51,7 @@ final class MethodMarkingStore implements MarkingStoreInterface
$method = 'get'.ucfirst($this->property);
if (!method_exists($subject, $method)) {
throw new LogicException(sprintf('The method "%s::%s()" does not exists.', \get_class($subject), $method));
throw new LogicException(sprintf('The method "%s::%s()" does not exist.', \get_class($subject), $method));
}
$marking = $subject->{$method}();
@ -81,7 +81,7 @@ final class MethodMarkingStore implements MarkingStoreInterface
$method = 'set'.ucfirst($this->property);
if (!method_exists($subject, $method)) {
throw new LogicException(sprintf('The method "%s::%s()" does not exists.', \get_class($subject), $method));
throw new LogicException(sprintf('The method "%s::%s()" does not exist.', \get_class($subject), $method));
}
$subject->{$method}($marking, $context);

View File

@ -5,6 +5,7 @@ namespace Symfony\Component\Workflow\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\Workflow\Event\GuardEvent;
use Symfony\Component\Workflow\Exception\NotEnabledTransitionException;
use Symfony\Component\Workflow\StateMachine;
use Symfony\Component\Workflow\TransitionBlocker;
@ -84,27 +85,52 @@ class StateMachineTest extends TestCase
$subject = new Subject();
// There may be multiple transitions with the same name. Make sure that transitions
// that are not enabled by the marking are evaluated.
// that are enabled by the marking are evaluated.
// see https://github.com/symfony/symfony/issues/28432
// Test if when you are in place "a"trying transition "t1" then returned
// Test if when you are in place "a" and trying to apply "t1" then it returns
// blocker list contains guard blocker instead blockedByMarking
$subject->setMarking('a');
$transitionBlockerList = $net->buildTransitionBlockerList($subject, 't1');
$this->assertCount(1, $transitionBlockerList);
$blockers = iterator_to_array($transitionBlockerList);
$this->assertSame('Transition blocker of place a', $blockers[0]->getMessage());
$this->assertSame('blocker', $blockers[0]->getCode());
// Test if when you are in place "d" trying transition "t1" then
// returned blocker list contains guard blocker instead blockedByMarking
// Test if when you are in place "d" and trying to apply "t1" then
// it returns blocker list contains guard blocker instead blockedByMarking
$subject->setMarking('d');
$transitionBlockerList = $net->buildTransitionBlockerList($subject, 't1');
$this->assertCount(1, $transitionBlockerList);
$blockers = iterator_to_array($transitionBlockerList);
$this->assertSame('Transition blocker of place d', $blockers[0]->getMessage());
$this->assertSame('blocker', $blockers[0]->getCode());
}
public function testApplyReturnsExpectedReasonOnBranchMerge()
{
$definition = $this->createComplexStateMachineDefinition();
$dispatcher = new EventDispatcher();
$net = new StateMachine($definition, null, $dispatcher);
$dispatcher->addListener('workflow.guard', function (GuardEvent $event) {
$event->addTransitionBlocker(new TransitionBlocker(sprintf('Transition blocker of place %s', $event->getTransition()->getFroms()[0]), 'blocker'));
});
$subject = new Subject();
// There may be multiple transitions with the same name. Make sure that all transitions
// that are enabled by the marking are evaluated.
// see https://github.com/symfony/symfony/issues/34489
try {
$net->apply($subject, 't1');
$this->fail();
} catch (NotEnabledTransitionException $e) {
$blockers = iterator_to_array($e->getTransitionBlockerList());
$this->assertSame('Transition blocker of place a', $blockers[0]->getMessage());
$this->assertSame('blocker', $blockers[0]->getCode());
}
}
}

View File

@ -154,25 +154,47 @@ class Workflow implements WorkflowInterface
{
$marking = $this->getMarking($subject);
$transitionBlockerList = null;
$applied = false;
$approvedTransitionQueue = [];
$transitionExist = false;
$approvedTransitions = [];
$bestTransitionBlockerList = null;
foreach ($this->definition->getTransitions() as $transition) {
if ($transition->getName() !== $transitionName) {
continue;
}
$transitionBlockerList = $this->buildTransitionBlockerListForTransition($subject, $marking, $transition);
if (!$transitionBlockerList->isEmpty()) {
$transitionExist = true;
$tmpTransitionBlockerList = $this->buildTransitionBlockerListForTransition($subject, $marking, $transition);
if ($tmpTransitionBlockerList->isEmpty()) {
$approvedTransitions[] = $transition;
continue;
}
$approvedTransitionQueue[] = $transition;
if (!$bestTransitionBlockerList) {
$bestTransitionBlockerList = $tmpTransitionBlockerList;
continue;
}
// We prefer to return transitions blocker by something else than
// marking. Because it means the marking was OK. Transitions are
// deterministic: it's not possible to have many transitions enabled
// at the same time that match the same marking with the same name
if (!$tmpTransitionBlockerList->has(TransitionBlocker::BLOCKED_BY_MARKING)) {
$bestTransitionBlockerList = $tmpTransitionBlockerList;
}
}
foreach ($approvedTransitionQueue as $transition) {
$applied = true;
if (!$transitionExist) {
throw new UndefinedTransitionException($subject, $transitionName, $this);
}
if (!$approvedTransitions) {
throw new NotEnabledTransitionException($subject, $transitionName, $this, $bestTransitionBlockerList);
}
foreach ($approvedTransitions as $transition) {
$this->leave($subject, $transition, $marking);
$context = $this->transition($subject, $transition, $marking, $context);
@ -188,14 +210,6 @@ class Workflow implements WorkflowInterface
$this->announce($subject, $transition, $marking);
}
if (!$transitionBlockerList) {
throw new UndefinedTransitionException($subject, $transitionName, $this);
}
if (!$applied) {
throw new NotEnabledTransitionException($subject, $transitionName, $this, $transitionBlockerList);
}
return $marking;
}

View File

@ -155,6 +155,27 @@ switch ($vars['REQUEST_URI']) {
usleep(500);
}
exit;
case '/json':
header("Content-Type: application/json");
echo json_encode([
'documents' => [
['id' => '/json/1'],
['id' => '/json/2'],
['id' => '/json/3'],
],
]);
exit;
case '/json/1':
case '/json/2':
case '/json/3':
header("Content-Type: application/json");
echo json_encode([
'title' => $vars['REQUEST_URI'],
]);
exit;
}
header('Content-Type: application/json', true);

View File

@ -24,8 +24,6 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
*/
abstract class HttpClientTestCase extends TestCase
{
private static $server;
public static function setUpBeforeClass(): void
{
TestHttpServer::start();

View File

@ -19,31 +19,22 @@ use Symfony\Component\Process\Process;
*/
class TestHttpServer
{
private static $server;
private static $started;
public static function start()
{
if (null !== self::$server) {
if (self::$started) {
return;
}
$finder = new PhpExecutableFinder();
$process = new Process(array_merge([$finder->find(false)], $finder->findArguments(), ['-dopcache.enable=0', '-dvariables_order=EGPCS', '-S', '127.0.0.1:8057']));
$process->setWorkingDirectory(__DIR__.'/Fixtures/web');
$process->setTimeout(300);
$process->start();
self::$server = new class() {
public $process;
public function __destruct()
{
$this->process->stop();
}
};
self::$server->process = $process;
register_shutdown_function([$process, 'stop']);
sleep('\\' === \DIRECTORY_SEPARATOR ? 10 : 1);
self::$started = true;
}
}