diff --git a/CHANGELOG-4.1.md b/CHANGELOG-4.1.md index d5f851276a7b2..5437c76fd6bad 100644 --- a/CHANGELOG-4.1.md +++ b/CHANGELOG-4.1.md @@ -7,6 +7,50 @@ in 4.1 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.1.0...v4.1.1 +* 4.1.0-BETA2 (2018-05-21) + + * bug #27312 Supress deprecation notices thrown when getting private servies from container in tests (arderyp) + * feature #27275 [Messenger] Allow to scope handlers per bus (ogizanagi, sroze) + * bug #27264 [Validator] Use strict type in URL validator (mimol91) + * bug #27267 [DependencyInjection] resolve array env vars (jamesthomasonjr) + * bug #26781 [Form] Fix precision of MoneyToLocalizedStringTransformer's divisions on transform() (syastrebov) + * bug #27270 [Routing] Fix adding name prefix to canonical route names (ismail1432) + * bug #27286 [Translation] Add Occitan plural rule (kylekatarnls) + * bug #27271 [DI] Allow defining bindings on ChildDefinition (nicolas-grekas) + * bug #27246 Disallow invalid characters in session.name (ostrolucky) + * feature #27230 [Messenger] Select alternatives on missing receiver arg or typo (yceruto) + * bug #27287 [PropertyInfo] fix resolving parent|self type hints (nicolas-grekas) + * bug #27281 [HttpKernel] Fix dealing with self/parent in ArgumentMetadataFactory (fabpot) + * bug #24805 [Security] Fix logout (MatTheCat) + * bug #27265 [DI] Shared services should not be inlined in non-shared ones (nicolas-grekas) + * bug #27141 [Process] Suppress warnings when open_basedir is non-empty (cbj4074) + * bug #27250 [Session] limiting :key for GET_LOCK to 64 chars (oleg-andreyev) + * feature #27128 [Messenger] Middleware factories support in config (ogizanagi) + * bug #27214 [HttpKernel] Fix services are no longer injected into __invoke controllers method (ogizanagi) + * bug #27237 [Debug] Fix populating error_get_last() for handled silent errors (nicolas-grekas) + * bug #27232 [Cache][Lock] Fix usages of error_get_last() (nicolas-grekas) + * bug #27236 [Filesystem] Fix usages of error_get_last() (nicolas-grekas) + * feature #27202 [Messenger] Improve the profiler panel (ogizanagi) + * bug #27191 [DI] Display previous error messages when throwing unused bindings (nicolas-grekas) + * bug #27231 [FrameworkBundle] Fix cache:clear on vagrant (nicolas-grekas) + * bug #27222 [WebProfilerBundle][Cache] Fix misses calculation when calling getItems (fsevestre) + * bug #27227 [HttpKernel] Handle NoConfigurationException "onKernelException()" (nicolas-grekas) + * feature #27034 [Messenger][DX] Uses custom method names for handlers (sroze) + * bug #27228 [Messenger] Remove autoconfiguration for Sender/ReceiverInterface (kbond) + * bug #27229 [Messenger] Rename tag attribute "name" by "alias" (yceruto) + * bug #27224 [Messenger] Make sure default receiver name is set before command configuration (yceruto) + * feature #27225 [Messenger] Autoconfiguring TransportFactoryInterface classes (yceruto) + * bug #27220 [Messenger] Fix new AMQP Transport test with Envelope & fix contract (ogizanagi) + * bug #27184 [Messenger] Fix return senders based on the message parents/interfaces (yceruto) + * feature #27182 [Messenger] Re-introduce wrapped message configuration (with fix) (sroze, ogizanagi) + * bug #27209 [Workflow] add is deprecated since Symfony 4.1. Use addWorkflow() instead (xkobal) + * feature #26803 [Messenger] Add debug:messenger CLI command (ro0NL, sroze) + * bug #27189 [Profiler] Fix dump makes toolbar disappear (ogizanagi) + * bug #27199 [Messenger] Fix default bus name (ogizanagi) + * bug #27198 [Messenger] Fix the transport factory after moving it (sroze) + * bug #27197 [Messenger] Fix AMQP Transport factory & TransportFactoryInterface (ogizanagi) + * bug #27196 [Messenger] Fix AMQP Transport (yceruto) + * 4.1.0-BETA1 (2018-05-07) * feature #26945 [Messenger] Support configuring messages when dispatching (ogizanagi) diff --git a/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php b/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php index a46617db081d9..4a2a3a2fd8840 100644 --- a/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php +++ b/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php @@ -60,8 +60,8 @@ public function guessType($class, $property) case Type::DATETIMETZ: case 'vardatetime': return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\DateTimeType', array(), Guess::HIGH_CONFIDENCE); - case 'datetime_immutable'; - case 'datetimetz_immutable'; + case 'datetime_immutable': + case 'datetimetz_immutable': return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\DateTimeType', array('input' => 'datetime_immutable'), Guess::HIGH_CONFIDENCE); case 'dateinterval': return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\DateIntervalType', array(), Guess::HIGH_CONFIDENCE); diff --git a/src/Symfony/Bridge/Doctrine/Messenger/DoctrineTransactionMiddlewareFactory.php b/src/Symfony/Bridge/Doctrine/Messenger/DoctrineTransactionMiddlewareFactory.php new file mode 100644 index 0000000000000..0db646fdc888d --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Messenger/DoctrineTransactionMiddlewareFactory.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Messenger; + +use Symfony\Bridge\Doctrine\ManagerRegistry; + +/** + * Create a Doctrine ORM transaction middleware to be used in a message bus from an entity manager name. + * + * @author Maxime Steinhausser + * + * @experimental in 4.1 + * @final + */ +class DoctrineTransactionMiddlewareFactory +{ + private $managerRegistry; + + public function __construct(ManagerRegistry $managerRegistry) + { + $this->managerRegistry = $managerRegistry; + } + + public function createMiddleware(string $managerName): DoctrineTransactionMiddleware + { + return new DoctrineTransactionMiddleware($this->managerRegistry, $managerName); + } +} diff --git a/src/Symfony/Bridge/Monolog/Handler/FingersCrossed/NotFoundActivationStrategy.php b/src/Symfony/Bridge/Monolog/Handler/FingersCrossed/NotFoundActivationStrategy.php index 413b476f2938d..596fcdd84d2c5 100644 --- a/src/Symfony/Bridge/Monolog/Handler/FingersCrossed/NotFoundActivationStrategy.php +++ b/src/Symfony/Bridge/Monolog/Handler/FingersCrossed/NotFoundActivationStrategy.php @@ -42,7 +42,7 @@ public function isHandlerActivated(array $record) $isActivated && isset($record['context']['exception']) && $record['context']['exception'] instanceof HttpException - && $record['context']['exception']->getStatusCode() == 404 + && 404 == $record['context']['exception']->getStatusCode() && ($request = $this->requestStack->getMasterRequest()) ) { return !preg_match($this->blacklist, $request->getPathInfo()); diff --git a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php index 29b1960798b8c..f614ac99001b7 100644 --- a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php +++ b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php @@ -109,6 +109,30 @@ public static function register($mode = 0) } $trace = debug_backtrace(true); + + // Silence deprecation warnings about private service accessed + // from the service container if done so from a Test class. + // As of Symfony 4.1, there is a new TestContainer that allows + // fetching of private services within tests, so we no longer + // need to warn about this behavior. + // + // NOTE: the event at the top of the stack $trace (index 0) should + // always be the PhpUnitBridge's DeprecationErrorHandler; the + // second event (index 1) should be the trigger_error() event; + // the third event (index 2) should be the actual source of the + // triggered deprecation notice; and the fourth event (index 3) + // represents the action that called the deprecated code. In the + // scenario that we want to suppress, the 4th event will be an + // object instance of \PHPUnit\Framework\TestCase. + if (isset($trace[3]['object'])) { + $isPrivateServiceNotice = false !== strpos($msg, ' service is private, '); + $isNoticeForContainerGetHasUsage = 'Symfony\Component\DependencyInjection\Container' === $trace[2]['class'] && in_array($trace[2]['function'], array('get', 'has')); + $noticeWasTriggeredByPhpUnitTest = $trace[3]['object'] instanceof \PHPUnit\Framework\TestCase; + if ($isPrivateServiceNotice && $isNoticeForContainerGetHasUsage && $noticeWasTriggeredByPhpUnitTest) { + return false; + } + } + $group = 'other'; $isVendor = DeprecationErrorHandler::MODE_WEAK_VENDORS === $mode && $inVendors($file); diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/CoverageListenerTrait.php b/src/Symfony/Bridge/PhpUnit/Legacy/CoverageListenerTrait.php index 1c84ff32c0b74..ab1cda6702be5 100644 --- a/src/Symfony/Bridge/PhpUnit/Legacy/CoverageListenerTrait.php +++ b/src/Symfony/Bridge/PhpUnit/Legacy/CoverageListenerTrait.php @@ -11,7 +11,6 @@ namespace Symfony\Bridge\PhpUnit\Legacy; -use PHPUnit\Framework\Test; use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Warning; diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/weak_vendors_on_eval_d_deprecation.phpt b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/weak_vendors_on_eval_d_deprecation.phpt index 8fa436e20178b..8390d16332fa1 100644 --- a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/weak_vendors_on_eval_d_deprecation.phpt +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/weak_vendors_on_eval_d_deprecation.phpt @@ -15,7 +15,7 @@ while (!file_exists($vendor.'/vendor')) { define('PHPUNIT_COMPOSER_INSTALL', $vendor.'/vendor/autoload.php'); require PHPUNIT_COMPOSER_INSTALL; require_once __DIR__.'/../../bootstrap.php'; -eval("@trigger_error('who knows where I come from?', E_USER_DEPRECATED);") +eval("@trigger_error('who knows where I come from?', E_USER_DEPRECATED);"); ?> --EXPECTF-- diff --git a/src/Symfony/Bridge/Twig/NodeVisitor/Scope.php b/src/Symfony/Bridge/Twig/NodeVisitor/Scope.php index 1c3451bbebf46..59497dc961984 100644 --- a/src/Symfony/Bridge/Twig/NodeVisitor/Scope.php +++ b/src/Symfony/Bridge/Twig/NodeVisitor/Scope.php @@ -20,7 +20,7 @@ class Scope private $data = array(); private $left = false; - public function __construct(Scope $parent = null) + public function __construct(self $parent = null) { $this->parent = $parent; } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php index 31a12dbaba872..ee4edf31720ad 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php @@ -129,15 +129,33 @@ protected function execute(InputInterface $input, OutputInterface $output) $this->warmup($warmupDir, $realCacheDir, !$input->getOption('no-optional-warmers')); } - $containerDir = $fs->exists($warmupDir.'/'.$containerDir) ? false : $containerDir; + if (!$fs->exists($warmupDir.'/'.$containerDir)) { + $fs->rename($realCacheDir.'/'.$containerDir, $warmupDir.'/'.$containerDir); + touch($warmupDir.'/'.$containerDir.'.legacy'); + } - $fs->rename($realCacheDir, $oldCacheDir); - $fs->rename($warmupDir, $realCacheDir); + if ('/' === \DIRECTORY_SEPARATOR && $mounts = @file('/proc/mounts')) { + foreach ($mounts as $mount) { + $mount = array_slice(explode(' ', $mount), 1, -3); + if (!\in_array(array_pop($mount), array('vboxfs', 'nfs'))) { + continue; + } + $mount = implode(' ', $mount).'/'; + + if (0 === strpos($realCacheDir, $mount)) { + $io->note('For better performances, you should move the cache and log directories to a non-shared folder of the VM.'); + $oldCacheDir = false; + break; + } + } + } - if ($containerDir) { - $fs->rename($oldCacheDir.'/'.$containerDir, $realCacheDir.'/'.$containerDir); - touch($realCacheDir.'/'.$containerDir.'.legacy'); + if ($oldCacheDir) { + $fs->rename($realCacheDir, $oldCacheDir); + } else { + $fs->remove($realCacheDir); } + $fs->rename($warmupDir, $realCacheDir); if ($output->isVerbose()) { $io->comment('Removing old cache directory...'); diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 4314f17204a1f..237e757e2666a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -467,7 +467,16 @@ private function addSessionSection(ArrayNodeDefinition $rootNode) ->children() ->scalarNode('storage_id')->defaultValue('session.storage.native')->end() ->scalarNode('handler_id')->defaultValue('session.handler.native_file')->end() - ->scalarNode('name')->end() + ->scalarNode('name') + ->validate() + ->ifTrue(function ($v) { + parse_str($v, $parsed); + + return implode('&', array_keys($parsed)) !== (string) $v; + }) + ->thenInvalid('Session name %s contains illegal character(s)') + ->end() + ->end() ->scalarNode('cookie_lifetime')->end() ->scalarNode('cookie_path')->end() ->scalarNode('cookie_domain')->end() @@ -986,7 +995,10 @@ private function addMessengerSection(ArrayNodeDefinition $rootNode) $newConfig = array(); foreach ($config as $k => $v) { if (!\is_int($k)) { - $newConfig[$k] = array('senders' => \is_array($v) ? array_values($v) : array($v)); + $newConfig[$k] = array( + 'senders' => $v['senders'] ?? (\is_array($v) ? array_values($v) : array($v)), + 'send_and_handle' => $v['send_and_handle'] ?? false, + ); } else { $newConfig[$v['message-class']]['senders'] = array_map( function ($a) { @@ -994,6 +1006,7 @@ function ($a) { }, array_values($v['sender']) ); + $newConfig[$v['message-class']]['send-and-handle'] = $v['send-and-handle'] ?? false; } } @@ -1006,6 +1019,7 @@ function ($a) { ->requiresAtLeastOneElement() ->prototype('scalar')->end() ->end() + ->booleanNode('send_and_handle')->defaultFalse()->end() ->end() ->end() ->end() @@ -1047,7 +1061,7 @@ function ($a) { ->end() ->scalarNode('default_bus')->defaultValue(null)->end() ->arrayNode('buses') - ->defaultValue(array('default' => array('default_middleware' => true, 'middleware' => array()))) + ->defaultValue(array('messenger.bus.default' => array('default_middleware' => true, 'middleware' => array()))) ->useAttributeAsKey('name') ->prototype('array') ->addDefaultsIfNotSet() @@ -1061,7 +1075,36 @@ function ($a) { }) ->end() ->defaultValue(array()) - ->prototype('scalar')->end() + ->prototype('array') + ->beforeNormalization() + ->always() + ->then(function ($middleware): array { + if (!\is_array($middleware)) { + return array('id' => $middleware); + } + if (isset($middleware['id'])) { + return $middleware; + } + if (\count($middleware) > 1) { + throw new \InvalidArgumentException(sprintf('There is an error at path "framework.messenger" in one of the buses middleware definitions: expected a single entry for a middleware item config, with factory id as key and arguments as value. Got "%s".', json_encode($middleware))); + } + + return array( + 'id' => key($middleware), + 'arguments' => current($middleware), + ); + }) + ->end() + ->fixXmlConfig('argument') + ->children() + ->scalarNode('id')->isRequired()->cannotBeEmpty()->end() + ->arrayNode('arguments') + ->normalizeKeys(false) + ->defaultValue(array()) + ->prototype('variable') + ->end() + ->end() + ->end() ->end() ->end() ->end() diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 0d994c5f8f8f0..318af6c8b9a2f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -63,8 +63,9 @@ use Symfony\Component\Messenger\Handler\MessageHandlerInterface; use Symfony\Component\Messenger\MessageBus; use Symfony\Component\Messenger\MessageBusInterface; -use Symfony\Component\Messenger\Transport\ReceiverInterface; -use Symfony\Component\Messenger\Transport\SenderInterface; +use Symfony\Component\Messenger\Transport\ChainSender; +use Symfony\Component\Messenger\Transport\TransportFactoryInterface; +use Symfony\Component\Messenger\Transport\TransportInterface; use Symfony\Component\PropertyAccess\PropertyAccessor; use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface; use Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface; @@ -272,6 +273,7 @@ public function load(array $configs, ContainerBuilder $container) $this->registerMessengerConfiguration($config['messenger'], $container, $loader, $config['serializer'], $config['validation']); } else { $container->removeDefinition('console.command.messenger_consume_messages'); + $container->removeDefinition('console.command.messenger_debug'); } if ($this->isConfigEnabled($container, $config['web_link'])) { @@ -341,12 +343,10 @@ public function load(array $configs, ContainerBuilder $container) ->addTag('validator.constraint_validator'); $container->registerForAutoconfiguration(ObjectInitializerInterface::class) ->addTag('validator.initializer'); - $container->registerForAutoconfiguration(ReceiverInterface::class) - ->addTag('messenger.receiver'); - $container->registerForAutoconfiguration(SenderInterface::class) - ->addTag('messenger.sender'); $container->registerForAutoconfiguration(MessageHandlerInterface::class) ->addTag('messenger.message_handler'); + $container->registerForAutoconfiguration(TransportFactoryInterface::class) + ->addTag('messenger.transport_factory'); if (!$container->getParameter('kernel.debug')) { // remove tagged iterator argument for resource checkers @@ -571,10 +571,10 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $ foreach ($workflow['supports'] as $supportedClassName) { $strategyDefinition = new Definition(Workflow\SupportStrategy\InstanceOfSupportStrategy::class, array($supportedClassName)); $strategyDefinition->setPublic(false); - $registryDefinition->addMethodCall('add', array(new Reference($workflowId), $strategyDefinition)); + $registryDefinition->addMethodCall('addWorkflow', array(new Reference($workflowId), $strategyDefinition)); } } elseif (isset($workflow['support_strategy'])) { - $registryDefinition->addMethodCall('add', array(new Reference($workflowId), new Reference($workflow['support_strategy']))); + $registryDefinition->addMethodCall('addWorkflow', array(new Reference($workflowId), new Reference($workflow['support_strategy']))); } // Enable the AuditTrail @@ -1469,16 +1469,21 @@ private function registerMessengerConfiguration(array $config, ContainerBuilder $config['default_bus'] = key($config['buses']); } - $defaultMiddleware = array('before' => array('logging'), 'after' => array('route_messages', 'call_message_handler')); + $defaultMiddleware = array( + 'before' => array(array('id' => 'logging')), + 'after' => array(array('id' => 'route_messages'), array('id' => 'call_message_handler')), + ); foreach ($config['buses'] as $busId => $bus) { $middleware = $bus['default_middleware'] ? array_merge($defaultMiddleware['before'], $bus['middleware'], $defaultMiddleware['after']) : $bus['middleware']; - if (!$validationConfig['enabled'] && \in_array('messenger.middleware.validation', $middleware, true)) { - throw new LogicException('The Validation middleware is only available when the Validator component is installed and enabled. Try running "composer require symfony/validator".'); + foreach ($middleware as $middlewareItem) { + if (!$validationConfig['enabled'] && 'messenger.middleware.validation' === $middlewareItem['id']) { + throw new LogicException('The Validation middleware is only available when the Validator component is installed and enabled. Try running "composer require symfony/validator".'); + } } $container->setParameter($busId.'.middleware', $middleware); - $container->setDefinition($busId, (new Definition(MessageBus::class, array(array())))->addTag('messenger.bus')); + $container->register($busId, MessageBus::class)->addArgument(array())->addTag('messenger.bus'); if ($busId === $config['default_bus']) { $container->setAlias('message_bus', $busId); @@ -1490,35 +1495,41 @@ private function registerMessengerConfiguration(array $config, ContainerBuilder throw new LogicException(sprintf('The default bus named "%s" is not defined. Define it or change the default bus name.', $config['default_bus'])); } - $messageToSenderIdsMapping = array(); + $messageToSenderIdMapping = array(); + $messageToSendAndHandleMapping = array(); foreach ($config['routing'] as $message => $messageConfiguration) { if ('*' !== $message && !class_exists($message) && !interface_exists($message, false)) { throw new LogicException(sprintf('Messenger routing configuration contains a mistake: message "%s" does not exist. It needs to match an existing class or interface.', $message)); } - $messageToSenderIdsMapping[$message] = $messageConfiguration['senders']; + if (1 < \count($messageConfiguration['senders'])) { + $senders = array_map(function ($sender) { return new Reference($sender); }, $messageConfiguration['senders']); + $chainSenderDefinition = new Definition(ChainSender::class, array($senders)); + $chainSenderId = '.messenger.chain_sender.'.$message; + $container->setDefinition($chainSenderId, $chainSenderDefinition); + $messageToSenderIdMapping[$message] = $chainSenderId; + } else { + $messageToSenderIdMapping[$message] = $messageConfiguration['senders'][0]; + } + + $messageToSendAndHandleMapping[$message] = $messageConfiguration['send_and_handle']; } - $container->getDefinition('messenger.asynchronous.routing.sender_locator')->replaceArgument(1, $messageToSenderIdsMapping); + $container->getDefinition('messenger.asynchronous.routing.sender_locator')->replaceArgument(1, $messageToSenderIdMapping); + $container->getDefinition('messenger.middleware.route_messages')->replaceArgument(1, $messageToSendAndHandleMapping); foreach ($config['transports'] as $name => $transport) { if (0 === strpos($transport['dsn'], 'amqp://') && !$container->hasDefinition('messenger.transport.amqp.factory')) { throw new LogicException('The default AMQP transport is not available. Make sure you have installed and enabled the Serializer component. Try enable it or install it by running "composer require symfony/serializer-pack".'); } - $senderDefinition = (new Definition(SenderInterface::class)) - ->setFactory(array(new Reference('messenger.transport_factory'), 'createSender')) - ->setArguments(array($transport['dsn'], $transport['options'])) - ->addTag('messenger.sender', array('name' => $name)) - ; - $container->setDefinition('messenger.sender.'.$name, $senderDefinition); - - $receiverDefinition = (new Definition(ReceiverInterface::class)) - ->setFactory(array(new Reference('messenger.transport_factory'), 'createReceiver')) + $transportDefinition = (new Definition(TransportInterface::class)) + ->setFactory(array(new Reference('messenger.transport_factory'), 'createTransport')) ->setArguments(array($transport['dsn'], $transport['options'])) - ->addTag('messenger.receiver', array('name' => $name)) + ->addTag('messenger.receiver', array('alias' => $name)) + ->addTag('messenger.sender', array('alias' => $name)) ; - $container->setDefinition('messenger.receiver.'.$name, $receiverDefinition); + $container->setDefinition('messenger.transport.'.$name, $transportDefinition); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml index cb71bbb8de8f7..16732e2af480d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml @@ -73,10 +73,16 @@ + null + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.xml index 28eccb9a89878..0b09978d33870 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.xml @@ -7,18 +7,14 @@ - - - - - - + + @@ -31,7 +27,7 @@ - + @@ -60,7 +56,7 @@ - + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd index 96a13c8cb835e..2dbe54b5d938c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd @@ -375,6 +375,7 @@ + @@ -391,9 +392,16 @@ - + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTest.php index 9fd20649289de..452845cea8e34 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTest.php @@ -12,7 +12,6 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; -use Symfony\Component\HttpFoundation\File\File; class ControllerTest extends ControllerTraitTest { diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php index 8ab7b79172197..edac04c4c4f7c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -46,6 +46,41 @@ public function testDoNoDuplicateDefaultFormResources() $this->assertEquals(array('FrameworkBundle:Form'), $config['templating']['form']['resources']); } + public function getTestValidSessionName() + { + return array( + array(null), + array('PHPSESSID'), + array('a&b'), + array(',_-!@#$%^*(){}:<>/?'), + ); + } + + /** + * @dataProvider getTestInvalidSessionName + * @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException + */ + public function testInvalidSessionName($sessionName) + { + $processor = new Processor(); + $processor->processConfiguration( + new Configuration(true), + array(array('session' => array('name' => $sessionName))) + ); + } + + public function getTestInvalidSessionName() + { + return array( + array('a.b'), + array('a['), + array('a[]'), + array('a[b]'), + array('a=b'), + array('a+b'), + ); + } + public function testAssetsCanBeEnabled() { $processor = new Processor(); @@ -263,7 +298,7 @@ class_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphor 'encoder' => 'messenger.transport.serializer', 'decoder' => 'messenger.transport.serializer', 'default_bus' => null, - 'buses' => array('default' => array('default_middleware' => true, 'middleware' => array())), + 'buses' => array('messenger.bus.default' => array('default_middleware' => true, 'middleware' => array())), ), ); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_middleware_factory_erroneous_format.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_middleware_factory_erroneous_format.php new file mode 100644 index 0000000000000..d6cc86bd37d6c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_middleware_factory_erroneous_format.php @@ -0,0 +1,16 @@ +loadFromExtension('framework', array( + 'messenger' => array( + 'buses' => array( + 'command_bus' => array( + 'middleware' => array( + array( + 'foo' => array('qux'), + 'bar' => array('baz'), + ), + ), + ), + ), + ), +)); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_multiple_buses.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_multiple_buses.php index 51080f50368aa..5f3b2b23028a5 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_multiple_buses.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_multiple_buses.php @@ -7,6 +7,7 @@ 'messenger.bus.commands' => null, 'messenger.bus.events' => array( 'middleware' => array( + array('with_factory' => array('foo', true, array('bar' => 'baz'))), 'allow_no_handler', ), ), diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_routing.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_routing.php index 29891140ffdff..51b1184764dfe 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_routing.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_routing.php @@ -3,8 +3,11 @@ $container->loadFromExtension('framework', array( 'messenger' => array( 'routing' => array( - 'Symfony\Component\Messenger\Tests\Fixtures\DummyMessage' => array('amqp'), - 'Symfony\Component\Messenger\Tests\Fixtures\SecondMessage' => array('amqp', 'audit', null), + 'Symfony\Component\Messenger\Tests\Fixtures\DummyMessage' => array('amqp', 'audit'), + 'Symfony\Component\Messenger\Tests\Fixtures\SecondMessage' => array( + 'senders' => array('amqp', 'audit'), + 'send_and_handle' => true, + ), '*' => 'amqp', ), ), diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_buses.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_buses.xml index 88b43585361a9..f63df5bbbe4b2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_buses.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_buses.xml @@ -9,12 +9,19 @@ - allow_no_handler + + foo + true + + baz + + + - route_messages - allow_no_handler - call_message_handler + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_routing.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_routing.xml index 3772030e5e3e1..808b0ba9cd46b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_routing.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_routing.xml @@ -9,11 +9,11 @@ + - + - diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_middleware_factory_erroneous_format.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_middleware_factory_erroneous_format.yml new file mode 100644 index 0000000000000..74431414ba99c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_middleware_factory_erroneous_format.yml @@ -0,0 +1,7 @@ +framework: + messenger: + buses: + command_bus: + middleware: + - foo: ['qux'] + bar: ['baz'] diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_buses.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_buses.yml index 83c6213348f04..e279ef3bba06d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_buses.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_buses.yml @@ -5,6 +5,7 @@ framework: messenger.bus.commands: ~ messenger.bus.events: middleware: + - with_factory: [foo, true, { bar: baz }] - "allow_no_handler" messenger.bus.queries: default_middleware: false diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_routing.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_routing.yml index 2243a76f23efd..da096fcaed645 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_routing.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_routing.yml @@ -1,6 +1,8 @@ framework: messenger: routing: - 'Symfony\Component\Messenger\Tests\Fixtures\DummyMessage': amqp - 'Symfony\Component\Messenger\Tests\Fixtures\SecondMessage': [amqp, audit, ~] + 'Symfony\Component\Messenger\Tests\Fixtures\DummyMessage': [amqp, audit] + 'Symfony\Component\Messenger\Tests\Fixtures\SecondMessage': + senders: [amqp, audit] + send_and_handle: true '*': amqp diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index d7ce01d39fc21..2ae8411c22909 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -36,6 +36,7 @@ use Symfony\Component\HttpKernel\DependencyInjection\LoggerPass; use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage; use Symfony\Component\Messenger\Tests\Fixtures\SecondMessage; +use Symfony\Component\Messenger\Transport\TransportFactory; use Symfony\Component\PropertyAccess\PropertyAccessor; use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; use Symfony\Component\Serializer\Normalizer\DateIntervalNormalizer; @@ -528,35 +529,27 @@ public function testMessenger() $container = $this->createContainerFromFile('messenger'); $this->assertTrue($container->hasAlias('message_bus')); $this->assertFalse($container->hasDefinition('messenger.transport.amqp.factory')); + $this->assertTrue($container->hasDefinition('messenger.transport_factory')); + $this->assertSame(TransportFactory::class, $container->getDefinition('messenger.transport_factory')->getClass()); } public function testMessengerTransports() { $container = $this->createContainerFromFile('messenger_transports'); - $this->assertTrue($container->hasDefinition('messenger.sender.default')); - $this->assertTrue($container->getDefinition('messenger.sender.default')->hasTag('messenger.sender')); - $this->assertEquals(array(array('name' => 'default')), $container->getDefinition('messenger.sender.default')->getTag('messenger.sender')); - $this->assertTrue($container->hasDefinition('messenger.receiver.default')); - $this->assertTrue($container->getDefinition('messenger.receiver.default')->hasTag('messenger.receiver')); - $this->assertEquals(array(array('name' => 'default')), $container->getDefinition('messenger.receiver.default')->getTag('messenger.receiver')); - - $this->assertTrue($container->hasDefinition('messenger.sender.customised')); - $senderFactory = $container->getDefinition('messenger.sender.customised')->getFactory(); - $senderArguments = $container->getDefinition('messenger.sender.customised')->getArguments(); - - $this->assertEquals(array(new Reference('messenger.transport_factory'), 'createSender'), $senderFactory); - $this->assertCount(2, $senderArguments); - $this->assertSame('amqp://localhost/%2f/messages?exchange_name=exchange_name', $senderArguments[0]); - $this->assertSame(array('queue' => array('name' => 'Queue')), $senderArguments[1]); - - $this->assertTrue($container->hasDefinition('messenger.receiver.customised')); - $receiverFactory = $container->getDefinition('messenger.receiver.customised')->getFactory(); - $receiverArguments = $container->getDefinition('messenger.receiver.customised')->getArguments(); - - $this->assertEquals(array(new Reference('messenger.transport_factory'), 'createReceiver'), $receiverFactory); - $this->assertCount(2, $receiverArguments); - $this->assertSame('amqp://localhost/%2f/messages?exchange_name=exchange_name', $receiverArguments[0]); - $this->assertSame(array('queue' => array('name' => 'Queue')), $receiverArguments[1]); + $this->assertTrue($container->hasDefinition('messenger.transport.default')); + $this->assertTrue($container->getDefinition('messenger.transport.default')->hasTag('messenger.receiver')); + $this->assertTrue($container->getDefinition('messenger.transport.default')->hasTag('messenger.sender')); + $this->assertEquals(array(array('alias' => 'default')), $container->getDefinition('messenger.transport.default')->getTag('messenger.receiver')); + $this->assertEquals(array(array('alias' => 'default')), $container->getDefinition('messenger.transport.default')->getTag('messenger.sender')); + + $this->assertTrue($container->hasDefinition('messenger.transport.customised')); + $transportFactory = $container->getDefinition('messenger.transport.customised')->getFactory(); + $transportArguments = $container->getDefinition('messenger.transport.customised')->getArguments(); + + $this->assertEquals(array(new Reference('messenger.transport_factory'), 'createTransport'), $transportFactory); + $this->assertCount(2, $transportArguments); + $this->assertSame('amqp://localhost/%2f/messages?exchange_name=exchange_name', $transportArguments[0]); + $this->assertSame(array('queue' => array('name' => 'Queue')), $transportArguments[1]); $this->assertTrue($container->hasDefinition('messenger.transport.amqp.factory')); } @@ -565,14 +558,21 @@ public function testMessengerRouting() { $container = $this->createContainerFromFile('messenger_routing'); $senderLocatorDefinition = $container->getDefinition('messenger.asynchronous.routing.sender_locator'); + $sendMessageMiddlewareDefinition = $container->getDefinition('messenger.middleware.route_messages'); $messageToSenderIdsMapping = array( - DummyMessage::class => array('amqp'), - SecondMessage::class => array('amqp', 'audit', null), - '*' => array('amqp'), + DummyMessage::class => '.messenger.chain_sender.'.DummyMessage::class, + SecondMessage::class => '.messenger.chain_sender.'.SecondMessage::class, + '*' => 'amqp', + ); + $messageToSendAndHandleMapping = array( + DummyMessage::class => false, + SecondMessage::class => true, + '*' => false, ); $this->assertSame($messageToSenderIdsMapping, $senderLocatorDefinition->getArgument(1)); + $this->assertSame($messageToSendAndHandleMapping, $sendMessageMiddlewareDefinition->getArgument(1)); } /** @@ -611,18 +611,41 @@ public function testMessengerWithMultipleBuses() $this->assertTrue($container->has('messenger.bus.commands')); $this->assertSame(array(), $container->getDefinition('messenger.bus.commands')->getArgument(0)); - $this->assertEquals(array('logging', 'route_messages', 'call_message_handler'), $container->getParameter('messenger.bus.commands.middleware')); + $this->assertEquals(array( + array('id' => 'logging'), + array('id' => 'route_messages'), + array('id' => 'call_message_handler'), + ), $container->getParameter('messenger.bus.commands.middleware')); $this->assertTrue($container->has('messenger.bus.events')); $this->assertSame(array(), $container->getDefinition('messenger.bus.events')->getArgument(0)); - $this->assertEquals(array('logging', 'allow_no_handler', 'route_messages', 'call_message_handler'), $container->getParameter('messenger.bus.events.middleware')); + $this->assertEquals(array( + array('id' => 'logging'), + array('id' => 'with_factory', 'arguments' => array('foo', true, array('bar' => 'baz'))), + array('id' => 'allow_no_handler', 'arguments' => array()), + array('id' => 'route_messages'), + array('id' => 'call_message_handler'), + ), $container->getParameter('messenger.bus.events.middleware')); $this->assertTrue($container->has('messenger.bus.queries')); $this->assertSame(array(), $container->getDefinition('messenger.bus.queries')->getArgument(0)); - $this->assertEquals(array('route_messages', 'allow_no_handler', 'call_message_handler'), $container->getParameter('messenger.bus.queries.middleware')); + $this->assertEquals(array( + array('id' => 'route_messages', 'arguments' => array()), + array('id' => 'allow_no_handler', 'arguments' => array()), + array('id' => 'call_message_handler', 'arguments' => array()), + ), $container->getParameter('messenger.bus.queries.middleware')); $this->assertTrue($container->hasAlias('message_bus')); $this->assertSame('messenger.bus.commands', (string) $container->getAlias('message_bus')); } + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage There is an error at path "framework.messenger" in one of the buses middleware definitions: expected a single entry for a middleware item config, with factory id as key and arguments as value. Got "{"foo":["qux"],"bar":["baz"]}" + */ + public function testMessengerMiddlewareFactoryErroneousFormat() + { + $this->createContainerFromFile('messenger_middleware_factory_erroneous_format'); + } + public function testTranslator() { $container = $this->createContainerFromFile('full'); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/XmlFrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/XmlFrameworkExtensionTest.php index 03401e2482942..a272ab33276b0 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/XmlFrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/XmlFrameworkExtensionTest.php @@ -27,4 +27,9 @@ public function testAssetsHelperIsRemovedWhenPhpTemplatingEngineIsEnabledAndAsse { $this->markTestSkipped('The assets key cannot be set to false using the XML configuration format.'); } + + public function testMessengerMiddlewareFactoryErroneousFormat() + { + $this->markTestSkipped('XML configuration will not allow eeroneous format.'); + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index 8c1c32194fdea..2139633161d84 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -41,7 +41,7 @@ "symfony/security": "~3.4|~4.0", "symfony/form": "^4.1", "symfony/expression-language": "~3.4|~4.0", - "symfony/messenger": "^4.1", + "symfony/messenger": "^4.1-beta2", "symfony/process": "~3.4|~4.0", "symfony/security-core": "~3.4|~4.0", "symfony/security-csrf": "~3.4|~4.0", @@ -51,7 +51,7 @@ "symfony/templating": "~3.4|~4.0", "symfony/validator": "^4.1", "symfony/var-dumper": "~3.4|~4.0", - "symfony/workflow": "~3.4|~4.0", + "symfony/workflow": "^4.1", "symfony/yaml": "~3.4|~4.0", "symfony/property-info": "~3.4|~4.0", "symfony/lock": "~3.4|~4.0", @@ -72,7 +72,7 @@ "symfony/stopwatch": "<3.4", "symfony/translation": "<3.4", "symfony/validator": "<4.1", - "symfony/workflow": "<3.4" + "symfony/workflow": "<4.1" }, "suggest": { "ext-apcu": "For best performance of the system caches", diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 3282966f4772e..f0d8c0dc5a526 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -197,14 +197,15 @@ private function createFirewalls($config, ContainerBuilder $container) $configId = 'security.firewall.map.config.'.$name; - list($matcher, $listeners, $exceptionListener) = $this->createFirewall($container, $name, $firewall, $authenticationProviders, $providerIds, $configId); + list($matcher, $listeners, $exceptionListener, $logoutListener) = $this->createFirewall($container, $name, $firewall, $authenticationProviders, $providerIds, $configId); $contextId = 'security.firewall.map.context.'.$name; $context = $container->setDefinition($contextId, new ChildDefinition('security.firewall.context')); $context ->replaceArgument(0, new IteratorArgument($listeners)) ->replaceArgument(1, $exceptionListener) - ->replaceArgument(2, new Reference($configId)) + ->replaceArgument(2, $logoutListener) + ->replaceArgument(3, new Reference($configId)) ; $contextRefs[$contextId] = new Reference($contextId); @@ -250,7 +251,7 @@ private function createFirewall(ContainerBuilder $container, $id, $firewall, &$a // Security disabled? if (false === $firewall['security']) { - return array($matcher, array(), null); + return array($matcher, array(), null, null); } $config->replaceArgument(4, $firewall['stateless']); @@ -289,16 +290,15 @@ private function createFirewall(ContainerBuilder $container, $id, $firewall, &$a $config->replaceArgument(6, $contextKey); // Logout listener + $logoutListenerId = null; if (isset($firewall['logout'])) { - $listenerKeys[] = 'logout'; - $listenerId = 'security.logout_listener.'.$id; - $listener = $container->setDefinition($listenerId, new ChildDefinition('security.logout_listener')); - $listener->replaceArgument(3, array( + $logoutListenerId = 'security.logout_listener.'.$id; + $logoutListener = $container->setDefinition($logoutListenerId, new ChildDefinition('security.logout_listener')); + $logoutListener->replaceArgument(3, array( 'csrf_parameter' => $firewall['logout']['csrf_parameter'], 'csrf_token_id' => $firewall['logout']['csrf_token_id'], 'logout_path' => $firewall['logout']['path'], )); - $listeners[] = new Reference($listenerId); // add logout success handler if (isset($firewall['logout']['success_handler'])) { @@ -308,16 +308,16 @@ private function createFirewall(ContainerBuilder $container, $id, $firewall, &$a $logoutSuccessHandler = $container->setDefinition($logoutSuccessHandlerId, new ChildDefinition('security.logout.success_handler')); $logoutSuccessHandler->replaceArgument(1, $firewall['logout']['target']); } - $listener->replaceArgument(2, new Reference($logoutSuccessHandlerId)); + $logoutListener->replaceArgument(2, new Reference($logoutSuccessHandlerId)); // add CSRF provider if (isset($firewall['logout']['csrf_token_generator'])) { - $listener->addArgument(new Reference($firewall['logout']['csrf_token_generator'])); + $logoutListener->addArgument(new Reference($firewall['logout']['csrf_token_generator'])); } // add session logout handler if (true === $firewall['logout']['invalidate_session'] && false === $firewall['stateless']) { - $listener->addMethodCall('addHandler', array(new Reference('security.logout.handler.session'))); + $logoutListener->addMethodCall('addHandler', array(new Reference('security.logout.handler.session'))); } // add cookie logout handler @@ -326,12 +326,12 @@ private function createFirewall(ContainerBuilder $container, $id, $firewall, &$a $cookieHandler = $container->setDefinition($cookieHandlerId, new ChildDefinition('security.logout.handler.cookie_clearing')); $cookieHandler->addArgument($firewall['logout']['delete_cookies']); - $listener->addMethodCall('addHandler', array(new Reference($cookieHandlerId))); + $logoutListener->addMethodCall('addHandler', array(new Reference($cookieHandlerId))); } // add custom handlers foreach ($firewall['logout']['handlers'] as $handlerId) { - $listener->addMethodCall('addHandler', array(new Reference($handlerId))); + $logoutListener->addMethodCall('addHandler', array(new Reference($handlerId))); } // register with LogoutUrlGenerator @@ -391,7 +391,7 @@ private function createFirewall(ContainerBuilder $container, $id, $firewall, &$a $config->replaceArgument(10, $listenerKeys); $config->replaceArgument(11, isset($firewall['switch_user']) ? $firewall['switch_user'] : null); - return array($matcher, $listeners, $exceptionListener); + return array($matcher, $listeners, $exceptionListener, null !== $logoutListenerId ? new Reference($logoutListenerId) : null); } private function createContextListener($container, $contextKey) diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml index dfc2bea84ea3e..9cbdd2061e119 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml @@ -141,6 +141,7 @@ + diff --git a/src/Symfony/Bundle/SecurityBundle/Security/FirewallContext.php b/src/Symfony/Bundle/SecurityBundle/Security/FirewallContext.php index 2375e6b0e3ab6..a3b7f15406919 100644 --- a/src/Symfony/Bundle/SecurityBundle/Security/FirewallContext.php +++ b/src/Symfony/Bundle/SecurityBundle/Security/FirewallContext.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\Security\Http\Firewall\ExceptionListener; +use Symfony\Component\Security\Http\Firewall\LogoutListener; /** * This is a wrapper around the actual firewall configuration which allows us @@ -23,13 +24,24 @@ class FirewallContext { private $listeners; private $exceptionListener; + private $logoutListener; private $config; - public function __construct(iterable $listeners, ExceptionListener $exceptionListener = null, FirewallConfig $config = null) + /** + * @param LogoutListener|null $logoutListener + */ + public function __construct(iterable $listeners, ExceptionListener $exceptionListener = null, $logoutListener = null, FirewallConfig $config = null) { $this->listeners = $listeners; $this->exceptionListener = $exceptionListener; - $this->config = $config; + if ($logoutListener instanceof FirewallConfig) { + $this->config = $logoutListener; + } elseif (null === $logoutListener || $logoutListener instanceof LogoutListener) { + $this->logoutListener = $logoutListener; + $this->config = $config; + } else { + throw new \InvalidArgumentException(sprintf('Argument 3 passed to %s() must be instance of %s or null, %s given.', __METHOD__, LogoutListener::class, is_object($logoutListener) ? get_class($logoutListener) : gettype($logoutListener))); + } } public function getConfig() @@ -46,4 +58,9 @@ public function getExceptionListener() { return $this->exceptionListener; } + + public function getLogoutListener() + { + return $this->logoutListener; + } } diff --git a/src/Symfony/Bundle/SecurityBundle/Security/FirewallMap.php b/src/Symfony/Bundle/SecurityBundle/Security/FirewallMap.php index 3b98b0e4cadd3..e11d5dc0f5863 100644 --- a/src/Symfony/Bundle/SecurityBundle/Security/FirewallMap.php +++ b/src/Symfony/Bundle/SecurityBundle/Security/FirewallMap.php @@ -40,10 +40,10 @@ public function getListeners(Request $request) $context = $this->getFirewallContext($request); if (null === $context) { - return array(array(), null); + return array(array(), null, null); } - return array($context->getListeners(), $context->getExceptionListener()); + return array($context->getListeners(), $context->getExceptionListener(), $context->getLogoutListener()); } /** diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php index 1366a2608c872..7e5ecd5d9e649 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php @@ -71,7 +71,7 @@ public function testFirewalls() $arguments = $contextDef->getArguments(); $listeners[] = array_map('strval', $arguments['index_0']->getValues()); - $configDef = $container->getDefinition((string) $arguments['index_2']); + $configDef = $container->getDefinition((string) $arguments['index_3']); $configs[] = array_values($configDef->getArguments()); } @@ -99,7 +99,6 @@ public function testFirewalls() null, null, array( - 'logout', 'switch_user', 'x509', 'remote_user', @@ -171,7 +170,6 @@ public function testFirewalls() array(), array( 'security.channel_listener', - 'security.logout_listener.secure', 'security.authentication.listener.x509.secure', 'security.authentication.listener.remote_user.secure', 'security.authentication.listener.form.secure', diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/LogoutTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/LogoutTest.php new file mode 100644 index 0000000000000..7eeb7c21171ce --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/LogoutTest.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\Functional; + +class LogoutTest extends WebTestCase +{ + public function testSessionLessRememberMeLogout() + { + $client = $this->createClient(array('test_case' => 'RememberMeLogout', 'root_config' => 'config.yml')); + + $client->request('POST', '/login', array( + '_username' => 'johannes', + '_password' => 'test', + )); + + $cookieJar = $client->getCookieJar(); + $cookieJar->expire(session_name()); + + $this->assertNotNull($cookieJar->get('REMEMBERME')); + + $client->request('GET', '/logout'); + + $this->assertNull($cookieJar->get('REMEMBERME')); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeLogout/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeLogout/bundles.php new file mode 100644 index 0000000000000..d90f774abde2b --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeLogout/bundles.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Bundle\SecurityBundle\SecurityBundle; +use Symfony\Bundle\FrameworkBundle\FrameworkBundle; + +return array( + new FrameworkBundle(), + new SecurityBundle(), +); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeLogout/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeLogout/config.yml new file mode 100644 index 0000000000000..60e9cb89a229a --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeLogout/config.yml @@ -0,0 +1,25 @@ +imports: + - { resource: ./../config/framework.yml } + +security: + encoders: + Symfony\Component\Security\Core\User\User: plaintext + + providers: + in_memory: + memory: + users: + johannes: { password: test, roles: [ROLE_USER] } + + firewalls: + default: + form_login: + check_path: login + remember_me: true + require_previous_session: false + remember_me: + always_remember_me: true + secret: key + logout: ~ + anonymous: ~ + stateless: true diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeLogout/routing.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeLogout/routing.yml new file mode 100644 index 0000000000000..1dddfca2f8154 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeLogout/routing.yml @@ -0,0 +1,5 @@ +login: + path: /login + +logout: + path: /logout diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Security/FirewallContextTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Security/FirewallContextTest.php index 983e8288214a2..520a129716f4f 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Security/FirewallContextTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Security/FirewallContextTest.php @@ -16,6 +16,7 @@ use Symfony\Bundle\SecurityBundle\Security\FirewallContext; use Symfony\Component\Security\Http\Firewall\ExceptionListener; use Symfony\Component\Security\Http\Firewall\ListenerInterface; +use Symfony\Component\Security\Http\Firewall\LogoutListener; class FirewallContextTest extends TestCase { @@ -23,6 +24,7 @@ public function testGetters() { $config = new FirewallConfig('main', 'user_checker', 'request_matcher'); $exceptionListener = $this->getExceptionListenerMock(); + $logoutListener = $this->getLogoutListenerMock(); $listeners = array( $this ->getMockBuilder(ListenerInterface::class) @@ -30,10 +32,11 @@ public function testGetters() ->getMock(), ); - $context = new FirewallContext($listeners, $exceptionListener, $config); + $context = new FirewallContext($listeners, $exceptionListener, $logoutListener, $config); $this->assertEquals($listeners, $context->getListeners()); $this->assertEquals($exceptionListener, $context->getExceptionListener()); + $this->assertEquals($logoutListener, $context->getLogoutListener()); $this->assertEquals($config, $context->getConfig()); } @@ -44,4 +47,12 @@ private function getExceptionListenerMock() ->disableOriginalConstructor() ->getMock(); } + + private function getLogoutListenerMock() + { + return $this + ->getMockBuilder(LogoutListener::class) + ->disableOriginalConstructor() + ->getMock(); + } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Security/FirewallMapTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Security/FirewallMapTest.php index 3e6f0a1ac74c6..42162369cb15c 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Security/FirewallMapTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Security/FirewallMapTest.php @@ -20,6 +20,7 @@ use Symfony\Component\HttpFoundation\RequestMatcherInterface; use Symfony\Component\Security\Http\Firewall\ExceptionListener; use Symfony\Component\Security\Http\Firewall\ListenerInterface; +use Symfony\Component\Security\Http\Firewall\LogoutListener; class FirewallMapTest extends TestCase { @@ -35,7 +36,7 @@ public function testGetListenersWithEmptyMap() $firewallMap = new FirewallMap($container, $map); - $this->assertEquals(array(array(), null), $firewallMap->getListeners($request)); + $this->assertEquals(array(array(), null, null), $firewallMap->getListeners($request)); $this->assertNull($firewallMap->getFirewallConfig($request)); $this->assertFalse($request->attributes->has(self::ATTRIBUTE_FIREWALL_CONTEXT)); } @@ -51,7 +52,7 @@ public function testGetListenersWithInvalidParameter() $firewallMap = new FirewallMap($container, $map); - $this->assertEquals(array(array(), null), $firewallMap->getListeners($request)); + $this->assertEquals(array(array(), null, null), $firewallMap->getListeners($request)); $this->assertNull($firewallMap->getFirewallConfig($request)); $this->assertFalse($request->attributes->has(self::ATTRIBUTE_FIREWALL_CONTEXT)); } @@ -71,6 +72,9 @@ public function testGetListeners() $exceptionListener = $this->getMockBuilder(ExceptionListener::class)->disableOriginalConstructor()->getMock(); $firewallContext->expects($this->once())->method('getExceptionListener')->willReturn($exceptionListener); + $logoutListener = $this->getMockBuilder(LogoutListener::class)->disableOriginalConstructor()->getMock(); + $firewallContext->expects($this->once())->method('getLogoutListener')->willReturn($logoutListener); + $matcher = $this->getMockBuilder(RequestMatcherInterface::class)->getMock(); $matcher->expects($this->once()) ->method('matches') @@ -82,7 +86,7 @@ public function testGetListeners() $firewallMap = new FirewallMap($container, array('security.firewall.map.context.foo' => $matcher)); - $this->assertEquals(array(array($listener), $exceptionListener), $firewallMap->getListeners($request)); + $this->assertEquals(array(array($listener), $exceptionListener, $logoutListener), $firewallMap->getListeners($request)); $this->assertEquals($firewallConfig, $firewallMap->getFirewallConfig($request)); $this->assertEquals('security.firewall.map.context.foo', $request->attributes->get(self::ATTRIBUTE_FIREWALL_CONTEXT)); } diff --git a/src/Symfony/Bundle/SecurityBundle/composer.json b/src/Symfony/Bundle/SecurityBundle/composer.json index 58e34ac55ae6e..8a64bcea04c60 100644 --- a/src/Symfony/Bundle/SecurityBundle/composer.json +++ b/src/Symfony/Bundle/SecurityBundle/composer.json @@ -44,6 +44,7 @@ "twig/twig": "~1.34|~2.4" }, "conflict": { + "symfony/security": "4.1.0-beta1", "symfony/var-dumper": "<3.4", "symfony/event-dispatcher": "<3.4", "symfony/framework-bundle": "<4.1", diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/TwigEnvironmentPass.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/TwigEnvironmentPass.php index 082870249ba44..3529a6f2b1bbf 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/TwigEnvironmentPass.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/TwigEnvironmentPass.php @@ -14,7 +14,6 @@ use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; -use Symfony\Component\DependencyInjection\Reference; /** * Adds tagged twig.extension services to twig service. diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/messenger.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/messenger.html.twig index 908efe8771016..d8befbbf8dca6 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/messenger.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/messenger.html.twig @@ -4,17 +4,33 @@ {% block toolbar %} {% if collector.messages|length > 0 %} + {% set status_color = collector.exceptionsCount ? 'red' %} {% set icon %} {{ include('@WebProfiler/Icon/messenger.svg') }} {{ collector.messages|length }} {% endset %} - {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: 'messenger' }) }} + {% set text %} + {% for bus in collector.buses %} + {% set exceptionsCount = collector.exceptionsCount(bus) %} +
+ {{ bus }} + + {{ collector.messages(bus)|length }} + +
+ {% endfor %} + {% endset %} + + {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: 'messenger', status: status_color }) }} {% endif %} {% endblock %} {% block menu %} - + {{ include('@WebProfiler/Icon/messenger.svg') }} Messages @@ -24,7 +40,28 @@ {% endblock %} +{% block head %} + {{ parent() }} + +{% endblock %} + {% block panel %} + {% import _self as helper %} +

