diff --git a/.travis.yml b/.travis.yml index 4638be7405..3123158d09 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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/ diff --git a/CHANGELOG-4.4.md b/CHANGELOG-4.4.md index 0f975d9e83..64e77d4a4d 100644 --- a/CHANGELOG-4.4.md +++ b/CHANGELOG-4.4.md @@ -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) diff --git a/README.md b/README.md index 5796b1acd7..da9e6156c0 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@

-[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). diff --git a/UPGRADE-4.4.md b/UPGRADE-4.4.md index 1d1863e4f8..4bb7edc5d9 100644 --- a/UPGRADE-4.4.md +++ b/UPGRADE-4.4.md @@ -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') diff --git a/UPGRADE-5.0.md b/UPGRADE-5.0.md index 9dbd28e2a3..8a0b3ebe27 100644 --- a/UPGRADE-5.0.md +++ b/UPGRADE-5.0.md @@ -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 -------------- diff --git a/src/Symfony/Bridge/Monolog/Handler/ChromePhpHandler.php b/src/Symfony/Bridge/Monolog/Handler/ChromePhpHandler.php index b1d9fcd51e..e7049c4b61 100644 --- a/src/Symfony/Bridge/Monolog/Handler/ChromePhpHandler.php +++ b/src/Symfony/Bridge/Monolog/Handler/ChromePhpHandler.php @@ -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; } diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index bf4c4e9a7d..8188769bdd 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -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..middleware` config key is not deeply merged anymore. diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php index c5cba6c9a5..cba4ac5840 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php @@ -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')); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsListCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsListCommand.php index 1b0fbdf4ce..cc322847d0 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsListCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsListCommand.php @@ -64,7 +64,7 @@ EOF { $io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output); - $io->comment('Use "%env(secret:)%" to reference a secret in a config file.'); + $io->comment('Use "%env()%" to reference a secret in a config file.'); if (!$reveal = $input->getOption('reveal')) { $io->comment(sprintf('To reveal the secrets run php %s %s --reveal', $_SERVER['PHP_SELF'], $this->getName())); diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsSetCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsSetCommand.php index 555d616712..5cca8d7011 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsSetCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsSetCommand.php @@ -56,7 +56,7 @@ The %command.name% command stores a secret in the vault. %command.full_name% To reference secrets in services.yaml or any other config -files, use "%env(secret:)%". +files, use "%env()%". By default, the secret value should be entered interactively. Alternatively, provide a file where to read the secret from: diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 952079cd17..9d6e9e9645 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -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) diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index c4a77c5567..d043b4e248 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -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".'); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml index 4698c50593..ec7d286fd3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml @@ -40,6 +40,11 @@ + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php index 9c3ab3cd8a..8d8e3a3a9e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -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 [ diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 9b54e1f914..b2b064b8cb 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -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; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php index 8979ea8ded..4b6c05bfb3 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php @@ -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, ], ], ]); diff --git a/src/Symfony/Bundle/WebServerBundle/Command/ServerLogCommand.php b/src/Symfony/Bundle/WebServerBundle/Command/ServerLogCommand.php new file mode 100644 index 0000000000..f38fd6fa58 --- /dev/null +++ b/src/Symfony/Bundle/WebServerBundle/Command/ServerLogCommand.php @@ -0,0 +1,163 @@ + + * + * 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 + * + * @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' +%command.name% starts a log server to display in real time the log +messages generated by your application: + + php %command.full_name% + +To get the information as a machine readable format, use the +--filter option: + +php %command.full_name% --filter=port +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(' ', self::$bgColor[$clientId % 8]); + $output->write($logBlock); + + $this->handler->handle($record); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Argument/TaggedIteratorArgument.php b/src/Symfony/Component/DependencyInjection/Argument/TaggedIteratorArgument.php index 1a2518812f..d1d5f6d8a4 100644 --- a/src/Symfony/Component/DependencyInjection/Argument/TaggedIteratorArgument.php +++ b/src/Symfony/Component/DependencyInjection/Argument/TaggedIteratorArgument.php @@ -21,6 +21,7 @@ class TaggedIteratorArgument extends IteratorArgument private $tag; private $indexAttribute; private $defaultIndexMethod; + private $defaultPriorityMethod; private $needsIndexes = false; /** diff --git a/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeDeclarationsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeDeclarationsPass.php index 2147d53f12..ccd11e9d48 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeDeclarationsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeDeclarationsPass.php @@ -152,6 +152,10 @@ final class CheckTypeDeclarationsPass extends AbstractRecursivePass return; } + if ('object' === $type) { + return; + } + if (is_a($class, $type, true)) { return; } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckTypeDeclarationsPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckTypeDeclarationsPassTest.php index 51bc7c6779..b012a65625 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckTypeDeclarationsPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckTypeDeclarationsPassTest.php @@ -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 @@ -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(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/FooObject.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/FooObject.php new file mode 100644 index 0000000000..44cfaf0d0a --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/FooObject.php @@ -0,0 +1,10 @@ +createFormatException(sprintf('Unsupported character "%s" found in the default value of variable "$%s".', $unsupportedChars[0], $name)); diff --git a/src/Symfony/Component/Dotenv/Tests/DotenvTest.php b/src/Symfony/Component/Dotenv/Tests/DotenvTest.php index ed54d2a689..489c7fa6d1 100644 --- a/src/Symfony/Component/Dotenv/Tests/DotenvTest.php +++ b/src/Symfony/Component/Dotenv/Tests/DotenvTest.php @@ -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) { diff --git a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php index bb1484ab67..d4299da4d9 100644 --- a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php +++ b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php @@ -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)); diff --git a/src/Symfony/Component/HttpClient/CurlHttpClient.php b/src/Symfony/Component/HttpClient/CurlHttpClient.php index 5e9fe4221b..0f872dfa97 100644 --- a/src/Symfony/Component/HttpClient/CurlHttpClient.php +++ b/src/Symfony/Component/HttpClient/CurlHttpClient.php @@ -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 */ -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)); diff --git a/src/Symfony/Component/HttpClient/DataCollector/HttpClientDataCollector.php b/src/Symfony/Component/HttpClient/DataCollector/HttpClientDataCollector.php index 948b2d8eae..fb9228b106 100644 --- a/src/Symfony/Component/HttpClient/DataCollector/HttpClientDataCollector.php +++ b/src/Symfony/Component/HttpClient/DataCollector/HttpClientDataCollector.php @@ -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 */ -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' => [], diff --git a/src/Symfony/Component/HttpClient/Response/CurlResponse.php b/src/Symfony/Component/HttpClient/Response/CurlResponse.php index e06f9a58ba..6188587288 100644 --- a/src/Symfony/Component/HttpClient/Response/CurlResponse.php +++ b/src/Symfony/Component/HttpClient/Response/CurlResponse.php @@ -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 = []; diff --git a/src/Symfony/Component/HttpClient/ScopingHttpClient.php b/src/Symfony/Component/HttpClient/ScopingHttpClient.php index 3f071720f0..a55d011953 100644 --- a/src/Symfony/Component/HttpClient/ScopingHttpClient.php +++ b/src/Symfony/Component/HttpClient/ScopingHttpClient.php @@ -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 */ -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(); + } + } } diff --git a/src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php index 4fd367fd9d..7aedba1523 100644 --- a/src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php +++ b/src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php @@ -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; } } diff --git a/src/Symfony/Component/HttpClient/Tests/Fixtures/tls/server.crt b/src/Symfony/Component/HttpClient/Tests/Fixtures/tls/server.crt new file mode 100644 index 0000000000..3903667223 --- /dev/null +++ b/src/Symfony/Component/HttpClient/Tests/Fixtures/tls/server.crt @@ -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----- diff --git a/src/Symfony/Component/HttpClient/Tests/Fixtures/tls/server.key b/src/Symfony/Component/HttpClient/Tests/Fixtures/tls/server.key new file mode 100644 index 0000000000..8c278f843d --- /dev/null +++ b/src/Symfony/Component/HttpClient/Tests/Fixtures/tls/server.key @@ -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----- diff --git a/src/Symfony/Component/HttpClient/TraceableHttpClient.php b/src/Symfony/Component/HttpClient/TraceableHttpClient.php index 4acbc8ee42..d60d0849cd 100644 --- a/src/Symfony/Component/HttpClient/TraceableHttpClient.php +++ b/src/Symfony/Component/HttpClient/TraceableHttpClient.php @@ -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 */ -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 = []; } } diff --git a/src/Symfony/Component/HttpKernel/DataCollector/MemoryDataCollector.php b/src/Symfony/Component/HttpKernel/DataCollector/MemoryDataCollector.php index bb99c475b5..37302128ad 100644 --- a/src/Symfony/Component/HttpKernel/DataCollector/MemoryDataCollector.php +++ b/src/Symfony/Component/HttpKernel/DataCollector/MemoryDataCollector.php @@ -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; diff --git a/src/Symfony/Component/HttpKernel/DependencyInjection/ResettableServicePass.php b/src/Symfony/Component/HttpKernel/DependencyInjection/ResettableServicePass.php index c1199f639e..b5e46106a7 100644 --- a/src/Symfony/Component/HttpKernel/DependencyInjection/ResettableServicePass.php +++ b/src/Symfony/Component/HttpKernel/DependencyInjection/ResettableServicePass.php @@ -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'); diff --git a/src/Symfony/Component/HttpKernel/DependencyInjection/ServicesResetter.php b/src/Symfony/Component/HttpKernel/DependencyInjection/ServicesResetter.php index 734fadbd74..d9e0028ce1 100644 --- a/src/Symfony/Component/HttpKernel/DependencyInjection/ServicesResetter.php +++ b/src/Symfony/Component/HttpKernel/DependencyInjection/ServicesResetter.php @@ -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(); + } } } } diff --git a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php index 9475c47992..698af72ce2 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php @@ -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 diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 0bf90dbbce..cc23624d33 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -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); diff --git a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ResettableServicePassTest.php b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ResettableServicePassTest.php index d3c6869320..9dbc2b08a4 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ResettableServicePassTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ResettableServicePassTest.php @@ -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'); diff --git a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ServicesResetterTest.php b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ServicesResetterTest.php index 5be6026c90..604d2b0d13 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ServicesResetterTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ServicesResetterTest.php @@ -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); } } diff --git a/src/Symfony/Component/HttpKernel/Tests/Fixtures/MultiResettableService.php b/src/Symfony/Component/HttpKernel/Tests/Fixtures/MultiResettableService.php new file mode 100644 index 0000000000..4930fd6a30 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/Fixtures/MultiResettableService.php @@ -0,0 +1,19 @@ +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')) { diff --git a/src/Symfony/Component/HttpKernel/Tests/KernelTest.php b/src/Symfony/Component/HttpKernel/Tests/KernelTest.php index e185c6f862..e9f100a8af 100644 --- a/src/Symfony/Component/HttpKernel/Tests/KernelTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/KernelTest.php @@ -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'); diff --git a/src/Symfony/Component/Intl/Currencies.php b/src/Symfony/Component/Intl/Currencies.php index 7459d633cb..c155c8f09f 100644 --- a/src/Symfony/Component/Intl/Currencies.php +++ b/src/Symfony/Component/Intl/Currencies.php @@ -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 { diff --git a/src/Symfony/Component/Intl/Locales.php b/src/Symfony/Component/Intl/Locales.php index aee16beb16..1b2d0d2adf 100644 --- a/src/Symfony/Component/Intl/Locales.php +++ b/src/Symfony/Component/Intl/Locales.php @@ -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 { diff --git a/src/Symfony/Component/Intl/Scripts.php b/src/Symfony/Component/Intl/Scripts.php index 943ef8b469..bb29f0095b 100644 --- a/src/Symfony/Component/Intl/Scripts.php +++ b/src/Symfony/Component/Intl/Scripts.php @@ -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 { diff --git a/src/Symfony/Component/Mailer/Bridge/Mailchimp/Transport/MandrillHttpTransport.php b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Transport/MandrillHttpTransport.php index 2c90472fc6..17f7a7fcf3 100644 --- a/src/Symfony/Component/Mailer/Bridge/Mailchimp/Transport/MandrillHttpTransport.php +++ b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Transport/MandrillHttpTransport.php @@ -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; } diff --git a/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpTransportFactoryTest.php b/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpTransportFactoryTest.php index 60d5e806e3..b3cb7a6dc8 100644 --- a/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpTransportFactoryTest.php +++ b/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpTransportFactoryTest.php @@ -28,6 +28,9 @@ class AmqpTransportFactoryTest extends TestCase $this->assertFalse($factory->supports('invalid-dsn', [])); } + /** + * @requires extension amqp + */ public function testItCreatesTheTransport() { $factory = new AmqpTransportFactory(); diff --git a/src/Symfony/Component/Messenger/Tests/Transport/InMemoryTransportTest.php b/src/Symfony/Component/Messenger/Tests/Transport/InMemoryTransportTest.php index 22149f8a39..6fddc3fbbc 100644 --- a/src/Symfony/Component/Messenger/Tests/Transport/InMemoryTransportTest.php +++ b/src/Symfony/Component/Messenger/Tests/Transport/InMemoryTransportTest.php @@ -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()); diff --git a/src/Symfony/Component/Messenger/Transport/AmqpExt/Connection.php b/src/Symfony/Component/Messenger/Transport/AmqpExt/Connection.php index 8e37e3873f..6d4db85b0b 100644 --- a/src/Symfony/Component/Messenger/Transport/AmqpExt/Connection.php +++ b/src/Symfony/Component/Messenger/Transport/AmqpExt/Connection.php @@ -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', diff --git a/src/Symfony/Component/Messenger/Transport/Doctrine/DoctrineTransportFactory.php b/src/Symfony/Component/Messenger/Transport/Doctrine/DoctrineTransportFactory.php index 4541a1095d..50e95ef5e2 100644 --- a/src/Symfony/Component/Messenger/Transport/Doctrine/DoctrineTransportFactory.php +++ b/src/Symfony/Component/Messenger/Transport/Doctrine/DoctrineTransportFactory.php @@ -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; } diff --git a/src/Symfony/Component/Messenger/Transport/InMemoryTransport.php b/src/Symfony/Component/Messenger/Transport/InMemoryTransport.php index 354bb601a1..09cbb31a04 100644 --- a/src/Symfony/Component/Messenger/Transport/InMemoryTransport.php +++ b/src/Symfony/Component/Messenger/Transport/InMemoryTransport.php @@ -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; diff --git a/src/Symfony/Component/Routing/Loader/ObjectLoader.php b/src/Symfony/Component/Routing/Loader/ObjectLoader.php index aefb829511..1a375d95ff 100644 --- a/src/Symfony/Component/Routing/Loader/ObjectLoader.php +++ b/src/Symfony/Component/Routing/Loader/ObjectLoader.php @@ -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'; diff --git a/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php b/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php index eadd23f91e..6eaee110d0 100644 --- a/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php @@ -144,7 +144,6 @@ class SwitchUserListener try { $this->provider->loadUserByUsername($nonExistentUsername); - throw new \LogicException('AuthenticationException expected'); } catch (AuthenticationException $e) { } } catch (AuthenticationException $e) { diff --git a/src/Symfony/Component/Serializer/Mapping/Factory/ClassResolverTrait.php b/src/Symfony/Component/Serializer/Mapping/Factory/ClassResolverTrait.php index 15b535b0c7..b32a04b5a9 100644 --- a/src/Symfony/Component/Serializer/Mapping/Factory/ClassResolverTrait.php +++ b/src/Symfony/Component/Serializer/Mapping/Factory/ClassResolverTrait.php @@ -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 { diff --git a/src/Symfony/Component/Validator/Constraint.php b/src/Symfony/Component/Validator/Constraint.php index 224c7edd41..b349d8675d 100644 --- a/src/Symfony/Component/Validator/Constraint.php +++ b/src/Symfony/Component/Validator/Constraint.php @@ -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; diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.ja.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.ja.xlf index 5a391a2e66..21e2392c7d 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.ja.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.ja.xlf @@ -362,6 +362,10 @@ This password has been leaked in a data breach, it must not be used. Please use another password. このパスワードは漏洩している為使用できません。 + + This value should be between {{ min }} and {{ max }}. + {{ min }}以上{{ max }}以下でなければなりません。 + diff --git a/src/Symfony/Component/Validator/Test/ConstraintValidatorTestCase.php b/src/Symfony/Component/Validator/Test/ConstraintValidatorTestCase.php index 3db44b150a..8e4fc6ba1b 100644 --- a/src/Symfony/Component/Validator/Test/ConstraintValidatorTestCase.php +++ b/src/Symfony/Component/Validator/Test/ConstraintValidatorTestCase.php @@ -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() diff --git a/src/Symfony/Component/Workflow/MarkingStore/MethodMarkingStore.php b/src/Symfony/Component/Workflow/MarkingStore/MethodMarkingStore.php index 3ef65b315a..39d2ccec21 100644 --- a/src/Symfony/Component/Workflow/MarkingStore/MethodMarkingStore.php +++ b/src/Symfony/Component/Workflow/MarkingStore/MethodMarkingStore.php @@ -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); diff --git a/src/Symfony/Component/Workflow/Tests/StateMachineTest.php b/src/Symfony/Component/Workflow/Tests/StateMachineTest.php index 9224f7cb12..a6c7362f79 100644 --- a/src/Symfony/Component/Workflow/Tests/StateMachineTest.php +++ b/src/Symfony/Component/Workflow/Tests/StateMachineTest.php @@ -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()); + } + } } diff --git a/src/Symfony/Component/Workflow/Workflow.php b/src/Symfony/Component/Workflow/Workflow.php index 6d630d7792..09418d48fb 100644 --- a/src/Symfony/Component/Workflow/Workflow.php +++ b/src/Symfony/Component/Workflow/Workflow.php @@ -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; } diff --git a/src/Symfony/Contracts/HttpClient/Test/Fixtures/web/index.php b/src/Symfony/Contracts/HttpClient/Test/Fixtures/web/index.php index d3c4f0f1db..96486ca316 100644 --- a/src/Symfony/Contracts/HttpClient/Test/Fixtures/web/index.php +++ b/src/Symfony/Contracts/HttpClient/Test/Fixtures/web/index.php @@ -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); diff --git a/src/Symfony/Contracts/HttpClient/Test/HttpClientTestCase.php b/src/Symfony/Contracts/HttpClient/Test/HttpClientTestCase.php index 4badb4c358..aad712e3e8 100644 --- a/src/Symfony/Contracts/HttpClient/Test/HttpClientTestCase.php +++ b/src/Symfony/Contracts/HttpClient/Test/HttpClientTestCase.php @@ -24,8 +24,6 @@ use Symfony\Contracts\HttpClient\HttpClientInterface; */ abstract class HttpClientTestCase extends TestCase { - private static $server; - public static function setUpBeforeClass(): void { TestHttpServer::start(); diff --git a/src/Symfony/Contracts/HttpClient/Test/TestHttpServer.php b/src/Symfony/Contracts/HttpClient/Test/TestHttpServer.php index 8e7a469c42..0adb1a52a3 100644 --- a/src/Symfony/Contracts/HttpClient/Test/TestHttpServer.php +++ b/src/Symfony/Contracts/HttpClient/Test/TestHttpServer.php @@ -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; } }