Messages

{% if collector.messages is empty %} @@ -32,41 +69,99 @@

No messages have been collected.

{% else %} - - - - - - - - - - {% for message in collector.messages %} - - - - - - {% endfor %} - -
BusMessageResult
{{ message.bus }} - {% if message.result.object is defined %} - {{ profiler_dump(message.message.object, maxDepth=2) }} - {% else %} - {{ message.message.type }} - {% endif %} - - {% if message.result.object is defined %} - {{ profiler_dump(message.result.object, maxDepth=2) }} - {% elseif message.result.type is defined %} - {{ message.result.type }} - {% if message.result.value is defined %} - {{ message.result.value }} - {% endif %} - {% endif %} - {% if message.exception.type is defined %} - {{ message.exception.type }} - {% endif %} -
+ +
+
+ {% set messages = collector.messages %} + {% set exceptionsCount = collector.exceptionsCount %} +

All{{ messages|length }}

+ +
+

Ordered list of dispatched messages across all your buses

+ {{ helper.render_bus_messages(messages, true) }} +
+
+ + {% for bus in collector.buses %} +
+ {% set messages = collector.messages(bus) %} + {% set exceptionsCount = collector.exceptionsCount(bus) %} +

{{ bus }}{{ messages|length }}

+ +
+

Ordered list of messages dispatched on the {{ bus }} bus

+ {{ helper.render_bus_messages(messages) }} +
+
+ {% endfor %} {% endif %} + {% endblock %} + +{% macro render_bus_messages(messages, showBus = false) %} + {% set discr = random() %} + {% for i, dispatchCall in messages %} + + + + + + + + {% if showBus %} + + + + + {% endif %} + + + + + + + + + + + + + {% if dispatchCall.exception is defined %} + + + + + {% endif %} + +
+ {{ profiler_dump(dispatchCall.message.type) }} + {% if showBus %} + {{ dispatchCall.bus }} + {% endif %} + {% if dispatchCall.exception is defined %} + exception + {% endif %} + + {{ include('@Twig/images/icon-minus-square.svg') }} + {{ include('@Twig/images/icon-plus-square.svg') }} + +
Bus{{ dispatchCall.bus }}
Message{{ profiler_dump(dispatchCall.message.value, maxDepth=2) }}
Envelope items + {% for item in dispatchCall.envelopeItems %} + {{ profiler_dump(item) }} + {% else %} + No items + {% endfor %} +
Result + {% if dispatchCall.result is defined %} + {{ profiler_dump(dispatchCall.result.seek('value'), maxDepth=2) }} + {% elseif dispatchCall.exception is defined %} + No result as an exception occurred + {% endif %} +
Exception + {{ profiler_dump(dispatchCall.exception.value, maxDepth=1) }} +
+ {% endfor %} +{% endmacro %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/time.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/time.html.twig index be77f891034c6..88a5829905eed 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/time.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/time.html.twig @@ -7,10 +7,8 @@ 'default': '#999', 'section': '#444', 'event_listener': '#00B8F5', - 'event_listener_loading': '#00B8F5', 'template': '#66CC00', 'doctrine': '#FF6633', - 'propel': '#FF6633', } %} {% endif %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig index 1553f304dabe4..0c5967b4e75ce 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig @@ -418,12 +418,10 @@ '{{ path("_wdt", { "token": "xxxxxx" }) }}'.replace(/xxxxxx/, newToken), function(xhr, el) { - /* Evaluate embedded scripts inside the toolbar */ - var i, scripts = [].slice.call(el.querySelectorAll('script')); - - for (i = 0; i < scripts.length; ++i) { - eval(scripts[i].firstChild.nodeValue); - } + /* Evaluate in global scope scripts embedded inside the toolbar */ + eval.call({}, ([].slice.call(el.querySelectorAll('script')).map(function (script) { + return script.firstChild.nodeValue; + }).join(';\n'))); el.style.display = -1 !== xhr.responseText.indexOf('sf-toolbarreset') ? 'block' : 'none'; @@ -442,7 +440,7 @@ } /* Handle toolbar-info position */ - var toolbarBlocks = [].slice.call(el.querySelectorAll('.sf-toolbar-block')); + var i, toolbarBlocks = [].slice.call(el.querySelectorAll('.sf-toolbar-block')); for (i = 0; i < toolbarBlocks.length; ++i) { toolbarBlocks[i].onmouseover = function () { var toolbarInfo = this.querySelectorAll('.sf-toolbar-info')[0]; diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig index 96cd8878a8091..f9bc41d6a1b54 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig @@ -215,6 +215,9 @@ table tbody ul { .text-muted { color: #999; } +.text-danger { + color: {{ colors.error|raw }}; +} .text-bold { font-weight: bold; } diff --git a/src/Symfony/Component/Cache/DataCollector/CacheDataCollector.php b/src/Symfony/Component/Cache/DataCollector/CacheDataCollector.php index e4b08790ade4c..91763e5a9f33b 100644 --- a/src/Symfony/Component/Cache/DataCollector/CacheDataCollector.php +++ b/src/Symfony/Component/Cache/DataCollector/CacheDataCollector.php @@ -119,31 +119,30 @@ private function calculateStatistics(): array ); /** @var TraceableAdapterEvent $call */ foreach ($calls as $call) { - $statistics[$name]['calls'] += 1; + ++$statistics[$name]['calls']; $statistics[$name]['time'] += $call->end - $call->start; if ('getItem' === $call->name) { - $statistics[$name]['reads'] += 1; + ++$statistics[$name]['reads']; if ($call->hits) { - $statistics[$name]['hits'] += 1; + ++$statistics[$name]['hits']; } else { - $statistics[$name]['misses'] += 1; + ++$statistics[$name]['misses']; } } elseif ('getItems' === $call->name) { - $count = $call->hits + $call->misses; - $statistics[$name]['reads'] += $count; + $statistics[$name]['reads'] += $call->hits + $call->misses; $statistics[$name]['hits'] += $call->hits; - $statistics[$name]['misses'] += $count - $call->misses; + $statistics[$name]['misses'] += $call->misses; } elseif ('hasItem' === $call->name) { - $statistics[$name]['reads'] += 1; + ++$statistics[$name]['reads']; if (false === $call->result) { - $statistics[$name]['misses'] += 1; + ++$statistics[$name]['misses']; } else { - $statistics[$name]['hits'] += 1; + ++$statistics[$name]['hits']; } } elseif ('save' === $call->name) { - $statistics[$name]['writes'] += 1; + ++$statistics[$name]['writes']; } elseif ('deleteItem' === $call->name) { - $statistics[$name]['deletes'] += 1; + ++$statistics[$name]['deletes']; } } if ($statistics[$name]['reads']) { diff --git a/src/Symfony/Component/Cache/Traits/RedisTrait.php b/src/Symfony/Component/Cache/Traits/RedisTrait.php index b8e05d9e417c9..9fc6177c6bd78 100644 --- a/src/Symfony/Component/Cache/Traits/RedisTrait.php +++ b/src/Symfony/Component/Cache/Traits/RedisTrait.php @@ -126,9 +126,12 @@ public static function createConnection($dsn, array $options = array()) throw new InvalidArgumentException(sprintf('Redis connection failed (%s): %s', $e->getMessage(), $dsn)); } - if (@!$redis->isConnected()) { - $e = ($e = error_get_last()) && preg_match('/^Redis::p?connect\(\): (.*)/', $e['message'], $e) ? sprintf(' (%s)', $e[1]) : ''; - throw new InvalidArgumentException(sprintf('Redis connection failed%s: %s', $e, $dsn)); + set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; }); + $isConnected = $redis->isConnected(); + restore_error_handler(); + if (!$isConnected) { + $error = preg_match('/^Redis::p?connect\(\): (.*)/', $error, $error) ? sprintf(' (%s)', $error[1]) : ''; + throw new InvalidArgumentException(sprintf('Redis connection failed%s: %s', $error, $dsn)); } if ((null !== $auth && !$redis->auth($auth)) diff --git a/src/Symfony/Component/Config/Tests/Util/XmlUtilsTest.php b/src/Symfony/Component/Config/Tests/Util/XmlUtilsTest.php index 9d61c9cd83284..10b4a7a9b4dc7 100644 --- a/src/Symfony/Component/Config/Tests/Util/XmlUtilsTest.php +++ b/src/Symfony/Component/Config/Tests/Util/XmlUtilsTest.php @@ -55,7 +55,7 @@ public function testLoadFile() XmlUtils::loadFile($fixtures.'valid.xml', array($mock, 'validate')); $this->fail(); } catch (\InvalidArgumentException $e) { - $this->assertRegExp('/The XML file "[\w:\/\\\.~+-]+" is not valid\./', $e->getMessage()); + $this->assertRegExp('/The XML file ".+" is not valid\./', $e->getMessage()); } $this->assertInstanceOf('DOMDocument', XmlUtils::loadFile($fixtures.'valid.xml', array($mock, 'validate'))); diff --git a/src/Symfony/Component/Console/Command/Command.php b/src/Symfony/Component/Console/Command/Command.php index f42ee945f5111..b64d9af361e55 100644 --- a/src/Symfony/Component/Console/Command/Command.php +++ b/src/Symfony/Component/Console/Command/Command.php @@ -218,12 +218,11 @@ public function run(InputInterface $input, OutputInterface $output) if (null !== $this->processTitle) { if (function_exists('cli_set_process_title')) { - if (false === @cli_set_process_title($this->processTitle)) { + if (!@cli_set_process_title($this->processTitle)) { if ('Darwin' === PHP_OS) { $output->writeln('Running "cli_get_process_title" as an unprivileged user is not supported on MacOS.'); } else { - $error = error_get_last(); - trigger_error($error['message'], E_USER_WARNING); + cli_set_process_title($this->processTitle); } } } elseif (function_exists('setproctitle')) { diff --git a/src/Symfony/Component/CssSelector/Node/Specificity.php b/src/Symfony/Component/CssSelector/Node/Specificity.php index 11228f7246d3a..9b35cb411632c 100644 --- a/src/Symfony/Component/CssSelector/Node/Specificity.php +++ b/src/Symfony/Component/CssSelector/Node/Specificity.php @@ -40,7 +40,7 @@ public function __construct(int $a, int $b, int $c) $this->c = $c; } - public function plus(Specificity $specificity): Specificity + public function plus(self $specificity): self { return new self($this->a + $specificity->a, $this->b + $specificity->b, $this->c + $specificity->c); } @@ -56,7 +56,7 @@ public function getValue(): int * * @return int */ - public function compareTo(Specificity $specificity) + public function compareTo(self $specificity) { if ($this->a !== $specificity->a) { return $this->a > $specificity->a ? 1 : -1; diff --git a/src/Symfony/Component/CssSelector/XPath/Translator.php b/src/Symfony/Component/CssSelector/XPath/Translator.php index 28478b3db7e47..73b548215d671 100644 --- a/src/Symfony/Component/CssSelector/XPath/Translator.php +++ b/src/Symfony/Component/CssSelector/XPath/Translator.php @@ -114,7 +114,7 @@ public function selectorToXPath(SelectorNode $selector, string $prefix = 'descen return ($prefix ?: '').$this->nodeToXPath($selector); } - public function registerExtension(Extension\ExtensionInterface $extension): Translator + public function registerExtension(Extension\ExtensionInterface $extension): self { $this->extensions[$extension->getName()] = $extension; @@ -139,7 +139,7 @@ public function getExtension(string $name): Extension\ExtensionInterface return $this->extensions[$name]; } - public function registerParserShortcut(ParserInterface $shortcut): Translator + public function registerParserShortcut(ParserInterface $shortcut): self { $this->shortcutParsers[] = $shortcut; diff --git a/src/Symfony/Component/CssSelector/XPath/XPathExpr.php b/src/Symfony/Component/CssSelector/XPath/XPathExpr.php index 8090df99075fa..638cbd0fe0619 100644 --- a/src/Symfony/Component/CssSelector/XPath/XPathExpr.php +++ b/src/Symfony/Component/CssSelector/XPath/XPathExpr.php @@ -43,7 +43,7 @@ public function getElement(): string return $this->element; } - public function addCondition(string $condition): XPathExpr + public function addCondition(string $condition): self { $this->condition = $this->condition ? sprintf('(%s) and (%s)', $this->condition, $condition) : $condition; @@ -55,7 +55,7 @@ public function getCondition(): string return $this->condition; } - public function addNameTest(): XPathExpr + public function addNameTest(): self { if ('*' !== $this->element) { $this->addCondition('name() = '.Translator::getXpathLiteral($this->element)); @@ -65,7 +65,7 @@ public function addNameTest(): XPathExpr return $this; } - public function addStarPrefix(): XPathExpr + public function addStarPrefix(): self { $this->path .= '*/'; diff --git a/src/Symfony/Component/Debug/ErrorHandler.php b/src/Symfony/Component/Debug/ErrorHandler.php index 5dbdd6933898e..2702b4157f6df 100644 --- a/src/Symfony/Component/Debug/ErrorHandler.php +++ b/src/Symfony/Component/Debug/ErrorHandler.php @@ -380,14 +380,16 @@ private function reRegister($prev) public function handleError($type, $message, $file, $line) { // Level is the current error reporting level to manage silent error. + $level = error_reporting(); + $silenced = 0 === ($level & $type); // Strong errors are not authorized to be silenced. - $level = error_reporting() | E_RECOVERABLE_ERROR | E_USER_ERROR | E_DEPRECATED | E_USER_DEPRECATED; + $level |= E_RECOVERABLE_ERROR | E_USER_ERROR | E_DEPRECATED | E_USER_DEPRECATED; $log = $this->loggedErrors & $type; $throw = $this->thrownErrors & $type & $level; $type &= $level | $this->screamedErrors; if (!$type || (!$log && !$throw)) { - return $type && $log; + return !$silenced && $type && $log; } $scope = $this->scopedErrors & $type; @@ -493,7 +495,7 @@ public function handleError($type, $message, $file, $line) } } - return $type && $log; + return !$silenced && $type && $log; } /** diff --git a/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php b/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php index afe16ea4f759c..eee8e9dc57539 100644 --- a/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php +++ b/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php @@ -65,6 +65,30 @@ public function testRegister() } } + public function testErrorGetLast() + { + $handler = ErrorHandler::register(); + $logger = $this->getMockBuilder('Psr\Log\LoggerInterface')->getMock(); + $handler->setDefaultLogger($logger); + $handler->screamAt(E_ALL); + + try { + @trigger_error('Hello', E_USER_WARNING); + $expected = array( + 'type' => E_USER_WARNING, + 'message' => 'Hello', + 'file' => __FILE__, + 'line' => __LINE__ - 5, + ); + $this->assertSame($expected, error_get_last()); + } catch (\Exception $e) { + restore_error_handler(); + restore_exception_handler(); + + throw $e; + } + } + public function testNotice() { ErrorHandler::register(); diff --git a/src/Symfony/Component/DependencyInjection/ChildDefinition.php b/src/Symfony/Component/DependencyInjection/ChildDefinition.php index bcb7ff4838f64..0f67e960dffce 100644 --- a/src/Symfony/Component/DependencyInjection/ChildDefinition.php +++ b/src/Symfony/Component/DependencyInjection/ChildDefinition.php @@ -121,12 +121,4 @@ public function setInstanceofConditionals(array $instanceof) { throw new BadMethodCallException('A ChildDefinition cannot have instanceof conditionals set on it.'); } - - /** - * @internal - */ - public function setBindings(array $bindings) - { - throw new BadMethodCallException('A ChildDefinition cannot have bindings set on it.'); - } } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php b/src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php index 5d2d4429e4a97..901dc06ffaee5 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php @@ -118,8 +118,12 @@ protected function getConstructor(Definition $definition, $required) $class = $definition->getClass(); - if (!$r = $this->container->getReflectionClass($class)) { - throw new RuntimeException(sprintf('Invalid service "%s": class "%s" does not exist.', $this->currentId, $class)); + try { + if (!$r = $this->container->getReflectionClass($class)) { + throw new RuntimeException(sprintf('Invalid service "%s": class "%s" does not exist.', $this->currentId, $class)); + } + } catch (\ReflectionException $e) { + throw new RuntimeException(sprintf('Invalid service "%s": %s.', $this->currentId, lcfirst(rtrim($e->getMessage(), '.')))); } if (!$r = $r->getConstructor()) { if ($required) { diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php b/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php index f94284d2e0722..0fece9802e205 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php @@ -308,7 +308,7 @@ private function populateAvailableType(string $id, Definition $definition) return; } - if (preg_match('/^\d+_[^~]++~[._a-zA-Z\d]{7}$/', $id) || $definition->isDeprecated() || !$reflectionClass = $this->container->getReflectionClass($definition->getClass(), false)) { + if ('' === $id || '.' === $id[0] || $definition->isDeprecated() || !$reflectionClass = $this->container->getReflectionClass($definition->getClass(), false)) { return; } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php index d3b379034bd7f..b153b84764bc1 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php @@ -120,6 +120,6 @@ private function isInlineableDefinition($id, Definition $definition, ServiceRefe return false; } - return true; + return $this->container->getDefinition($ids[0])->isShared(); } } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveBindingsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveBindingsPass.php index c82a974360674..0051fb5ac3df0 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ResolveBindingsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveBindingsPass.php @@ -27,6 +27,7 @@ class ResolveBindingsPass extends AbstractRecursivePass { private $usedBindings = array(); private $unusedBindings = array(); + private $errorMessages = array(); /** * {@inheritdoc} @@ -37,11 +38,19 @@ public function process(ContainerBuilder $container) parent::process($container); foreach ($this->unusedBindings as list($key, $serviceId)) { - throw new InvalidArgumentException(sprintf('Unused binding "%s" in service "%s".', $key, $serviceId)); + $message = sprintf('Unused binding "%s" in service "%s".', $key, $serviceId); + if ($this->errorMessages) { + $message .= sprintf("\nCould be related to%s:", 1 < \count($this->errorMessages) ? ' one of' : ''); + } + foreach ($this->errorMessages as $m) { + $message .= "\n - ".$m; + } + throw new InvalidArgumentException($message); } } finally { $this->usedBindings = array(); $this->unusedBindings = array(); + $this->errorMessages = array(); } } @@ -94,6 +103,7 @@ protected function processValue($value, $isRoot = false) $calls[] = array($constructor, $value->getArguments()); } } catch (RuntimeException $e) { + $this->errorMessages[] = $e->getMessage(); $this->container->getDefinition($this->currentId)->addError($e->getMessage()); return parent::processValue($value, $isRoot); diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveChildDefinitionsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveChildDefinitionsPass.php index 62d0e7c680cb0..5dd766b3f145b 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ResolveChildDefinitionsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveChildDefinitionsPass.php @@ -100,7 +100,7 @@ private function doResolveDefinition(ChildDefinition $definition) $def->setAutowired($parentDef->isAutowired()); $def->setChanges($parentDef->getChanges()); - $def->setBindings($parentDef->getBindings()); + $def->setBindings($definition->getBindings() + $parentDef->getBindings()); // overwrite with values specified in the decorator $changes = $definition->getChanges(); diff --git a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php index 955250f479b64..392e47032a644 100644 --- a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php +++ b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php @@ -1363,6 +1363,7 @@ public function resolveEnvPlaceholders($value, $format = null, array &$usedEnvs } $envPlaceholders = $bag instanceof EnvPlaceholderParameterBag ? $bag->getEnvPlaceholders() : $this->envPlaceholders; + $completed = false; foreach ($envPlaceholders as $env => $placeholders) { foreach ($placeholders as $placeholder) { if (false !== stripos($value, $placeholder)) { @@ -1373,14 +1374,19 @@ public function resolveEnvPlaceholders($value, $format = null, array &$usedEnvs } if ($placeholder === $value) { $value = $resolved; + $completed = true; } else { if (!is_string($resolved) && !is_numeric($resolved)) { - throw new RuntimeException(sprintf('A string value must be composed of strings and/or numbers, but found parameter "env(%s)" of type %s inside string value "%s".', $env, gettype($resolved), $value)); + throw new RuntimeException(sprintf('A string value must be composed of strings and/or numbers, but found parameter "env(%s)" of type %s inside string value "%s".', $env, gettype($resolved), $this->resolveEnvPlaceholders($value))); } $value = str_ireplace($placeholder, $resolved, $value); } $usedEnvs[$env] = $env; $this->envCounters[$env] = isset($this->envCounters[$env]) ? 1 + $this->envCounters[$env] : 1; + + if ($completed) { + break 2; + } } } } diff --git a/src/Symfony/Component/DependencyInjection/Loader/Configurator/InstanceofConfigurator.php b/src/Symfony/Component/DependencyInjection/Loader/Configurator/InstanceofConfigurator.php index 9ecb2a21c3deb..78a8e3c327f85 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/Configurator/InstanceofConfigurator.php +++ b/src/Symfony/Component/DependencyInjection/Loader/Configurator/InstanceofConfigurator.php @@ -30,7 +30,7 @@ class InstanceofConfigurator extends AbstractServiceConfigurator /** * Defines an instanceof-conditional to be applied to following service definitions. */ - final public function instanceof(string $fqcn): InstanceofConfigurator + final public function instanceof(string $fqcn): self { return $this->parent->instanceof($fqcn); } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/InlineServiceDefinitionsPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/InlineServiceDefinitionsPassTest.php index bb60af4d19e64..78556b2ed21cd 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/InlineServiceDefinitionsPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/InlineServiceDefinitionsPassTest.php @@ -92,7 +92,7 @@ public function testProcessDoesInlineNonSharedService() $this->assertNotSame($container->getDefinition('bar'), $arguments[2]); } - public function testProcessInlinesMixedServicesLoop() + public function testProcessDoesNotInlineMixedServicesLoop() { $container = new ContainerBuilder(); $container @@ -108,7 +108,7 @@ public function testProcessInlinesMixedServicesLoop() $this->process($container); - $this->assertEquals($container->getDefinition('foo')->getArgument(0), $container->getDefinition('bar')); + $this->assertEquals(new Reference('bar'), $container->getDefinition('foo')->getArgument(0)); } /** diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveBindingsPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveBindingsPassTest.php index 16e486afafe2e..65f5ceb80fd0c 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveBindingsPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveBindingsPassTest.php @@ -18,6 +18,7 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Tests\Fixtures\NamedArgumentsDummy; +use Symfony\Component\DependencyInjection\Tests\Fixtures\ParentNotExists; use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass; use Symfony\Component\DependencyInjection\TypedReference; @@ -61,6 +62,21 @@ public function testUnusedBinding() $pass->process($container); } + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessageRegexp Unused binding "$quz" in service [\s\S]+ Invalid service ".*\\ParentNotExists": class NotExists not found\. + */ + public function testMissingParent() + { + $container = new ContainerBuilder(); + + $definition = $container->register(ParentNotExists::class, ParentNotExists::class); + $definition->setBindings(array('$quz' => '123')); + + $pass = new ResolveBindingsPass(); + $pass->process($container); + } + public function testTypedReferenceSupport() { $container = new ContainerBuilder(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveChildDefinitionsPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveChildDefinitionsPassTest.php index d15be74ecd969..e633f843198f7 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveChildDefinitionsPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveChildDefinitionsPassTest.php @@ -355,6 +355,27 @@ public function testProcessSetsArguments() $this->assertSame(array(2, 1, 'foo' => 3), $def->getArguments()); } + public function testBindings() + { + $container = new ContainerBuilder(); + + $container->register('parent', 'stdClass') + ->setBindings(array('a' => '1', 'b' => '2')) + ; + + $child = $container->setDefinition('child', new ChildDefinition('parent')) + ->setBindings(array('b' => 'B', 'c' => 'C')) + ; + + $this->process($container); + + $bindings = array(); + foreach ($container->getDefinition('child')->getBindings() as $k => $v) { + $bindings[$k] = $v->getValues()[0]; + } + $this->assertEquals(array('b' => 'B', 'c' => 'C', 'a' => '1'), $bindings); + } + public function testSetAutoconfiguredOnServiceIsParent() { $container = new ContainerBuilder(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php index a792a151eb95a..a8402569905a3 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php @@ -665,17 +665,49 @@ public function testCompileWithResolveEnv() putenv('DUMMY_ENV_VAR'); } + public function testCompileWithArrayResolveEnv() + { + putenv('ARRAY={"foo":"bar"}'); + + $container = new ContainerBuilder(); + $container->setParameter('foo', '%env(json:ARRAY)%'); + $container->compile(true); + + $this->assertSame(array('foo' => 'bar'), $container->getParameter('foo')); + + putenv('ARRAY'); + } + + public function testCompileWithArrayAndAnotherResolveEnv() + { + putenv('DUMMY_ENV_VAR=abc'); + putenv('ARRAY={"foo":"bar"}'); + + $container = new ContainerBuilder(); + $container->setParameter('foo', '%env(json:ARRAY)%'); + $container->setParameter('bar', '%env(DUMMY_ENV_VAR)%'); + $container->compile(true); + + $this->assertSame(array('foo' => 'bar'), $container->getParameter('foo')); + $this->assertSame('abc', $container->getParameter('bar')); + + putenv('DUMMY_ENV_VAR'); + putenv('ARRAY'); + } + /** * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException - * @expectedExceptionMessage A string value must be composed of strings and/or numbers, but found parameter "env(ARRAY)" of type array inside string value "ABC %env(ARRAY)%". + * @expectedExceptionMessage A string value must be composed of strings and/or numbers, but found parameter "env(json:ARRAY)" of type array inside string value "ABC %env(json:ARRAY)%". */ - public function testCompileWithArrayResolveEnv() + public function testCompileWithArrayInStringResolveEnv() { - $bag = new TestingEnvPlaceholderParameterBag(); - $container = new ContainerBuilder($bag); - $container->setParameter('foo', '%env(ARRAY)%'); - $container->setParameter('bar', 'ABC %env(ARRAY)%'); + putenv('ARRAY={"foo":"bar"}'); + + $container = new ContainerBuilder(); + $container->setParameter('foo', 'ABC %env(json:ARRAY)%'); $container->compile(true); + + putenv('ARRAY'); } /** @@ -1415,11 +1447,3 @@ public function __construct(A $a) { } } - -class TestingEnvPlaceholderParameterBag extends EnvPlaceholderParameterBag -{ - public function get($name) - { - return 'env(array)' === strtolower($name) ? array(123) : parent::get($name); - } -} diff --git a/src/Symfony/Component/Filesystem/Filesystem.php b/src/Symfony/Component/Filesystem/Filesystem.php index db5df5cbeb472..046dbb940e391 100644 --- a/src/Symfony/Component/Filesystem/Filesystem.php +++ b/src/Symfony/Component/Filesystem/Filesystem.php @@ -22,6 +22,8 @@ */ class Filesystem { + private static $lastError; + /** * Copies a file. * @@ -96,12 +98,11 @@ public function mkdir($dirs, $mode = 0777) continue; } - if (true !== @mkdir($dir, $mode, true)) { - $error = error_get_last(); + if (!self::box('mkdir', $dir, $mode, true)) { if (!is_dir($dir)) { // The directory was not created by a concurrent process. Let's throw an exception with a developer friendly error message if we have one - if ($error) { - throw new IOException(sprintf('Failed to create "%s": %s.', $dir, $error['message']), 0, null, $dir); + if (self::$lastError) { + throw new IOException(sprintf('Failed to create "%s": %s.', $dir, self::$lastError), 0, null, $dir); } throw new IOException(sprintf('Failed to create "%s"', $dir), 0, null, $dir); } @@ -170,20 +171,17 @@ public function remove($files) foreach ($files as $file) { if (is_link($file)) { // See https://bugs.php.net/52176 - if (!@(unlink($file) || '\\' !== DIRECTORY_SEPARATOR || rmdir($file)) && file_exists($file)) { - $error = error_get_last(); - throw new IOException(sprintf('Failed to remove symlink "%s": %s.', $file, $error['message'])); + if (!(self::box('unlink', $file) || '\\' !== DIRECTORY_SEPARATOR || self::box('rmdir', $file)) && file_exists($file)) { + throw new IOException(sprintf('Failed to remove symlink "%s": %s.', $file, self::$lastError)); } } elseif (is_dir($file)) { $this->remove(new \FilesystemIterator($file, \FilesystemIterator::CURRENT_AS_PATHNAME | \FilesystemIterator::SKIP_DOTS)); - if (!@rmdir($file) && file_exists($file)) { - $error = error_get_last(); - throw new IOException(sprintf('Failed to remove directory "%s": %s.', $file, $error['message'])); + if (!self::box('rmdir', $file) && file_exists($file)) { + throw new IOException(sprintf('Failed to remove directory "%s": %s.', $file, self::$lastError)); } - } elseif (!@unlink($file) && file_exists($file)) { - $error = error_get_last(); - throw new IOException(sprintf('Failed to remove file "%s": %s.', $file, $error['message'])); + } elseif (!self::box('unlink', $file) && file_exists($file)) { + throw new IOException(sprintf('Failed to remove file "%s": %s.', $file, self::$lastError)); } } } @@ -337,16 +335,14 @@ public function symlink($originDir, $targetDir, $copyOnWindows = false) $this->mkdir(dirname($targetDir)); - $ok = false; if (is_link($targetDir)) { - if (readlink($targetDir) != $originDir) { - $this->remove($targetDir); - } else { - $ok = true; + if (readlink($targetDir) === $originDir) { + return; } + $this->remove($targetDir); } - if (!$ok && true !== @symlink($originDir, $targetDir)) { + if (!self::box('symlink', $originDir, $targetDir)) { $this->linkException($originDir, $targetDir, 'symbolic'); } } @@ -378,7 +374,7 @@ public function hardlink($originFile, $targetFiles) $this->remove($targetFile); } - if (true !== @link($originFile, $targetFile)) { + if (!self::box('link', $originFile, $targetFile)) { $this->linkException($originFile, $targetFile, 'hard'); } } @@ -391,9 +387,8 @@ public function hardlink($originFile, $targetFiles) */ private function linkException($origin, $target, $linkType) { - $report = error_get_last(); - if (is_array($report)) { - if ('\\' === DIRECTORY_SEPARATOR && false !== strpos($report['message'], 'error code(1314)')) { + if (self::$lastError) { + if ('\\' === DIRECTORY_SEPARATOR && false !== strpos(self::$lastError, 'error code(1314)')) { throw new IOException(sprintf('Unable to create %s link due to error code 1314: \'A required privilege is not held by the client\'. Do you have the required Administrator-rights?', $linkType), 0, null, $target); } } @@ -743,4 +738,29 @@ private function getSchemeAndHierarchy(string $filename): array return 2 === count($components) ? array($components[0], $components[1]) : array(null, $components[0]); } + + private static function box($func) + { + self::$lastError = null; + \set_error_handler(__CLASS__.'::handleError'); + try { + $result = \call_user_func_array($func, \array_slice(\func_get_args(), 1)); + \restore_error_handler(); + + return $result; + } catch (\Throwable $e) { + } catch (\Exception $e) { + } + \restore_error_handler(); + + throw $e; + } + + /** + * @internal + */ + public static function handleError($type, $msg) + { + self::$lastError = $msg; + } } diff --git a/src/Symfony/Component/Finder/SplFileInfo.php b/src/Symfony/Component/Finder/SplFileInfo.php index 6516113a3ece4..b8143ed799162 100644 --- a/src/Symfony/Component/Finder/SplFileInfo.php +++ b/src/Symfony/Component/Finder/SplFileInfo.php @@ -66,12 +66,11 @@ public function getRelativePathname() */ public function getContents() { - $level = error_reporting(0); + set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; }); $content = file_get_contents($this->getPathname()); - error_reporting($level); + restore_error_handler(); if (false === $content) { - $error = error_get_last(); - throw new \RuntimeException($error['message']); + throw new \RuntimeException($error); } return $content; diff --git a/src/Symfony/Component/Form/FormInterface.php b/src/Symfony/Component/Form/FormInterface.php index 921ae2829c5ff..a0e9b636c5f38 100644 --- a/src/Symfony/Component/Form/FormInterface.php +++ b/src/Symfony/Component/Form/FormInterface.php @@ -29,7 +29,7 @@ interface FormInterface extends \ArrayAccess, \Traversable, \Countable * @throws Exception\LogicException when trying to set a parent for a form with * an empty name */ - public function setParent(FormInterface $parent = null); + public function setParent(self $parent = null); /** * Returns the parent form. diff --git a/src/Symfony/Component/Form/FormView.php b/src/Symfony/Component/Form/FormView.php index 21a3d5036ad2b..9fbca4379688a 100644 --- a/src/Symfony/Component/Form/FormView.php +++ b/src/Symfony/Component/Form/FormView.php @@ -51,7 +51,7 @@ class FormView implements \ArrayAccess, \IteratorAggregate, \Countable private $methodRendered = false; - public function __construct(FormView $parent = null) + public function __construct(self $parent = null) { $this->parent = $parent; } diff --git a/src/Symfony/Component/Form/Tests/AbstractBootstrap4LayoutTest.php b/src/Symfony/Component/Form/Tests/AbstractBootstrap4LayoutTest.php index 7f1abc7c57ab1..8cc1df5f52f27 100644 --- a/src/Symfony/Component/Form/Tests/AbstractBootstrap4LayoutTest.php +++ b/src/Symfony/Component/Form/Tests/AbstractBootstrap4LayoutTest.php @@ -11,6 +11,15 @@ namespace Symfony\Component\Form\Tests; +use Symfony\Component\Form\Extension\Core\Type\ButtonType; +use Symfony\Component\Form\Extension\Core\Type\CheckboxType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\DateType; +use Symfony\Component\Form\Extension\Core\Type\FileType; +use Symfony\Component\Form\Extension\Core\Type\MoneyType; +use Symfony\Component\Form\Extension\Core\Type\PercentType; +use Symfony\Component\Form\Extension\Core\Type\RadioType; +use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormError; /** @@ -22,7 +31,7 @@ abstract class AbstractBootstrap4LayoutTest extends AbstractBootstrap3LayoutTest { public function testRow() { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType'); + $form = $this->factory->createNamed('name', TextType::class); $form->addError(new FormError('[trans]Error![/trans]')); $view = $form->createView(); $html = $this->renderRow($view); @@ -47,7 +56,7 @@ public function testRow() public function testLabelOnForm() { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\DateType'); + $form = $this->factory->createNamed('name', DateType::class); $view = $form->createView(); $this->renderWidget($view, array('label' => 'foo')); $html = $this->renderLabel($view); @@ -62,7 +71,7 @@ public function testLabelOnForm() public function testLabelDoesNotRenderFieldAttributes() { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType'); + $form = $this->factory->createNamed('name', TextType::class); $html = $this->renderLabel($form->createView(), null, array( 'attr' => array( 'class' => 'my&class', @@ -79,7 +88,7 @@ public function testLabelDoesNotRenderFieldAttributes() public function testLabelWithCustomAttributesPassedDirectly() { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType'); + $form = $this->factory->createNamed('name', TextType::class); $html = $this->renderLabel($form->createView(), null, array( 'label_attr' => array( 'class' => 'my&class', @@ -96,7 +105,7 @@ public function testLabelWithCustomAttributesPassedDirectly() public function testLabelWithCustomTextAndCustomAttributesPassedDirectly() { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType'); + $form = $this->factory->createNamed('name', TextType::class); $html = $this->renderLabel($form->createView(), 'Custom label', array( 'label_attr' => array( 'class' => 'my&class', @@ -114,7 +123,7 @@ public function testLabelWithCustomTextAndCustomAttributesPassedDirectly() public function testLabelWithCustomTextAsOptionAndCustomAttributesPassedDirectly() { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType', null, array( + $form = $this->factory->createNamed('name', TextType::class, null, array( 'label' => 'Custom label', )); $html = $this->renderLabel($form->createView(), null, array( @@ -134,7 +143,7 @@ public function testLabelWithCustomTextAsOptionAndCustomAttributesPassedDirectly public function testLegendOnExpandedType() { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', null, array( + $form = $this->factory->createNamed('name', ChoiceType::class, null, array( 'label' => 'Custom label', 'expanded' => true, 'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b'), @@ -153,7 +162,7 @@ public function testLegendOnExpandedType() public function testHelp() { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType', null, array( + $form = $this->factory->createNamed('name', TextType::class, null, array( 'help' => 'Help text test!', )); $view = $form->createView(); @@ -170,7 +179,7 @@ public function testHelp() public function testErrors() { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType'); + $form = $this->factory->createNamed('name', TextType::class); $form->addError(new FormError('[trans]Error 1[/trans]')); $form->addError(new FormError('[trans]Error 2[/trans]')); $view = $form->createView(); @@ -195,7 +204,7 @@ public function testErrors() public function testErrorWithNoLabel() { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType', array('label' => false)); + $form = $this->factory->createNamed('name', TextType::class, array('label' => false)); $form->addError(new FormError('[trans]Error 1[/trans]')); $view = $form->createView(); $html = $this->renderLabel($view); @@ -205,7 +214,7 @@ public function testErrorWithNoLabel() public function testCheckedCheckbox() { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\CheckboxType', true); + $form = $this->factory->createNamed('name', CheckboxType::class, true); $this->assertWidgetMatchesXpath($form->createView(), array('id' => 'my&id', 'attr' => array('class' => 'my&class')), '/div @@ -222,7 +231,7 @@ public function testCheckedCheckbox() public function testSingleChoiceAttributesWithMainAttributes() { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', array( + $form = $this->factory->createNamed('name', ChoiceType::class, '&a', array( 'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b'), 'multiple' => false, 'expanded' => false, @@ -245,7 +254,7 @@ public function testSingleChoiceAttributesWithMainAttributes() public function testSingleExpandedChoiceAttributesWithMainAttributes() { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', array( + $form = $this->factory->createNamed('name', ChoiceType::class, '&a', array( 'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b'), 'multiple' => false, 'expanded' => true, @@ -278,7 +287,7 @@ public function testSingleExpandedChoiceAttributesWithMainAttributes() public function testUncheckedCheckbox() { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\CheckboxType', false); + $form = $this->factory->createNamed('name', CheckboxType::class, false); $this->assertWidgetMatchesXpath($form->createView(), array('id' => 'my&id', 'attr' => array('class' => 'my&class')), '/div @@ -294,7 +303,7 @@ public function testUncheckedCheckbox() public function testCheckboxWithValue() { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\CheckboxType', false, array( + $form = $this->factory->createNamed('name', CheckboxType::class, false, array( 'value' => 'foo&bar', )); @@ -312,7 +321,7 @@ public function testCheckboxWithValue() public function testSingleChoiceExpanded() { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', array( + $form = $this->factory->createNamed('name', ChoiceType::class, '&a', array( 'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b'), 'multiple' => false, 'expanded' => true, @@ -343,7 +352,7 @@ public function testSingleChoiceExpanded() public function testSingleChoiceExpandedWithLabelsAsFalse() { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', array( + $form = $this->factory->createNamed('name', ChoiceType::class, '&a', array( 'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b'), 'choice_label' => false, 'multiple' => false, @@ -373,7 +382,7 @@ public function testSingleChoiceExpandedWithLabelsAsFalse() public function testSingleChoiceExpandedWithLabelsSetByCallable() { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', array( + $form = $this->factory->createNamed('name', ChoiceType::class, '&a', array( 'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b', 'Choice&C' => '&c'), 'choice_label' => function ($choice, $label, $value) { if ('&b' === $choice) { @@ -417,7 +426,7 @@ public function testSingleChoiceExpandedWithLabelsSetByCallable() public function testSingleChoiceExpandedWithLabelsSetFalseByCallable() { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', array( + $form = $this->factory->createNamed('name', ChoiceType::class, '&a', array( 'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b'), 'choice_label' => function () { return false; @@ -449,7 +458,7 @@ public function testSingleChoiceExpandedWithLabelsSetFalseByCallable() public function testSingleChoiceExpandedWithoutTranslation() { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', array( + $form = $this->factory->createNamed('name', ChoiceType::class, '&a', array( 'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b'), 'multiple' => false, 'expanded' => true, @@ -481,7 +490,7 @@ public function testSingleChoiceExpandedWithoutTranslation() public function testSingleChoiceExpandedAttributes() { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', array( + $form = $this->factory->createNamed('name', ChoiceType::class, '&a', array( 'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b'), 'choice_attr' => array('Choice&B' => array('class' => 'foo&bar')), 'multiple' => false, @@ -513,7 +522,7 @@ public function testSingleChoiceExpandedAttributes() public function testSingleChoiceExpandedWithPlaceholder() { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', array( + $form = $this->factory->createNamed('name', ChoiceType::class, '&a', array( 'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b'), 'multiple' => false, 'expanded' => true, @@ -553,7 +562,7 @@ public function testSingleChoiceExpandedWithPlaceholder() public function testSingleChoiceExpandedWithPlaceholderWithoutTranslation() { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', array( + $form = $this->factory->createNamed('name', ChoiceType::class, '&a', array( 'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b'), 'multiple' => false, 'expanded' => true, @@ -594,7 +603,7 @@ public function testSingleChoiceExpandedWithPlaceholderWithoutTranslation() public function testSingleChoiceExpandedWithBooleanValue() { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', true, array( + $form = $this->factory->createNamed('name', ChoiceType::class, true, array( 'choices' => array('Choice&A' => '1', 'Choice&B' => '0'), 'multiple' => false, 'expanded' => true, @@ -625,7 +634,7 @@ public function testSingleChoiceExpandedWithBooleanValue() public function testMultipleChoiceExpanded() { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', array('&a', '&c'), array( + $form = $this->factory->createNamed('name', ChoiceType::class, array('&a', '&c'), array( 'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b', 'Choice&C' => '&c'), 'multiple' => true, 'expanded' => true, @@ -664,7 +673,7 @@ public function testMultipleChoiceExpanded() public function testMultipleChoiceExpandedWithLabelsAsFalse() { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', array('&a'), array( + $form = $this->factory->createNamed('name', ChoiceType::class, array('&a'), array( 'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b'), 'choice_label' => false, 'multiple' => true, @@ -694,7 +703,7 @@ public function testMultipleChoiceExpandedWithLabelsAsFalse() public function testMultipleChoiceExpandedWithLabelsSetByCallable() { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', array('&a'), array( + $form = $this->factory->createNamed('name', ChoiceType::class, array('&a'), array( 'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b', 'Choice&C' => '&c'), 'choice_label' => function ($choice, $label, $value) { if ('&b' === $choice) { @@ -738,7 +747,7 @@ public function testMultipleChoiceExpandedWithLabelsSetByCallable() public function testMultipleChoiceExpandedWithLabelsSetFalseByCallable() { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', array('&a'), array( + $form = $this->factory->createNamed('name', ChoiceType::class, array('&a'), array( 'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b'), 'choice_label' => function () { return false; @@ -770,7 +779,7 @@ public function testMultipleChoiceExpandedWithLabelsSetFalseByCallable() public function testMultipleChoiceExpandedWithoutTranslation() { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', array('&a', '&c'), array( + $form = $this->factory->createNamed('name', ChoiceType::class, array('&a', '&c'), array( 'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b', 'Choice&C' => '&c'), 'multiple' => true, 'expanded' => true, @@ -810,7 +819,7 @@ public function testMultipleChoiceExpandedWithoutTranslation() public function testMultipleChoiceExpandedAttributes() { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', array('&a', '&c'), array( + $form = $this->factory->createNamed('name', ChoiceType::class, array('&a', '&c'), array( 'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b', 'Choice&C' => '&c'), 'choice_attr' => array('Choice&B' => array('class' => 'foo&bar')), 'multiple' => true, @@ -850,7 +859,7 @@ public function testMultipleChoiceExpandedAttributes() public function testCheckedRadio() { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\RadioType', true); + $form = $this->factory->createNamed('name', RadioType::class, true); $this->assertWidgetMatchesXpath($form->createView(), array('id' => 'my&id', 'attr' => array('class' => 'my&class')), '/div @@ -872,7 +881,7 @@ public function testCheckedRadio() public function testUncheckedRadio() { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\RadioType', false); + $form = $this->factory->createNamed('name', RadioType::class, false); $this->assertWidgetMatchesXpath($form->createView(), array('id' => 'my&id', 'attr' => array('class' => 'my&class')), '/div @@ -893,7 +902,7 @@ public function testUncheckedRadio() public function testRadioWithValue() { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\RadioType', false, array( + $form = $this->factory->createNamed('name', RadioType::class, false, array( 'value' => 'foo&bar', )); @@ -917,7 +926,7 @@ public function testRadioWithValue() public function testButtonAttributeNameRepeatedIfTrue() { - $form = $this->factory->createNamed('button', 'Symfony\Component\Form\Extension\Core\Type\ButtonType', null, array( + $form = $this->factory->createNamed('button', ButtonType::class, null, array( 'attr' => array('foo' => true), )); @@ -929,7 +938,7 @@ public function testButtonAttributeNameRepeatedIfTrue() public function testFile() { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\FileType'); + $form = $this->factory->createNamed('name', FileType::class); $this->assertWidgetMatchesXpath($form->createView(), array('attr' => array('class' => 'my&class form-control-file')), '/input @@ -940,7 +949,7 @@ public function testFile() public function testMoney() { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\MoneyType', 1234.56, array( + $form = $this->factory->createNamed('name', MoneyType::class, 1234.56, array( 'currency' => 'EUR', )); @@ -968,7 +977,7 @@ public function testMoney() public function testPercent() { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\PercentType', 0.1); + $form = $this->factory->createNamed('name', PercentType::class, 0.1); $this->assertWidgetMatchesXpath($form->createView(), array('id' => 'my&id', 'attr' => array('class' => 'my&class')), '/div diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformerTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformerTest.php index 1ad3aa1615c98..d9fafdff13a35 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformerTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformerTest.php @@ -78,6 +78,16 @@ public function testFloatToIntConversionMismatchOnReversTransform() $transformer = new MoneyToLocalizedStringTransformer(null, null, null, 100); IntlTestHelper::requireFullIntl($this, false); \Locale::setDefault('de_AT'); + $this->assertSame(3655, (int) $transformer->reverseTransform('36,55')); } + + public function testFloatToIntConversionMismatchOnTransform() + { + $transformer = new MoneyToLocalizedStringTransformer(null, null, MoneyToLocalizedStringTransformer::ROUND_DOWN, 100); + IntlTestHelper::requireFullIntl($this, false); + \Locale::setDefault('de_AT'); + + $this->assertSame('10,20', $transformer->transform(1020)); + } } diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php index 9cbe941581418..1f24323251790 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php @@ -2000,7 +2000,7 @@ public function testStripLeadingUnderscoresAndDigitsFromId() $this->assertEquals('_09name', $view->vars['full_name']); } - /** + /** * @dataProvider provideTrimCases */ public function testTrimIsDisabled($multiple, $expanded) diff --git a/src/Symfony/Component/HttpFoundation/File/File.php b/src/Symfony/Component/HttpFoundation/File/File.php index 4e6887a52ad48..44ac3925114a9 100644 --- a/src/Symfony/Component/HttpFoundation/File/File.php +++ b/src/Symfony/Component/HttpFoundation/File/File.php @@ -93,9 +93,11 @@ public function move($directory, $name = null) { $target = $this->getTargetFile($directory, $name); - if (!@rename($this->getPathname(), $target)) { - $error = error_get_last(); - throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s)', $this->getPathname(), $target, strip_tags($error['message']))); + set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; }); + $renamed = rename($this->getPathname(), $target); + restore_error_handler(); + if (!$renamed) { + throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s)', $this->getPathname(), $target, strip_tags($error))); } @chmod($target, 0666 & ~umask()); diff --git a/src/Symfony/Component/HttpFoundation/File/UploadedFile.php b/src/Symfony/Component/HttpFoundation/File/UploadedFile.php index c4abec390dd7f..252f46ad8e59b 100644 --- a/src/Symfony/Component/HttpFoundation/File/UploadedFile.php +++ b/src/Symfony/Component/HttpFoundation/File/UploadedFile.php @@ -207,9 +207,11 @@ public function move($directory, $name = null) $target = $this->getTargetFile($directory, $name); - if (!@move_uploaded_file($this->getPathname(), $target)) { - $error = error_get_last(); - throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s)', $this->getPathname(), $target, strip_tags($error['message']))); + set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; }); + $moved = move_uploaded_file($this->getPathname(), $target); + restore_error_handler(); + if (!$moved) { + throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s)', $this->getPathname(), $target, strip_tags($error))); } @chmod($target, 0666 & ~umask()); diff --git a/src/Symfony/Component/HttpFoundation/Request.php b/src/Symfony/Component/HttpFoundation/Request.php index 26c13ebd152d7..82240738ce56d 100644 --- a/src/Symfony/Component/HttpFoundation/Request.php +++ b/src/Symfony/Component/HttpFoundation/Request.php @@ -1243,7 +1243,7 @@ public function getRealMethod() * * @param string $format The format * - * @return string The associated mime type (null if not found) + * @return string|null The associated mime type (null if not found) */ public function getMimeType($format) { diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php index 5ae3d52cc30b1..43f07f25de1dd 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php @@ -674,14 +674,16 @@ private function doAdvisoryLock(string $sessionId) { switch ($this->driver) { case 'mysql': + // MySQL 5.7.5 and later enforces a maximum length on lock names of 64 characters. Previously, no limit was enforced. + $lockId = \substr($sessionId, 0, 64); // should we handle the return value? 0 on timeout, null on error // we use a timeout of 50 seconds which is also the default for innodb_lock_wait_timeout $stmt = $this->pdo->prepare('SELECT GET_LOCK(:key, 50)'); - $stmt->bindValue(':key', $sessionId, \PDO::PARAM_STR); + $stmt->bindValue(':key', $lockId, \PDO::PARAM_STR); $stmt->execute(); $releaseStmt = $this->pdo->prepare('DO RELEASE_LOCK(:key)'); - $releaseStmt->bindValue(':key', $sessionId, \PDO::PARAM_STR); + $releaseStmt->bindValue(':key', $lockId, \PDO::PARAM_STR); return $releaseStmt; case 'pgsql': @@ -833,7 +835,7 @@ private function getUpdateStatement($sessionId, $sessionData, $maxlifetime) /** * Returns a merge/upsert (i.e. insert or update) statement when supported by the database for writing session data. */ - private function getMergeStatement(string $sessionId, string $data, int$maxlifetime): ?\PDOStatement + private function getMergeStatement(string $sessionId, string $data, int $maxlifetime): ?\PDOStatement { switch (true) { case 'mysql' === $this->driver: diff --git a/src/Symfony/Component/HttpFoundation/Tests/ResponseTest.php b/src/Symfony/Component/HttpFoundation/Tests/ResponseTest.php index b35727962eacd..b4af82ddf68de 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/ResponseTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/ResponseTest.php @@ -300,7 +300,7 @@ public function testGetMaxAge() $response = new Response(); $response->headers->set('Cache-Control', 'must-revalidate'); $response->headers->set('Expires', -1); - $this->assertLessThanOrEqual(time() - 2*86400, $response->getExpires()->format('U')); + $this->assertLessThanOrEqual(time() - 2 * 86400, $response->getExpires()->format('U')); $response = new Response(); $this->assertNull($response->getMaxAge(), '->getMaxAge() returns null if no freshness information available'); diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/TraceableValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/TraceableValueResolver.php index 9837a057a6ae0..4d60aa15f7adb 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/TraceableValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/TraceableValueResolver.php @@ -26,10 +26,10 @@ final class TraceableValueResolver implements ArgumentValueResolverInterface private $inner; private $stopwatch; - public function __construct(ArgumentValueResolverInterface $inner, ?Stopwatch $stopwatch = null) + public function __construct(ArgumentValueResolverInterface $inner, Stopwatch $stopwatch) { $this->inner = $inner; - $this->stopwatch = $stopwatch ?? new Stopwatch(); + $this->stopwatch = $stopwatch; } /** diff --git a/src/Symfony/Component/HttpKernel/Controller/ControllerResolver.php b/src/Symfony/Component/HttpKernel/Controller/ControllerResolver.php index 274602a6ad470..dce640e31dadc 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ControllerResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ControllerResolver.php @@ -199,7 +199,7 @@ private function getClassMethodsWithoutMagicMethods($classOrObject) { $methods = get_class_methods($classOrObject); - return array_filter($methods, function(string $method) { + return array_filter($methods, function (string $method) { return 0 !== strncmp($method, '__', 2); }); } diff --git a/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactory.php b/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactory.php index 646adc074bf6a..86683d55de3d1 100644 --- a/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactory.php +++ b/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactory.php @@ -34,7 +34,7 @@ public function createArgumentMetadata($controller) } foreach ($reflection->getParameters() as $param) { - $arguments[] = new ArgumentMetadata($param->getName(), $this->getType($param), $param->isVariadic(), $param->isDefaultValueAvailable(), $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null, $param->allowsNull()); + $arguments[] = new ArgumentMetadata($param->getName(), $this->getType($param, $reflection), $param->isVariadic(), $param->isDefaultValueAvailable(), $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null, $param->allowsNull()); } return $arguments; @@ -47,12 +47,25 @@ public function createArgumentMetadata($controller) * * @return null|string */ - private function getType(\ReflectionParameter $parameter) + private function getType(\ReflectionParameter $parameter, \ReflectionFunctionAbstract $function) { if (!$type = $parameter->getType()) { return; } + $name = $type->getName(); + $lcName = strtolower($name); - return $type->getName(); + if ('self' !== $lcName && 'parent' !== $lcName) { + return $name; + } + if (!$function instanceof \ReflectionMethod) { + return; + } + if ('self' === $lcName) { + return $function->getDeclaringClass()->name; + } + if ($parent = $function->getDeclaringClass()->getParentClass()) { + return $parent->name; + } } } diff --git a/src/Symfony/Component/HttpKernel/DependencyInjection/ControllerArgumentValueResolverPass.php b/src/Symfony/Component/HttpKernel/DependencyInjection/ControllerArgumentValueResolverPass.php index 1b12a581f3ba3..77c0e479ae0a2 100644 --- a/src/Symfony/Component/HttpKernel/DependencyInjection/ControllerArgumentValueResolverPass.php +++ b/src/Symfony/Component/HttpKernel/DependencyInjection/ControllerArgumentValueResolverPass.php @@ -15,7 +15,6 @@ use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait; use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\TraceableValueResolver; use Symfony\Component\Stopwatch\Stopwatch; @@ -31,11 +30,13 @@ class ControllerArgumentValueResolverPass implements CompilerPassInterface private $argumentResolverService; private $argumentValueResolverTag; + private $traceableResolverStopwatch; - public function __construct(string $argumentResolverService = 'argument_resolver', string $argumentValueResolverTag = 'controller.argument_value_resolver') + public function __construct(string $argumentResolverService = 'argument_resolver', string $argumentValueResolverTag = 'controller.argument_value_resolver', string $traceableResolverStopwatch = 'debug.stopwatch') { $this->argumentResolverService = $argumentResolverService; $this->argumentValueResolverTag = $argumentValueResolverTag; + $this->traceableResolverStopwatch = $traceableResolverStopwatch; } public function process(ContainerBuilder $container) @@ -46,12 +47,12 @@ public function process(ContainerBuilder $container) $resolvers = $this->findAndSortTaggedServices($this->argumentValueResolverTag, $container); - if ($container->getParameter('kernel.debug') && class_exists(Stopwatch::class)) { + if ($container->getParameter('kernel.debug') && class_exists(Stopwatch::class) && $container->has($this->traceableResolverStopwatch)) { foreach ($resolvers as $resolverReference) { $id = (string) $resolverReference; $container->register("debug.$id", TraceableValueResolver::class) ->setDecoratedService($id) - ->setArguments(array(new Reference("debug.$id.inner"), new Reference('debug.stopwatch', ContainerInterface::NULL_ON_INVALID_REFERENCE))); + ->setArguments(array(new Reference("debug.$id.inner"), new Reference($this->traceableResolverStopwatch))); } } diff --git a/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php b/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php index cd76e56caab83..490b595b94153 100644 --- a/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php +++ b/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php @@ -33,11 +33,13 @@ class RegisterControllerArgumentLocatorsPass implements CompilerPassInterface { private $resolverServiceId; private $controllerTag; + private $controllerLocator; - public function __construct(string $resolverServiceId = 'argument_resolver.service', string $controllerTag = 'controller.service_arguments') + public function __construct(string $resolverServiceId = 'argument_resolver.service', string $controllerTag = 'controller.service_arguments', string $controllerLocator = 'argument_resolver.controller_locator') { $this->resolverServiceId = $resolverServiceId; $this->controllerTag = $controllerTag; + $this->controllerLocator = $controllerLocator; } public function process(ContainerBuilder $container) @@ -179,6 +181,8 @@ public function process(ContainerBuilder $container) } $container->getDefinition($this->resolverServiceId) - ->replaceArgument(0, ServiceLocatorTagPass::register($container, $controllers)); + ->replaceArgument(0, $controllerLocatorRef = ServiceLocatorTagPass::register($container, $controllers)); + + $container->setAlias($this->controllerLocator, (string) $controllerLocatorRef); } } diff --git a/src/Symfony/Component/HttpKernel/DependencyInjection/RemoveEmptyControllerArgumentLocatorsPass.php b/src/Symfony/Component/HttpKernel/DependencyInjection/RemoveEmptyControllerArgumentLocatorsPass.php index b7d64994cec87..596b6188f66cb 100644 --- a/src/Symfony/Component/HttpKernel/DependencyInjection/RemoveEmptyControllerArgumentLocatorsPass.php +++ b/src/Symfony/Component/HttpKernel/DependencyInjection/RemoveEmptyControllerArgumentLocatorsPass.php @@ -21,21 +21,16 @@ */ class RemoveEmptyControllerArgumentLocatorsPass implements CompilerPassInterface { - private $resolverServiceId; + private $controllerLocator; - public function __construct(string $resolverServiceId = 'argument_resolver.service') + public function __construct(string $controllerLocator = 'argument_resolver.controller_locator') { - $this->resolverServiceId = $resolverServiceId; + $this->controllerLocator = $controllerLocator; } public function process(ContainerBuilder $container) { - if (false === $container->hasDefinition($this->resolverServiceId)) { - return; - } - - $serviceResolver = $container->getDefinition($this->resolverServiceId); - $controllerLocator = $container->getDefinition((string) $serviceResolver->getArgument(0)); + $controllerLocator = $container->findDefinition($this->controllerLocator); $controllers = $controllerLocator->getArgument(0); foreach ($controllers as $controller => $argumentRef) { diff --git a/src/Symfony/Component/HttpKernel/EventListener/RouterListener.php b/src/Symfony/Component/HttpKernel/EventListener/RouterListener.php index 98a397a2e6841..f7cf446e52fae 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/RouterListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/RouterListener.php @@ -14,6 +14,7 @@ use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; use Symfony\Component\HttpKernel\Event\FinishRequestEvent; use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\HttpKernel\KernelEvents; @@ -129,12 +130,6 @@ public function onKernelRequest(GetResponseEvent $event) unset($parameters['_route'], $parameters['_controller']); $request->attributes->set('_route_params', $parameters); } catch (ResourceNotFoundException $e) { - if ($this->debug && $e instanceof NoConfigurationException) { - $event->setResponse($this->createWelcomeResponse()); - - return; - } - $message = sprintf('No route found for "%s %s"', $request->getMethod(), $request->getPathInfo()); if ($referer = $request->headers->get('referer')) { @@ -149,11 +144,23 @@ public function onKernelRequest(GetResponseEvent $event) } } + public function onKernelException(GetResponseForExceptionEvent $event) + { + if (!$this->debug || !($e = $event->getException()) instanceof NotFoundHttpException) { + return; + } + + if ($e->getPrevious() instanceof NoConfigurationException) { + $event->setResponse($this->createWelcomeResponse()); + } + } + public static function getSubscribedEvents() { return array( KernelEvents::REQUEST => array(array('onKernelRequest', 32)), KernelEvents::FINISH_REQUEST => array(array('onKernelFinishRequest', 0)), + KernelEvents::EXCEPTION => array('onKernelException', -64), ); } diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index a45b2b9d02b61..ae3ea7243749f 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -63,12 +63,12 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl private $requestStackSize = 0; private $resetServices = false; - const VERSION = '4.1.0-BETA1'; + const VERSION = '4.1.0-BETA2'; const VERSION_ID = 40100; const MAJOR_VERSION = 4; const MINOR_VERSION = 1; const RELEASE_VERSION = 0; - const EXTRA_VERSION = 'BETA1'; + const EXTRA_VERSION = 'BETA2'; const END_OF_MAINTENANCE = '01/2019'; const END_OF_LIFE = '07/2019'; @@ -465,7 +465,7 @@ protected function initializeContainer() $errorLevel = error_reporting(\E_ALL ^ \E_WARNING); $fresh = $oldContainer = false; try { - if (\is_object($this->container = include $cache->getPath())) { + if (file_exists($cache->getPath()) && \is_object($this->container = include $cache->getPath())) { $this->container->set('kernel', $this); $oldContainer = $this->container; $fresh = true; @@ -528,7 +528,7 @@ protected function initializeContainer() } } - if (null === $oldContainer) { + if (null === $oldContainer && file_exists($cache->getPath())) { $errorLevel = error_reporting(\E_ALL ^ \E_WARNING); try { $oldContainer = include $cache->getPath(); diff --git a/src/Symfony/Component/HttpKernel/Profiler/Profile.php b/src/Symfony/Component/HttpKernel/Profiler/Profile.php index f03c872672f5f..bb18c7bcab80d 100644 --- a/src/Symfony/Component/HttpKernel/Profiler/Profile.php +++ b/src/Symfony/Component/HttpKernel/Profiler/Profile.php @@ -71,7 +71,7 @@ public function getToken() /** * Sets the parent token. */ - public function setParent(Profile $parent) + public function setParent(self $parent) { $this->parent = $parent; } @@ -210,7 +210,7 @@ public function setChildren(array $children) /** * Adds the child token. */ - public function addChild(Profile $child) + public function addChild(self $child) { $this->children[] = $child; $child->setParent($this); diff --git a/src/Symfony/Component/HttpKernel/Tests/ControllerMetadata/ArgumentMetadataFactoryTest.php b/src/Symfony/Component/HttpKernel/Tests/ControllerMetadata/ArgumentMetadataFactoryTest.php index 2ddc6e7a5ee10..a667705f50813 100644 --- a/src/Symfony/Component/HttpKernel/Tests/ControllerMetadata/ArgumentMetadataFactoryTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/ControllerMetadata/ArgumentMetadataFactoryTest.php @@ -117,11 +117,11 @@ public function testNullableTypesSignature() ), $arguments); } - private function signature1(ArgumentMetadataFactoryTest $foo, array $bar, callable $baz) + private function signature1(self $foo, array $bar, callable $baz) { } - private function signature2(ArgumentMetadataFactoryTest $foo = null, FakeClassThatDoesNotExist $bar = null, ImportedAndFake $baz = null) + private function signature2(self $foo = null, FakeClassThatDoesNotExist $bar = null, ImportedAndFake $baz = null) { } diff --git a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ControllerArgumentValueResolverPassTest.php b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ControllerArgumentValueResolverPassTest.php index 3cbc62131fa24..49bbd0f9c1ccd 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ControllerArgumentValueResolverPassTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ControllerArgumentValueResolverPassTest.php @@ -17,6 +17,7 @@ use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Symfony\Component\HttpKernel\DependencyInjection\ControllerArgumentValueResolverPass; +use Symfony\Component\Stopwatch\Stopwatch; class ControllerArgumentValueResolverPassTest extends TestCase { @@ -52,7 +53,7 @@ public function testServicesAreOrderedAccordingToPriority() $this->assertFalse($container->hasDefinition('n3.traceable')); } - public function testInDebug() + public function testInDebugWithStopWatchDefinition() { $services = array( 'n3' => array(array()), @@ -68,6 +69,7 @@ public function testInDebug() $definition = new Definition(ArgumentResolver::class, array(null, array())); $container = new ContainerBuilder(); + $container->register('debug.stopwatch', Stopwatch::class); $container->setDefinition('argument_resolver', $definition); foreach ($services as $id => list($tag)) { @@ -88,6 +90,24 @@ public function testInDebug() $this->assertTrue($container->hasDefinition('n3')); } + public function testInDebugWithouStopWatchDefinition() + { + $expected = array(new Reference('n1')); + + $definition = new Definition(ArgumentResolver::class, array(null, array())); + $container = new ContainerBuilder(); + $container->register('n1')->addTag('controller.argument_value_resolver'); + $container->setDefinition('argument_resolver', $definition); + + $container->setParameter('kernel.debug', true); + + (new ControllerArgumentValueResolverPass())->process($container); + $this->assertEquals($expected, $definition->getArgument(1)->getValues()); + + $this->assertFalse($container->hasDefinition('debug.n1')); + $this->assertTrue($container->hasDefinition('n1')); + } + public function testReturningEmptyArrayWhenNoService() { $definition = new Definition(ArgumentResolver::class, array(null, array())); diff --git a/src/Symfony/Component/HttpKernel/Tests/Exception/UnprocessableEntityHttpExceptionTest.php b/src/Symfony/Component/HttpKernel/Tests/Exception/UnprocessableEntityHttpExceptionTest.php index 760366c6943a1..05d8d787aa5c5 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Exception/UnprocessableEntityHttpExceptionTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Exception/UnprocessableEntityHttpExceptionTest.php @@ -6,21 +6,6 @@ class UnprocessableEntityHttpExceptionTest extends HttpExceptionTest { - /** - * Test that setting the headers using the setter function - * is working as expected. - * - * @param array $headers The headers to set - * - * @dataProvider headerDataProvider - */ - public function testHeadersSetter($headers) - { - $exception = new UnprocessableEntityHttpException(10); - $exception->setHeaders($headers); - $this->assertSame($headers, $exception->getHeaders()); - } - protected function createException() { return new UnprocessableEntityHttpException(); diff --git a/src/Symfony/Component/HttpKernel/Tests/Exception/UnsupportedMediaTypeHttpExceptionTest.php b/src/Symfony/Component/HttpKernel/Tests/Exception/UnsupportedMediaTypeHttpExceptionTest.php index d47287a1fbc69..4dae039c11fc1 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Exception/UnsupportedMediaTypeHttpExceptionTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Exception/UnsupportedMediaTypeHttpExceptionTest.php @@ -6,17 +6,7 @@ class UnsupportedMediaTypeHttpExceptionTest extends HttpExceptionTest { - /** - * @dataProvider headerDataProvider - */ - public function testHeadersSetter($headers) - { - $exception = new UnsupportedMediaTypeHttpException(10); - $exception->setHeaders($headers); - $this->assertSame($headers, $exception->getHeaders()); - } - - protected function createException($headers = array()) + protected function createException() { return new UnsupportedMediaTypeHttpException(); } diff --git a/src/Symfony/Component/HttpKernel/Tests/Profiler/ProfilerTest.php b/src/Symfony/Component/HttpKernel/Tests/Profiler/ProfilerTest.php index 243c3c5c5a7cb..2d5f0ca5ad59e 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Profiler/ProfilerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Profiler/ProfilerTest.php @@ -44,7 +44,7 @@ public function testCollect() public function testReset() { $collector = $this->getMockBuilder(DataCollectorInterface::class) - ->setMethods(['collect', 'getName', 'reset']) + ->setMethods(array('collect', 'getName', 'reset')) ->getMock(); $collector->expects($this->any())->method('getName')->willReturn('mock'); $collector->expects($this->once())->method('reset'); diff --git a/src/Symfony/Component/Intl/NumberFormatter/NumberFormatter.php b/src/Symfony/Component/Intl/NumberFormatter/NumberFormatter.php index 874a7e290b776..6503e09bfce46 100644 --- a/src/Symfony/Component/Intl/NumberFormatter/NumberFormatter.php +++ b/src/Symfony/Component/Intl/NumberFormatter/NumberFormatter.php @@ -710,6 +710,7 @@ private function round($value, $precision) } elseif (isset(self::$customRoundingList[$roundingModeAttribute])) { $roundingCoef = pow(10, $precision); $value *= $roundingCoef; + $value = (float) (string) $value; switch ($roundingModeAttribute) { case self::ROUND_CEILING: diff --git a/src/Symfony/Component/Intl/Tests/NumberFormatter/AbstractNumberFormatterTest.php b/src/Symfony/Component/Intl/Tests/NumberFormatter/AbstractNumberFormatterTest.php index ace8db70afff5..3f1b29870357e 100644 --- a/src/Symfony/Component/Intl/Tests/NumberFormatter/AbstractNumberFormatterTest.php +++ b/src/Symfony/Component/Intl/Tests/NumberFormatter/AbstractNumberFormatterTest.php @@ -428,6 +428,7 @@ public function formatRoundingModeRoundHalfUpProvider() // array(1.125, '1.13'), array(1.127, '1.13'), array(1.129, '1.13'), + array(1020 / 100, '10.20'), ); } @@ -451,6 +452,7 @@ public function formatRoundingModeRoundHalfDownProvider() array(1.125, '1.12'), array(1.127, '1.13'), array(1.129, '1.13'), + array(1020 / 100, '10.20'), ); } @@ -474,6 +476,7 @@ public function formatRoundingModeRoundHalfEvenProvider() array(1.125, '1.12'), array(1.127, '1.13'), array(1.129, '1.13'), + array(1020 / 100, '10.20'), ); } @@ -498,6 +501,7 @@ public function formatRoundingModeRoundCeilingProvider() array(-1.123, '-1.12'), array(-1.125, '-1.12'), array(-1.127, '-1.12'), + array(1020 / 100, '10.20'), ); } @@ -522,6 +526,7 @@ public function formatRoundingModeRoundFloorProvider() array(-1.123, '-1.13'), array(-1.125, '-1.13'), array(-1.127, '-1.13'), + array(1020 / 100, '10.20'), ); } @@ -546,6 +551,7 @@ public function formatRoundingModeRoundDownProvider() array(-1.123, '-1.12'), array(-1.125, '-1.12'), array(-1.127, '-1.12'), + array(1020 / 100, '10.20'), ); } @@ -570,6 +576,7 @@ public function formatRoundingModeRoundUpProvider() array(-1.123, '-1.13'), array(-1.125, '-1.13'), array(-1.127, '-1.13'), + array(1020 / 100, '10.20'), ); } diff --git a/src/Symfony/Component/Ldap/Ldap.php b/src/Symfony/Component/Ldap/Ldap.php index 26fd4f946ccc9..de1ab1a249eda 100644 --- a/src/Symfony/Component/Ldap/Ldap.php +++ b/src/Symfony/Component/Ldap/Ldap.php @@ -70,7 +70,7 @@ public function escape($subject, $ignore = '', $flags = 0) * * @return static */ - public static function create($adapter, array $config = array()): Ldap + public static function create($adapter, array $config = array()): self { if (!isset(self::$adapterMap[$adapter])) { throw new DriverNotFoundException(sprintf( diff --git a/src/Symfony/Component/Lock/Store/FlockStore.php b/src/Symfony/Component/Lock/Store/FlockStore.php index 64438fd461b2e..bf72eaefe2e3c 100644 --- a/src/Symfony/Component/Lock/Store/FlockStore.php +++ b/src/Symfony/Component/Lock/Store/FlockStore.php @@ -78,8 +78,7 @@ private function lock(Key $key, $blocking) ); // Silence error reporting - set_error_handler(function () { - }); + set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; }); if (!$handle = fopen($fileName, 'r')) { if ($handle = fopen($fileName, 'x')) { chmod($fileName, 0444); @@ -91,8 +90,7 @@ private function lock(Key $key, $blocking) restore_error_handler(); if (!$handle) { - $error = error_get_last(); - throw new LockStorageException($error['message'], 0, null); + throw new LockStorageException($error, 0, null); } // On Windows, even if PHP doc says the contrary, LOCK_NB works, see diff --git a/src/Symfony/Component/Lock/Tests/Store/BlockingStoreTestTrait.php b/src/Symfony/Component/Lock/Tests/Store/BlockingStoreTestTrait.php index 2fe15cd921bf9..1e3436eb5a094 100644 --- a/src/Symfony/Component/Lock/Tests/Store/BlockingStoreTestTrait.php +++ b/src/Symfony/Component/Lock/Tests/Store/BlockingStoreTestTrait.php @@ -31,6 +31,7 @@ abstract protected function getStore(); * This test is time sensible: the $clockDelay could be adjust. * * @requires extension pcntl + * @requires extension posix * @requires function pcntl_sigwaitinfo */ public function testBlockingLocks() diff --git a/src/Symfony/Component/Messenger/Asynchronous/Middleware/SendMessageMiddleware.php b/src/Symfony/Component/Messenger/Asynchronous/Middleware/SendMessageMiddleware.php index eeba363cc87c2..16fdfd81f1083 100644 --- a/src/Symfony/Component/Messenger/Asynchronous/Middleware/SendMessageMiddleware.php +++ b/src/Symfony/Component/Messenger/Asynchronous/Middleware/SendMessageMiddleware.php @@ -11,20 +11,26 @@ namespace Symfony\Component\Messenger\Asynchronous\Middleware; +use Symfony\Component\Messenger\Asynchronous\Routing\SenderLocator; use Symfony\Component\Messenger\Asynchronous\Routing\SenderLocatorInterface; use Symfony\Component\Messenger\Asynchronous\Transport\ReceivedMessage; +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\EnvelopeAwareInterface; use Symfony\Component\Messenger\Middleware\MiddlewareInterface; /** * @author Samuel Roze + * @author Tobias Schultze */ -class SendMessageMiddleware implements MiddlewareInterface +class SendMessageMiddleware implements MiddlewareInterface, EnvelopeAwareInterface { private $senderLocator; + private $messagesToSendAndHandleMapping; - public function __construct(SenderLocatorInterface $senderLocator) + public function __construct(SenderLocatorInterface $senderLocator, array $messagesToSendAndHandleMapping = array()) { $this->senderLocator = $senderLocator; + $this->messagesToSendAndHandleMapping = $messagesToSendAndHandleMapping; } /** @@ -32,24 +38,27 @@ public function __construct(SenderLocatorInterface $senderLocator) */ public function handle($message, callable $next) { - if ($message instanceof ReceivedMessage) { - return $next($message->getMessage()); + $envelope = Envelope::wrap($message); + if ($envelope->get(ReceivedMessage::class)) { + // It's a received message. Do not send it back: + return $next($message); } - if (!empty($senders = $this->senderLocator->getSendersForMessage($message))) { - foreach ($senders as $sender) { - if (null === $sender) { - continue; - } + $sender = $this->senderLocator->getSenderForMessage($envelope->getMessage()); - $sender->send($message); - } + if ($sender) { + $sender->send($envelope); - if (!\in_array(null, $senders, true)) { + if (!$this->mustSendAndHandle($envelope->getMessage())) { return; } } return $next($message); } + + private function mustSendAndHandle($message): bool + { + return (bool) SenderLocator::getValueFromMessageRouting($this->messagesToSendAndHandleMapping, $message); + } } diff --git a/src/Symfony/Component/Messenger/Asynchronous/Routing/SenderLocator.php b/src/Symfony/Component/Messenger/Asynchronous/Routing/SenderLocator.php index 9b62457626c5f..e85453691bd92 100644 --- a/src/Symfony/Component/Messenger/Asynchronous/Routing/SenderLocator.php +++ b/src/Symfony/Component/Messenger/Asynchronous/Routing/SenderLocator.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Messenger\Asynchronous\Routing; use Psr\Container\ContainerInterface; +use Symfony\Component\Messenger\Transport\SenderInterface; /** * @author Samuel Roze @@ -19,26 +20,47 @@ class SenderLocator implements SenderLocatorInterface { private $senderServiceLocator; - private $messageToSenderIdsMapping; + private $messageToSenderIdMapping; - public function __construct(ContainerInterface $senderServiceLocator, array $messageToSenderIdsMapping) + public function __construct(ContainerInterface $senderServiceLocator, array $messageToSenderIdMapping) { $this->senderServiceLocator = $senderServiceLocator; - $this->messageToSenderIdsMapping = $messageToSenderIdsMapping; + $this->messageToSenderIdMapping = $messageToSenderIdMapping; } /** * {@inheritdoc} */ - public function getSendersForMessage($message): array + public function getSenderForMessage($message): ?SenderInterface { - $senderIds = $this->messageToSenderIdsMapping[\get_class($message)] ?? $this->messageToSenderIdsMapping['*'] ?? array(); + $senderId = $this->getSenderId($message); - $senders = array(); - foreach ($senderIds as $senderId) { - $senders[] = $this->senderServiceLocator->get($senderId); + return $senderId ? $this->senderServiceLocator->get($senderId) : null; + } + + private function getSenderId($message): ?string + { + return self::getValueFromMessageRouting($this->messageToSenderIdMapping, $message); + } + + /** + * @internal + */ + public static function getValueFromMessageRouting(array $mapping, $message) + { + if (isset($mapping[\get_class($message)])) { + return $mapping[\get_class($message)]; + } + if ($parentsMapping = array_intersect_key($mapping, class_parents($message))) { + return current($parentsMapping); + } + if ($interfaceMapping = array_intersect_key($mapping, class_implements($message))) { + return current($interfaceMapping); + } + if (isset($mapping['*'])) { + return $mapping['*']; } - return $senders; + return null; } } diff --git a/src/Symfony/Component/Messenger/Asynchronous/Routing/SenderLocatorInterface.php b/src/Symfony/Component/Messenger/Asynchronous/Routing/SenderLocatorInterface.php index d97508b31a2f1..ffce0cf3a8554 100644 --- a/src/Symfony/Component/Messenger/Asynchronous/Routing/SenderLocatorInterface.php +++ b/src/Symfony/Component/Messenger/Asynchronous/Routing/SenderLocatorInterface.php @@ -15,6 +15,7 @@ /** * @author Samuel Roze + * @author Tobias Schultze * * @experimental in 4.1 */ @@ -25,7 +26,7 @@ interface SenderLocatorInterface * * @param object $message * - * @return SenderInterface[] + * @return SenderInterface|null */ - public function getSendersForMessage($message): array; + public function getSenderForMessage($message): ?SenderInterface; } diff --git a/src/Symfony/Component/Messenger/Asynchronous/Transport/ReceivedMessage.php b/src/Symfony/Component/Messenger/Asynchronous/Transport/ReceivedMessage.php index 1b1298da63d32..c713a589ad53c 100644 --- a/src/Symfony/Component/Messenger/Asynchronous/Transport/ReceivedMessage.php +++ b/src/Symfony/Component/Messenger/Asynchronous/Transport/ReceivedMessage.php @@ -12,26 +12,26 @@ namespace Symfony\Component\Messenger\Asynchronous\Transport; use Symfony\Component\Messenger\Asynchronous\Middleware\SendMessageMiddleware; +use Symfony\Component\Messenger\EnvelopeItemInterface; /** - * Wraps a received message. This is mainly used by the `SendMessageMiddleware` middleware to identify + * Marker config for a received message. + * This is mainly used by the `SendMessageMiddleware` middleware to identify * a message should not be sent if it was just received. * * @see SendMessageMiddleware * * @author Samuel Roze */ -final class ReceivedMessage +final class ReceivedMessage implements EnvelopeItemInterface { - private $message; - - public function __construct($message) + public function serialize() { - $this->message = $message; + return ''; } - public function getMessage() + public function unserialize($serialized) { - return $this->message; + // noop } } diff --git a/src/Symfony/Component/Messenger/Asynchronous/Transport/WrapIntoReceivedMessage.php b/src/Symfony/Component/Messenger/Asynchronous/Transport/WrapIntoReceivedMessage.php index 8af87a2a4514b..4b10d6445fc5f 100644 --- a/src/Symfony/Component/Messenger/Asynchronous/Transport/WrapIntoReceivedMessage.php +++ b/src/Symfony/Component/Messenger/Asynchronous/Transport/WrapIntoReceivedMessage.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Messenger\Asynchronous\Transport; +use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\Transport\ReceiverInterface; /** @@ -27,12 +28,12 @@ public function __construct(ReceiverInterface $decoratedConsumer) public function receive(callable $handler): void { - $this->decoratedReceiver->receive(function ($message) use ($handler) { - if (null !== $message) { - $message = new ReceivedMessage($message); + $this->decoratedReceiver->receive(function (?Envelope $envelope) use ($handler) { + if (null !== $envelope) { + $envelope = $envelope->with(new ReceivedMessage()); } - $handler($message); + $handler($envelope); }); } diff --git a/src/Symfony/Component/Messenger/Command/ConsumeMessagesCommand.php b/src/Symfony/Component/Messenger/Command/ConsumeMessagesCommand.php index 94684aacfbce2..06b48a50c5ac3 100644 --- a/src/Symfony/Component/Messenger/Command/ConsumeMessagesCommand.php +++ b/src/Symfony/Component/Messenger/Command/ConsumeMessagesCommand.php @@ -14,15 +14,16 @@ use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Messenger\Transport\Enhancers\StopWhenMemoryUsageIsExceededReceiver; use Symfony\Component\Messenger\Transport\Enhancers\StopWhenMessageCountIsExceededReceiver; use Symfony\Component\Messenger\Transport\Enhancers\StopWhenTimeLimitIsReachedReceiver; -use Symfony\Component\Messenger\Transport\ReceiverInterface; use Symfony\Component\Messenger\Worker; /** @@ -37,14 +38,16 @@ class ConsumeMessagesCommand extends Command private $bus; private $receiverLocator; private $logger; + private $receiverNames; - public function __construct(MessageBusInterface $bus, ContainerInterface $receiverLocator, LoggerInterface $logger = null) + public function __construct(MessageBusInterface $bus, ContainerInterface $receiverLocator, LoggerInterface $logger = null, array $receiverNames = array()) { - parent::__construct(); - $this->bus = $bus; $this->receiverLocator = $receiverLocator; $this->logger = $logger; + $this->receiverNames = $receiverNames; + + parent::__construct(); } /** @@ -52,9 +55,11 @@ public function __construct(MessageBusInterface $bus, ContainerInterface $receiv */ protected function configure(): void { + $defaultReceiverName = 1 === \count($this->receiverNames) ? current($this->receiverNames) : null; + $this ->setDefinition(array( - new InputArgument('receiver', InputArgument::REQUIRED, 'Name of the receiver'), + new InputArgument('receiver', $defaultReceiverName ? InputArgument::OPTIONAL : InputArgument::REQUIRED, 'Name of the receiver', $defaultReceiverName), new InputOption('limit', 'l', InputOption::VALUE_REQUIRED, 'Limit the number of received messages'), new InputOption('memory-limit', 'm', InputOption::VALUE_REQUIRED, 'The memory limit the worker can consume'), new InputOption('time-limit', 't', InputOption::VALUE_REQUIRED, 'The time limit in seconds the worker can run'), @@ -81,18 +86,37 @@ protected function configure(): void ; } + /** + * {@inheritdoc} + */ + protected function interact(InputInterface $input, OutputInterface $output) + { + if (!$this->receiverNames || $this->receiverLocator->has($receiverName = $input->getArgument('receiver'))) { + return; + } + + $style = new SymfonyStyle($input, $output); + if (null === $receiverName) { + $style->block('Missing receiver argument.', null, 'error', ' ', true); + $input->setArgument('receiver', $style->choice('Select one of the available receivers', $this->receiverNames)); + } elseif ($alternatives = $this->findAlternatives($receiverName, $this->receiverNames)) { + $style->block(sprintf('Receiver "%s" is not defined.', $receiverName), null, 'error', ' ', true); + if ($style->confirm(sprintf('Do you want to receive from "%s" instead? ', $alternatives[0]), false)) { + $input->setArgument('receiver', $alternatives[0]); + } + } + } + /** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output): void { if (!$this->receiverLocator->has($receiverName = $input->getArgument('receiver'))) { - throw new \RuntimeException(sprintf('Receiver "%s" does not exist.', $receiverName)); + throw new RuntimeException(sprintf('Receiver "%s" does not exist.', $receiverName)); } - if (!($receiver = $this->receiverLocator->get($receiverName)) instanceof ReceiverInterface) { - throw new \RuntimeException(sprintf('Receiver "%s" is not a valid message consumer. It must implement the "%s" interface.', $receiverName, ReceiverInterface::class)); - } + $receiver = $this->receiverLocator->get($receiverName); if ($limit = $input->getOption('limit')) { $receiver = new StopWhenMessageCountIsExceededReceiver($receiver, $limit, $this->logger); @@ -115,9 +139,9 @@ private function convertToBytes(string $memoryLimit): int $memoryLimit = strtolower($memoryLimit); $max = strtolower(ltrim($memoryLimit, '+')); if (0 === strpos($max, '0x')) { - $max = intval($max, 16); + $max = \intval($max, 16); } elseif (0 === strpos($max, '0')) { - $max = intval($max, 8); + $max = \intval($max, 8); } else { $max = (int) $max; } @@ -134,4 +158,21 @@ private function convertToBytes(string $memoryLimit): int return $max; } + + private function findAlternatives($name, array $collection) + { + $alternatives = array(); + foreach ($collection as $item) { + $lev = levenshtein($name, $item); + if ($lev <= \strlen($name) / 3 || false !== strpos($item, $name)) { + $alternatives[$item] = isset($alternatives[$item]) ? $alternatives[$item] - $lev : $lev; + } + } + + $threshold = 1e3; + $alternatives = array_filter($alternatives, function ($lev) use ($threshold) { return $lev < 2 * $threshold; }); + ksort($alternatives, SORT_NATURAL | SORT_FLAG_CASE); + + return array_keys($alternatives); + } } diff --git a/src/Symfony/Component/Messenger/Command/DebugCommand.php b/src/Symfony/Component/Messenger/Command/DebugCommand.php new file mode 100644 index 0000000000000..38d4e4253cec2 --- /dev/null +++ b/src/Symfony/Component/Messenger/Command/DebugCommand.php @@ -0,0 +1,100 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Command; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Exception\RuntimeException; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +/** + * A console command to debug Messenger information. + * + * @author Roland Franssen + * + * @experimental in 4.1 + */ +class DebugCommand extends Command +{ + protected static $defaultName = 'debug:messenger'; + + private $mapping; + + public function __construct(array $mapping) + { + $this->mapping = $mapping; + + parent::__construct(); + } + + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->addArgument('bus', InputArgument::OPTIONAL, sprintf('The bus id (one of %s)', implode(', ', array_keys($this->mapping))), null) + ->setDescription('Lists messages you can dispatch using the message buses') + ->setHelp(<<<'EOF' +The %command.name% command displays all messages that can be +dispatched using the message buses: + + php %command.full_name% + +Or for a specific bus only: + + php %command.full_name% command_bus + +EOF + ) + ; + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $io = new SymfonyStyle($input, $output); + $io->title('Messenger'); + + $mapping = $this->mapping; + if ($bus = $input->getArgument('bus')) { + if (!isset($mapping[$bus])) { + throw new RuntimeException(sprintf('Bus "%s" does not exist. Known buses are %s.', $bus, implode(', ', array_keys($this->mapping)))); + } + $mapping = array($bus => $mapping[$bus]); + } + + foreach ($mapping as $bus => $handlersByMessage) { + $io->section($bus); + + $tableRows = array(); + foreach ($handlersByMessage as $message => $handlers) { + $tableRows[] = array(sprintf('%s', $message)); + foreach ($handlers as $handler) { + $tableRows[] = array(sprintf(' handled by %s', $handler)); + } + } + + if ($tableRows) { + $io->text('The following messages can be dispatched:'); + $io->newLine(); + $io->table(array(), $tableRows); + } else { + $io->warning(sprintf('No handled message found in bus "%s".', $bus)); + } + } + } +} diff --git a/src/Symfony/Component/Messenger/DataCollector/MessengerDataCollector.php b/src/Symfony/Component/Messenger/DataCollector/MessengerDataCollector.php index 0fe44d62fea16..ed98f4cfa43f5 100644 --- a/src/Symfony/Component/Messenger/DataCollector/MessengerDataCollector.php +++ b/src/Symfony/Component/Messenger/DataCollector/MessengerDataCollector.php @@ -16,6 +16,8 @@ use Symfony\Component\HttpKernel\DataCollector\DataCollector; use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface; use Symfony\Component\Messenger\TraceableMessageBus; +use Symfony\Component\VarDumper\Caster\ClassStub; +use Symfony\Component\VarDumper\Cloner\Data; /** * @author Samuel Roze @@ -44,13 +46,25 @@ public function collect(Request $request, Response $response, \Exception $except */ public function lateCollect() { - $this->data = array('messages' => array()); + $this->data = array('messages' => array(), 'buses' => array_keys($this->traceableBuses)); + $messages = array(); foreach ($this->traceableBuses as $busName => $bus) { foreach ($bus->getDispatchedMessages() as $message) { - $this->data['messages'][] = $this->collectMessage($busName, $message); + $debugRepresentation = $this->cloneVar($this->collectMessage($busName, $message)); + $messages[] = array($debugRepresentation, $message['callTime']); } } + + // Order by call time + usort($messages, function (array $a, array $b): int { + return $a[1] > $b[1] ? 1 : -1; + }); + + // Keep the messages clones only + $this->data['messages'] = array_map(function (array $item): Data { + return $item[0]; + }, $messages); } /** @@ -78,31 +92,19 @@ private function collectMessage(string $busName, array $tracedMessage) $debugRepresentation = array( 'bus' => $busName, + 'envelopeItems' => $tracedMessage['envelopeItems'] ?? null, 'message' => array( - 'type' => \get_class($message), - 'object' => $this->cloneVar($message), + 'type' => new ClassStub(\get_class($message)), + 'value' => $message, ), ); if (array_key_exists('result', $tracedMessage)) { $result = $tracedMessage['result']; - - if (\is_object($result)) { - $debugRepresentation['result'] = array( - 'type' => \get_class($result), - 'object' => $this->cloneVar($result), - ); - } elseif (\is_array($result)) { - $debugRepresentation['result'] = array( - 'type' => 'array', - 'object' => $this->cloneVar($result), - ); - } else { - $debugRepresentation['result'] = array( - 'type' => \gettype($result), - 'value' => $result, - ); - } + $debugRepresentation['result'] = array( + 'type' => \is_object($result) ? \get_class($result) : gettype($result), + 'value' => $result, + ); } if (isset($tracedMessage['exception'])) { @@ -110,15 +112,31 @@ private function collectMessage(string $busName, array $tracedMessage) $debugRepresentation['exception'] = array( 'type' => \get_class($exception), - 'message' => $exception->getMessage(), + 'value' => $exception, ); } return $debugRepresentation; } - public function getMessages(): array + public function getExceptionsCount(string $bus = null): int + { + return array_reduce($this->getMessages($bus), function (int $carry, Data $message) { + return $carry += isset($message['exception']) ? 1 : 0; + }, 0); + } + + public function getMessages(string $bus = null): array + { + $messages = $this->data['messages'] ?? array(); + + return $bus ? array_filter($messages, function (Data $message) use ($bus): bool { + return $bus === $message['bus']; + }) : $messages; + } + + public function getBuses(): array { - return $this->data['messages'] ?? array(); + return $this->data['buses']; } } diff --git a/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php b/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php index 62f63dc05894d..7836637dde86c 100644 --- a/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php +++ b/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php @@ -20,9 +20,12 @@ use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\Messenger\Handler\ChainHandler; +use Symfony\Component\Messenger\Handler\Locator\ContainerHandlerLocator; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; use Symfony\Component\Messenger\Handler\MessageSubscriberInterface; use Symfony\Component\Messenger\TraceableMessageBus; +use Symfony\Component\Messenger\Transport\ReceiverInterface; +use Symfony\Component\Messenger\Transport\SenderInterface; /** * @author Samuel Roze @@ -49,11 +52,13 @@ public function __construct(string $handlerTag = 'messenger.message_handler', st */ public function process(ContainerBuilder $container) { - if (!$container->hasDefinition('messenger.handler_resolver')) { + if (!$container->has('message_bus')) { return; } + $busIds = array(); foreach ($container->findTaggedServiceIds($this->busTag) as $busId => $tags) { + $busIds[] = $busId; if ($container->hasParameter($busMiddlewareParameter = $busId.'.middleware')) { $this->registerBusMiddleware($container, $busId, $container->getParameter($busMiddlewareParameter)); @@ -61,25 +66,43 @@ public function process(ContainerBuilder $container) } if ($container->hasDefinition('messenger.data_collector')) { - $this->registerBusToCollector($container, $busId, $tags[0]); + $this->registerBusToCollector($container, $busId); } } $this->registerReceivers($container); $this->registerSenders($container); - $this->registerHandlers($container); + $this->registerHandlers($container, $busIds); } - private function registerHandlers(ContainerBuilder $container) + private function registerHandlers(ContainerBuilder $container, array $busIds) { - $handlersByMessage = array(); + $definitions = array(); + $handlersByBusAndMessage = array(); foreach ($container->findTaggedServiceIds($this->handlerTag, true) as $serviceId => $tags) { foreach ($tags as $tag) { - $handles = isset($tag['handles']) ? array($tag['handles']) : $this->guessHandledClasses($r = $container->getReflectionClass($container->getDefinition($serviceId)->getClass()), $serviceId); + if (isset($tag['bus']) && !\in_array($tag['bus'], $busIds, true)) { + throw new RuntimeException(sprintf('Invalid handler service "%s": bus "%s" specified on the tag "%s" does not exist (known ones are: %s).', $serviceId, $tag['bus'], $this->handlerTag, implode(', ', $busIds))); + } + + $r = $container->getReflectionClass($container->getDefinition($serviceId)->getClass()); + + if (isset($tag['handles'])) { + $handles = isset($tag['method']) ? array($tag['handles'] => $tag['method']) : array($tag['handles']); + } else { + $handles = $this->guessHandledClasses($r, $serviceId); + } + $priority = $tag['priority'] ?? 0; + $handlerBuses = (array) ($tag['bus'] ?? $busIds); + + foreach ($handles as $messageClass => $method) { + if (\is_int($messageClass)) { + $messageClass = $method; + $method = '__invoke'; + } - foreach ($handles as $messageClass) { if (\is_array($messageClass)) { $messagePriority = $messageClass[1]; $messageClass = $messageClass[0]; @@ -87,43 +110,79 @@ private function registerHandlers(ContainerBuilder $container) $messagePriority = $priority; } - if (!class_exists($messageClass)) { - $messageClassLocation = isset($tag['handles']) ? 'declared in your tag attribute "handles"' : sprintf($r->implementsInterface(MessageHandlerInterface::class) ? 'returned by method "%s::getHandledMessages()"' : 'used as argument type in method "%s::__invoke()"', $r->getName()); + if (\is_array($method)) { + $messagePriority = $method[1]; + $method = $method[0]; + } + + if (!\class_exists($messageClass)) { + $messageClassLocation = isset($tag['handles']) ? 'declared in your tag attribute "handles"' : $r->implementsInterface(MessageHandlerInterface::class) ? sprintf('returned by method "%s::getHandledMessages()"', $r->getName()) : sprintf('used as argument type in method "%s::%s()"', $r->getName(), $method); throw new RuntimeException(sprintf('Invalid handler service "%s": message class "%s" %s does not exist.', $serviceId, $messageClass, $messageClassLocation)); } - $handlersByMessage[$messageClass][$messagePriority][] = new Reference($serviceId); + if (!$r->hasMethod($method)) { + throw new RuntimeException(sprintf('Invalid handler service "%s": method "%s::%s()" does not exist.', $serviceId, $r->getName(), $method)); + } + + if ('__invoke' !== $method) { + $wrapperDefinition = (new Definition('callable'))->addArgument(array(new Reference($serviceId), $method))->setFactory('Closure::fromCallable'); + + $definitions[$serviceId = '.messenger.method_on_object_wrapper.'.ContainerBuilder::hash($messageClass.':'.$messagePriority.':'.$serviceId.':'.$method)] = $wrapperDefinition; + } + + foreach ($handlerBuses as $handlerBus) { + $handlersByBusAndMessage[$handlerBus][$messageClass][$messagePriority][] = $serviceId; + } } } } - foreach ($handlersByMessage as $message => $handlers) { - krsort($handlersByMessage[$message]); - $handlersByMessage[$message] = array_merge(...$handlersByMessage[$message]); + foreach ($handlersByBusAndMessage as $bus => $handlersByMessage) { + foreach ($handlersByMessage as $message => $handlersByPriority) { + krsort($handlersByPriority); + $handlersByBusAndMessage[$bus][$message] = array_unique(array_merge(...$handlersByPriority)); + } } - $definitions = array(); - foreach ($handlersByMessage as $message => $handlers) { - if (1 === \count($handlers)) { - $handlersByMessage[$message] = current($handlers); - } else { - $d = new Definition(ChainHandler::class, array($handlers)); - $d->setPrivate(true); - $serviceId = hash('sha1', $message); - $definitions[$serviceId] = $d; - $handlersByMessage[$message] = new Reference($serviceId); + $handlersLocatorMappingByBus = array(); + foreach ($handlersByBusAndMessage as $bus => $handlersByMessage) { + foreach ($handlersByMessage as $message => $handlersIds) { + if (1 === \count($handlersIds)) { + $handlersLocatorMappingByBus[$bus]['handler.'.$message] = new Reference(current($handlersIds)); + } else { + $chainHandler = new Definition(ChainHandler::class, array(array_map(function (string $handlerId): Reference { + return new Reference($handlerId); + }, $handlersIds))); + $chainHandler->setPrivate(true); + $serviceId = '.messenger.chain_handler.'.ContainerBuilder::hash($bus.$message); + $definitions[$serviceId] = $chainHandler; + $handlersLocatorMappingByBus[$bus]['handler.'.$message] = new Reference($serviceId); + } } } $container->addDefinitions($definitions); - $handlersLocatorMapping = array(); - foreach ($handlersByMessage as $message => $handler) { - $handlersLocatorMapping['handler.'.$message] = $handler; + foreach ($busIds as $bus) { + $container->register($resolverName = "$bus.messenger.handler_resolver", ContainerHandlerLocator::class) + ->setArgument(0, ServiceLocatorTagPass::register($container, $handlersLocatorMappingByBus[$bus] ?? array())) + ; + if ($container->has($callMessageHandlerId = "$bus.middleware.call_message_handler")) { + $container->getDefinition($callMessageHandlerId) + ->replaceArgument(0, new Reference($resolverName)) + ; + } } - $handlerResolver = $container->getDefinition('messenger.handler_resolver'); - $handlerResolver->replaceArgument(0, ServiceLocatorTagPass::register($container, $handlersLocatorMapping)); + if ($container->hasDefinition('console.command.messenger_debug')) { + $debugCommandMapping = $handlersByBusAndMessage; + foreach ($busIds as $bus) { + if (!isset($debugCommandMapping[$bus])) { + $debugCommandMapping[$bus] = array(); + } + } + $container->getDefinition('console.command.messenger_debug')->replaceArgument(0, $debugCommandMapping); + } } private function guessHandledClasses(\ReflectionClass $handlerClass, string $serviceId): array @@ -161,16 +220,30 @@ private function guessHandledClasses(\ReflectionClass $handlerClass, string $ser private function registerReceivers(ContainerBuilder $container) { $receiverMapping = array(); + foreach ($container->findTaggedServiceIds($this->receiverTag) as $id => $tags) { - foreach ($tags as $tag) { - $receiverMapping[$id] = new Reference($id); + $receiverClass = $container->findDefinition($id)->getClass(); + if (!is_subclass_of($receiverClass, ReceiverInterface::class)) { + throw new RuntimeException(sprintf('Invalid receiver "%s": class "%s" must implement interface "%s".', $id, $receiverClass, ReceiverInterface::class)); + } + + $receiverMapping[$id] = new Reference($id); - if (isset($tag['name'])) { - $receiverMapping[$tag['name']] = $receiverMapping[$id]; + foreach ($tags as $tag) { + if (isset($tag['alias'])) { + $receiverMapping[$tag['alias']] = $receiverMapping[$id]; } } } + if ($container->hasDefinition('console.command.messenger_consume_messages')) { + $receiverNames = array(); + foreach ($receiverMapping as $name => $reference) { + $receiverNames[(string) $reference] = $name; + } + $container->getDefinition('console.command.messenger_consume_messages')->replaceArgument(3, array_values($receiverNames)); + } + $container->getDefinition('messenger.receiver_locator')->replaceArgument(0, $receiverMapping); } @@ -178,11 +251,16 @@ private function registerSenders(ContainerBuilder $container) { $senderLocatorMapping = array(); foreach ($container->findTaggedServiceIds($this->senderTag) as $id => $tags) { - foreach ($tags as $tag) { - $senderLocatorMapping[$id] = new Reference($id); + $senderClass = $container->findDefinition($id)->getClass(); + if (!is_subclass_of($senderClass, SenderInterface::class)) { + throw new RuntimeException(sprintf('Invalid sender "%s": class "%s" must implement interface "%s".', $id, $senderClass, SenderInterface::class)); + } + + $senderLocatorMapping[$id] = new Reference($id); - if (isset($tag['name'])) { - $senderLocatorMapping[$tag['name']] = $senderLocatorMapping[$id]; + foreach ($tags as $tag) { + if (isset($tag['alias'])) { + $senderLocatorMapping[$tag['alias']] = $senderLocatorMapping[$id]; } } } @@ -190,7 +268,7 @@ private function registerSenders(ContainerBuilder $container) $container->getDefinition('messenger.sender_locator')->replaceArgument(0, $senderLocatorMapping); } - private function registerBusToCollector(ContainerBuilder $container, string $busId, array $tag) + private function registerBusToCollector(ContainerBuilder $container, string $busId) { $container->setDefinition( $tracedBusId = 'debug.traced.'.$busId, @@ -200,24 +278,37 @@ private function registerBusToCollector(ContainerBuilder $container, string $bus $container->getDefinition('messenger.data_collector')->addMethodCall('registerBus', array($busId, new Reference($tracedBusId))); } - private function registerBusMiddleware(ContainerBuilder $container, string $busId, array $middleware) + private function registerBusMiddleware(ContainerBuilder $container, string $busId, array $middlewareCollection) { - $container->getDefinition($busId)->replaceArgument(0, array_map(function (string $name) use ($container, $busId) { - if (!$container->has($messengerMiddlewareId = 'messenger.middleware.'.$name)) { - $messengerMiddlewareId = $name; + $middlewareReferences = array(); + foreach ($middlewareCollection as $middlewareItem) { + $id = $middlewareItem['id']; + $arguments = $middlewareItem['arguments'] ?? array(); + if (!$container->has($messengerMiddlewareId = 'messenger.middleware.'.$id)) { + $messengerMiddlewareId = $id; } if (!$container->has($messengerMiddlewareId)) { - throw new RuntimeException(sprintf('Invalid middleware "%s": define such service to be able to use it.', $name)); + throw new RuntimeException(sprintf('Invalid middleware "%s": define such service to be able to use it.', $id)); } - if ($container->getDefinition($messengerMiddlewareId)->isAbstract()) { + if (($definition = $container->findDefinition($messengerMiddlewareId))->isAbstract()) { $childDefinition = new ChildDefinition($messengerMiddlewareId); + $count = \count($definition->getArguments()); + foreach (array_values($arguments ?? array()) as $key => $argument) { + // Parent definition can provide default arguments. + // Replace each explicitly or add if not set: + $key < $count ? $childDefinition->replaceArgument($key, $argument) : $childDefinition->addArgument($argument); + } - $container->setDefinition($messengerMiddlewareId = $busId.'.middleware.'.$name, $childDefinition); + $container->setDefinition($messengerMiddlewareId = $busId.'.middleware.'.$id, $childDefinition); + } elseif ($arguments) { + throw new RuntimeException(sprintf('Invalid middleware factory "%s": a middleware factory must be an abstract definition.', $id)); } - return new Reference($messengerMiddlewareId); - }, $middleware)); + $middlewareReferences[] = new Reference($messengerMiddlewareId); + } + + $container->getDefinition($busId)->replaceArgument(0, $middlewareReferences); } } diff --git a/src/Symfony/Component/Messenger/Envelope.php b/src/Symfony/Component/Messenger/Envelope.php new file mode 100644 index 0000000000000..4d5f7a02a9d5d --- /dev/null +++ b/src/Symfony/Component/Messenger/Envelope.php @@ -0,0 +1,89 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger; + +/** + * A message wrapped in an envelope with items (configurations, markers, ...). + * + * @author Maxime Steinhausser + * + * @experimental in 4.1 + */ +final class Envelope +{ + private $items = array(); + private $message; + + /** + * @param object $message + * @param EnvelopeItemInterface[] $items + */ + public function __construct($message, array $items = array()) + { + $this->message = $message; + foreach ($items as $item) { + $this->items[\get_class($item)] = $item; + } + } + + /** + * Wrap a message into an envelope if not already wrapped. + * + * @param Envelope|object $message + */ + public static function wrap($message): self + { + return $message instanceof self ? $message : new self($message); + } + + /** + * @return Envelope a new Envelope instance with additional item + */ + public function with(EnvelopeItemInterface $item): self + { + $cloned = clone $this; + + $cloned->items[\get_class($item)] = $item; + + return $cloned; + } + + public function withMessage($message): self + { + $cloned = clone $this; + + $cloned->message = $message; + + return $cloned; + } + + public function get(string $itemFqcn): ?EnvelopeItemInterface + { + return $this->items[$itemFqcn] ?? null; + } + + /** + * @return EnvelopeItemInterface[] indexed by fqcn + */ + public function all(): array + { + return $this->items; + } + + /** + * @return object The original message contained in the envelope + */ + public function getMessage() + { + return $this->message; + } +} diff --git a/src/Symfony/Component/Messenger/EnvelopeAwareInterface.php b/src/Symfony/Component/Messenger/EnvelopeAwareInterface.php new file mode 100644 index 0000000000000..c19bc8436286a --- /dev/null +++ b/src/Symfony/Component/Messenger/EnvelopeAwareInterface.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger; + +/** + * A Messenger protagonist aware of the message envelope and its content. + * + * @author Maxime Steinhausser + * + * @experimental in 4.1 + */ +interface EnvelopeAwareInterface +{ +} diff --git a/src/Symfony/Component/Messenger/EnvelopeItemInterface.php b/src/Symfony/Component/Messenger/EnvelopeItemInterface.php new file mode 100644 index 0000000000000..2561a127546c0 --- /dev/null +++ b/src/Symfony/Component/Messenger/EnvelopeItemInterface.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger; + +/** + * An envelope item related to a message. + * This item must be serializable for transport. + * + * @author Maxime Steinhausser + * + * @experimental in 4.1 + */ +interface EnvelopeItemInterface extends \Serializable +{ +} diff --git a/src/Symfony/Component/Messenger/Exception/InvalidArgumentException.php b/src/Symfony/Component/Messenger/Exception/InvalidArgumentException.php new file mode 100644 index 0000000000000..a75c722484c1f --- /dev/null +++ b/src/Symfony/Component/Messenger/Exception/InvalidArgumentException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Exception; + +/** + * @author Yonel Ceruto + */ +class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/src/Symfony/Component/Messenger/Handler/MessageSubscriberInterface.php b/src/Symfony/Component/Messenger/Handler/MessageSubscriberInterface.php index 6a751482bff0b..ba4ae3ee79870 100644 --- a/src/Symfony/Component/Messenger/Handler/MessageSubscriberInterface.php +++ b/src/Symfony/Component/Messenger/Handler/MessageSubscriberInterface.php @@ -34,9 +34,14 @@ interface MessageSubscriberInterface extends MessageHandlerInterface * [SecondMessage::class, -10], * ]; * - * The `__invoke` method of the handler will be called as usual with the message to handle. + * It can also specify a method and/or a priority per message: * - * @return array + * return [ + * FirstMessage::class => 'firstMessageMethod', + * SecondMessage::class => ['secondMessageMethod', 20], + * ]; + * + * The `__invoke` method of the handler will be called as usual with the message to handle. */ - public static function getHandledMessages(): array; + public static function getHandledMessages(): iterable; } diff --git a/src/Symfony/Component/Messenger/MessageBus.php b/src/Symfony/Component/Messenger/MessageBus.php index 904eafcdd94f5..89fa8e04fa23c 100644 --- a/src/Symfony/Component/Messenger/MessageBus.php +++ b/src/Symfony/Component/Messenger/MessageBus.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Messenger; +use Symfony\Component\Messenger\Exception\InvalidArgumentException; use Symfony\Component\Messenger\Middleware\MiddlewareInterface; /** @@ -39,10 +40,14 @@ public function __construct(iterable $middlewareHandlers = array()) */ public function dispatch($message) { - return \call_user_func($this->callableForNextMiddleware(0), $message); + if (!\is_object($message)) { + throw new InvalidArgumentException(sprintf('Invalid type for message argument. Expected object, but got "%s".', \gettype($message))); + } + + return \call_user_func($this->callableForNextMiddleware(0, Envelope::wrap($message)), $message); } - private function callableForNextMiddleware(int $index): callable + private function callableForNextMiddleware(int $index, Envelope $currentEnvelope): callable { if (null === $this->indexedMiddlewareHandlers) { $this->indexedMiddlewareHandlers = \is_array($this->middlewareHandlers) ? array_values($this->middlewareHandlers) : iterator_to_array($this->middlewareHandlers, false); @@ -54,8 +59,19 @@ private function callableForNextMiddleware(int $index): callable $middleware = $this->indexedMiddlewareHandlers[$index]; - return function ($message) use ($middleware, $index) { - return $middleware->handle($message, $this->callableForNextMiddleware($index + 1)); + return function ($message) use ($middleware, $index, $currentEnvelope) { + if ($message instanceof Envelope) { + $currentEnvelope = $message; + } else { + $message = $currentEnvelope->withMessage($message); + } + + if (!$middleware instanceof EnvelopeAwareInterface) { + // Do not provide the envelope if the middleware cannot read it: + $message = $message->getMessage(); + } + + return $middleware->handle($message, $this->callableForNextMiddleware($index + 1, $currentEnvelope)); }; } } diff --git a/src/Symfony/Component/Messenger/MessageBusInterface.php b/src/Symfony/Component/Messenger/MessageBusInterface.php index 1d441ea568ff7..8c2b91d1af3ae 100644 --- a/src/Symfony/Component/Messenger/MessageBusInterface.php +++ b/src/Symfony/Component/Messenger/MessageBusInterface.php @@ -23,7 +23,7 @@ interface MessageBusInterface * * The bus can return a value coming from handlers, but is not required to do so. * - * @param object $message + * @param object|Envelope $message The message or the message pre-wrapped in an envelope * * @return mixed */ diff --git a/src/Symfony/Component/Messenger/Middleware/Configuration/ValidationConfiguration.php b/src/Symfony/Component/Messenger/Middleware/Configuration/ValidationConfiguration.php new file mode 100644 index 0000000000000..1b6180857b31d --- /dev/null +++ b/src/Symfony/Component/Messenger/Middleware/Configuration/ValidationConfiguration.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Middleware\Configuration; + +use Symfony\Component\Messenger\EnvelopeItemInterface; +use Symfony\Component\Validator\Constraints\GroupSequence; + +/** + * @author Maxime Steinhausser + * + * @experimental in 4.1 + */ +final class ValidationConfiguration implements EnvelopeItemInterface +{ + private $groups; + + /** + * @param string[]|GroupSequence $groups + */ + public function __construct($groups) + { + $this->groups = $groups; + } + + public function getGroups() + { + return $this->groups; + } + + public function serialize() + { + $isGroupSequence = $this->groups instanceof GroupSequence; + + return serialize(array( + 'groups' => $isGroupSequence ? $this->groups->groups : $this->groups, + 'is_group_sequence' => $isGroupSequence, + )); + } + + public function unserialize($serialized) + { + list( + 'groups' => $groups, + 'is_group_sequence' => $isGroupSequence + ) = unserialize($serialized, array('allowed_classes' => false)); + + $this->__construct($isGroupSequence ? new GroupSequence($groups) : $groups); + } +} diff --git a/src/Symfony/Component/Messenger/Middleware/LoggingMiddleware.php b/src/Symfony/Component/Messenger/Middleware/LoggingMiddleware.php index 4de6e42575805..ebaf8525c0407 100644 --- a/src/Symfony/Component/Messenger/Middleware/LoggingMiddleware.php +++ b/src/Symfony/Component/Messenger/Middleware/LoggingMiddleware.php @@ -11,7 +11,6 @@ namespace Symfony\Component\Messenger\Middleware; -use Symfony\Component\Messenger\Asynchronous\Transport\ReceivedMessage; use Psr\Log\LoggerInterface; /** @@ -51,10 +50,6 @@ public function handle($message, callable $next) private function createContext($message): array { - if ($message instanceof ReceivedMessage) { - $message = $message->getMessage(); - } - return array( 'message' => $message, 'class' => \get_class($message), diff --git a/src/Symfony/Component/Messenger/Middleware/ValidationMiddleware.php b/src/Symfony/Component/Messenger/Middleware/ValidationMiddleware.php index 3b168367cdd33..e588d9256bd32 100644 --- a/src/Symfony/Component/Messenger/Middleware/ValidationMiddleware.php +++ b/src/Symfony/Component/Messenger/Middleware/ValidationMiddleware.php @@ -11,13 +11,16 @@ namespace Symfony\Component\Messenger\Middleware; +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\EnvelopeAwareInterface; use Symfony\Component\Messenger\Exception\ValidationFailedException; +use Symfony\Component\Messenger\Middleware\Configuration\ValidationConfiguration; use Symfony\Component\Validator\Validator\ValidatorInterface; /** * @author Tobias Nyholm */ -class ValidationMiddleware implements MiddlewareInterface +class ValidationMiddleware implements MiddlewareInterface, EnvelopeAwareInterface { private $validator; @@ -28,9 +31,17 @@ public function __construct(ValidatorInterface $validator) public function handle($message, callable $next) { - $violations = $this->validator->validate($message); + $envelope = Envelope::wrap($message); + $subject = $envelope->getMessage(); + $groups = null; + /** @var ValidationConfiguration|null $validationConfig */ + if ($validationConfig = $envelope->get(ValidationConfiguration::class)) { + $groups = $validationConfig->getGroups(); + } + + $violations = $this->validator->validate($subject, null, $groups); if (\count($violations)) { - throw new ValidationFailedException($message, $violations); + throw new ValidationFailedException($subject, $violations); } return $next($message); diff --git a/src/Symfony/Component/Messenger/Tests/Asynchronous/Middleware/SendMessageMiddlewareTest.php b/src/Symfony/Component/Messenger/Tests/Asynchronous/Middleware/SendMessageMiddlewareTest.php index 6398aff361684..c9d548360c7b0 100644 --- a/src/Symfony/Component/Messenger/Tests/Asynchronous/Middleware/SendMessageMiddlewareTest.php +++ b/src/Symfony/Component/Messenger/Tests/Asynchronous/Middleware/SendMessageMiddlewareTest.php @@ -15,7 +15,10 @@ use Symfony\Component\Messenger\Asynchronous\Middleware\SendMessageMiddleware; use Symfony\Component\Messenger\Asynchronous\Routing\SenderLocatorInterface; use Symfony\Component\Messenger\Asynchronous\Transport\ReceivedMessage; +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Tests\Asynchronous\Routing\ChildDummyMessage; use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage; +use Symfony\Component\Messenger\Tests\Fixtures\DummyMessageInterface; use Symfony\Component\Messenger\Transport\SenderInterface; class SendMessageMiddlewareTest extends TestCase @@ -26,28 +29,87 @@ public function testItSendsTheMessageToAssignedSender() $sender = $this->getMockBuilder(SenderInterface::class)->getMock(); $next = $this->createPartialMock(\stdClass::class, array('__invoke')); - $middleware = new SendMessageMiddleware(new InMemorySenderLocator(array( - $sender, - ))); + $middleware = new SendMessageMiddleware(new InMemorySenderLocator($sender)); - $sender->expects($this->once())->method('send')->with($message); + $sender->expects($this->once())->method('send')->with(Envelope::wrap($message)); $next->expects($this->never())->method($this->anything()); $middleware->handle($message, $next); } - public function testItAlsoCallsTheNextMiddlewareIfASenderIsNull() + public function testItSendsTheMessageToAssignedSenderWithPreWrappedMessage() + { + $envelope = Envelope::wrap(new DummyMessage('Hey')); + $sender = $this->getMockBuilder(SenderInterface::class)->getMock(); + $next = $this->createPartialMock(\stdClass::class, array('__invoke')); + + $middleware = new SendMessageMiddleware(new InMemorySenderLocator($sender)); + + $sender->expects($this->once())->method('send')->with($envelope); + $next->expects($this->never())->method($this->anything()); + + $middleware->handle($envelope, $next); + } + + public function testItAlsoCallsTheNextMiddlewareBasedOnTheMessageClass() + { + $message = new DummyMessage('Hey'); + $sender = $this->getMockBuilder(SenderInterface::class)->getMock(); + $next = $this->createPartialMock(\stdClass::class, array('__invoke')); + + $middleware = new SendMessageMiddleware(new InMemorySenderLocator($sender), array( + DummyMessage::class => true, + )); + + $sender->expects($this->once())->method('send')->with(Envelope::wrap($message)); + $next->expects($this->once())->method($this->anything()); + + $middleware->handle($message, $next); + } + + public function testItAlsoCallsTheNextMiddlewareBasedOnTheMessageParentClass() + { + $message = new ChildDummyMessage('Hey'); + $sender = $this->getMockBuilder(SenderInterface::class)->getMock(); + $next = $this->createPartialMock(\stdClass::class, array('__invoke')); + + $middleware = new SendMessageMiddleware(new InMemorySenderLocator($sender), array( + DummyMessage::class => true, + )); + + $sender->expects($this->once())->method('send')->with(Envelope::wrap($message)); + $next->expects($this->once())->method($this->anything()); + + $middleware->handle($message, $next); + } + + public function testItAlsoCallsTheNextMiddlewareBasedOnTheMessageInterface() { $message = new DummyMessage('Hey'); $sender = $this->getMockBuilder(SenderInterface::class)->getMock(); $next = $this->createPartialMock(\stdClass::class, array('__invoke')); - $middleware = new SendMessageMiddleware(new InMemorySenderLocator(array( - $sender, - null, - ))); + $middleware = new SendMessageMiddleware(new InMemorySenderLocator($sender), array( + DummyMessageInterface::class => true, + )); - $sender->expects($this->once())->method('send')->with($message); + $sender->expects($this->once())->method('send')->with(Envelope::wrap($message)); + $next->expects($this->once())->method($this->anything()); + + $middleware->handle($message, $next); + } + + public function testItAlsoCallsTheNextMiddlewareBasedOnWildcard() + { + $message = new DummyMessage('Hey'); + $sender = $this->getMockBuilder(SenderInterface::class)->getMock(); + $next = $this->createPartialMock(\stdClass::class, array('__invoke')); + + $middleware = new SendMessageMiddleware(new InMemorySenderLocator($sender), array( + '*' => true, + )); + + $sender->expects($this->once())->method('send')->with(Envelope::wrap($message)); $next->expects($this->once())->method($this->anything()); $middleware->handle($message, $next); @@ -58,7 +120,7 @@ public function testItCallsTheNextMiddlewareWhenNoSenderForThisMessage() $message = new DummyMessage('Hey'); $next = $this->createPartialMock(\stdClass::class, array('__invoke')); - $middleware = new SendMessageMiddleware(new InMemorySenderLocator(array())); + $middleware = new SendMessageMiddleware(new InMemorySenderLocator(null)); $next->expects($this->once())->method($this->anything()); @@ -67,34 +129,31 @@ public function testItCallsTheNextMiddlewareWhenNoSenderForThisMessage() public function testItSkipsReceivedMessages() { - $innerMessage = new DummyMessage('Hey'); - $message = new ReceivedMessage($innerMessage); + $envelope = Envelope::wrap(new DummyMessage('Hey'))->with(new ReceivedMessage()); $sender = $this->getMockBuilder(SenderInterface::class)->getMock(); $next = $this->createPartialMock(\stdClass::class, array('__invoke')); - $middleware = new SendMessageMiddleware(new InMemorySenderLocator(array( - $sender, - ))); + $middleware = new SendMessageMiddleware(new InMemorySenderLocator($sender)); $sender->expects($this->never())->method('send'); - $next->expects($this->once())->method('__invoke')->with($innerMessage); + $next->expects($this->once())->method('__invoke')->with($envelope); - $middleware->handle($message, $next); + $middleware->handle($envelope, $next); } } class InMemorySenderLocator implements SenderLocatorInterface { - private $senders; + private $sender; - public function __construct(array $senders) + public function __construct(?SenderInterface $sender) { - $this->senders = $senders; + $this->sender = $sender; } - public function getSendersForMessage($message): array + public function getSenderForMessage($message): ?SenderInterface { - return $this->senders; + return $this->sender; } } diff --git a/src/Symfony/Component/Messenger/Tests/Asynchronous/Routing/SenderLocatorTest.php b/src/Symfony/Component/Messenger/Tests/Asynchronous/Routing/SenderLocatorTest.php index caf952526439a..22cf6bac81e67 100644 --- a/src/Symfony/Component/Messenger/Tests/Asynchronous/Routing/SenderLocatorTest.php +++ b/src/Symfony/Component/Messenger/Tests/Asynchronous/Routing/SenderLocatorTest.php @@ -15,6 +15,7 @@ use Symfony\Component\DependencyInjection\Container; use Symfony\Component\Messenger\Asynchronous\Routing\SenderLocator; use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage; +use Symfony\Component\Messenger\Tests\Fixtures\DummyMessageInterface; use Symfony\Component\Messenger\Tests\Fixtures\SecondMessage; use Symfony\Component\Messenger\Transport\SenderInterface; @@ -27,13 +28,45 @@ public function testItReturnsTheSenderBasedOnTheMessageClass() $container->set('my_amqp_sender', $sender); $locator = new SenderLocator($container, array( - DummyMessage::class => array( - 'my_amqp_sender', - ), + DummyMessage::class => 'my_amqp_sender', )); - $this->assertEquals(array($sender), $locator->getSendersForMessage(new DummyMessage('Hello'))); - $this->assertEquals(array(), $locator->getSendersForMessage(new SecondMessage())); + $this->assertSame($sender, $locator->getSenderForMessage(new DummyMessage('Hello'))); + $this->assertNull($locator->getSenderForMessage(new SecondMessage())); + } + + public function testItReturnsTheSenderBasedOnTheMessageParentClass() + { + $container = new Container(); + + $sender = $this->getMockBuilder(SenderInterface::class)->getMock(); + $container->set('my_amqp_sender', $sender); + + $apiSender = $this->getMockBuilder(SenderInterface::class)->getMock(); + $container->set('my_api_sender', $apiSender); + + $locator = new SenderLocator($container, array( + DummyMessageInterface::class => 'my_api_sender', + DummyMessage::class => 'my_amqp_sender', + )); + + $this->assertSame($sender, $locator->getSenderForMessage(new ChildDummyMessage('Hello'))); + $this->assertNull($locator->getSenderForMessage(new SecondMessage())); + } + + public function testItReturnsTheSenderBasedOnTheMessageInterface() + { + $container = new Container(); + + $sender = $this->getMockBuilder(SenderInterface::class)->getMock(); + $container->set('my_amqp_sender', $sender); + + $locator = new SenderLocator($container, array( + DummyMessageInterface::class => 'my_amqp_sender', + )); + + $this->assertSame($sender, $locator->getSenderForMessage(new DummyMessage('Hello'))); + $this->assertNull($locator->getSenderForMessage(new SecondMessage())); } public function testItSupportsAWildcardInsteadOfTheMessageClass() @@ -47,15 +80,15 @@ public function testItSupportsAWildcardInsteadOfTheMessageClass() $container->set('my_api_sender', $apiSender); $locator = new SenderLocator($container, array( - DummyMessage::class => array( - 'my_amqp_sender', - ), - '*' => array( - 'my_api_sender', - ), + DummyMessage::class => 'my_amqp_sender', + '*' => 'my_api_sender', )); - $this->assertEquals(array($sender), $locator->getSendersForMessage(new DummyMessage('Hello'))); - $this->assertEquals(array($apiSender), $locator->getSendersForMessage(new SecondMessage())); + $this->assertSame($sender, $locator->getSenderForMessage(new DummyMessage('Hello'))); + $this->assertSame($apiSender, $locator->getSenderForMessage(new SecondMessage())); } } + +class ChildDummyMessage extends DummyMessage +{ +} diff --git a/src/Symfony/Component/Messenger/Tests/Asynchronous/Transport/Serialization/SerializerConfigurationTest.php b/src/Symfony/Component/Messenger/Tests/Asynchronous/Transport/Serialization/SerializerConfigurationTest.php new file mode 100644 index 0000000000000..6ebcc8ddfa0ca --- /dev/null +++ b/src/Symfony/Component/Messenger/Tests/Asynchronous/Transport/Serialization/SerializerConfigurationTest.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Tests\Asynchronous\Serialization; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Messenger\Transport\Serialization\SerializerConfiguration; +use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; + +/** + * @author Maxime Steinhausser + */ +class SerializerConfigurationTest extends TestCase +{ + public function testSerialiazable() + { + $config = new SerializerConfiguration(array(ObjectNormalizer::GROUPS => array('Default', 'Extra'))); + + $this->assertTrue(is_subclass_of(SerializerConfiguration::class, \Serializable::class, true)); + $this->assertEquals($config, unserialize(serialize($config))); + } +} diff --git a/src/Symfony/Component/Messenger/Tests/Command/ConsumeMessagesCommandTest.php b/src/Symfony/Component/Messenger/Tests/Command/ConsumeMessagesCommandTest.php new file mode 100644 index 0000000000000..de489b545f0e4 --- /dev/null +++ b/src/Symfony/Component/Messenger/Tests/Command/ConsumeMessagesCommandTest.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Tests\Command; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\ServiceLocator; +use Symfony\Component\Messenger\Command\ConsumeMessagesCommand; +use Symfony\Component\Messenger\MessageBus; + +class ConsumeMessagesCommandTest extends TestCase +{ + public function testConfigurationWithDefaultReceiver() + { + $command = new ConsumeMessagesCommand($this->createMock(MessageBus::class), $this->createMock(ServiceLocator::class), null, array('amqp')); + $inputArgument = $command->getDefinition()->getArgument('receiver'); + $this->assertFalse($inputArgument->isRequired()); + $this->assertSame('amqp', $inputArgument->getDefault()); + } + + public function testConfigurationWithoutDefaultReceiver() + { + $command = new ConsumeMessagesCommand($this->createMock(MessageBus::class), $this->createMock(ServiceLocator::class), null, array('amqp', 'dummy')); + $inputArgument = $command->getDefinition()->getArgument('receiver'); + $this->assertTrue($inputArgument->isRequired()); + $this->assertNull($inputArgument->getDefault()); + } +} diff --git a/src/Symfony/Component/Messenger/Tests/Command/DebugCommandTest.php b/src/Symfony/Component/Messenger/Tests/Command/DebugCommandTest.php new file mode 100644 index 0000000000000..deb01cf474d97 --- /dev/null +++ b/src/Symfony/Component/Messenger/Tests/Command/DebugCommandTest.php @@ -0,0 +1,155 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Tests\Command; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Tester\CommandTester; +use Symfony\Component\Messenger\Command\DebugCommand; +use Symfony\Component\Messenger\Tests\Fixtures\DummyCommand; +use Symfony\Component\Messenger\Tests\Fixtures\DummyCommandHandler; +use Symfony\Component\Messenger\Tests\Fixtures\DummyQuery; +use Symfony\Component\Messenger\Tests\Fixtures\DummyQueryHandler; +use Symfony\Component\Messenger\Tests\Fixtures\MultipleBusesMessage; +use Symfony\Component\Messenger\Tests\Fixtures\MultipleBusesMessageHandler; + +/** + * @author Maxime Steinhausser + */ +class DebugCommandTest extends TestCase +{ + protected function setUp() + { + putenv('COLUMNS='.(119 + strlen(PHP_EOL))); + } + + protected function tearDown() + { + putenv('COLUMNS='); + } + + public function testOutput() + { + $command = new DebugCommand( + array( + 'command_bus' => array( + DummyCommand::class => array(DummyCommandHandler::class), + MultipleBusesMessage::class => array(MultipleBusesMessageHandler::class), + ), + 'query_bus' => array( + DummyQuery::class => array(DummyQueryHandler::class), + MultipleBusesMessage::class => array(MultipleBusesMessageHandler::class), + ), + ) + ); + + $tester = new CommandTester($command); + $tester->execute(array(), array('decorated' => false)); + + $this->assertSame(<<getDisplay(true) + ); + + $tester->execute(array('bus' => 'query_bus'), array('decorated' => false)); + + $this->assertSame(<<getDisplay(true) + ); + } + + public function testOutputWithoutMessages() + { + $command = new DebugCommand(array('command_bus' => array(), 'query_bus' => array())); + + $tester = new CommandTester($command); + $tester->execute(array(), array('decorated' => false)); + + $this->assertSame(<<getDisplay(true) + ); + } + + /** + * @expectedException \Symfony\Component\Console\Exception\RuntimeException + * @expectedExceptionMessage Bus "unknown_bus" does not exist. Known buses are command_bus, query_bus. + */ + public function testExceptionOnUnknownBusArgument() + { + $command = new DebugCommand(array('command_bus' => array(), 'query_bus' => array())); + + $tester = new CommandTester($command); + $tester->execute(array('bus' => 'unknown_bus'), array('decorated' => false)); + } +} diff --git a/src/Symfony/Component/Messenger/Tests/DataCollector/MessengerDataCollectorTest.php b/src/Symfony/Component/Messenger/Tests/DataCollector/MessengerDataCollectorTest.php index 91f54490ac9ec..d88593e3e747d 100644 --- a/src/Symfony/Component/Messenger/Tests/DataCollector/MessengerDataCollectorTest.php +++ b/src/Symfony/Component/Messenger/Tests/DataCollector/MessengerDataCollectorTest.php @@ -16,14 +16,22 @@ use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage; use Symfony\Component\Messenger\TraceableMessageBus; -use Symfony\Component\VarDumper\Test\VarDumperTestTrait; +use Symfony\Component\VarDumper\Cloner\Data; +use Symfony\Component\VarDumper\Dumper\CliDumper; /** * @author Maxime Steinhausser */ class MessengerDataCollectorTest extends TestCase { - use VarDumperTestTrait; + /** @var CliDumper */ + private $dumper; + + protected function setUp() + { + $this->dumper = new CliDumper(); + $this->dumper->setColors(false); + } /** * @dataProvider getHandleTestData @@ -46,17 +54,18 @@ public function testHandle($returnedValue, $expected) $messages = $collector->getMessages(); $this->assertCount(1, $messages); - $this->assertDumpMatchesFormat($expected, $messages[0]); + $this->assertStringMatchesFormat($expected, $this->getDataAsString($messages[0])); } public function getHandleTestData() { $messageDump = << "default" + "envelopeItems" => null "message" => array:2 [ "type" => "Symfony\Component\Messenger\Tests\Fixtures\DummyMessage" - "object" => Symfony\Component\VarDumper\Cloner\Data {%A - %A+class: "Symfony\Component\Messenger\Tests\Fixtures\DummyMessage"%A + "value" => Symfony\Component\Messenger\Tests\Fixtures\DummyMessage %A + -message: "dummy message" } ] DUMP; @@ -64,7 +73,7 @@ public function getHandleTestData() yield 'no returned value' => array( null, << array:2 [ "type" => "NULL" @@ -77,7 +86,7 @@ public function getHandleTestData() yield 'scalar returned value' => array( 'returned value', << array:2 [ "type" => "string" @@ -90,11 +99,13 @@ public function getHandleTestData() yield 'array returned value' => array( array('returned value'), << array:2 [ "type" => "array" - "object" => Symfony\Component\VarDumper\Cloner\Data {%A + "value" => array:1 [ + 0 => "returned value" + ] ] ] DUMP @@ -123,21 +134,66 @@ public function testHandleWithException() $messages = $collector->getMessages(); $this->assertCount(1, $messages); - $this->assertDumpMatchesFormat(<<assertStringMatchesFormat(<< "default" + "envelopeItems" => null "message" => array:2 [ "type" => "Symfony\Component\Messenger\Tests\Fixtures\DummyMessage" - "object" => Symfony\Component\VarDumper\Cloner\Data {%A - %A+class: "Symfony\Component\Messenger\Tests\Fixtures\DummyMessage"%A + "value" => Symfony\Component\Messenger\Tests\Fixtures\DummyMessage %A + -message: "dummy message" } ] "exception" => array:2 [ "type" => "RuntimeException" - "message" => "foo" + "value" => RuntimeException %A ] -] +] DUMP - , $messages[0]); + , $this->getDataAsString($messages[0])); + } + + public function testKeepsOrderedDispatchCalls() + { + $firstBus = $this->getMockBuilder(MessageBusInterface::class)->getMock(); + $firstBus = new TraceableMessageBus($firstBus); + + $secondBus = $this->getMockBuilder(MessageBusInterface::class)->getMock(); + $secondBus = new TraceableMessageBus($secondBus); + + $collector = new MessengerDataCollector(); + $collector->registerBus('first bus', $firstBus); + $collector->registerBus('second bus', $secondBus); + + $firstBus->dispatch(new DummyMessage('#1')); + $secondBus->dispatch(new DummyMessage('#2')); + $secondBus->dispatch(new DummyMessage('#3')); + $firstBus->dispatch(new DummyMessage('#4')); + $secondBus->dispatch(new DummyMessage('#5')); + + $collector->lateCollect(); + + $messages = $collector->getMessages(); + $this->assertCount(5, $messages); + + $this->assertSame('#1', $messages[0]['message']['value']['message']); + $this->assertSame('first bus', $messages[0]['bus']); + + $this->assertSame('#2', $messages[1]['message']['value']['message']); + $this->assertSame('second bus', $messages[1]['bus']); + + $this->assertSame('#3', $messages[2]['message']['value']['message']); + $this->assertSame('second bus', $messages[2]['bus']); + + $this->assertSame('#4', $messages[3]['message']['value']['message']); + $this->assertSame('first bus', $messages[3]['bus']); + + $this->assertSame('#5', $messages[4]['message']['value']['message']); + $this->assertSame('second bus', $messages[4]['bus']); + } + + private function getDataAsString(Data $data): string + { + return rtrim($this->dumper->dump($data, true)); } } diff --git a/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php b/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php index b230a9c6df60a..6c77704adb90f 100644 --- a/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php +++ b/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php @@ -13,28 +13,39 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; +use Symfony\Component\DependencyInjection\Compiler\ResolveChildDefinitionsPass; +use Symfony\Component\DependencyInjection\Compiler\ResolveClassPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\ServiceLocator; -use Symfony\Component\Messenger\Transport\AmqpExt\AmqpReceiver; -use Symfony\Component\Messenger\Transport\AmqpExt\AmqpSender; -use Symfony\Component\Messenger\Handler\Locator\ContainerHandlerLocator; +use Symfony\Component\Messenger\Command\ConsumeMessagesCommand; +use Symfony\Component\Messenger\Command\DebugCommand; use Symfony\Component\Messenger\DataCollector\MessengerDataCollector; use Symfony\Component\Messenger\DependencyInjection\MessengerPass; +use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\Handler\ChainHandler; use Symfony\Component\Messenger\Handler\MessageSubscriberInterface; use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Messenger\Middleware\AllowNoHandlerMiddleware; +use Symfony\Component\Messenger\Middleware\HandleMessageMiddleware; use Symfony\Component\Messenger\Middleware\MiddlewareInterface; +use Symfony\Component\Messenger\Tests\Fixtures\DummyCommand; +use Symfony\Component\Messenger\Tests\Fixtures\DummyCommandHandler; use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage; +use Symfony\Component\Messenger\Tests\Fixtures\DummyQuery; +use Symfony\Component\Messenger\Tests\Fixtures\DummyQueryHandler; +use Symfony\Component\Messenger\Tests\Fixtures\MultipleBusesMessage; +use Symfony\Component\Messenger\Tests\Fixtures\MultipleBusesMessageHandler; use Symfony\Component\Messenger\Tests\Fixtures\SecondMessage; +use Symfony\Component\Messenger\Transport\AmqpExt\AmqpReceiver; +use Symfony\Component\Messenger\Transport\AmqpExt\AmqpSender; use Symfony\Component\Messenger\Transport\ReceiverInterface; class MessengerPassTest extends TestCase { public function testProcess() { - $container = $this->getContainerBuilder(); + $container = $this->getContainerBuilder($busId = 'message_bus'); $container ->register(DummyHandler::class, DummyHandler::class) ->addTag('messenger.message_handler') @@ -52,7 +63,7 @@ public function testProcess() $this->assertFalse($container->hasDefinition('messenger.middleware.debug.logging')); - $handlerLocatorDefinition = $container->getDefinition($container->getDefinition('messenger.handler_resolver')->getArgument(0)); + $handlerLocatorDefinition = $container->getDefinition($container->getDefinition("$busId.messenger.handler_resolver")->getArgument(0)); $this->assertSame(ServiceLocator::class, $handlerLocatorDefinition->getClass()); $this->assertEquals( array( @@ -68,9 +79,68 @@ public function testProcess() ); } + public function testProcessHandlersByBus() + { + $container = $this->getContainerBuilder($commandBusId = 'command_bus'); + $container->register($queryBusId = 'query_bus', MessageBusInterface::class)->setArgument(0, array())->addTag('messenger.bus'); + $container->register('messenger.middleware.call_message_handler', HandleMessageMiddleware::class) + ->addArgument(null) + ->setAbstract(true) + ; + + $middlewares = array(array('id' => 'call_message_handler')); + + $container->setParameter($commandBusId.'.middleware', $middlewares); + $container->setParameter($queryBusId.'.middleware', $middlewares); + + $container->register(DummyCommandHandler::class)->addTag('messenger.message_handler', array('bus' => $commandBusId)); + $container->register(DummyQueryHandler::class)->addTag('messenger.message_handler', array('bus' => $queryBusId)); + $container->register(MultipleBusesMessageHandler::class) + ->addTag('messenger.message_handler', array('bus' => $commandBusId)) + ->addTag('messenger.message_handler', array('bus' => $queryBusId)) + ; + + (new ResolveClassPass())->process($container); + (new MessengerPass())->process($container); + + $commandBusHandlerLocatorDefinition = $container->getDefinition($container->getDefinition("$commandBusId.messenger.handler_resolver")->getArgument(0)); + $this->assertSame(ServiceLocator::class, $commandBusHandlerLocatorDefinition->getClass()); + $this->assertEquals( + array( + 'handler.'.DummyCommand::class => new ServiceClosureArgument(new Reference(DummyCommandHandler::class)), + 'handler.'.MultipleBusesMessage::class => new ServiceClosureArgument(new Reference(MultipleBusesMessageHandler::class)), + ), + $commandBusHandlerLocatorDefinition->getArgument(0) + ); + + $queryBusHandlerLocatorDefinition = $container->getDefinition($container->getDefinition("$queryBusId.messenger.handler_resolver")->getArgument(0)); + $this->assertSame(ServiceLocator::class, $queryBusHandlerLocatorDefinition->getClass()); + $this->assertEquals( + array( + 'handler.'.DummyQuery::class => new ServiceClosureArgument(new Reference(DummyQueryHandler::class)), + 'handler.'.MultipleBusesMessage::class => new ServiceClosureArgument(new Reference(MultipleBusesMessageHandler::class)), + ), + $queryBusHandlerLocatorDefinition->getArgument(0) + ); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException + * @expectedExceptionMessage Invalid handler service "Symfony\Component\Messenger\Tests\Fixtures\DummyCommandHandler": bus "unknown_bus" specified on the tag "messenger.message_handler" does not exist (known ones are: command_bus). + */ + public function testProcessTagWithUnknownBus() + { + $container = $this->getContainerBuilder($commandBusId = 'command_bus'); + + $container->register(DummyCommandHandler::class)->addTag('messenger.message_handler', array('bus' => 'unknown_bus')); + + (new ResolveClassPass())->process($container); + (new MessengerPass())->process($container); + } + public function testGetClassesFromTheHandlerSubscriberInterface() { - $container = $this->getContainerBuilder(); + $container = $this->getContainerBuilder($busId = 'message_bus'); $container ->register(HandlerWithMultipleMessages::class, HandlerWithMultipleMessages::class) ->addTag('messenger.message_handler') @@ -82,7 +152,7 @@ public function testGetClassesFromTheHandlerSubscriberInterface() (new MessengerPass())->process($container); - $handlerLocatorDefinition = $container->getDefinition($container->getDefinition('messenger.handler_resolver')->getArgument(0)); + $handlerLocatorDefinition = $container->getDefinition($container->getDefinition("$busId.messenger.handler_resolver")->getArgument(0)); $handlerMapping = $handlerLocatorDefinition->getArgument(0); $this->assertArrayHasKey('handler.'.DummyMessage::class, $handlerMapping); @@ -96,10 +166,59 @@ public function testGetClassesFromTheHandlerSubscriberInterface() $this->assertEquals(array(new Reference(PrioritizedHandler::class), new Reference(HandlerWithMultipleMessages::class)), $definition->getArgument(0)); } + public function testGetClassesAndMethodsAndPrioritiesFromTheSubscriber() + { + $container = $this->getContainerBuilder($busId = 'message_bus'); + $container + ->register(HandlerMappingMethods::class, HandlerMappingMethods::class) + ->addTag('messenger.message_handler') + ; + $container + ->register(PrioritizedHandler::class, PrioritizedHandler::class) + ->addTag('messenger.message_handler') + ; + + (new MessengerPass())->process($container); + + $handlerLocatorDefinition = $container->getDefinition($container->getDefinition("$busId.messenger.handler_resolver")->getArgument(0)); + $handlerMapping = $handlerLocatorDefinition->getArgument(0); + + $this->assertArrayHasKey('handler.'.DummyMessage::class, $handlerMapping); + $this->assertArrayHasKey('handler.'.SecondMessage::class, $handlerMapping); + + $dummyHandlerReference = (string) $handlerMapping['handler.'.DummyMessage::class]->getValues()[0]; + $dummyHandlerDefinition = $container->getDefinition($dummyHandlerReference); + $this->assertSame('callable', $dummyHandlerDefinition->getClass()); + $this->assertEquals(array(new Reference(HandlerMappingMethods::class), 'dummyMethod'), $dummyHandlerDefinition->getArgument(0)); + $this->assertSame(array('Closure', 'fromCallable'), $dummyHandlerDefinition->getFactory()); + + $secondHandlerReference = (string) $handlerMapping['handler.'.SecondMessage::class]->getValues()[0]; + $secondHandlerDefinition = $container->getDefinition($secondHandlerReference); + $this->assertSame(ChainHandler::class, $secondHandlerDefinition->getClass()); + $this->assertEquals(new Reference(PrioritizedHandler::class), $secondHandlerDefinition->getArgument(0)[1]); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException + * @expectedExceptionMessage Invalid handler service "Symfony\Component\Messenger\Tests\DependencyInjection\HandlerMappingWithNonExistentMethod": method "Symfony\Component\Messenger\Tests\DependencyInjection\HandlerMappingWithNonExistentMethod::dummyMethod()" does not exist. + */ + public function testThrowsExceptionIfTheHandlerMethodDoesNotExist() + { + $container = $this->getContainerBuilder(); + $container->register('message_bus', MessageBusInterface::class)->addTag('messenger.bus'); + $container + ->register(HandlerMappingWithNonExistentMethod::class, HandlerMappingWithNonExistentMethod::class) + ->addTag('messenger.message_handler') + ; + + (new MessengerPass())->process($container); + } + public function testItRegistersReceivers() { $container = $this->getContainerBuilder(); - $container->register(AmqpReceiver::class, AmqpReceiver::class)->addTag('messenger.receiver', array('name' => 'amqp')); + $container->register('message_bus', MessageBusInterface::class)->addTag('messenger.bus'); + $container->register(AmqpReceiver::class, AmqpReceiver::class)->addTag('messenger.receiver', array('alias' => 'amqp')); (new MessengerPass())->process($container); @@ -109,6 +228,7 @@ public function testItRegistersReceivers() public function testItRegistersReceiversWithoutTagName() { $container = $this->getContainerBuilder(); + $container->register('message_bus', MessageBusInterface::class)->addTag('messenger.bus'); $container->register(AmqpReceiver::class, AmqpReceiver::class)->addTag('messenger.receiver'); (new MessengerPass())->process($container); @@ -116,10 +236,28 @@ public function testItRegistersReceiversWithoutTagName() $this->assertEquals(array(AmqpReceiver::class => new Reference(AmqpReceiver::class)), $container->getDefinition('messenger.receiver_locator')->getArgument(0)); } + public function testItRegistersMultipleReceiversAndSetsTheReceiverNamesOnTheCommand() + { + $container = $this->getContainerBuilder(); + $container->register('console.command.messenger_consume_messages', ConsumeMessagesCommand::class)->setArguments(array( + new Reference('message_bus'), + new Reference('messenger.receiver_locator'), + null, + null, + )); + + $container->register(AmqpReceiver::class, AmqpReceiver::class)->addTag('messenger.receiver', array('alias' => 'amqp')); + $container->register(DummyReceiver::class, DummyReceiver::class)->addTag('messenger.receiver', array('alias' => 'dummy')); + + (new MessengerPass())->process($container); + + $this->assertSame(array('amqp', 'dummy'), $container->getDefinition('console.command.messenger_consume_messages')->getArgument(3)); + } + public function testItRegistersSenders() { $container = $this->getContainerBuilder(); - $container->register(AmqpSender::class, AmqpSender::class)->addTag('messenger.sender', array('name' => 'amqp')); + $container->register(AmqpSender::class, AmqpSender::class)->addTag('messenger.sender', array('alias' => 'amqp')); (new MessengerPass())->process($container); @@ -136,6 +274,19 @@ public function testItRegistersSenderWithoutTagName() $this->assertEquals(array(AmqpSender::class => new Reference(AmqpSender::class)), $container->getDefinition('messenger.sender_locator')->getArgument(0)); } + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException + * @expectedExceptionMessage Invalid sender "app.messenger.sender": class "Symfony\Component\Messenger\Tests\DependencyInjection\InvalidSender" must implement interface "Symfony\Component\Messenger\Transport\SenderInterface". + */ + public function testItDoesNotRegisterInvalidSender() + { + $container = $this->getContainerBuilder(); + $container->register('app.messenger.sender', InvalidSender::class) + ->addTag('messenger.sender'); + + (new MessengerPass())->process($container); + } + /** * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException * @expectedExceptionMessage Invalid handler service "Symfony\Component\Messenger\Tests\DependencyInjection\UndefinedMessageHandler": message class "Symfony\Component\Messenger\Tests\DependencyInjection\UndefinedMessage" used as argument type in method "Symfony\Component\Messenger\Tests\DependencyInjection\UndefinedMessageHandler::__invoke()" does not exist. @@ -245,9 +396,8 @@ public function testRegistersTraceableBusesToCollector() { $dataCollector = $this->getMockBuilder(MessengerDataCollector::class)->getMock(); - $container = $this->getContainerBuilder(); + $container = $this->getContainerBuilder($fooBusId = 'messenger.bus.foo'); $container->register('messenger.data_collector', $dataCollector); - $container->register($fooBusId = 'messenger.bus.foo', MessageBusInterface::class)->addTag('messenger.bus'); $container->setParameter('kernel.debug', true); (new MessengerPass())->process($container); @@ -259,17 +409,44 @@ public function testRegistersTraceableBusesToCollector() public function testRegistersMiddlewareFromServices() { - $container = $this->getContainerBuilder(); - $container->register($fooBusId = 'messenger.bus.foo', MessageBusInterface::class)->setArgument(0, array())->addTag('messenger.bus'); + $container = $this->getContainerBuilder($fooBusId = 'messenger.bus.foo'); $container->register('messenger.middleware.allow_no_handler', AllowNoHandlerMiddleware::class)->setAbstract(true); + $container->register('middleware_with_factory', UselessMiddleware::class)->addArgument('some_default')->setAbstract(true); + $container->register('middleware_with_factory_using_default', UselessMiddleware::class)->addArgument('some_default')->setAbstract(true); $container->register(UselessMiddleware::class, UselessMiddleware::class); - $container->setParameter($middlewareParameter = $fooBusId.'.middleware', array(UselessMiddleware::class, 'allow_no_handler')); + $container->setParameter($middlewareParameter = $fooBusId.'.middleware', array( + array('id' => UselessMiddleware::class), + array('id' => 'middleware_with_factory', 'arguments' => array('foo', 'bar')), + array('id' => 'middleware_with_factory_using_default'), + array('id' => 'allow_no_handler'), + )); (new MessengerPass())->process($container); + (new ResolveChildDefinitionsPass())->process($container); $this->assertTrue($container->hasDefinition($childMiddlewareId = $fooBusId.'.middleware.allow_no_handler')); - $this->assertEquals(array(new Reference(UselessMiddleware::class), new Reference($childMiddlewareId)), $container->getDefinition($fooBusId)->getArgument(0)); + + $this->assertTrue($container->hasDefinition($factoryChildMiddlewareId = $fooBusId.'.middleware.middleware_with_factory')); + $this->assertEquals( + array('foo', 'bar'), + $container->getDefinition($factoryChildMiddlewareId)->getArguments(), + 'parent default argument is overridden, and next ones appended' + ); + + $this->assertTrue($container->hasDefinition($factoryWithDefaultChildMiddlewareId = $fooBusId.'.middleware.middleware_with_factory_using_default')); + $this->assertEquals( + array('some_default'), + $container->getDefinition($factoryWithDefaultChildMiddlewareId)->getArguments(), + 'parent default argument is used' + ); + + $this->assertEquals(array( + new Reference(UselessMiddleware::class), + new Reference($factoryChildMiddlewareId), + new Reference($factoryWithDefaultChildMiddlewareId), + new Reference($childMiddlewareId), + ), $container->getDefinition($fooBusId)->getArgument(0)); $this->assertFalse($container->hasParameter($middlewareParameter)); } @@ -279,25 +456,81 @@ public function testRegistersMiddlewareFromServices() */ public function testCannotRegistersAnUndefinedMiddleware() { - $container = $this->getContainerBuilder(); - $container->register($fooBusId = 'messenger.bus.foo', MessageBusInterface::class)->setArgument(0, array())->addTag('messenger.bus'); - $container->setParameter($middlewareParameter = $fooBusId.'.middleware', array('not_defined_middleware')); + $container = $this->getContainerBuilder($fooBusId = 'messenger.bus.foo'); + $container->setParameter($middlewareParameter = $fooBusId.'.middleware', array( + array('id' => 'not_defined_middleware', 'arguments' => array()), + )); + + (new MessengerPass())->process($container); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException + * @expectedExceptionMessage Invalid middleware factory "not_an_abstract_definition": a middleware factory must be an abstract definition. + */ + public function testMiddlewareFactoryDefinitionMustBeAbstract() + { + $container = $this->getContainerBuilder($fooBusId = 'messenger.bus.foo'); + $container->register('not_an_abstract_definition', UselessMiddleware::class); + $container->setParameter($middlewareParameter = $fooBusId.'.middleware', array( + array('id' => 'not_an_abstract_definition', 'arguments' => array('foo')), + )); + + (new MessengerPass())->process($container); + } + + public function testItRegistersTheDebugCommand() + { + $container = $this->getContainerBuilder($commandBusId = 'command_bus'); + $container->register($queryBusId = 'query_bus', MessageBusInterface::class)->setArgument(0, array())->addTag('messenger.bus'); + $container->register($emptyBus = 'empty_bus', MessageBusInterface::class)->setArgument(0, array())->addTag('messenger.bus'); + $container->register('messenger.middleware.call_message_handler', HandleMessageMiddleware::class) + ->addArgument(null) + ->setAbstract(true) + ; + + $container->register('console.command.messenger_debug', DebugCommand::class)->addArgument(array()); + + $middlewares = array(array('id' => 'call_message_handler')); + + $container->setParameter($commandBusId.'.middleware', $middlewares); + $container->setParameter($queryBusId.'.middleware', $middlewares); + $container->register(DummyCommandHandler::class)->addTag('messenger.message_handler', array('bus' => $commandBusId)); + $container->register(DummyQueryHandler::class)->addTag('messenger.message_handler', array('bus' => $queryBusId)); + $container->register(MultipleBusesMessageHandler::class) + ->addTag('messenger.message_handler', array('bus' => $commandBusId)) + ->addTag('messenger.message_handler', array('bus' => $queryBusId)) + ; + + (new ResolveClassPass())->process($container); (new MessengerPass())->process($container); + + $this->assertEquals(array( + $commandBusId => array( + DummyCommand::class => array(DummyCommandHandler::class), + MultipleBusesMessage::class => array(MultipleBusesMessageHandler::class), + ), + $queryBusId => array( + DummyQuery::class => array(DummyQueryHandler::class), + MultipleBusesMessage::class => array(MultipleBusesMessageHandler::class), + ), + $emptyBus => array(), + ), $container->getDefinition('console.command.messenger_debug')->getArgument(0)); } - private function getContainerBuilder(): ContainerBuilder + private function getContainerBuilder(string $busId = 'message_bus'): ContainerBuilder { $container = new ContainerBuilder(); $container->setParameter('kernel.debug', true); - $container - ->register('messenger.sender_locator', ServiceLocator::class) - ->addArgument(new Reference('service_container')) - ; + $container->register($busId, MessageBusInterface::class)->addTag('messenger.bus')->setArgument(0, array()); + if ('message_bus' !== $busId) { + $container->setAlias('message_bus', $busId); + } $container - ->register('messenger.handler_resolver', ContainerHandlerLocator::class) + ->register('messenger.sender_locator', ServiceLocator::class) ->addArgument(new Reference('service_container')) ; @@ -321,7 +554,7 @@ class DummyReceiver implements ReceiverInterface public function receive(callable $handler): void { for ($i = 0; $i < 3; ++$i) { - $handler(new DummyMessage("Dummy $i")); + $handler(Envelope::wrap(new DummyMessage("Dummy $i"))); } } @@ -330,6 +563,14 @@ public function stop(): void } } +class InvalidReceiver +{ +} + +class InvalidSender +{ +} + class UndefinedMessageHandler { public function __invoke(UndefinedMessage $message) @@ -339,7 +580,7 @@ public function __invoke(UndefinedMessage $message) class UndefinedMessageHandlerViaInterface implements MessageSubscriberInterface { - public static function getHandledMessages(): array + public static function getHandledMessages(): iterable { return array(UndefinedMessage::class); } @@ -376,31 +617,72 @@ public function __invoke(string $message) class HandlerWithMultipleMessages implements MessageSubscriberInterface { - public static function getHandledMessages(): array + public static function getHandledMessages(): iterable { return array( DummyMessage::class, SecondMessage::class, ); } + + public function __invoke() + { + } } class PrioritizedHandler implements MessageSubscriberInterface { - public static function getHandledMessages(): array + public static function getHandledMessages(): iterable { return array( array(SecondMessage::class, 10), ); } + + public function __invoke() + { + } +} + +class HandlerMappingMethods implements MessageSubscriberInterface +{ + public static function getHandledMessages(): iterable + { + return array( + DummyMessage::class => 'dummyMethod', + SecondMessage::class => array('secondMessage', 20), + ); + } + + public function dummyMethod() + { + } + + public function secondMessage() + { + } +} + +class HandlerMappingWithNonExistentMethod implements MessageSubscriberInterface +{ + public static function getHandledMessages(): iterable + { + return array( + DummyMessage::class => 'dummyMethod', + ); + } } class HandleNoMessageHandler implements MessageSubscriberInterface { - public static function getHandledMessages(): array + public static function getHandledMessages(): iterable { return array(); } + + public function __invoke() + { + } } class UselessMiddleware implements MiddlewareInterface diff --git a/src/Symfony/Component/Messenger/Tests/EnvelopeTest.php b/src/Symfony/Component/Messenger/Tests/EnvelopeTest.php new file mode 100644 index 0000000000000..053275b6e714c --- /dev/null +++ b/src/Symfony/Component/Messenger/Tests/EnvelopeTest.php @@ -0,0 +1,82 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Messenger\Asynchronous\Transport\ReceivedMessage; +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\EnvelopeAwareInterface; +use Symfony\Component\Messenger\Middleware\Configuration\ValidationConfiguration; +use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage; + +/** + * @author Maxime Steinhausser + */ +class EnvelopeTest extends TestCase +{ + public function testConstruct() + { + $envelope = new Envelope($dummy = new DummyMessage('dummy'), array( + $receivedConfig = new ReceivedMessage(), + )); + + $this->assertSame($dummy, $envelope->getMessage()); + $this->assertArrayHasKey(ReceivedMessage::class, $configs = $envelope->all()); + $this->assertSame($receivedConfig, $configs[ReceivedMessage::class]); + } + + public function testWrap() + { + $first = Envelope::wrap($dummy = new DummyMessage('dummy')); + + $this->assertInstanceOf(Envelope::class, $first); + $this->assertSame($dummy, $first->getMessage()); + + $envelope = Envelope::wrap($first); + $this->assertSame($first, $envelope); + } + + public function testWithReturnsNewInstance() + { + $envelope = Envelope::wrap($dummy = new DummyMessage('dummy')); + + $this->assertNotSame($envelope, $envelope->with(new ReceivedMessage())); + } + + public function testGet() + { + $envelope = Envelope::wrap($dummy = new DummyMessage('dummy')) + ->with($config = new ReceivedMessage()) + ; + + $this->assertSame($config, $envelope->get(ReceivedMessage::class)); + $this->assertNull($envelope->get(ValidationConfiguration::class)); + } + + public function testAll() + { + $envelope = Envelope::wrap($dummy = new DummyMessage('dummy')) + ->with($receivedConfig = new ReceivedMessage()) + ->with($validationConfig = new ValidationConfiguration(array('foo'))) + ; + + $configs = $envelope->all(); + $this->assertArrayHasKey(ReceivedMessage::class, $configs); + $this->assertSame($receivedConfig, $configs[ReceivedMessage::class]); + $this->assertArrayHasKey(ValidationConfiguration::class, $configs); + $this->assertSame($validationConfig, $configs[ValidationConfiguration::class]); + } +} + +class FooConfigurationConsumer implements EnvelopeAwareInterface +{ +} diff --git a/src/Symfony/Component/Messenger/Tests/Fixtures/AnEnvelopeItem.php b/src/Symfony/Component/Messenger/Tests/Fixtures/AnEnvelopeItem.php new file mode 100644 index 0000000000000..9e5bb0c92b63d --- /dev/null +++ b/src/Symfony/Component/Messenger/Tests/Fixtures/AnEnvelopeItem.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Tests\Fixtures; + +use Symfony\Component\Messenger\EnvelopeItemInterface; + +class AnEnvelopeItem implements EnvelopeItemInterface +{ + /** + * {@inheritdoc} + */ + public function serialize() + { + return ''; + } + + /** + * {@inheritdoc} + */ + public function unserialize($serialized) + { + // noop + } +} diff --git a/src/Symfony/Component/Messenger/Tests/Fixtures/DummyCommand.php b/src/Symfony/Component/Messenger/Tests/Fixtures/DummyCommand.php new file mode 100644 index 0000000000000..c69375a7c0847 --- /dev/null +++ b/src/Symfony/Component/Messenger/Tests/Fixtures/DummyCommand.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Tests\Fixtures; + +class DummyCommand +{ +} diff --git a/src/Symfony/Component/Messenger/Tests/Fixtures/DummyCommandHandler.php b/src/Symfony/Component/Messenger/Tests/Fixtures/DummyCommandHandler.php new file mode 100644 index 0000000000000..1665bb0da9148 --- /dev/null +++ b/src/Symfony/Component/Messenger/Tests/Fixtures/DummyCommandHandler.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Tests\Fixtures; + +class DummyCommandHandler +{ + public function __invoke(DummyCommand $command) + { + } +} diff --git a/src/Symfony/Component/Messenger/Tests/Fixtures/DummyMessage.php b/src/Symfony/Component/Messenger/Tests/Fixtures/DummyMessage.php index fb02ca7b866ae..2a9c70b1c5349 100644 --- a/src/Symfony/Component/Messenger/Tests/Fixtures/DummyMessage.php +++ b/src/Symfony/Component/Messenger/Tests/Fixtures/DummyMessage.php @@ -2,7 +2,7 @@ namespace Symfony\Component\Messenger\Tests\Fixtures; -class DummyMessage +class DummyMessage implements DummyMessageInterface { private $message; diff --git a/src/Symfony/Component/Messenger/Tests/Fixtures/DummyMessageInterface.php b/src/Symfony/Component/Messenger/Tests/Fixtures/DummyMessageInterface.php new file mode 100644 index 0000000000000..557b958ddc62e --- /dev/null +++ b/src/Symfony/Component/Messenger/Tests/Fixtures/DummyMessageInterface.php @@ -0,0 +1,7 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Tests\Fixtures; + +class DummyQuery +{ +} diff --git a/src/Symfony/Component/Messenger/Tests/Fixtures/DummyQueryHandler.php b/src/Symfony/Component/Messenger/Tests/Fixtures/DummyQueryHandler.php new file mode 100644 index 0000000000000..827de27e3fc51 --- /dev/null +++ b/src/Symfony/Component/Messenger/Tests/Fixtures/DummyQueryHandler.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Tests\Fixtures; + +class DummyQueryHandler +{ + public function __invoke(DummyQuery $query) + { + } +} diff --git a/src/Symfony/Component/Messenger/Tests/Fixtures/MultipleBusesMessage.php b/src/Symfony/Component/Messenger/Tests/Fixtures/MultipleBusesMessage.php new file mode 100644 index 0000000000000..663d713a5d504 --- /dev/null +++ b/src/Symfony/Component/Messenger/Tests/Fixtures/MultipleBusesMessage.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Tests\Fixtures; + +class MultipleBusesMessage +{ +} diff --git a/src/Symfony/Component/Messenger/Tests/Fixtures/MultipleBusesMessageHandler.php b/src/Symfony/Component/Messenger/Tests/Fixtures/MultipleBusesMessageHandler.php new file mode 100644 index 0000000000000..3deea4f455069 --- /dev/null +++ b/src/Symfony/Component/Messenger/Tests/Fixtures/MultipleBusesMessageHandler.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Tests\Fixtures; + +class MultipleBusesMessageHandler +{ + public function __invoke(MultipleBusesMessage $message) + { + } +} diff --git a/src/Symfony/Component/Messenger/Tests/MessageBusTest.php b/src/Symfony/Component/Messenger/Tests/MessageBusTest.php index e8249b307f806..9dc93a9d15213 100644 --- a/src/Symfony/Component/Messenger/Tests/MessageBusTest.php +++ b/src/Symfony/Component/Messenger/Tests/MessageBusTest.php @@ -12,9 +12,13 @@ namespace Symfony\Component\Messenger\Tests; use PHPUnit\Framework\TestCase; +use Symfony\Component\Messenger\Asynchronous\Transport\ReceivedMessage; +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\EnvelopeAwareInterface; use Symfony\Component\Messenger\MessageBus; use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Messenger\Middleware\MiddlewareInterface; +use Symfony\Component\Messenger\Tests\Fixtures\AnEnvelopeItem; use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage; class MessageBusTest extends TestCase @@ -26,6 +30,15 @@ public function testItHasTheRightInterface() $this->assertInstanceOf(MessageBusInterface::class, $bus); } + /** + * @expectedException \Symfony\Component\Messenger\Exception\InvalidArgumentException + * @expectedExceptionMessage Invalid type for message argument. Expected object, but got "string". + */ + public function testItDispatchInvalidMessageType() + { + (new MessageBus())->dispatch('wrong'); + } + public function testItCallsMiddlewareAndChainTheReturnValue() { $message = new DummyMessage('Hello'); @@ -52,4 +65,98 @@ public function testItCallsMiddlewareAndChainTheReturnValue() $this->assertEquals($responseFromDepthMiddleware, $bus->dispatch($message)); } + + public function testItKeepsTheEnvelopeEvenThroughAMiddlewareThatIsNotEnvelopeAware() + { + $message = new DummyMessage('Hello'); + $envelope = new Envelope($message, array(new ReceivedMessage())); + + $firstMiddleware = $this->getMockBuilder(MiddlewareInterface::class)->getMock(); + $firstMiddleware->expects($this->once()) + ->method('handle') + ->with($message, $this->anything()) + ->will($this->returnCallback(function ($message, $next) { + return $next($message); + })); + + $secondMiddleware = $this->getMockBuilder(array(MiddlewareInterface::class, EnvelopeAwareInterface::class))->getMock(); + $secondMiddleware->expects($this->once()) + ->method('handle') + ->with($envelope, $this->anything()) + ; + + $bus = new MessageBus(array( + $firstMiddleware, + $secondMiddleware, + )); + + $bus->dispatch($envelope); + } + + public function testThatAMiddlewareCanAddSomeItemsToTheEnvelope() + { + $message = new DummyMessage('Hello'); + $envelope = new Envelope($message, array(new ReceivedMessage())); + $envelopeWithAnotherItem = $envelope->with(new AnEnvelopeItem()); + + $firstMiddleware = $this->getMockBuilder(array(MiddlewareInterface::class, EnvelopeAwareInterface::class))->getMock(); + $firstMiddleware->expects($this->once()) + ->method('handle') + ->with($envelope, $this->anything()) + ->will($this->returnCallback(function ($message, $next) { + return $next($message->with(new AnEnvelopeItem())); + })); + + $secondMiddleware = $this->getMockBuilder(MiddlewareInterface::class)->getMock(); + $secondMiddleware->expects($this->once()) + ->method('handle') + ->with($message, $this->anything()) + ->will($this->returnCallback(function ($message, $next) { + return $next($message); + })); + + $thirdMiddleware = $this->getMockBuilder(array(MiddlewareInterface::class, EnvelopeAwareInterface::class))->getMock(); + $thirdMiddleware->expects($this->once()) + ->method('handle') + ->with($envelopeWithAnotherItem, $this->anything()) + ; + + $bus = new MessageBus(array( + $firstMiddleware, + $secondMiddleware, + $thirdMiddleware, + )); + + $bus->dispatch($envelope); + } + + public function testThatAMiddlewareCanUpdateTheMessageWhileKeepingTheEnvelopeItems() + { + $message = new DummyMessage('Hello'); + $envelope = new Envelope($message, $items = array(new ReceivedMessage())); + + $changedMessage = new DummyMessage('Changed'); + $expectedEnvelope = new Envelope($changedMessage, $items); + + $firstMiddleware = $this->getMockBuilder(MiddlewareInterface::class)->getMock(); + $firstMiddleware->expects($this->once()) + ->method('handle') + ->with($message, $this->anything()) + ->will($this->returnCallback(function ($message, $next) use ($changedMessage) { + return $next($changedMessage); + })); + + $secondMiddleware = $this->getMockBuilder(array(MiddlewareInterface::class, EnvelopeAwareInterface::class))->getMock(); + $secondMiddleware->expects($this->once()) + ->method('handle') + ->with($expectedEnvelope, $this->anything()) + ; + + $bus = new MessageBus(array( + $firstMiddleware, + $secondMiddleware, + )); + + $bus->dispatch($envelope); + } } diff --git a/src/Symfony/Component/Messenger/Tests/Middleware/Configuration/ValidationConfigurationTest.php b/src/Symfony/Component/Messenger/Tests/Middleware/Configuration/ValidationConfigurationTest.php new file mode 100644 index 0000000000000..6fd6962994f4f --- /dev/null +++ b/src/Symfony/Component/Messenger/Tests/Middleware/Configuration/ValidationConfigurationTest.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Tests\Middleware\Configuration; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Messenger\Middleware\Configuration\ValidationConfiguration; +use Symfony\Component\Validator\Constraints\GroupSequence; + +/** + * @author Maxime Steinhausser + */ +class ValidationConfigurationTest extends TestCase +{ + public function testConfig() + { + $config = new ValidationConfiguration($groups = array('Default', 'Extra')); + $this->assertSame($groups, $config->getGroups()); + + $config = new ValidationConfiguration($groups = new GroupSequence(array('Default', 'Then'))); + $this->assertSame($groups, $config->getGroups()); + } + + public function testSerialiazable() + { + $this->assertTrue(is_subclass_of(ValidationConfiguration::class, \Serializable::class, true)); + $this->assertEquals($config = new ValidationConfiguration(array('Default', 'Extra')), unserialize(serialize($config))); + $this->assertEquals($config = new ValidationConfiguration(new GroupSequence(array('Default', 'Then'))), unserialize(serialize($config))); + } +} diff --git a/src/Symfony/Component/Messenger/Tests/Middleware/ValidationMiddlewareTest.php b/src/Symfony/Component/Messenger/Tests/Middleware/ValidationMiddlewareTest.php index 2bd2ba8af22e4..b2a44b5fa6723 100644 --- a/src/Symfony/Component/Messenger/Tests/Middleware/ValidationMiddlewareTest.php +++ b/src/Symfony/Component/Messenger/Tests/Middleware/ValidationMiddlewareTest.php @@ -12,6 +12,8 @@ namespace Symfony\Component\Messenger\Tests\Middleware; use PHPUnit\Framework\TestCase; +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Middleware\Configuration\ValidationConfiguration; use Symfony\Component\Messenger\Middleware\ValidationMiddleware; use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage; use Symfony\Component\Validator\ConstraintViolationListInterface; @@ -43,6 +45,30 @@ public function testValidateAndNextMiddleware() $this->assertSame('Hello', $result); } + public function testValidateWithConfigurationAndNextMiddleware() + { + $envelope = Envelope::wrap($message = new DummyMessage('Hey'))->with(new ValidationConfiguration($groups = array('Default', 'Extra'))); + + $validator = $this->createMock(ValidatorInterface::class); + $validator + ->expects($this->once()) + ->method('validate') + ->with($message, null, $groups) + ->willReturn($this->createMock(ConstraintViolationListInterface::class)) + ; + $next = $this->createPartialMock(\stdClass::class, array('__invoke')); + $next + ->expects($this->once()) + ->method('__invoke') + ->with($envelope) + ->willReturn('Hello') + ; + + $result = (new ValidationMiddleware($validator))->handle($envelope, $next); + + $this->assertSame('Hello', $result); + } + /** * @expectedException \Symfony\Component\Messenger\Exception\ValidationFailedException * @expectedExceptionMessage Message of type "Symfony\Component\Messenger\Tests\Fixtures\DummyMessage" failed validation. diff --git a/src/Symfony/Component/Messenger/Tests/TraceableMessageBusTest.php b/src/Symfony/Component/Messenger/Tests/TraceableMessageBusTest.php index 8e4895d3babc8..8a2946ee42778 100644 --- a/src/Symfony/Component/Messenger/Tests/TraceableMessageBusTest.php +++ b/src/Symfony/Component/Messenger/Tests/TraceableMessageBusTest.php @@ -12,7 +12,9 @@ namespace Symfony\Component\Messenger\Tests; use PHPUnit\Framework\TestCase; +use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\MessageBusInterface; +use Symfony\Component\Messenger\Tests\Fixtures\AnEnvelopeItem; use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage; use Symfony\Component\Messenger\TraceableMessageBus; @@ -27,7 +29,29 @@ public function testItTracesResult() $traceableBus = new TraceableMessageBus($bus); $this->assertSame($result, $traceableBus->dispatch($message)); - $this->assertSame(array(array('message' => $message, 'result' => $result)), $traceableBus->getDispatchedMessages()); + $this->assertCount(1, $tracedMessages = $traceableBus->getDispatchedMessages()); + $this->assertArraySubset(array( + 'message' => $message, + 'result' => $result, + 'envelopeItems' => null, + ), $tracedMessages[0], true); + } + + public function testItTracesResultWithEnvelope() + { + $envelope = Envelope::wrap($message = new DummyMessage('Hello'))->with($envelopeItem = new AnEnvelopeItem()); + + $bus = $this->getMockBuilder(MessageBusInterface::class)->getMock(); + $bus->expects($this->once())->method('dispatch')->with($envelope)->willReturn($result = array('foo' => 'bar')); + + $traceableBus = new TraceableMessageBus($bus); + $this->assertSame($result, $traceableBus->dispatch($envelope)); + $this->assertCount(1, $tracedMessages = $traceableBus->getDispatchedMessages()); + $this->assertArraySubset(array( + 'message' => $message, + 'result' => $result, + 'envelopeItems' => array($envelopeItem), + ), $tracedMessages[0], true); } public function testItTracesExceptions() @@ -45,6 +69,11 @@ public function testItTracesExceptions() $this->assertSame($exception, $e); } - $this->assertSame(array(array('message' => $message, 'exception' => $exception)), $traceableBus->getDispatchedMessages()); + $this->assertCount(1, $tracedMessages = $traceableBus->getDispatchedMessages()); + $this->assertArraySubset(array( + 'message' => $message, + 'exception' => $exception, + 'envelopeItems' => null, + ), $tracedMessages[0], true); } } diff --git a/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpExtIntegrationTest.php b/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpExtIntegrationTest.php index 5ce9c457e9841..c01ffdadb7f0a 100644 --- a/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpExtIntegrationTest.php +++ b/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpExtIntegrationTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Messenger\Tests\Transport\AmqpExt; use PHPUnit\Framework\TestCase; +use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\Transport\AmqpExt\AmqpReceiver; use Symfony\Component\Messenger\Transport\AmqpExt\AmqpSender; use Symfony\Component\Messenger\Transport\AmqpExt\Connection; @@ -50,12 +51,12 @@ public function testItSendsAndReceivesMessages() $sender = new AmqpSender($serializer, $connection); $receiver = new AmqpReceiver($serializer, $connection); - $sender->send($firstMessage = new DummyMessage('First')); - $sender->send($secondMessage = new DummyMessage('Second')); + $sender->send($first = Envelope::wrap(new DummyMessage('First'))); + $sender->send($second = Envelope::wrap(new DummyMessage('Second'))); $receivedMessages = 0; - $receiver->receive(function ($message) use ($receiver, &$receivedMessages, $firstMessage, $secondMessage) { - $this->assertEquals(0 == $receivedMessages ? $firstMessage : $secondMessage, $message); + $receiver->receive(function (?Envelope $envelope) use ($receiver, &$receivedMessages, $first, $second) { + $this->assertEquals(0 == $receivedMessages ? $first : $second, $envelope); if (2 === ++$receivedMessages) { $receiver->stop(); @@ -74,7 +75,7 @@ public function testItReceivesSignals() $connection->queue()->purge(); $sender = new AmqpSender($serializer, $connection); - $sender->send(new DummyMessage('Hello')); + $sender->send(Envelope::wrap(new DummyMessage('Hello'))); $amqpReadTimeout = 30; $dsn = getenv('MESSENGER_AMQP_DSN').'?read_timeout='.$amqpReadTimeout; @@ -98,7 +99,15 @@ public function testItReceivesSignals() $this->assertFalse($process->isRunning()); $this->assertLessThan($amqpReadTimeout, microtime(true) - $signalTime); - $this->assertEquals($expectedOutput."Get message: Symfony\Component\Messenger\Asynchronous\Transport\ReceivedMessage\nDone.\n", $process->getOutput()); + $this->assertSame($expectedOutput.<<<'TXT' +Get envelope with message: Symfony\Component\Messenger\Tests\Fixtures\DummyMessage +with items: [ + "Symfony\\Component\\Messenger\\Asynchronous\\Transport\\ReceivedMessage" +] +Done. + +TXT + , $process->getOutput()); } /** @@ -114,12 +123,11 @@ public function testItSupportsTimeoutAndTicksNullMessagesToTheHandler() $connection->setup(); $connection->queue()->purge(); - $sender = new AmqpSender($serializer, $connection); $receiver = new AmqpReceiver($serializer, $connection); $receivedMessages = 0; - $receiver->receive(function ($message) use ($receiver, &$receivedMessages) { - $this->assertNull($message); + $receiver->receive(function (?Envelope $envelope) use ($receiver, &$receivedMessages) { + $this->assertNull($envelope); if (2 === ++$receivedMessages) { $receiver->stop(); diff --git a/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpReceiverTest.php b/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpReceiverTest.php index 2045764216ca3..4d0b779e3b3f4 100644 --- a/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpReceiverTest.php +++ b/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpReceiverTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Messenger\Tests\Transport\AmqpExt; use PHPUnit\Framework\TestCase; +use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\Transport\AmqpExt\AmqpReceiver; use Symfony\Component\Messenger\Transport\AmqpExt\Connection; use Symfony\Component\Messenger\Transport\AmqpExt\Exception\RejectMessageExceptionInterface; @@ -44,8 +45,8 @@ public function testItSendTheDecodedMessageToTheHandlerAndAcknowledgeIt() $connection->expects($this->once())->method('ack')->with($envelope); $receiver = new AmqpReceiver($serializer, $connection); - $receiver->receive(function ($message) use ($receiver) { - $this->assertEquals(new DummyMessage('Hi'), $message); + $receiver->receive(function (?Envelope $envelope) use ($receiver) { + $this->assertEquals(new DummyMessage('Hi'), $envelope->getMessage()); $receiver->stop(); }); } diff --git a/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpSenderTest.php b/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpSenderTest.php index 54859a58acf1b..8848b022f6aad 100644 --- a/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpSenderTest.php +++ b/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpSenderTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Messenger\Tests\Transport\AmqpExt; +use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\Transport\AmqpExt\AmqpSender; use Symfony\Component\Messenger\Transport\AmqpExt\Connection; use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage; @@ -24,16 +25,16 @@ class AmqpSenderTest extends TestCase { public function testItSendsTheEncodedMessage() { - $message = new DummyMessage('Oy'); + $envelope = Envelope::wrap(new DummyMessage('Oy')); $encoded = array('body' => '...', 'headers' => array('type' => DummyMessage::class)); $encoder = $this->getMockBuilder(EncoderInterface::class)->getMock(); - $encoder->method('encode')->with($message)->willReturnOnConsecutiveCalls($encoded); + $encoder->method('encode')->with($envelope)->willReturnOnConsecutiveCalls($encoded); $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->getMock(); $connection->expects($this->once())->method('publish')->with($encoded['body'], $encoded['headers']); $sender = new AmqpSender($encoder, $connection); - $sender->send($message); + $sender->send($envelope); } } diff --git a/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpTransportFactoryTest.php b/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpTransportFactoryTest.php new file mode 100644 index 0000000000000..53a98e2263a07 --- /dev/null +++ b/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpTransportFactoryTest.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Tests\Transport\AmqpExt; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Messenger\Transport\AmqpExt\AmqpTransport; +use Symfony\Component\Messenger\Transport\AmqpExt\AmqpTransportFactory; +use Symfony\Component\Messenger\Transport\AmqpExt\Connection; +use Symfony\Component\Messenger\Transport\Serialization\DecoderInterface; +use Symfony\Component\Messenger\Transport\Serialization\EncoderInterface; + +class AmqpTransportFactoryTest extends TestCase +{ + public function testSupportsOnlyAmqpTransports() + { + $factory = new AmqpTransportFactory( + $this->getMockBuilder(EncoderInterface::class)->getMock(), + $this->getMockBuilder(DecoderInterface::class)->getMock(), + true + ); + + $this->assertTrue($factory->supports('amqp://localhost', array())); + $this->assertFalse($factory->supports('sqs://localhost', array())); + $this->assertFalse($factory->supports('invalid-dsn', array())); + } + + public function testItCreatesTheTransport() + { + $factory = new AmqpTransportFactory( + $encoder = $this->getMockBuilder(EncoderInterface::class)->getMock(), + $decoder = $this->getMockBuilder(DecoderInterface::class)->getMock(), + true + ); + + $expectedTransport = new AmqpTransport($encoder, $decoder, Connection::fromDsn('amqp://localhost', array('foo' => 'bar'), true), array('foo' => 'bar'), true); + + $this->assertEquals($expectedTransport, $factory->createTransport('amqp://localhost', array('foo' => 'bar'))); + } +} diff --git a/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpTransportTest.php b/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpTransportTest.php new file mode 100644 index 0000000000000..3d003a270d062 --- /dev/null +++ b/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpTransportTest.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Tests\Transport\AmqpExt; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage; +use Symfony\Component\Messenger\Transport\AmqpExt\AmqpTransport; +use Symfony\Component\Messenger\Transport\AmqpExt\Connection; +use Symfony\Component\Messenger\Transport\Serialization\DecoderInterface; +use Symfony\Component\Messenger\Transport\Serialization\EncoderInterface; +use Symfony\Component\Messenger\Transport\TransportInterface; + +/** + * @requires extension amqp + */ +class AmqpTransportTest extends TestCase +{ + public function testItIsATransport() + { + $transport = $this->getTransport(); + + $this->assertInstanceOf(TransportInterface::class, $transport); + } + + public function testReceivesMessages() + { + $transport = $this->getTransport( + null, + $decoder = $this->getMockBuilder(DecoderInterface::class)->getMock(), + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->getMock() + ); + + $decodedMessage = new DummyMessage('Decoded.'); + + $amqpEnvelope = $this->getMockBuilder(\AMQPEnvelope::class)->getMock(); + $amqpEnvelope->method('getBody')->willReturn('body'); + $amqpEnvelope->method('getHeaders')->willReturn(array('my' => 'header')); + + $decoder->method('decode')->with(array('body' => 'body', 'headers' => array('my' => 'header')))->willReturn(Envelope::wrap($decodedMessage)); + $connection->method('get')->willReturn($amqpEnvelope); + + $transport->receive(function (Envelope $envelope) use ($transport, $decodedMessage) { + $this->assertSame($decodedMessage, $envelope->getMessage()); + + $transport->stop(); + }); + } + + private function getTransport(EncoderInterface $encoder = null, DecoderInterface $decoder = null, Connection $connection = null) + { + $encoder = $encoder ?: $this->getMockBuilder(EncoderInterface::class)->getMock(); + $decoder = $decoder ?: $this->getMockBuilder(DecoderInterface::class)->getMock(); + $connection = $connection ?: $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->getMock(); + + return new AmqpTransport($encoder, $decoder, $connection); + } +} diff --git a/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/Fixtures/long_receiver.php b/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/Fixtures/long_receiver.php index 2115b1a81dc23..b7231a5d47fac 100644 --- a/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/Fixtures/long_receiver.php +++ b/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/Fixtures/long_receiver.php @@ -12,10 +12,9 @@ require_once $autoload; +use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Messenger\Transport\AmqpExt\AmqpReceiver; -use Symfony\Component\Messenger\Transport\AmqpExt\AmqpSender; use Symfony\Component\Messenger\Transport\AmqpExt\Connection; -use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Messenger\Transport\Serialization\Serializer; use Symfony\Component\Messenger\Worker; use Symfony\Component\Serializer as SerializerComponent; @@ -27,13 +26,14 @@ ); $connection = Connection::fromDsn(getenv('DSN')); -$sender = new AmqpSender($serializer, $connection); $receiver = new AmqpReceiver($serializer, $connection); $worker = new Worker($receiver, new class() implements MessageBusInterface { - public function dispatch($message) + public function dispatch($envelope) { - echo 'Get message: '.get_class($message)."\n"; + echo 'Get envelope with message: '.get_class($envelope->getMessage())."\n"; + echo sprintf("with items: %s\n", json_encode(array_keys($envelope->all()), JSON_PRETTY_PRINT)); + sleep(30); echo "Done.\n"; } diff --git a/src/Symfony/Component/Messenger/Tests/Transport/Enhancers/StopWhenMemoryUsageIsExceededReceiverTest.php b/src/Symfony/Component/Messenger/Tests/Transport/Enhancers/StopWhenMemoryUsageIsExceededReceiverTest.php index f317c6a5bfe94..a34be3bfc2ce5 100644 --- a/src/Symfony/Component/Messenger/Tests/Transport/Enhancers/StopWhenMemoryUsageIsExceededReceiverTest.php +++ b/src/Symfony/Component/Messenger/Tests/Transport/Enhancers/StopWhenMemoryUsageIsExceededReceiverTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; +use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\Tests\Fixtures\CallbackReceiver; use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage; use Symfony\Component\Messenger\Transport\Enhancers\StopWhenMemoryUsageIsExceededReceiver; @@ -25,7 +26,7 @@ class StopWhenMemoryUsageIsExceededReceiverTest extends TestCase public function testReceiverStopsWhenMemoryLimitExceeded(int $memoryUsage, int $memoryLimit, bool $shouldStop) { $callable = function ($handler) { - $handler(new DummyMessage('API')); + $handler(Envelope::wrap(new DummyMessage('API'))); }; $decoratedReceiver = $this->getMockBuilder(CallbackReceiver::class) @@ -58,7 +59,7 @@ public function memoryProvider() public function testReceiverLogsMemoryExceededWhenLoggerIsGiven() { $callable = function ($handler) { - $handler(new DummyMessage('API')); + $handler(Envelope::wrap(new DummyMessage('API'))); }; $decoratedReceiver = $this->getMockBuilder(CallbackReceiver::class) diff --git a/src/Symfony/Component/Messenger/Tests/Transport/Enhancers/StopWhenMessageCountIsExceededReceiverTest.php b/src/Symfony/Component/Messenger/Tests/Transport/Enhancers/StopWhenMessageCountIsExceededReceiverTest.php index 7a516744e1893..e5c51335b3b8e 100644 --- a/src/Symfony/Component/Messenger/Tests/Transport/Enhancers/StopWhenMessageCountIsExceededReceiverTest.php +++ b/src/Symfony/Component/Messenger/Tests/Transport/Enhancers/StopWhenMessageCountIsExceededReceiverTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; +use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\Tests\Fixtures\CallbackReceiver; use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage; use Symfony\Component\Messenger\Transport\Enhancers\StopWhenMessageCountIsExceededReceiver; @@ -25,9 +26,9 @@ class StopWhenMessageCountIsExceededReceiverTest extends TestCase public function testReceiverStopsWhenMaximumCountExceeded($max, $shouldStop) { $callable = function ($handler) { - $handler(new DummyMessage('First message')); - $handler(new DummyMessage('Second message')); - $handler(new DummyMessage('Third message')); + $handler(Envelope::wrap(new DummyMessage('First message'))); + $handler(Envelope::wrap(new DummyMessage('Second message'))); + $handler(Envelope::wrap(new DummyMessage('Third message'))); }; $decoratedReceiver = $this->getMockBuilder(CallbackReceiver::class) @@ -78,7 +79,7 @@ public function testReceiverDoesntIncreaseItsCounterWhenReceiveNullMessage() public function testReceiverLogsMaximumCountExceededWhenLoggerIsGiven() { $callable = function ($handler) { - $handler(new DummyMessage('First message')); + $handler(Envelope::wrap(new DummyMessage('First message'))); }; $decoratedReceiver = $this->getMockBuilder(CallbackReceiver::class) diff --git a/src/Symfony/Component/Messenger/Tests/Transport/Serialization/SerializerTest.php b/src/Symfony/Component/Messenger/Tests/Transport/Serialization/SerializerTest.php index 2e227c0f2f717..214b773092e8c 100644 --- a/src/Symfony/Component/Messenger/Tests/Transport/Serialization/SerializerTest.php +++ b/src/Symfony/Component/Messenger/Tests/Transport/Serialization/SerializerTest.php @@ -12,8 +12,11 @@ namespace Symfony\Component\Messenger\Tests\Transport\Serialization; use PHPUnit\Framework\TestCase; +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Middleware\Configuration\ValidationConfiguration; use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage; use Symfony\Component\Messenger\Transport\Serialization\Serializer; +use Symfony\Component\Messenger\Transport\Serialization\SerializerConfiguration; use Symfony\Component\Serializer as SerializerComponent; use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; @@ -26,9 +29,23 @@ public function testEncodedIsDecodable() new SerializerComponent\Serializer(array(new ObjectNormalizer()), array('json' => new JsonEncoder())) ); - $message = new DummyMessage('Hello'); + $envelope = Envelope::wrap(new DummyMessage('Hello')); - $this->assertEquals($message, $serializer->decode($serializer->encode($message))); + $this->assertEquals($envelope, $serializer->decode($serializer->encode($envelope))); + } + + public function testEncodedWithConfigurationIsDecodable() + { + $serializer = new Serializer( + new SerializerComponent\Serializer(array(new ObjectNormalizer()), array('json' => new JsonEncoder())) + ); + + $envelope = Envelope::wrap(new DummyMessage('Hello')) + ->with(new SerializerConfiguration(array(ObjectNormalizer::GROUPS => array('foo')))) + ->with(new ValidationConfiguration(array('foo', 'bar'))) + ; + + $this->assertEquals($envelope, $serializer->decode($serializer->encode($envelope))); } public function testEncodedIsHavingTheBodyAndTypeHeader() @@ -37,11 +54,12 @@ public function testEncodedIsHavingTheBodyAndTypeHeader() new SerializerComponent\Serializer(array(new ObjectNormalizer()), array('json' => new JsonEncoder())) ); - $encoded = $serializer->encode(new DummyMessage('Hello')); + $encoded = $serializer->encode(Envelope::wrap(new DummyMessage('Hello'))); $this->assertArrayHasKey('body', $encoded); $this->assertArrayHasKey('headers', $encoded); $this->assertArrayHasKey('type', $encoded['headers']); + $this->assertArrayNotHasKey('X-Message-Envelope-Items', $encoded['headers']); $this->assertEquals(DummyMessage::class, $encoded['headers']['type']); } @@ -55,10 +73,31 @@ public function testUsesTheCustomFormatAndContext() $encoder = new Serializer($serializer, 'csv', array('foo' => 'bar')); - $encoded = $encoder->encode($message); + $encoded = $encoder->encode(Envelope::wrap($message)); $decoded = $encoder->decode($encoded); $this->assertSame('Yay', $encoded['body']); - $this->assertSame($message, $decoded); + $this->assertSame($message, $decoded->getMessage()); + } + + public function testEncodedWithSerializationConfiguration() + { + $serializer = new Serializer( + new SerializerComponent\Serializer(array(new ObjectNormalizer()), array('json' => new JsonEncoder())) + ); + + $envelope = Envelope::wrap(new DummyMessage('Hello')) + ->with(new SerializerConfiguration(array(ObjectNormalizer::GROUPS => array('foo')))) + ->with(new ValidationConfiguration(array('foo', 'bar'))) + ; + + $encoded = $serializer->encode($envelope); + + $this->assertArrayHasKey('body', $encoded); + $this->assertArrayHasKey('headers', $encoded); + $this->assertArrayHasKey('type', $encoded['headers']); + $this->assertEquals(DummyMessage::class, $encoded['headers']['type']); + $this->assertArrayHasKey('X-Message-Envelope-Items', $encoded['headers']); + $this->assertSame('a:2:{s:75:"Symfony\Component\Messenger\Transport\Serialization\SerializerConfiguration";C:75:"Symfony\Component\Messenger\Transport\Serialization\SerializerConfiguration":59:{a:1:{s:7:"context";a:1:{s:6:"groups";a:1:{i:0;s:3:"foo";}}}}s:76:"Symfony\Component\Messenger\Middleware\Configuration\ValidationConfiguration";C:76:"Symfony\Component\Messenger\Middleware\Configuration\ValidationConfiguration":82:{a:2:{s:6:"groups";a:2:{i:0;s:3:"foo";i:1;s:3:"bar";}s:17:"is_group_sequence";b:0;}}}', $encoded['headers']['X-Message-Envelope-Items']); } } diff --git a/src/Symfony/Component/Messenger/Tests/WorkerTest.php b/src/Symfony/Component/Messenger/Tests/WorkerTest.php index 7599d8dadc221..bdfa6fe188d62 100644 --- a/src/Symfony/Component/Messenger/Tests/WorkerTest.php +++ b/src/Symfony/Component/Messenger/Tests/WorkerTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Messenger\Asynchronous\Transport\ReceivedMessage; +use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Messenger\Tests\Fixtures\CallbackReceiver; use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage; @@ -22,29 +23,33 @@ class WorkerTest extends TestCase { public function testWorkerDispatchTheReceivedMessage() { - $receiver = new CallbackReceiver(function ($handler) { - $handler(new DummyMessage('API')); - $handler(new DummyMessage('IPA')); + $apiMessage = new DummyMessage('API'); + $ipaMessage = new DummyMessage('IPA'); + + $receiver = new CallbackReceiver(function ($handler) use ($apiMessage, $ipaMessage) { + $handler(Envelope::wrap($apiMessage)); + $handler(Envelope::wrap($ipaMessage)); }); $bus = $this->getMockBuilder(MessageBusInterface::class)->getMock(); - $bus->expects($this->at(0))->method('dispatch')->with(new ReceivedMessage(new DummyMessage('API'))); - $bus->expects($this->at(1))->method('dispatch')->with(new ReceivedMessage(new DummyMessage('IPA'))); + $bus->expects($this->at(0))->method('dispatch')->with(Envelope::wrap($apiMessage)->with(new ReceivedMessage())); + $bus->expects($this->at(1))->method('dispatch')->with(Envelope::wrap($ipaMessage)->with(new ReceivedMessage())); $worker = new Worker($receiver, $bus); $worker->run(); } - public function testWorkerDoesNotWrapMessagesAlreadyWrappedInReceivedMessages() + public function testWorkerDoesNotWrapMessagesAlreadyWrappedWithReceivedMessage() { - $receiver = new CallbackReceiver(function ($handler) { - $handler(new ReceivedMessage(new DummyMessage('API'))); + $envelop = Envelope::wrap(new DummyMessage('API'))->with(new ReceivedMessage()); + $receiver = new CallbackReceiver(function ($handler) use ($envelop) { + $handler($envelop); }); $bus = $this->getMockBuilder(MessageBusInterface::class)->getMock(); - $bus->expects($this->at(0))->method('dispatch')->with(new ReceivedMessage(new DummyMessage('API'))); + $bus->expects($this->at(0))->method('dispatch')->with($envelop); $worker = new Worker($receiver, $bus); $worker->run(); @@ -54,7 +59,7 @@ public function testWorkerIsThrowingExceptionsBackToGenerators() { $receiver = new CallbackReceiver(function ($handler) { try { - $handler(new DummyMessage('Hello')); + $handler(Envelope::wrap(new DummyMessage('Hello'))); $this->assertTrue(false, 'This should not be called because the exception is sent back to the generator.'); } catch (\InvalidArgumentException $e) { diff --git a/src/Symfony/Component/Messenger/TraceableMessageBus.php b/src/Symfony/Component/Messenger/TraceableMessageBus.php index ecf0c5658cce2..b60d220b15ce1 100644 --- a/src/Symfony/Component/Messenger/TraceableMessageBus.php +++ b/src/Symfony/Component/Messenger/TraceableMessageBus.php @@ -29,19 +29,27 @@ public function __construct(MessageBusInterface $decoratedBus) */ public function dispatch($message) { + $callTime = microtime(true); + $messageToTrace = $message instanceof Envelope ? $message->getMessage() : $message; + $envelopeItems = $message instanceof Envelope ? array_values($message->all()) : null; + try { $result = $this->decoratedBus->dispatch($message); $this->dispatchedMessages[] = array( - 'message' => $message, + 'envelopeItems' => $envelopeItems, + 'message' => $messageToTrace, 'result' => $result, + 'callTime' => $callTime, ); return $result; } catch (\Throwable $e) { $this->dispatchedMessages[] = array( - 'message' => $message, + 'envelopeItems' => $envelopeItems, + 'message' => $messageToTrace, 'exception' => $e, + 'callTime' => $callTime, ); throw $e; diff --git a/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpReceiver.php b/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpReceiver.php index e61ef0389531e..0e6fbff8ee340 100644 --- a/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpReceiver.php +++ b/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpReceiver.php @@ -22,13 +22,13 @@ */ class AmqpReceiver implements ReceiverInterface { - private $messageDecoder; + private $decoder; private $connection; private $shouldStop; - public function __construct(DecoderInterface $messageDecoder, Connection $connection) + public function __construct(DecoderInterface $decoder, Connection $connection) { - $this->messageDecoder = $messageDecoder; + $this->decoder = $decoder; $this->connection = $connection; } @@ -38,8 +38,8 @@ public function __construct(DecoderInterface $messageDecoder, Connection $connec public function receive(callable $handler): void { while (!$this->shouldStop) { - $message = $this->connection->get(); - if (null === $message) { + $AMQPEnvelope = $this->connection->get(); + if (null === $AMQPEnvelope) { $handler(null); usleep($this->connection->getConnectionCredentials()['loop_sleep'] ?? 200000); @@ -51,18 +51,18 @@ public function receive(callable $handler): void } try { - $handler($this->messageDecoder->decode(array( - 'body' => $message->getBody(), - 'headers' => $message->getHeaders(), + $handler($this->decoder->decode(array( + 'body' => $AMQPEnvelope->getBody(), + 'headers' => $AMQPEnvelope->getHeaders(), ))); - $this->connection->ack($message); + $this->connection->ack($AMQPEnvelope); } catch (RejectMessageExceptionInterface $e) { - $this->connection->reject($message); + $this->connection->reject($AMQPEnvelope); throw $e; } catch (\Throwable $e) { - $this->connection->nack($message, AMQP_REQUEUE); + $this->connection->nack($AMQPEnvelope, AMQP_REQUEUE); throw $e; } finally { diff --git a/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpSender.php b/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpSender.php index 0c4bb18f31ca7..397c52dec2a20 100644 --- a/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpSender.php +++ b/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpSender.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Messenger\Transport\AmqpExt; +use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\Transport\SenderInterface; use Symfony\Component\Messenger\Transport\Serialization\EncoderInterface; @@ -21,21 +22,21 @@ */ class AmqpSender implements SenderInterface { - private $messageEncoder; + private $encoder; private $connection; - public function __construct(EncoderInterface $messageEncoder, Connection $connection) + public function __construct(EncoderInterface $encoder, Connection $connection) { - $this->messageEncoder = $messageEncoder; + $this->encoder = $encoder; $this->connection = $connection; } /** * {@inheritdoc} */ - public function send($message) + public function send(Envelope $envelope) { - $encodedMessage = $this->messageEncoder->encode($message); + $encodedMessage = $this->encoder->encode($envelope); $this->connection->publish($encodedMessage['body'], $encodedMessage['headers']); } diff --git a/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpTransport.php b/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpTransport.php new file mode 100644 index 0000000000000..3edefd0ab1c8a --- /dev/null +++ b/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpTransport.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport\AmqpExt; + +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Transport\Serialization\DecoderInterface; +use Symfony\Component\Messenger\Transport\Serialization\EncoderInterface; +use Symfony\Component\Messenger\Transport\TransportInterface; + +/** + * @author Nicolas Grekas + */ +class AmqpTransport implements TransportInterface +{ + private $encoder; + private $decoder; + private $connection; + private $receiver; + private $sender; + + public function __construct(EncoderInterface $encoder, DecoderInterface $decoder, Connection $connection) + { + $this->encoder = $encoder; + $this->decoder = $decoder; + $this->connection = $connection; + } + + /** + * {@inheritdoc} + */ + public function receive(callable $handler): void + { + ($this->receiver ?? $this->getReceiver())->receive($handler); + } + + /** + * {@inheritdoc} + */ + public function stop(): void + { + ($this->receiver ?? $this->getReceiver())->stop(); + } + + /** + * {@inheritdoc} + */ + public function send(Envelope $envelope): void + { + ($this->sender ?? $this->getSender())->send($envelope); + } + + private function getReceiver() + { + return $this->receiver = new AmqpReceiver($this->decoder, $this->connection); + } + + private function getSender() + { + return $this->sender = new AmqpSender($this->encoder, $this->connection); + } +} diff --git a/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpTransportFactory.php b/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpTransportFactory.php index ddb385af52aef..29fb4ae4aa3e0 100644 --- a/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpTransportFactory.php +++ b/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpTransportFactory.php @@ -11,11 +11,10 @@ namespace Symfony\Component\Messenger\Transport\AmqpExt; -use Symfony\Component\Messenger\Transport\Factory\TransportFactoryInterface; -use Symfony\Component\Messenger\Transport\ReceiverInterface; -use Symfony\Component\Messenger\Transport\SenderInterface; use Symfony\Component\Messenger\Transport\Serialization\DecoderInterface; use Symfony\Component\Messenger\Transport\Serialization\EncoderInterface; +use Symfony\Component\Messenger\Transport\TransportFactoryInterface; +use Symfony\Component\Messenger\Transport\TransportInterface; /** * @author Samuel Roze @@ -33,14 +32,9 @@ public function __construct(EncoderInterface $encoder, DecoderInterface $decoder $this->debug = $debug; } - public function createReceiver(string $dsn, array $options): ReceiverInterface + public function createTransport(string $dsn, array $options): TransportInterface { - return new AmqpReceiver($this->decoder, Connection::fromDsn($dsn, $options, $this->debug)); - } - - public function createSender(string $dsn, array $options): SenderInterface - { - return new AmqpSender($this->encoder, Connection::fromDsn($dsn, $options, $this->debug)); + return new AmqpTransport($this->encoder, $this->decoder, Connection::fromDsn($dsn, $options, $this->debug)); } public function supports(string $dsn, array $options): bool diff --git a/src/Symfony/Component/Messenger/Transport/ChainSender.php b/src/Symfony/Component/Messenger/Transport/ChainSender.php new file mode 100644 index 0000000000000..97e3ce44bbaf1 --- /dev/null +++ b/src/Symfony/Component/Messenger/Transport/ChainSender.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport; + +/** + * @author Tobias Schultze + */ +class ChainSender implements SenderInterface +{ + private $senders; + + /** + * @param SenderInterface[] $senders + */ + public function __construct(iterable $senders) + { + $this->senders = $senders; + } + + /** + * {@inheritdoc} + */ + public function send($message): void + { + foreach ($this->senders as $sender) { + $sender->send($message); + } + } +} diff --git a/src/Symfony/Component/Messenger/Transport/Enhancers/StopWhenMemoryUsageIsExceededReceiver.php b/src/Symfony/Component/Messenger/Transport/Enhancers/StopWhenMemoryUsageIsExceededReceiver.php index 4a05afe7707fb..37b1978233e9f 100644 --- a/src/Symfony/Component/Messenger/Transport/Enhancers/StopWhenMemoryUsageIsExceededReceiver.php +++ b/src/Symfony/Component/Messenger/Transport/Enhancers/StopWhenMemoryUsageIsExceededReceiver.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Messenger\Transport\Enhancers; use Psr\Log\LoggerInterface; +use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\Transport\ReceiverInterface; /** @@ -36,8 +37,8 @@ public function __construct(ReceiverInterface $decoratedReceiver, int $memoryLim public function receive(callable $handler): void { - $this->decoratedReceiver->receive(function ($message) use ($handler) { - $handler($message); + $this->decoratedReceiver->receive(function (?Envelope $envelope) use ($handler) { + $handler($envelope); $memoryResolver = $this->memoryResolver; if ($memoryResolver() > $this->memoryLimit) { diff --git a/src/Symfony/Component/Messenger/Transport/Enhancers/StopWhenMessageCountIsExceededReceiver.php b/src/Symfony/Component/Messenger/Transport/Enhancers/StopWhenMessageCountIsExceededReceiver.php index dc61466bbf401..420eb8fa63a40 100644 --- a/src/Symfony/Component/Messenger/Transport/Enhancers/StopWhenMessageCountIsExceededReceiver.php +++ b/src/Symfony/Component/Messenger/Transport/Enhancers/StopWhenMessageCountIsExceededReceiver.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Messenger\Transport\Enhancers; use Psr\Log\LoggerInterface; +use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\Transport\ReceiverInterface; /** @@ -34,10 +35,10 @@ public function receive(callable $handler): void { $receivedMessages = 0; - $this->decoratedReceiver->receive(function ($message) use ($handler, &$receivedMessages) { - $handler($message); + $this->decoratedReceiver->receive(function (?Envelope $envelope) use ($handler, &$receivedMessages) { + $handler($envelope); - if (null !== $message && ++$receivedMessages >= $this->maximumNumberOfMessages) { + if (null !== $envelope && ++$receivedMessages >= $this->maximumNumberOfMessages) { $this->stop(); if (null !== $this->logger) { $this->logger->info('Receiver stopped due to maximum count of {count} exceeded', array('count' => $this->maximumNumberOfMessages)); diff --git a/src/Symfony/Component/Messenger/Transport/ReceiverInterface.php b/src/Symfony/Component/Messenger/Transport/ReceiverInterface.php index 1c29fbe43abe7..0f61fe3428d7e 100644 --- a/src/Symfony/Component/Messenger/Transport/ReceiverInterface.php +++ b/src/Symfony/Component/Messenger/Transport/ReceiverInterface.php @@ -21,10 +21,10 @@ interface ReceiverInterface /** * Receive some messages to the given handler. * - * The handler will have, as argument, the received message. Note that this message - * can be `null` if the timeout to receive something has expired. + * The handler will have, as argument, the received {@link \Symfony\Component\Messenger\Envelope} containing the message. + * Note that this envelope can be `null` if the timeout to receive something has expired. */ - public function receive(callable $handler) : void; + public function receive(callable $handler): void; /** * Stop receiving some messages. diff --git a/src/Symfony/Component/Messenger/Transport/SenderInterface.php b/src/Symfony/Component/Messenger/Transport/SenderInterface.php index a142e1f00995e..ba36b80fe2af2 100644 --- a/src/Symfony/Component/Messenger/Transport/SenderInterface.php +++ b/src/Symfony/Component/Messenger/Transport/SenderInterface.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Messenger\Transport; +use Symfony\Component\Messenger\Envelope; + /** * @author Samuel Roze * @@ -19,9 +21,9 @@ interface SenderInterface { /** - * Sends the given message. + * Sends the given envelope. * - * @param object $message + * @param Envelope $envelope */ - public function send($message); + public function send(Envelope $envelope); } diff --git a/src/Symfony/Component/Messenger/Transport/Serialization/DecoderInterface.php b/src/Symfony/Component/Messenger/Transport/Serialization/DecoderInterface.php index a232dfbf10e30..026a32103316b 100644 --- a/src/Symfony/Component/Messenger/Transport/Serialization/DecoderInterface.php +++ b/src/Symfony/Component/Messenger/Transport/Serialization/DecoderInterface.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Messenger\Transport\Serialization; +use Symfony\Component\Messenger\Envelope; + /** * @author Samuel Roze * @@ -19,16 +21,16 @@ interface DecoderInterface { /** - * Decodes the message from an encoded-form. + * Decodes an envelope and its message from an encoded-form. * - * The `$encodedMessage` parameter is a key-value array that - * describes the message, that will be used by the different transports. + * The `$encodedEnvelope` parameter is a key-value array that + * describes the envelope and its content, that will be used by the different transports. * * The most common keys are: * - `body` (string) - the message body * - `headers` (string) - a key/value pair of headers * - * @return object + * @return Envelope */ - public function decode(array $encodedMessage); + public function decode(array $encodedEnvelope): Envelope; } diff --git a/src/Symfony/Component/Messenger/Transport/Serialization/EncoderInterface.php b/src/Symfony/Component/Messenger/Transport/Serialization/EncoderInterface.php index 658b9e5b5e681..b5ea2acd937d3 100644 --- a/src/Symfony/Component/Messenger/Transport/Serialization/EncoderInterface.php +++ b/src/Symfony/Component/Messenger/Transport/Serialization/EncoderInterface.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Messenger\Transport\Serialization; +use Symfony\Component\Messenger\Envelope; + /** * @author Samuel Roze * @@ -19,14 +21,14 @@ interface EncoderInterface { /** - * Encodes a message to a common format understandable by transports. The encoded array should only - * contain scalar and arrays. + * Encodes an envelope content (message & items) to a common format understandable by transports. + * The encoded array should only contain scalar and arrays. * * The most common keys of the encoded array are: * - `body` (string) - the message body * - `headers` (string) - a key/value pair of headers * - * @param object $message The object that is put on the MessageBus by the user + * @param Envelope $envelope The envelop containing the message put on the MessageBus by the user */ - public function encode($message): array; + public function encode(Envelope $envelope): array; } diff --git a/src/Symfony/Component/Messenger/Transport/Serialization/Serializer.php b/src/Symfony/Component/Messenger/Transport/Serialization/Serializer.php index 65c2ac55e8886..cfb30090e5dfa 100644 --- a/src/Symfony/Component/Messenger/Transport/Serialization/Serializer.php +++ b/src/Symfony/Component/Messenger/Transport/Serialization/Serializer.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Messenger\Transport\Serialization; +use Symfony\Component\Messenger\Envelope; use Symfony\Component\Serializer\SerializerInterface; /** @@ -32,27 +33,48 @@ public function __construct(SerializerInterface $serializer, string $format = 'j /** * {@inheritdoc} */ - public function decode(array $encodedMessage) + public function decode(array $encodedEnvelope): Envelope { - if (empty($encodedMessage['body']) || empty($encodedMessage['headers'])) { - throw new \InvalidArgumentException('Encoded message should have at least a `body` and some `headers`.'); + if (empty($encodedEnvelope['body']) || empty($encodedEnvelope['headers'])) { + throw new \InvalidArgumentException('Encoded envelope should have at least a `body` and some `headers`.'); } - if (empty($encodedMessage['headers']['type'])) { - throw new \InvalidArgumentException('Encoded message does not have a `type` header.'); + if (empty($encodedEnvelope['headers']['type'])) { + throw new \InvalidArgumentException('Encoded envelope does not have a `type` header.'); } - return $this->serializer->deserialize($encodedMessage['body'], $encodedMessage['headers']['type'], $this->format, $this->context); + $envelopeItems = isset($encodedEnvelope['headers']['X-Message-Envelope-Items']) ? unserialize($encodedEnvelope['headers']['X-Message-Envelope-Items']) : array(); + + $context = $this->context; + /** @var SerializerConfiguration|null $serializerConfig */ + if ($serializerConfig = $envelopeItems[SerializerConfiguration::class] ?? null) { + $context = $serializerConfig->getContext() + $context; + } + + $message = $this->serializer->deserialize($encodedEnvelope['body'], $encodedEnvelope['headers']['type'], $this->format, $context); + + return new Envelope($message, $envelopeItems); } /** * {@inheritdoc} */ - public function encode($message): array + public function encode(Envelope $envelope): array { + $context = $this->context; + /** @var SerializerConfiguration|null $serializerConfig */ + if ($serializerConfig = $envelope->get(SerializerConfiguration::class)) { + $context = $serializerConfig->getContext() + $context; + } + + $headers = array('type' => \get_class($envelope->getMessage())); + if ($configurations = $envelope->all()) { + $headers['X-Message-Envelope-Items'] = serialize($configurations); + } + return array( - 'body' => $this->serializer->serialize($message, $this->format, $this->context), - 'headers' => array('type' => \get_class($message)), + 'body' => $this->serializer->serialize($envelope->getMessage(), $this->format, $context), + 'headers' => $headers, ); } } diff --git a/src/Symfony/Component/Messenger/Transport/Serialization/SerializerConfiguration.php b/src/Symfony/Component/Messenger/Transport/Serialization/SerializerConfiguration.php new file mode 100644 index 0000000000000..478e197080adf --- /dev/null +++ b/src/Symfony/Component/Messenger/Transport/Serialization/SerializerConfiguration.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport\Serialization; + +use Symfony\Component\Messenger\EnvelopeItemInterface; + +/** + * @author Maxime Steinhausser + * + * @experimental in 4.1 + */ +final class SerializerConfiguration implements EnvelopeItemInterface +{ + private $context; + + public function __construct(array $context) + { + $this->context = $context; + } + + public function getContext(): array + { + return $this->context; + } + + public function serialize() + { + return serialize(array('context' => $this->context)); + } + + public function unserialize($serialized) + { + list('context' => $context) = unserialize($serialized, array('allowed_classes' => false)); + + $this->__construct($context); + } +} diff --git a/src/Symfony/Component/Messenger/Transport/Factory/ChainTransportFactory.php b/src/Symfony/Component/Messenger/Transport/TransportFactory.php similarity index 56% rename from src/Symfony/Component/Messenger/Transport/Factory/ChainTransportFactory.php rename to src/Symfony/Component/Messenger/Transport/TransportFactory.php index 779d365dc47e2..2c20533bfb8a9 100644 --- a/src/Symfony/Component/Messenger/Transport/Factory/ChainTransportFactory.php +++ b/src/Symfony/Component/Messenger/Transport/TransportFactory.php @@ -9,15 +9,12 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Messenger\Transport\Factory; - -use Symfony\Component\Messenger\Transport\ReceiverInterface; -use Symfony\Component\Messenger\Transport\SenderInterface; +namespace Symfony\Component\Messenger\Transport; /** * @author Samuel Roze */ -class ChainTransportFactory implements TransportFactoryInterface +class TransportFactory implements TransportFactoryInterface { private $factories; @@ -29,22 +26,11 @@ public function __construct(iterable $factories) $this->factories = $factories; } - public function createReceiver(string $dsn, array $options): ReceiverInterface - { - foreach ($this->factories as $factory) { - if ($factory->supports($dsn, $options)) { - return $factory->createReceiver($dsn, $options); - } - } - - throw new \InvalidArgumentException(sprintf('No transport supports the given DSN "%s".', $dsn)); - } - - public function createSender(string $dsn, array $options): SenderInterface + public function createTransport(string $dsn, array $options): TransportInterface { foreach ($this->factories as $factory) { if ($factory->supports($dsn, $options)) { - return $factory->createSender($dsn, $options); + return $factory->createTransport($dsn, $options); } } diff --git a/src/Symfony/Component/Messenger/Transport/Factory/TransportFactoryInterface.php b/src/Symfony/Component/Messenger/Transport/TransportFactoryInterface.php similarity index 57% rename from src/Symfony/Component/Messenger/Transport/Factory/TransportFactoryInterface.php rename to src/Symfony/Component/Messenger/Transport/TransportFactoryInterface.php index 47ded446bf06c..ac1c72f1b9ad3 100644 --- a/src/Symfony/Component/Messenger/Transport/Factory/TransportFactoryInterface.php +++ b/src/Symfony/Component/Messenger/Transport/TransportFactoryInterface.php @@ -9,10 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Messenger\Transport\Factory; - -use Symfony\Component\Messenger\Transport\ReceiverInterface; -use Symfony\Component\Messenger\Transport\SenderInterface; +namespace Symfony\Component\Messenger\Transport; /** * Creates a Messenger transport. @@ -23,9 +20,7 @@ */ interface TransportFactoryInterface { - public function createReceiver(string $dsn, array $options): ReceiverInterface; - - public function createSender(string $dsn, array $options): SenderInterface; + public function createTransport(string $dsn, array $options): TransportInterface; public function supports(string $dsn, array $options): bool; } diff --git a/src/Symfony/Component/Messenger/Transport/TransportInterface.php b/src/Symfony/Component/Messenger/Transport/TransportInterface.php new file mode 100644 index 0000000000000..066cf5cda7e4b --- /dev/null +++ b/src/Symfony/Component/Messenger/Transport/TransportInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport; + +/** + * @author Nicolas Grekas + * + * @experimental in 4.1 + */ +interface TransportInterface extends ReceiverInterface, SenderInterface +{ +} diff --git a/src/Symfony/Component/Messenger/Worker.php b/src/Symfony/Component/Messenger/Worker.php index 2033f3a770fd1..918b61a52a18c 100644 --- a/src/Symfony/Component/Messenger/Worker.php +++ b/src/Symfony/Component/Messenger/Worker.php @@ -41,16 +41,12 @@ public function run() }); } - $this->receiver->receive(function ($message) { - if (null === $message) { + $this->receiver->receive(function (?Envelope $envelope) { + if (null === $envelope) { return; } - if (!$message instanceof ReceivedMessage) { - $message = new ReceivedMessage($message); - } - - $this->bus->dispatch($message); + $this->bus->dispatch($envelope->with(new ReceivedMessage())); }); } } diff --git a/src/Symfony/Component/Messenger/composer.json b/src/Symfony/Component/Messenger/composer.json index 1767f4dab967e..adbd0e2a9f671 100644 --- a/src/Symfony/Component/Messenger/composer.json +++ b/src/Symfony/Component/Messenger/composer.json @@ -20,6 +20,7 @@ }, "require-dev": { "psr/log": "~1.0", + "symfony/console": "~3.4|~4.0", "symfony/dependency-injection": "~3.4.6|~4.0", "symfony/http-kernel": "~3.4|~4.0", "symfony/process": "~3.4|~4.0", diff --git a/src/Symfony/Component/OptionsResolver/OptionsResolver.php b/src/Symfony/Component/OptionsResolver/OptionsResolver.php index 8edd6b5c89791..68b4154b10da2 100644 --- a/src/Symfony/Component/OptionsResolver/OptionsResolver.php +++ b/src/Symfony/Component/OptionsResolver/OptionsResolver.php @@ -975,7 +975,7 @@ public function count() * parameters should usually not be included in messages aimed at * non-technical people. * - * @param mixed $value The value to return the type of + * @param mixed $value The value to return the type of */ private function formatTypeOf($value, ?string $type): string { diff --git a/src/Symfony/Component/Process/ExecutableFinder.php b/src/Symfony/Component/Process/ExecutableFinder.php index d042a5b13a211..1ec6526d45efd 100644 --- a/src/Symfony/Component/Process/ExecutableFinder.php +++ b/src/Symfony/Component/Process/ExecutableFinder.php @@ -77,7 +77,7 @@ public function find($name, $default = null, array $extraDirs = array()) } foreach ($suffixes as $suffix) { foreach ($dirs as $dir) { - if (@is_file($file = $dir.DIRECTORY_SEPARATOR.$name.$suffix) && ('\\' === DIRECTORY_SEPARATOR || is_executable($file))) { + if (@is_file($file = $dir.DIRECTORY_SEPARATOR.$name.$suffix) && ('\\' === DIRECTORY_SEPARATOR || @is_executable($file))) { return $file; } } diff --git a/src/Symfony/Component/Process/PhpExecutableFinder.php b/src/Symfony/Component/Process/PhpExecutableFinder.php index f5f301a7ea019..abc41b6630567 100644 --- a/src/Symfony/Component/Process/PhpExecutableFinder.php +++ b/src/Symfony/Component/Process/PhpExecutableFinder.php @@ -52,7 +52,7 @@ public function find($includeArgs = true) } if ($php = getenv('PHP_PATH')) { - if (!is_executable($php)) { + if (!@is_executable($php)) { return false; } @@ -60,12 +60,12 @@ public function find($includeArgs = true) } if ($php = getenv('PHP_PEAR_PHP_BIN')) { - if (is_executable($php)) { + if (@is_executable($php)) { return $php; } } - if (is_executable($php = PHP_BINDIR.('\\' === DIRECTORY_SEPARATOR ? '\\php.exe' : '/php'))) { + if (@is_executable($php = PHP_BINDIR.('\\' === DIRECTORY_SEPARATOR ? '\\php.exe' : '/php'))) { return $php; } diff --git a/src/Symfony/Component/Process/Pipes/AbstractPipes.php b/src/Symfony/Component/Process/Pipes/AbstractPipes.php index 2bd1fe75b7ff9..9a7d88be896f4 100644 --- a/src/Symfony/Component/Process/Pipes/AbstractPipes.php +++ b/src/Symfony/Component/Process/Pipes/AbstractPipes.php @@ -25,6 +25,7 @@ abstract class AbstractPipes implements PipesInterface private $inputBuffer = ''; private $input; private $blocked = true; + private $lastError; /** * @param resource|string|int|float|bool|\Iterator|null $input @@ -58,10 +59,11 @@ public function close() */ protected function hasSystemCallBeenInterrupted() { - $lastError = error_get_last(); + $lastError = $this->lastError; + $this->lastError = null; // stream_select returns false when the `select` system call is interrupted by an incoming signal - return isset($lastError['message']) && false !== stripos($lastError['message'], 'interrupted system call'); + return null !== $lastError && false !== stripos($lastError, 'interrupted system call'); } /** @@ -165,4 +167,12 @@ protected function write() return array($this->pipes[0]); } } + + /** + * @internal + */ + public function handleError($type, $msg) + { + $this->lastError = $msg; + } } diff --git a/src/Symfony/Component/Process/Pipes/UnixPipes.php b/src/Symfony/Component/Process/Pipes/UnixPipes.php index badaf34d42012..23f873471d874 100644 --- a/src/Symfony/Component/Process/Pipes/UnixPipes.php +++ b/src/Symfony/Component/Process/Pipes/UnixPipes.php @@ -99,7 +99,9 @@ public function readAndWrite($blocking, $close = false) unset($r[0]); // let's have a look if something changed in streams - if (($r || $w) && false === @stream_select($r, $w, $e, 0, $blocking ? Process::TIMEOUT_PRECISION * 1E6 : 0)) { + set_error_handler(array($this, 'handleError')); + if (($r || $w) && false === stream_select($r, $w, $e, 0, $blocking ? Process::TIMEOUT_PRECISION * 1E6 : 0)) { + restore_error_handler(); // if a system call has been interrupted, forget about it, let's try again // otherwise, an error occurred, let's reset pipes if (!$this->hasSystemCallBeenInterrupted()) { @@ -108,6 +110,7 @@ public function readAndWrite($blocking, $close = false) return $read; } + restore_error_handler(); foreach ($r as $pipe) { // prior PHP 5.4 the array passed to stream_select is modified and diff --git a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php index 89c1d4c361483..ff6f7cdce1d13 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php @@ -162,7 +162,7 @@ private function extractFromMutator(string $class, string $property): ?array if (!$reflectionType = $reflectionParameter->getType()) { return null; } - $type = $this->extractFromReflectionType($reflectionType); + $type = $this->extractFromReflectionType($reflectionType, $reflectionMethod); if (\in_array($prefix, $this->arrayMutatorPrefixes)) { $type = new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), $type); @@ -184,7 +184,7 @@ private function extractFromAccessor(string $class, string $property): ?array } if ($reflectionType = $reflectionMethod->getReturnType()) { - return array($this->extractFromReflectionType($reflectionType)); + return array($this->extractFromReflectionType($reflectionType, $reflectionMethod)); } if (\in_array($prefix, array('is', 'can', 'has'))) { @@ -217,8 +217,9 @@ private function extractFromConstructor(string $class, string $property): ?array if ($property !== $parameter->name) { continue; } + $reflectionType = $parameter->getType(); - return array($this->extractFromReflectionType($parameter->getType())); + return $reflectionType ? array($this->extractFromReflectionType($reflectionType, $constructor)) : null; } if ($parentClass = $reflectionClass->getParentClass()) { @@ -228,7 +229,7 @@ private function extractFromConstructor(string $class, string $property): ?array return null; } - private function extractFromReflectionType(\ReflectionType $reflectionType): Type + private function extractFromReflectionType(\ReflectionType $reflectionType, \ReflectionMethod $reflectionMethod): Type { $phpTypeOrClass = $reflectionType->getName(); $nullable = $reflectionType->allowsNull(); @@ -240,12 +241,24 @@ private function extractFromReflectionType(\ReflectionType $reflectionType): Typ } elseif ($reflectionType->isBuiltin()) { $type = new Type($phpTypeOrClass, $nullable); } else { - $type = new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, $phpTypeOrClass); + $type = new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, $this->resolveTypeName($phpTypeOrClass, $reflectionMethod)); } return $type; } + private function resolveTypeName(string $name, \ReflectionMethod $reflectionMethod): string + { + if ('self' === $lcName = strtolower($name)) { + return $reflectionMethod->getDeclaringClass()->name; + } + if ('parent' === $lcName && $parent = $reflectionMethod->getDeclaringClass()->getParentClass()) { + return $parent->name; + } + + return $name; + } + private function isPublicProperty(string $class, string $property): bool { try { diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractors/PhpDocExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php similarity index 99% rename from src/Symfony/Component/PropertyInfo/Tests/Extractors/PhpDocExtractorTest.php rename to src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php index 38a9ff9e632d3..af22aa9c918b7 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractors/PhpDocExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\PropertyInfo\Tests\PhpDocExtractors; +namespace Symfony\Component\PropertyInfo\Tests\PhpDocExtractor; use PHPUnit\Framework\TestCase; use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractors/ReflectionExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php similarity index 94% rename from src/Symfony/Component/PropertyInfo/Tests/Extractors/ReflectionExtractorTest.php rename to src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php index 09a2d3580c740..f5b47196ee5f4 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractors/ReflectionExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php @@ -52,6 +52,8 @@ public function testGetProperties() 'DOB', 'Id', '123', + 'self', + 'realParent', 'c', 'd', 'e', @@ -154,6 +156,8 @@ public function typesProvider() array('donotexist', null), array('staticGetter', null), array('staticSetter', null), + array('self', array(new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy'))), + array('realParent', array(new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy'))), ); } @@ -171,6 +175,8 @@ public function php7TypesProvider() array('foo', array(new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true))), array('bar', array(new Type(Type::BUILTIN_TYPE_INT))), array('baz', array(new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING)))), + array('buz', array(new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Symfony\Component\PropertyInfo\Tests\Fixtures\Php7Dummy'))), + array('biz', array(new Type(Type::BUILTIN_TYPE_OBJECT, false, 'stdClass'))), array('donotexist', null), ); } diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractors/SerializerExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/SerializerExtractorTest.php similarity index 95% rename from src/Symfony/Component/PropertyInfo/Tests/Extractors/SerializerExtractorTest.php rename to src/Symfony/Component/PropertyInfo/Tests/Extractor/SerializerExtractorTest.php index b1be5ee11821e..91cdf80f8d608 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractors/SerializerExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/SerializerExtractorTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\PropertyInfo\Tests\Extractors; +namespace Symfony\Component\PropertyInfo\Tests\Extractor; use Doctrine\Common\Annotations\AnnotationReader; use PHPUnit\Framework\TestCase; diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php index 0916217020835..afe09ef929dfe 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php @@ -127,4 +127,18 @@ public function getId() public function get123() { } + + /** + * @param self $self + */ + public function setSelf(self $self) + { + } + + /** + * @param parent $realParent + */ + public function setRealParent(parent $realParent) + { + } } diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/ParentDummy.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/ParentDummy.php index dc9109d7d1722..b3f6d779077d3 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/ParentDummy.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/ParentDummy.php @@ -29,7 +29,7 @@ class ParentDummy public $foo2; /** - * @var callback + * @var callable */ public $foo3; diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php7Dummy.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php7Dummy.php index cd5ba380d9d53..5dcb4c565e768 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php7Dummy.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php7Dummy.php @@ -14,7 +14,7 @@ /** * @author Kévin Dunglas */ -class Php7Dummy +class Php7Dummy extends \stdClass { public function getFoo(): array { @@ -27,4 +27,12 @@ public function setBar(int $bar) public function addBaz(string $baz) { } + + public function getBuz(): self + { + } + + public function getBiz(): parent + { + } } diff --git a/src/Symfony/Component/PropertyInfo/Type.php b/src/Symfony/Component/PropertyInfo/Type.php index 71aa162f70b79..461495ed510ac 100644 --- a/src/Symfony/Component/PropertyInfo/Type.php +++ b/src/Symfony/Component/PropertyInfo/Type.php @@ -59,7 +59,7 @@ class Type /** * @throws \InvalidArgumentException */ - public function __construct(string $builtinType, bool $nullable = false, string $class = null, bool $collection = false, Type $collectionKeyType = null, Type $collectionValueType = null) + public function __construct(string $builtinType, bool $nullable = false, string $class = null, bool $collection = false, self $collectionKeyType = null, self $collectionValueType = null) { if (!in_array($builtinType, self::$builtinTypes)) { throw new \InvalidArgumentException(sprintf('"%s" is not a valid PHP type.', $builtinType)); diff --git a/src/Symfony/Component/Routing/Loader/AnnotationClassLoader.php b/src/Symfony/Component/Routing/Loader/AnnotationClassLoader.php index 36a41c039b9d0..c909da17bbdbe 100644 --- a/src/Symfony/Component/Routing/Loader/AnnotationClassLoader.php +++ b/src/Symfony/Component/Routing/Loader/AnnotationClassLoader.php @@ -13,7 +13,6 @@ use Doctrine\Common\Annotations\Reader; use Symfony\Component\Config\Resource\FileResource; -use Symfony\Component\Routing\Annotation\Route as RouteAnnotation; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; use Symfony\Component\Config\Loader\LoaderInterface; diff --git a/src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php b/src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php index bbdb8af0076ea..0a79379160256 100644 --- a/src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php +++ b/src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php @@ -473,7 +473,7 @@ private function compileStaticPrefixCollection(StaticPrefixCollection $tree, \st $code .= $this->indent($this->compileStaticPrefixCollection($route, $state, $prefixLen + strlen($prefix))); $code .= "\n .')'"; $state->regex .= ')'; - $state->markTail += 1; + ++$state->markTail; continue; } diff --git a/src/Symfony/Component/Routing/RouteCollection.php b/src/Symfony/Component/Routing/RouteCollection.php index 84719e2c82fa5..4d14ca96db416 100644 --- a/src/Symfony/Component/Routing/RouteCollection.php +++ b/src/Symfony/Component/Routing/RouteCollection.php @@ -162,6 +162,9 @@ public function addNamePrefix(string $prefix) foreach ($this->routes as $name => $route) { $prefixedRoutes[$prefix.$name] = $route; + if (null !== $name = $route->getDefault('_canonical_route')) { + $route->setDefault('_canonical_route', $prefix.$name); + } } $this->routes = $prefixedRoutes; diff --git a/src/Symfony/Component/Routing/RouteCollectionBuilder.php b/src/Symfony/Component/Routing/RouteCollectionBuilder.php index d63c6138f7983..c98d642a06650 100644 --- a/src/Symfony/Component/Routing/RouteCollectionBuilder.php +++ b/src/Symfony/Component/Routing/RouteCollectionBuilder.php @@ -118,7 +118,7 @@ public function createBuilder() * @param string $prefix * @param RouteCollectionBuilder $builder */ - public function mount($prefix, RouteCollectionBuilder $builder) + public function mount($prefix, self $builder) { $builder->prefix = trim(trim($prefix), '/'); $this->routes[] = $builder; @@ -251,11 +251,9 @@ public function setMethods($methods) /** * Adds a resource for this collection. * - * @param ResourceInterface $resource - * * @return $this */ - private function addResource(ResourceInterface $resource): RouteCollectionBuilder + private function addResource(ResourceInterface $resource): self { $this->resources[] = $resource; diff --git a/src/Symfony/Component/Routing/Tests/Generator/Dumper/PhpGeneratorDumperTest.php b/src/Symfony/Component/Routing/Tests/Generator/Dumper/PhpGeneratorDumperTest.php index dc84e29345b60..dfcac06b40b2f 100644 --- a/src/Symfony/Component/Routing/Tests/Generator/Dumper/PhpGeneratorDumperTest.php +++ b/src/Symfony/Component/Routing/Tests/Generator/Dumper/PhpGeneratorDumperTest.php @@ -89,9 +89,9 @@ public function testDumpWithLocalizedRoutes() $this->routeCollection->add('test.en', (new Route('/testing/is/fun'))->setDefault('_locale', 'en')->setDefault('_canonical_route', 'test')); $this->routeCollection->add('test.nl', (new Route('/testen/is/leuk'))->setDefault('_locale', 'nl')->setDefault('_canonical_route', 'test')); - $code = $this->generatorDumper->dump([ + $code = $this->generatorDumper->dump(array( 'class' => 'LocalizedProjectUrlGenerator', - ]); + )); file_put_contents($this->testTmpFilepath, $code); include $this->testTmpFilepath; @@ -99,7 +99,7 @@ public function testDumpWithLocalizedRoutes() $projectUrlGenerator = new \LocalizedProjectUrlGenerator($context, null, 'en'); $urlWithDefaultLocale = $projectUrlGenerator->generate('test'); - $urlWithSpecifiedLocale = $projectUrlGenerator->generate('test', ['_locale' => 'nl']); + $urlWithSpecifiedLocale = $projectUrlGenerator->generate('test', array('_locale' => 'nl')); $context->setParameter('_locale', 'en'); $urlWithEnglishContext = $projectUrlGenerator->generate('test'); $context->setParameter('_locale', 'nl'); diff --git a/src/Symfony/Component/Routing/Tests/Loader/AnnotationClassLoaderTest.php b/src/Symfony/Component/Routing/Tests/Loader/AnnotationClassLoaderTest.php index ed517e692cf76..dd9af9db23f72 100644 --- a/src/Symfony/Component/Routing/Tests/Loader/AnnotationClassLoaderTest.php +++ b/src/Symfony/Component/Routing/Tests/Loader/AnnotationClassLoaderTest.php @@ -46,7 +46,9 @@ protected function setUp() { $reader = new AnnotationReader(); $this->loader = new class($reader) extends AnnotationClassLoader { - protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, $annot) {} + protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, $annot) + { + } }; AnnotationRegistry::registerLoader('class_exists'); } @@ -194,7 +196,9 @@ public function testInvokableClassMultipleRouteLoad() ->will($this->returnValue(array())) ; $loader = new class($reader) extends AnnotationClassLoader { - protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, $annot) {} + protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, $annot) + { + } }; $routeCollection = $loader->load('Symfony\Component\Routing\Tests\Fixtures\AnnotatedClasses\BazClass'); diff --git a/src/Symfony/Component/Routing/Tests/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/Routing/Tests/Loader/XmlFileLoaderTest.php index c3d59b29be49c..fff4154b17d5e 100644 --- a/src/Symfony/Component/Routing/Tests/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/Routing/Tests/Loader/XmlFileLoaderTest.php @@ -420,7 +420,6 @@ public function testImportRouteWithGlobMatchingMultipleFiles() $route = $routeCollection->get('baz_route'); $this->assertSame('AppBundle:Baz:view', $route->getDefault('_controller')); - } public function testImportRouteWithNamePrefix() diff --git a/src/Symfony/Component/Routing/Tests/RouteCollectionTest.php b/src/Symfony/Component/Routing/Tests/RouteCollectionTest.php index 3527e12895683..0ea23f673460f 100644 --- a/src/Symfony/Component/Routing/Tests/RouteCollectionTest.php +++ b/src/Symfony/Component/Routing/Tests/RouteCollectionTest.php @@ -317,4 +317,17 @@ public function testAddNamePrefix() $this->assertNull($collection->get('foo')); $this->assertNull($collection->get('bar')); } + + public function testAddNamePrefixCanonicalRouteName() + { + $collection = new RouteCollection(); + $collection->add('foo', new Route('/foo', array('_canonical_route' => 'foo'))); + $collection->add('bar', new Route('/bar', array('_canonical_route' => 'bar'))); + $collection->add('api_foo', new Route('/api/foo', array('_canonical_route' => 'api_foo'))); + $collection->addNamePrefix('api_'); + + $this->assertEquals('api_foo', $collection->get('api_foo')->getDefault('_canonical_route')); + $this->assertEquals('api_bar', $collection->get('api_bar')->getDefault('_canonical_route')); + $this->assertEquals('api_api_foo', $collection->get('api_api_foo')->getDefault('_canonical_route')); + } } diff --git a/src/Symfony/Component/Security/Csrf/Tests/TokenStorage/SessionTokenStorageTest.php b/src/Symfony/Component/Security/Csrf/Tests/TokenStorage/SessionTokenStorageTest.php index c629ca15255ff..306e19ad91bb9 100644 --- a/src/Symfony/Component/Security/Csrf/Tests/TokenStorage/SessionTokenStorageTest.php +++ b/src/Symfony/Component/Security/Csrf/Tests/TokenStorage/SessionTokenStorageTest.php @@ -12,6 +12,8 @@ namespace Symfony\Component\Security\Csrf\Tests\TokenStorage; use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Session\Session; +use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; use Symfony\Component\Security\Csrf\TokenStorage\SessionTokenStorage; /** @@ -22,7 +24,7 @@ class SessionTokenStorageTest extends TestCase const SESSION_NAMESPACE = 'foobar'; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var Session */ private $session; @@ -33,118 +35,53 @@ class SessionTokenStorageTest extends TestCase protected function setUp() { - $this->session = $this->getMockBuilder('Symfony\Component\HttpFoundation\Session\SessionInterface') - ->disableOriginalConstructor() - ->getMock(); + $this->session = new Session(new MockArraySessionStorage()); $this->storage = new SessionTokenStorage($this->session, self::SESSION_NAMESPACE); } - public function testStoreTokenInClosedSession() + public function testStoreTokenInNotStartedSessionStartsTheSession() { - $this->session->expects($this->any()) - ->method('isStarted') - ->will($this->returnValue(false)); - - $this->session->expects($this->once()) - ->method('start'); - - $this->session->expects($this->once()) - ->method('set') - ->with(self::SESSION_NAMESPACE.'/token_id', 'TOKEN'); - $this->storage->setToken('token_id', 'TOKEN'); + + $this->assertTrue($this->session->isStarted()); } public function testStoreTokenInActiveSession() { - $this->session->expects($this->any()) - ->method('isStarted') - ->will($this->returnValue(true)); - - $this->session->expects($this->never()) - ->method('start'); - - $this->session->expects($this->once()) - ->method('set') - ->with(self::SESSION_NAMESPACE.'/token_id', 'TOKEN'); - + $this->session->start(); $this->storage->setToken('token_id', 'TOKEN'); + + $this->assertSame('TOKEN', $this->session->get(self::SESSION_NAMESPACE.'/token_id')); } public function testCheckTokenInClosedSession() { - $this->session->expects($this->any()) - ->method('isStarted') - ->will($this->returnValue(false)); - - $this->session->expects($this->once()) - ->method('start'); + $this->session->set(self::SESSION_NAMESPACE.'/token_id', 'RESULT'); - $this->session->expects($this->once()) - ->method('has') - ->with(self::SESSION_NAMESPACE.'/token_id') - ->will($this->returnValue('RESULT')); - - $this->assertSame('RESULT', $this->storage->hasToken('token_id')); + $this->assertTrue($this->storage->hasToken('token_id')); + $this->assertTrue($this->session->isStarted()); } public function testCheckTokenInActiveSession() { - $this->session->expects($this->any()) - ->method('isStarted') - ->will($this->returnValue(true)); - - $this->session->expects($this->never()) - ->method('start'); + $this->session->start(); + $this->session->set(self::SESSION_NAMESPACE.'/token_id', 'RESULT'); - $this->session->expects($this->once()) - ->method('has') - ->with(self::SESSION_NAMESPACE.'/token_id') - ->will($this->returnValue('RESULT')); - - $this->assertSame('RESULT', $this->storage->hasToken('token_id')); + $this->assertTrue($this->storage->hasToken('token_id')); } public function testGetExistingTokenFromClosedSession() { - $this->session->expects($this->any()) - ->method('isStarted') - ->will($this->returnValue(false)); - - $this->session->expects($this->once()) - ->method('start'); - - $this->session->expects($this->once()) - ->method('has') - ->with(self::SESSION_NAMESPACE.'/token_id') - ->will($this->returnValue(true)); - - $this->session->expects($this->once()) - ->method('get') - ->with(self::SESSION_NAMESPACE.'/token_id') - ->will($this->returnValue('RESULT')); + $this->session->set(self::SESSION_NAMESPACE.'/token_id', 'RESULT'); $this->assertSame('RESULT', $this->storage->getToken('token_id')); + $this->assertTrue($this->session->isStarted()); } public function testGetExistingTokenFromActiveSession() { - $this->session->expects($this->any()) - ->method('isStarted') - ->will($this->returnValue(true)); - - $this->session->expects($this->never()) - ->method('start'); - - $this->session->expects($this->once()) - ->method('has') - ->with(self::SESSION_NAMESPACE.'/token_id') - ->will($this->returnValue(true)); - - $this->session->expects($this->once()) - ->method('get') - ->with(self::SESSION_NAMESPACE.'/token_id') - ->will($this->returnValue('RESULT')); + $this->session->start(); + $this->session->set(self::SESSION_NAMESPACE.'/token_id', 'RESULT'); $this->assertSame('RESULT', $this->storage->getToken('token_id')); } @@ -154,18 +91,6 @@ public function testGetExistingTokenFromActiveSession() */ public function testGetNonExistingTokenFromClosedSession() { - $this->session->expects($this->any()) - ->method('isStarted') - ->will($this->returnValue(false)); - - $this->session->expects($this->once()) - ->method('start'); - - $this->session->expects($this->once()) - ->method('has') - ->with(self::SESSION_NAMESPACE.'/token_id') - ->will($this->returnValue(false)); - $this->storage->getToken('token_id'); } @@ -174,85 +99,33 @@ public function testGetNonExistingTokenFromClosedSession() */ public function testGetNonExistingTokenFromActiveSession() { - $this->session->expects($this->any()) - ->method('isStarted') - ->will($this->returnValue(true)); - - $this->session->expects($this->never()) - ->method('start'); - - $this->session->expects($this->once()) - ->method('has') - ->with(self::SESSION_NAMESPACE.'/token_id') - ->will($this->returnValue(false)); - + $this->session->start(); $this->storage->getToken('token_id'); } public function testRemoveNonExistingTokenFromClosedSession() { - $this->session->expects($this->any()) - ->method('isStarted') - ->will($this->returnValue(false)); - - $this->session->expects($this->once()) - ->method('start'); - - $this->session->expects($this->once()) - ->method('remove') - ->with(self::SESSION_NAMESPACE.'/token_id') - ->will($this->returnValue(null)); - $this->assertNull($this->storage->removeToken('token_id')); } public function testRemoveNonExistingTokenFromActiveSession() { - $this->session->expects($this->any()) - ->method('isStarted') - ->will($this->returnValue(true)); - - $this->session->expects($this->never()) - ->method('start'); - - $this->session->expects($this->once()) - ->method('remove') - ->with(self::SESSION_NAMESPACE.'/token_id') - ->will($this->returnValue(null)); + $this->session->start(); $this->assertNull($this->storage->removeToken('token_id')); } public function testRemoveExistingTokenFromClosedSession() { - $this->session->expects($this->any()) - ->method('isStarted') - ->will($this->returnValue(false)); - - $this->session->expects($this->once()) - ->method('start'); - - $this->session->expects($this->once()) - ->method('remove') - ->with(self::SESSION_NAMESPACE.'/token_id') - ->will($this->returnValue('TOKEN')); + $this->session->set(self::SESSION_NAMESPACE.'/token_id', 'TOKEN'); $this->assertSame('TOKEN', $this->storage->removeToken('token_id')); } public function testRemoveExistingTokenFromActiveSession() { - $this->session->expects($this->any()) - ->method('isStarted') - ->will($this->returnValue(true)); - - $this->session->expects($this->never()) - ->method('start'); - - $this->session->expects($this->once()) - ->method('remove') - ->with(self::SESSION_NAMESPACE.'/token_id') - ->will($this->returnValue('TOKEN')); + $this->session->start(); + $this->session->set(self::SESSION_NAMESPACE.'/token_id', 'TOKEN'); $this->assertSame('TOKEN', $this->storage->removeToken('token_id')); } diff --git a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php b/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php index 80ae75caa2b85..b9859452b1585 100644 --- a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php +++ b/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php @@ -14,7 +14,6 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\GetResponseEvent; -use Symfony\Component\Security\Guard\AbstractGuardAuthenticator; use Symfony\Component\Security\Guard\GuardAuthenticatorHandler; use Symfony\Component\Security\Guard\AuthenticatorInterface; use Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken; diff --git a/src/Symfony/Component/Security/Http/Firewall.php b/src/Symfony/Component/Security/Http/Firewall.php index 28b56d5791bd4..9bee596759bd8 100644 --- a/src/Symfony/Component/Security/Http/Firewall.php +++ b/src/Symfony/Component/Security/Http/Firewall.php @@ -47,13 +47,22 @@ public function onKernelRequest(GetResponseEvent $event) } // register listeners for this firewall - list($listeners, $exceptionListener) = $this->map->getListeners($event->getRequest()); + $listeners = $this->map->getListeners($event->getRequest()); + + $authenticationListeners = $listeners[0]; + $exceptionListener = $listeners[1]; + $logoutListener = isset($listeners[2]) ? $listeners[2] : null; + if (null !== $exceptionListener) { $this->exceptionListeners[$event->getRequest()] = $exceptionListener; $exceptionListener->register($this->dispatcher); } - return $this->handleRequest($event, $listeners); + $this->handleRequest($event, $authenticationListeners); + + if (null !== $logoutListener) { + $logoutListener->handle($event); + } } public function onKernelFinishRequest(FinishRequestEvent $event) diff --git a/src/Symfony/Component/Security/Http/Firewall/ContextListener.php b/src/Symfony/Component/Security/Http/Firewall/ContextListener.php index fdc74a6cb61f7..62b7df0031402 100644 --- a/src/Symfony/Component/Security/Http/Firewall/ContextListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/ContextListener.php @@ -45,7 +45,6 @@ class ContextListener implements ListenerInterface private $trustResolver; /** - * @param TokenStorageInterface $tokenStorage * @param iterable|UserProviderInterface[] $userProviders */ public function __construct(TokenStorageInterface $tokenStorage, iterable $userProviders, string $contextKey, LoggerInterface $logger = null, EventDispatcherInterface $dispatcher = null, AuthenticationTrustResolverInterface $trustResolver = null) diff --git a/src/Symfony/Component/Security/Http/FirewallMap.php b/src/Symfony/Component/Security/Http/FirewallMap.php index e767d123cb03e..fc97410d4e698 100644 --- a/src/Symfony/Component/Security/Http/FirewallMap.php +++ b/src/Symfony/Component/Security/Http/FirewallMap.php @@ -14,6 +14,7 @@ use Symfony\Component\HttpFoundation\RequestMatcherInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Http\Firewall\ExceptionListener; +use Symfony\Component\Security\Http\Firewall\LogoutListener; /** * FirewallMap allows configuration of different firewalls for specific parts @@ -25,9 +26,9 @@ class FirewallMap implements FirewallMapInterface { private $map = array(); - public function add(RequestMatcherInterface $requestMatcher = null, array $listeners = array(), ExceptionListener $exceptionListener = null) + public function add(RequestMatcherInterface $requestMatcher = null, array $listeners = array(), ExceptionListener $exceptionListener = null, LogoutListener $logoutListener = null) { - $this->map[] = array($requestMatcher, $listeners, $exceptionListener); + $this->map[] = array($requestMatcher, $listeners, $exceptionListener, $logoutListener); } /** @@ -37,10 +38,10 @@ public function getListeners(Request $request) { foreach ($this->map as $elements) { if (null === $elements[0] || $elements[0]->matches($request)) { - return array($elements[1], $elements[2]); + return array($elements[1], $elements[2], $elements[3]); } } - return array(array(), null); + return array(array(), null, null); } } diff --git a/src/Symfony/Component/Serializer/Mapping/AttributeMetadataInterface.php b/src/Symfony/Component/Serializer/Mapping/AttributeMetadataInterface.php index 944a7b3133f35..d9a15d5ac0de5 100644 --- a/src/Symfony/Component/Serializer/Mapping/AttributeMetadataInterface.php +++ b/src/Symfony/Component/Serializer/Mapping/AttributeMetadataInterface.php @@ -60,5 +60,5 @@ public function getMaxDepth(); /** * Merges an {@see AttributeMetadataInterface} with in the current one. */ - public function merge(AttributeMetadataInterface $attributeMetadata); + public function merge(self $attributeMetadata); } diff --git a/src/Symfony/Component/Serializer/Mapping/ClassMetadataInterface.php b/src/Symfony/Component/Serializer/Mapping/ClassMetadataInterface.php index ddcffe97c9b3f..45ed03dfa1d90 100644 --- a/src/Symfony/Component/Serializer/Mapping/ClassMetadataInterface.php +++ b/src/Symfony/Component/Serializer/Mapping/ClassMetadataInterface.php @@ -46,7 +46,7 @@ public function getAttributesMetadata(); /** * Merges a {@link ClassMetadataInterface} in the current one. */ - public function merge(ClassMetadataInterface $classMetadata); + public function merge(self $classMetadata); /** * Returns a {@link \ReflectionClass} instance for this class. diff --git a/src/Symfony/Component/Serializer/Tests/Mapping/ClassDiscriminatorMappingTest.php b/src/Symfony/Component/Serializer/Tests/Mapping/ClassDiscriminatorMappingTest.php index 390a5b281e99d..68a092d7019f7 100644 --- a/src/Symfony/Component/Serializer/Tests/Mapping/ClassDiscriminatorMappingTest.php +++ b/src/Symfony/Component/Serializer/Tests/Mapping/ClassDiscriminatorMappingTest.php @@ -28,7 +28,7 @@ public function testGetClass() )); $this->assertEquals(AbstractDummyFirstChild::class, $mapping->getClassForType('first')); - $this->assertEquals(null, $mapping->getClassForType('second')); + $this->assertNull($mapping->getClassForType('second')); } public function testMappedObjectType() @@ -38,6 +38,6 @@ public function testMappedObjectType() )); $this->assertEquals('first', $mapping->getMappedObjectType(new AbstractDummyFirstChild())); - $this->assertEquals(null, $mapping->getMappedObjectType(new AbstractDummySecondChild())); + $this->assertNull($mapping->getMappedObjectType(new AbstractDummySecondChild())); } } diff --git a/src/Symfony/Component/Translation/MessageCatalogueInterface.php b/src/Symfony/Component/Translation/MessageCatalogueInterface.php index 4dad27fbf6aa3..e0dbb2bd962cb 100644 --- a/src/Symfony/Component/Translation/MessageCatalogueInterface.php +++ b/src/Symfony/Component/Translation/MessageCatalogueInterface.php @@ -105,7 +105,7 @@ public function add($messages, $domain = 'messages'); * * The two catalogues must have the same locale. */ - public function addCatalogue(MessageCatalogueInterface $catalogue); + public function addCatalogue(self $catalogue); /** * Merges translations from the given Catalogue into the current one @@ -113,7 +113,7 @@ public function addCatalogue(MessageCatalogueInterface $catalogue); * * This is used to provide default translations when they do not exist for the current locale. */ - public function addFallbackCatalogue(MessageCatalogueInterface $catalogue); + public function addFallbackCatalogue(self $catalogue); /** * Gets the fallback catalogue. diff --git a/src/Symfony/Component/Translation/PluralizationRules.php b/src/Symfony/Component/Translation/PluralizationRules.php index 38dde743acc4f..48a6c608cbfe2 100644 --- a/src/Symfony/Component/Translation/PluralizationRules.php +++ b/src/Symfony/Component/Translation/PluralizationRules.php @@ -107,6 +107,7 @@ public static function get($number, $locale) case 'nl': case 'nn': case 'no': + case 'oc': case 'om': case 'or': case 'pa': diff --git a/src/Symfony/Component/Translation/Tests/Command/XliffLintCommandTest.php b/src/Symfony/Component/Translation/Tests/Command/XliffLintCommandTest.php index d55bfbc143d1f..e37600c3ebb87 100644 --- a/src/Symfony/Component/Translation/Tests/Command/XliffLintCommandTest.php +++ b/src/Symfony/Component/Translation/Tests/Command/XliffLintCommandTest.php @@ -15,9 +15,6 @@ use Symfony\Component\Console\Application; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Tester\CommandTester; -use Symfony\Component\Console\Helper\HelperSet; -use Symfony\Component\Console\Input\InputDefinition; -use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Translation\Command\XliffLintCommand; /** diff --git a/src/Symfony/Component/Validator/ConstraintViolationListInterface.php b/src/Symfony/Component/Validator/ConstraintViolationListInterface.php index 0489ab500a1dc..47e986f51e50a 100644 --- a/src/Symfony/Component/Validator/ConstraintViolationListInterface.php +++ b/src/Symfony/Component/Validator/ConstraintViolationListInterface.php @@ -26,7 +26,7 @@ public function add(ConstraintViolationInterface $violation); /** * Merges an existing violation list into this list. */ - public function addAll(ConstraintViolationListInterface $otherList); + public function addAll(self $otherList); /** * Returns the violation at a given offset. diff --git a/src/Symfony/Component/Validator/Constraints/UrlValidator.php b/src/Symfony/Component/Validator/Constraints/UrlValidator.php index 8490d5c10e74f..e7b3a3b57012b 100644 --- a/src/Symfony/Component/Validator/Constraints/UrlValidator.php +++ b/src/Symfony/Component/Validator/Constraints/UrlValidator.php @@ -87,7 +87,7 @@ public function validate($value, Constraint $constraint) Url::CHECK_DNS_TYPE_SOA, Url::CHECK_DNS_TYPE_SRV, Url::CHECK_DNS_TYPE_TXT, - ))) { + ), true)) { throw new InvalidOptionsException(sprintf('Invalid value for option "checkDNS" in constraint %s', get_class($constraint)), array('checkDNS')); } diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php index dd0dd1fa46a97..d7773861445fb 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php @@ -330,7 +330,7 @@ public function addGetterMethodConstraints($property, $method, array $constraint /** * Merges the constraints of the given metadata into this object. */ - public function mergeConstraints(ClassMetadata $source) + public function mergeConstraints(self $source) { if ($source->isGroupSequenceProvider()) { $this->setGroupSequenceProvider(true); diff --git a/src/Symfony/Component/Workflow/Definition.php b/src/Symfony/Component/Workflow/Definition.php index 9e9e1e796fcce..310cec8f15467 100644 --- a/src/Symfony/Component/Workflow/Definition.php +++ b/src/Symfony/Component/Workflow/Definition.php @@ -30,7 +30,6 @@ final class Definition /** * @param string[] $places * @param Transition[] $transitions - * @param string|null $initialPlace */ public function __construct(array $places, array $transitions, string $initialPlace = null, MetadataStoreInterface $metadataStore = null) { diff --git a/src/Symfony/Component/Workflow/DefinitionBuilder.php b/src/Symfony/Component/Workflow/DefinitionBuilder.php index 94e1e2effe16f..308f950324135 100644 --- a/src/Symfony/Component/Workflow/DefinitionBuilder.php +++ b/src/Symfony/Component/Workflow/DefinitionBuilder.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Workflow; +use Symfony\Component\Workflow\Metadata\MetadataStoreInterface; + /** * Builds a definition. * @@ -23,6 +25,7 @@ class DefinitionBuilder private $places = array(); private $transitions = array(); private $initialPlace; + private $metadataStore; /** * @param string[] $places @@ -39,7 +42,7 @@ public function __construct(array $places = array(), array $transitions = array( */ public function build() { - return new Definition($this->places, $this->transitions, $this->initialPlace); + return new Definition($this->places, $this->transitions, $this->initialPlace, $this->metadataStore); } /** @@ -52,6 +55,7 @@ public function clear() $this->places = array(); $this->transitions = array(); $this->initialPlace = null; + $this->metadataStore = null; return $this; } @@ -122,6 +126,16 @@ public function addTransition(Transition $transition) return $this; } + /** + * @return $this + */ + public function setMetadataStore(MetadataStoreInterface $metadataStore) + { + $this->metadataStore = $metadataStore; + + return $this; + } + /** * @deprecated since Symfony 4.1, use the clear() method instead. * diff --git a/src/Symfony/Component/Workflow/Tests/DefinitionBuilderTest.php b/src/Symfony/Component/Workflow/Tests/DefinitionBuilderTest.php index 1939fb5713963..1af7a326935d6 100644 --- a/src/Symfony/Component/Workflow/Tests/DefinitionBuilderTest.php +++ b/src/Symfony/Component/Workflow/Tests/DefinitionBuilderTest.php @@ -4,6 +4,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Workflow\DefinitionBuilder; +use Symfony\Component\Workflow\Metadata\InMemoryMetadataStore; use Symfony\Component\Workflow\Transition; class DefinitionBuilderTest extends TestCase @@ -44,4 +45,14 @@ public function testAddPlace() $this->assertEquals('a', $definition->getPlaces()['a']); $this->assertEquals('b', $definition->getPlaces()['b']); } + + public function testSetMetadataStore() + { + $builder = new DefinitionBuilder(array('a')); + $metadataStore = new InMemoryMetadataStore(); + $builder->setMetadataStore($metadataStore); + $definition = $builder->build(); + + $this->assertSame($metadataStore, $definition->getMetadataStore()); + } }