Merge remote-tracking branch 'origin/4.3'

* origin/4.3:
  deprecate calling createChildContext without the format parameter
  [EventDispatcher] Fix interface name used in error messages
  [FrameworkBundle] Add cache configuration for PropertyInfo
  Update dependencies in the main component
  Drop useless executable bit
  [Doctrine][PropertyInfo] Detect if the ID is writeable
  Add transport in subscriber's phpdoc
  [Validator] Autovalidation: skip readonly props
  [DI] default to service id - *not* FQCN - when building tagged locators
  [Cache] Log a more readable error message when saving into cache fails
  Update WorkflowEvents.php
  [Messenger] On failure retry, make message appear received from original sender
  [Messenger] remove send_and_handle option which can be achieved with SyncTransport
  Fixing tests - passing pdo is not wrapped for some reason in dbal
  Changing how RoutableMessageBus fallback bus works
  [Serializer] Fix BC break: DEPTH_KEY_PATTERN must be public
  [FrameworkBundle] Fixed issue when a parameter container a '%'
  Fix the interface incompatibility of EventDispatchers
  [TwigBundle] fixed Mailer integration in Twig
  [Form] Add intl/choice_translation_locale option to TimezoneType
This commit is contained in:
Grégoire Pineau 2019-05-16 11:31:29 +02:00
commit 519ba3cddb
61 changed files with 893 additions and 309 deletions

View File

@ -18,7 +18,6 @@
"require": { "require": {
"php": "^7.1.3", "php": "^7.1.3",
"ext-xml": "*", "ext-xml": "*",
"doctrine/collections": "~1.0",
"doctrine/event-manager": "~1.0", "doctrine/event-manager": "~1.0",
"doctrine/persistence": "~1.0", "doctrine/persistence": "~1.0",
"fig/link-util": "^1.0", "fig/link-util": "^1.0",
@ -27,7 +26,6 @@
"psr/container": "^1.0", "psr/container": "^1.0",
"psr/link": "^1.0", "psr/link": "^1.0",
"psr/log": "~1.0", "psr/log": "~1.0",
"psr/simple-cache": "^1.0",
"symfony/contracts": "^1.1", "symfony/contracts": "^1.1",
"symfony/polyfill-ctype": "~1.8", "symfony/polyfill-ctype": "~1.8",
"symfony/polyfill-intl-icu": "~1.0", "symfony/polyfill-intl-icu": "~1.0",
@ -38,6 +36,7 @@
}, },
"replace": { "replace": {
"symfony/asset": "self.version", "symfony/asset": "self.version",
"symfony/amazon-mailer": "self.version",
"symfony/browser-kit": "self.version", "symfony/browser-kit": "self.version",
"symfony/cache": "self.version", "symfony/cache": "self.version",
"symfony/config": "self.version", "symfony/config": "self.version",
@ -55,6 +54,7 @@
"symfony/finder": "self.version", "symfony/finder": "self.version",
"symfony/form": "self.version", "symfony/form": "self.version",
"symfony/framework-bundle": "self.version", "symfony/framework-bundle": "self.version",
"symfony/google-mailer": "self.version",
"symfony/http-client": "self.version", "symfony/http-client": "self.version",
"symfony/http-foundation": "self.version", "symfony/http-foundation": "self.version",
"symfony/http-kernel": "self.version", "symfony/http-kernel": "self.version",
@ -62,10 +62,14 @@
"symfony/intl": "self.version", "symfony/intl": "self.version",
"symfony/ldap": "self.version", "symfony/ldap": "self.version",
"symfony/lock": "self.version", "symfony/lock": "self.version",
"symfony/mailchimp-mailer": "self.version",
"symfony/mailer": "self.version",
"symfony/mailgun-mailer": "self.version",
"symfony/messenger": "self.version", "symfony/messenger": "self.version",
"symfony/mime": "self.version", "symfony/mime": "self.version",
"symfony/monolog-bridge": "self.version", "symfony/monolog-bridge": "self.version",
"symfony/options-resolver": "self.version", "symfony/options-resolver": "self.version",
"symfony/postmark-mailer": "self.version",
"symfony/process": "self.version", "symfony/process": "self.version",
"symfony/property-access": "self.version", "symfony/property-access": "self.version",
"symfony/property-info": "self.version", "symfony/property-info": "self.version",
@ -77,6 +81,7 @@
"symfony/security-guard": "self.version", "symfony/security-guard": "self.version",
"symfony/security-http": "self.version", "symfony/security-http": "self.version",
"symfony/security-bundle": "self.version", "symfony/security-bundle": "self.version",
"symfony/sendgrid-mailer": "self.version",
"symfony/serializer": "self.version", "symfony/serializer": "self.version",
"symfony/stopwatch": "self.version", "symfony/stopwatch": "self.version",
"symfony/templating": "self.version", "symfony/templating": "self.version",
@ -96,6 +101,7 @@
"cache/integration-tests": "dev-master", "cache/integration-tests": "dev-master",
"doctrine/annotations": "~1.0", "doctrine/annotations": "~1.0",
"doctrine/cache": "~1.6", "doctrine/cache": "~1.6",
"doctrine/collections": "~1.0",
"doctrine/data-fixtures": "1.0.*", "doctrine/data-fixtures": "1.0.*",
"doctrine/dbal": "~2.4", "doctrine/dbal": "~2.4",
"doctrine/orm": "~2.4,>=2.4.5", "doctrine/orm": "~2.4,>=2.4.5",
@ -107,6 +113,7 @@
"ocramius/proxy-manager": "~0.4|~1.0|~2.0", "ocramius/proxy-manager": "~0.4|~1.0|~2.0",
"predis/predis": "~1.1", "predis/predis": "~1.1",
"psr/http-client": "^1.0", "psr/http-client": "^1.0",
"psr/simple-cache": "^1.0",
"egulias/email-validator": "~1.2,>=1.2.8|~2.0", "egulias/email-validator": "~1.2,>=1.2.8|~2.0",
"symfony/phpunit-bridge": "~3.4|~4.0", "symfony/phpunit-bridge": "~3.4|~4.0",
"symfony/security-acl": "~2.8|~3.0", "symfony/security-acl": "~2.8|~3.0",

View File

@ -15,8 +15,10 @@ use Doctrine\Common\Persistence\Mapping\ClassMetadataFactory;
use Doctrine\Common\Persistence\Mapping\MappingException; use Doctrine\Common\Persistence\Mapping\MappingException;
use Doctrine\DBAL\Types\Type as DBALType; use Doctrine\DBAL\Types\Type as DBALType;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\ClassMetadataInfo; use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Doctrine\ORM\Mapping\MappingException as OrmMappingException; use Doctrine\ORM\Mapping\MappingException as OrmMappingException;
use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; use Symfony\Component\PropertyInfo\PropertyListExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
use Symfony\Component\PropertyInfo\Type; use Symfony\Component\PropertyInfo\Type;
@ -26,7 +28,7 @@ use Symfony\Component\PropertyInfo\Type;
* *
* @author Kévin Dunglas <dunglas@gmail.com> * @author Kévin Dunglas <dunglas@gmail.com>
*/ */
class DoctrineExtractor implements PropertyListExtractorInterface, PropertyTypeExtractorInterface class DoctrineExtractor implements PropertyListExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface
{ {
private $entityManager; private $entityManager;
private $classMetadataFactory; private $classMetadataFactory;
@ -51,12 +53,8 @@ class DoctrineExtractor implements PropertyListExtractorInterface, PropertyTypeE
*/ */
public function getProperties($class, array $context = []) public function getProperties($class, array $context = [])
{ {
try { if (null === $metadata = $this->getMetadata($class)) {
$metadata = $this->entityManager ? $this->entityManager->getClassMetadata($class) : $this->classMetadataFactory->getMetadataFor($class); return null;
} catch (MappingException $exception) {
return;
} catch (OrmMappingException $exception) {
return;
} }
$properties = array_merge($metadata->getFieldNames(), $metadata->getAssociationNames()); $properties = array_merge($metadata->getFieldNames(), $metadata->getAssociationNames());
@ -77,12 +75,8 @@ class DoctrineExtractor implements PropertyListExtractorInterface, PropertyTypeE
*/ */
public function getTypes($class, $property, array $context = []) public function getTypes($class, $property, array $context = [])
{ {
try { if (null === $metadata = $this->getMetadata($class)) {
$metadata = $this->entityManager ? $this->entityManager->getClassMetadata($class) : $this->classMetadataFactory->getMetadataFor($class); return null;
} catch (MappingException $exception) {
return;
} catch (OrmMappingException $exception) {
return;
} }
if ($metadata->hasAssociation($property)) { if ($metadata->hasAssociation($property)) {
@ -176,6 +170,39 @@ class DoctrineExtractor implements PropertyListExtractorInterface, PropertyTypeE
} }
} }
/**
* {@inheritdoc}
*/
public function isReadable($class, $property, array $context = [])
{
return null;
}
/**
* {@inheritdoc}
*/
public function isWritable($class, $property, array $context = [])
{
if (
null === ($metadata = $this->getMetadata($class))
|| ClassMetadata::GENERATOR_TYPE_NONE === $metadata->generatorType
|| !\in_array($property, $metadata->getIdentifierFieldNames(), true)
) {
return null;
}
return false;
}
private function getMetadata(string $class): ?ClassMetadata
{
try {
return $this->entityManager ? $this->entityManager->getClassMetadata($class) : $this->classMetadataFactory->getMetadataFor($class);
} catch (MappingException | OrmMappingException $exception) {
return null;
}
}
/** /**
* Determines whether an association is nullable. * Determines whether an association is nullable.
* *

View File

@ -16,6 +16,7 @@ use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Tools\Setup; use Doctrine\ORM\Tools\Setup;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Bridge\Doctrine\PropertyInfo\DoctrineExtractor; use Symfony\Bridge\Doctrine\PropertyInfo\DoctrineExtractor;
use Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineGeneratedValue;
use Symfony\Component\PropertyInfo\Type; use Symfony\Component\PropertyInfo\Type;
/** /**
@ -223,4 +224,13 @@ class DoctrineExtractorTest extends TestCase
{ {
$this->assertNull($this->createExtractor($legacy)->getTypes('Not\Exist', 'baz')); $this->assertNull($this->createExtractor($legacy)->getTypes('Not\Exist', 'baz'));
} }
public function testGeneratedValueNotWritable()
{
$extractor = $this->createExtractor();
$this->assertFalse($extractor->isWritable(DoctrineGeneratedValue::class, 'id'));
$this->assertNull($extractor->isReadable(DoctrineGeneratedValue::class, 'id'));
$this->assertNull($extractor->isWritable(DoctrineGeneratedValue::class, 'foo'));
$this->assertNull($extractor->isReadable(DoctrineGeneratedValue::class, 'foo'));
}
} }

View File

@ -0,0 +1,37 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*
* @Entity
*/
class DoctrineGeneratedValue
{
/**
* @Id
* @GeneratedValue(strategy="AUTO")
* @Column(type="integer")
*/
public $id;
/**
* @Column
*/
public $foo;
}

View File

@ -22,6 +22,8 @@ CHANGELOG
`framework.messenger.default_serializer`, which holds the string service `framework.messenger.default_serializer`, which holds the string service
id and `framework.messenger.symfony_serializer`, which configures the id and `framework.messenger.symfony_serializer`, which configures the
options if you're using Symfony's serializer. options if you're using Symfony's serializer.
* [BC Break] Removed the `framework.messenger.routing.send_and_handle` configuration.
Instead of setting it to true, configure a `SyncTransport` and route messages to it.
* Added information about deprecated aliases in `debug:autowiring` * Added information about deprecated aliases in `debug:autowiring`
* Added php ini session options `sid_length` and `sid_bits_per_character` * Added php ini session options `sid_length` and `sid_bits_per_character`
to the `session` section of the configuration to the `session` section of the configuration

View File

@ -224,15 +224,12 @@ EOF
if (!$kernel->isDebug() || !(new ConfigCache($kernel->getContainer()->getParameter('debug.container.dump'), true))->isFresh()) { if (!$kernel->isDebug() || !(new ConfigCache($kernel->getContainer()->getParameter('debug.container.dump'), true))->isFresh()) {
$buildContainer = \Closure::bind(function () { return $this->buildContainer(); }, $kernel, \get_class($kernel)); $buildContainer = \Closure::bind(function () { return $this->buildContainer(); }, $kernel, \get_class($kernel));
$container = $buildContainer(); $container = $buildContainer();
$container->getCompilerPassConfig()->setRemovingPasses([]);
$container->compile();
} else { } else {
(new XmlFileLoader($container = new ContainerBuilder(), new FileLocator()))->load($kernel->getContainer()->getParameter('debug.container.dump')); (new XmlFileLoader($container = new ContainerBuilder(), new FileLocator()))->load($kernel->getContainer()->getParameter('debug.container.dump'));
$container->setParameter('container.build_hash', $hash = ContainerBuilder::hash(__METHOD__));
$container->setParameter('container.build_id', hash('crc32', $hash.time()));
} }
$container->getCompilerPassConfig()->setRemovingPasses([]);
$container->compile();
return $this->containerBuilder = $container; return $this->containerBuilder = $container;
} }

View File

@ -322,21 +322,41 @@ abstract class Descriptor implements DescriptorInterface
private function getContainerEnvVars(ContainerBuilder $container): array private function getContainerEnvVars(ContainerBuilder $container): array
{ {
if (!$container->hasParameter('debug.container.dump')) {
return [];
}
if (!is_file($container->getParameter('debug.container.dump'))) {
return [];
}
$file = file_get_contents($container->getParameter('debug.container.dump'));
preg_match_all('{%env\(((?:\w++:)*+\w++)\)%}', $file, $envVars);
$envVars = array_unique($envVars[1]);
$bag = $container->getParameterBag();
$getDefaultParameter = function (string $name) {
return parent::get($name);
};
$getDefaultParameter = $getDefaultParameter->bindTo($bag, \get_class($bag));
$getEnvReflection = new \ReflectionMethod($container, 'getEnv'); $getEnvReflection = new \ReflectionMethod($container, 'getEnv');
$getEnvReflection->setAccessible(true); $getEnvReflection->setAccessible(true);
$envs = []; $envs = [];
foreach (array_keys($container->getEnvCounters()) as $env) {
foreach ($envVars as $env) {
$processor = 'string'; $processor = 'string';
if (false !== $i = strrpos($name = $env, ':')) { if (false !== $i = strrpos($name = $env, ':')) {
$name = substr($env, $i + 1); $name = substr($env, $i + 1);
$processor = substr($env, 0, $i); $processor = substr($env, 0, $i);
} }
$defaultValue = ($hasDefault = $container->hasParameter("env($name)")) ? $container->getParameter("env($name)") : null; $defaultValue = ($hasDefault = $container->hasParameter("env($name)")) ? $getDefaultParameter("env($name)") : null;
if (false === ($runtimeValue = $_ENV[$name] ?? $_SERVER[$name] ?? getenv($name))) { if (false === ($runtimeValue = $_ENV[$name] ?? $_SERVER[$name] ?? getenv($name))) {
$runtimeValue = null; $runtimeValue = null;
} }
$processedValue = ($hasRuntime = null !== $runtimeValue) || $hasDefault ? $getEnvReflection->invoke($container, $env) : null; $processedValue = ($hasRuntime = null !== $runtimeValue) || $hasDefault ? $getEnvReflection->invoke($container, $env) : null;
$envs[$name.$processor] = [ $envs["$name$processor"] = [
'name' => $name, 'name' => $name,
'processor' => $processor, 'processor' => $processor,
'default_available' => $hasDefault, 'default_available' => $hasDefault,

View File

@ -1125,7 +1125,6 @@ class Configuration implements ConfigurationInterface
if (!\is_int($k)) { if (!\is_int($k)) {
$newConfig[$k] = [ $newConfig[$k] = [
'senders' => $v['senders'] ?? (\is_array($v) ? array_values($v) : [$v]), 'senders' => $v['senders'] ?? (\is_array($v) ? array_values($v) : [$v]),
'send_and_handle' => $v['send_and_handle'] ?? false,
]; ];
} else { } else {
$newConfig[$v['message-class']]['senders'] = array_map( $newConfig[$v['message-class']]['senders'] = array_map(
@ -1134,7 +1133,6 @@ class Configuration implements ConfigurationInterface
}, },
array_values($v['sender']) array_values($v['sender'])
); );
$newConfig[$v['message-class']]['send-and-handle'] = $v['send-and-handle'] ?? false;
} }
} }
@ -1147,7 +1145,6 @@ class Configuration implements ConfigurationInterface
->requiresAtLeastOneElement() ->requiresAtLeastOneElement()
->prototype('scalar')->end() ->prototype('scalar')->end()
->end() ->end()
->booleanNode('send_and_handle')->defaultFalse()->end()
->end() ->end()
->end() ->end()
->end() ->end()

View File

@ -99,7 +99,6 @@ use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Serializer\Encoder\DecoderInterface; use Symfony\Component\Serializer\Encoder\DecoderInterface;
use Symfony\Component\Serializer\Encoder\EncoderInterface; use Symfony\Component\Serializer\Encoder\EncoderInterface;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata; use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata;
use Symfony\Component\Serializer\Mapping\Factory\CacheClassMetadataFactory;
use Symfony\Component\Serializer\Normalizer\ConstraintViolationListNormalizer; use Symfony\Component\Serializer\Normalizer\ConstraintViolationListNormalizer;
use Symfony\Component\Serializer\Normalizer\DateIntervalNormalizer; use Symfony\Component\Serializer\Normalizer\DateIntervalNormalizer;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
@ -1505,18 +1504,8 @@ class FrameworkExtension extends Extension
$chainLoader->replaceArgument(0, $serializerLoaders); $chainLoader->replaceArgument(0, $serializerLoaders);
$container->getDefinition('serializer.mapping.cache_warmer')->replaceArgument(0, $serializerLoaders); $container->getDefinition('serializer.mapping.cache_warmer')->replaceArgument(0, $serializerLoaders);
if (!$container->getParameter('kernel.debug')) { if ($container->getParameter('kernel.debug')) {
$cacheMetadataFactory = new Definition( $container->removeDefinition('serializer.mapping.cache_class_metadata_factory');
CacheClassMetadataFactory::class,
[
new Reference('serializer.mapping.cache_class_metadata_factory.inner'),
new Reference('serializer.mapping.cache.symfony'),
]
);
$cacheMetadataFactory->setPublic(false);
$cacheMetadataFactory->setDecoratedService('serializer.mapping.class_metadata_factory');
$container->setDefinition('serializer.mapping.cache_class_metadata_factory', $cacheMetadataFactory);
} }
if (isset($config['name_converter']) && $config['name_converter']) { if (isset($config['name_converter']) && $config['name_converter']) {
@ -1551,6 +1540,10 @@ class FrameworkExtension extends Extension
$definition->addTag('property_info.description_extractor', ['priority' => -1000]); $definition->addTag('property_info.description_extractor', ['priority' => -1000]);
$definition->addTag('property_info.type_extractor', ['priority' => -1001]); $definition->addTag('property_info.type_extractor', ['priority' => -1001]);
} }
if ($container->getParameter('kernel.debug')) {
$container->removeDefinition('property_info.cache');
}
} }
private function registerLockConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) private function registerLockConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader)
@ -1659,6 +1652,7 @@ class FrameworkExtension extends Extension
'before' => [ 'before' => [
['id' => 'add_bus_name_stamp_middleware'], ['id' => 'add_bus_name_stamp_middleware'],
['id' => 'dispatch_after_current_bus'], ['id' => 'dispatch_after_current_bus'],
['id' => 'failed_message_processing_middleware'],
], ],
'after' => [ 'after' => [
['id' => 'send_message'], ['id' => 'send_message'],
@ -1744,7 +1738,6 @@ class FrameworkExtension extends Extension
} }
$messageToSendersMapping = []; $messageToSendersMapping = [];
$messagesToSendAndHandle = [];
foreach ($config['routing'] as $message => $messageConfiguration) { foreach ($config['routing'] as $message => $messageConfiguration) {
if ('*' !== $message && !class_exists($message) && !interface_exists($message, false)) { if ('*' !== $message && !class_exists($message) && !interface_exists($message, false)) {
throw new LogicException(sprintf('Invalid Messenger routing configuration: class or interface "%s" not found.', $message)); throw new LogicException(sprintf('Invalid Messenger routing configuration: class or interface "%s" not found.', $message));
@ -1758,7 +1751,6 @@ class FrameworkExtension extends Extension
} }
$messageToSendersMapping[$message] = $messageConfiguration['senders']; $messageToSendersMapping[$message] = $messageConfiguration['senders'];
$messagesToSendAndHandle[$message] = $messageConfiguration['send_and_handle'];
} }
$senderReferences = []; $senderReferences = [];
@ -1769,7 +1761,6 @@ class FrameworkExtension extends Extension
$container->getDefinition('messenger.senders_locator') $container->getDefinition('messenger.senders_locator')
->replaceArgument(0, $messageToSendersMapping) ->replaceArgument(0, $messageToSendersMapping)
->replaceArgument(1, ServiceLocatorTagPass::register($container, $senderReferences)) ->replaceArgument(1, ServiceLocatorTagPass::register($container, $senderReferences))
->replaceArgument(2, $messagesToSendAndHandle)
; ;
$container->getDefinition('messenger.retry_strategy_locator') $container->getDefinition('messenger.retry_strategy_locator')

View File

@ -36,6 +36,10 @@
<tag name="cache.pool" /> <tag name="cache.pool" />
</service> </service>
<service id="cache.property_info" parent="cache.system" public="false">
<tag name="cache.pool" />
</service>
<service id="cache.messenger.restart_workers_signal" parent="cache.app" public="false"> <service id="cache.messenger.restart_workers_signal" parent="cache.app" public="false">
<tag name="cache.pool" /> <tag name="cache.pool" />
</service> </service>

View File

@ -11,7 +11,6 @@
<service id="messenger.senders_locator" class="Symfony\Component\Messenger\Transport\Sender\SendersLocator"> <service id="messenger.senders_locator" class="Symfony\Component\Messenger\Transport\Sender\SendersLocator">
<argument type="collection" /> <!-- Per message senders map --> <argument type="collection" /> <!-- Per message senders map -->
<argument /> <!-- senders locator --> <argument /> <!-- senders locator -->
<argument type="collection" /> <!-- Messages to send and handle -->
</service> </service>
<service id="messenger.middleware.send_message" class="Symfony\Component\Messenger\Middleware\SendMessageMiddleware"> <service id="messenger.middleware.send_message" class="Symfony\Component\Messenger\Middleware\SendMessageMiddleware">
<tag name="monolog.logger" channel="messenger" /> <tag name="monolog.logger" channel="messenger" />
@ -48,6 +47,8 @@
<argument type="service" id="validator" /> <argument type="service" id="validator" />
</service> </service>
<service id="messenger.middleware.failed_message_processing_middleware" class="Symfony\Component\Messenger\Middleware\FailedMessageProcessingMiddleware" />
<service id="messenger.middleware.traceable" class="Symfony\Component\Messenger\Middleware\TraceableMiddleware" abstract="true"> <service id="messenger.middleware.traceable" class="Symfony\Component\Messenger\Middleware\TraceableMiddleware" abstract="true">
<argument type="service" id="debug.stopwatch" /> <argument type="service" id="debug.stopwatch" />
</service> </service>
@ -106,6 +107,7 @@
<!-- routable message bus --> <!-- routable message bus -->
<service id="messenger.routable_message_bus" class="Symfony\Component\Messenger\RoutableMessageBus"> <service id="messenger.routable_message_bus" class="Symfony\Component\Messenger\RoutableMessageBus">
<argument /> <!-- Message bus locator --> <argument /> <!-- Message bus locator -->
<argument type="service" id="messenger.default_bus" />
</service> </service>
</services> </services>
</container> </container>

View File

@ -21,6 +21,11 @@
<service id="Symfony\Component\PropertyInfo\PropertyListExtractorInterface" alias="property_info" /> <service id="Symfony\Component\PropertyInfo\PropertyListExtractorInterface" alias="property_info" />
<service id="Symfony\Component\PropertyInfo\PropertyInitializableExtractorInterface" alias="property_info" /> <service id="Symfony\Component\PropertyInfo\PropertyInitializableExtractorInterface" alias="property_info" />
<service id="property_info.cache" decorates="property_info" class="Symfony\Component\PropertyInfo\PropertyInfoCacheExtractor">
<argument type="service" id="property_info.cache.inner" />
<argument type="service" id="cache.property_info" />
</service>
<!-- Extractor --> <!-- Extractor -->
<service id="property_info.reflection_extractor" class="Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor"> <service id="property_info.reflection_extractor" class="Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor">
<tag name="property_info.list_extractor" priority="-1000" /> <tag name="property_info.list_extractor" priority="-1000" />

View File

@ -434,7 +434,6 @@
<xsd:element name="sender" type="messenger_routing_sender" /> <xsd:element name="sender" type="messenger_routing_sender" />
</xsd:choice> </xsd:choice>
<xsd:attribute name="message-class" type="xsd:string" use="required"/> <xsd:attribute name="message-class" type="xsd:string" use="required"/>
<xsd:attribute name="send-and-handle" type="xsd:boolean" default="false"/>
</xsd:complexType> </xsd:complexType>
<xsd:complexType name="messenger_routing_sender"> <xsd:complexType name="messenger_routing_sender">

View File

@ -104,6 +104,11 @@
<argument type="service" id="cache.serializer" /> <argument type="service" id="cache.serializer" />
</service> </service>
<service id="serializer.mapping.cache_class_metadata_factory" decorates="serializer.mapping.class_metadata_factory" class="Symfony\Component\Serializer\Mapping\Factory\CacheClassMetadataFactory">
<argument type="service" id="serializer.mapping.cache_class_metadata_factory.inner" />
<argument type="service" id="serializer.mapping.cache.symfony" />
</service>
<!-- Encoders --> <!-- Encoders -->
<service id="serializer.encoder.xml" class="Symfony\Component\Serializer\Encoder\XmlEncoder"> <service id="serializer.encoder.xml" class="Symfony\Component\Serializer\Encoder\XmlEncoder">
<tag name="serializer.encoder" /> <tag name="serializer.encoder" />

View File

@ -71,6 +71,7 @@
<service id="validator.property_info_loader" class="Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader"> <service id="validator.property_info_loader" class="Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader">
<argument type="service" id="property_info" /> <argument type="service" id="property_info" />
<argument type="service" id="property_info" /> <argument type="service" id="property_info" />
<argument type="service" id="property_info" />
<tag name="validator.auto_mapper" /> <tag name="validator.auto_mapper" />
</service> </service>

View File

@ -10,7 +10,6 @@ $container->loadFromExtension('framework', [
'Symfony\Component\Messenger\Tests\Fixtures\DummyMessage' => ['amqp', 'audit'], 'Symfony\Component\Messenger\Tests\Fixtures\DummyMessage' => ['amqp', 'audit'],
'Symfony\Component\Messenger\Tests\Fixtures\SecondMessage' => [ 'Symfony\Component\Messenger\Tests\Fixtures\SecondMessage' => [
'senders' => ['amqp', 'audit'], 'senders' => ['amqp', 'audit'],
'send_and_handle' => true,
], ],
'*' => 'amqp', '*' => 'amqp',
], ],

View File

@ -13,7 +13,7 @@
<framework:sender service="amqp" /> <framework:sender service="amqp" />
<framework:sender service="audit" /> <framework:sender service="audit" />
</framework:routing> </framework:routing>
<framework:routing message-class="Symfony\Component\Messenger\Tests\Fixtures\SecondMessage" send-and-handle="true"> <framework:routing message-class="Symfony\Component\Messenger\Tests\Fixtures\SecondMessage">
<framework:sender service="amqp" /> <framework:sender service="amqp" />
<framework:sender service="audit" /> <framework:sender service="audit" />
</framework:routing> </framework:routing>

View File

@ -7,7 +7,6 @@ framework:
'Symfony\Component\Messenger\Tests\Fixtures\DummyMessage': [amqp, audit] 'Symfony\Component\Messenger\Tests\Fixtures\DummyMessage': [amqp, audit]
'Symfony\Component\Messenger\Tests\Fixtures\SecondMessage': 'Symfony\Component\Messenger\Tests\Fixtures\SecondMessage':
senders: [amqp, audit] senders: [amqp, audit]
send_and_handle: true
'*': amqp '*': amqp
transports: transports:
amqp: 'amqp://localhost/%2f/messages' amqp: 'amqp://localhost/%2f/messages'

View File

@ -40,7 +40,6 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpClient\ScopingHttpClient; use Symfony\Component\HttpClient\ScopingHttpClient;
use Symfony\Component\HttpKernel\DependencyInjection\LoggerPass; use Symfony\Component\HttpKernel\DependencyInjection\LoggerPass;
use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage; use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage;
use Symfony\Component\Messenger\Tests\Fixtures\SecondMessage;
use Symfony\Component\Messenger\Transport\TransportFactory; use Symfony\Component\Messenger\Transport\TransportFactory;
use Symfony\Component\PropertyAccess\PropertyAccessor; use Symfony\Component\PropertyAccess\PropertyAccessor;
use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
@ -715,13 +714,6 @@ abstract class FrameworkExtensionTest extends TestCase
$container = $this->createContainerFromFile('messenger_routing'); $container = $this->createContainerFromFile('messenger_routing');
$senderLocatorDefinition = $container->getDefinition('messenger.senders_locator'); $senderLocatorDefinition = $container->getDefinition('messenger.senders_locator');
$messageToSendAndHandleMapping = [
DummyMessage::class => false,
SecondMessage::class => true,
'*' => false,
];
$this->assertSame($messageToSendAndHandleMapping, $senderLocatorDefinition->getArgument(2));
$sendersMapping = $senderLocatorDefinition->getArgument(0); $sendersMapping = $senderLocatorDefinition->getArgument(0);
$this->assertEquals([ $this->assertEquals([
'amqp', 'amqp',
@ -753,6 +745,7 @@ abstract class FrameworkExtensionTest extends TestCase
$this->assertEquals([ $this->assertEquals([
['id' => 'add_bus_name_stamp_middleware', 'arguments' => ['messenger.bus.commands']], ['id' => 'add_bus_name_stamp_middleware', 'arguments' => ['messenger.bus.commands']],
['id' => 'dispatch_after_current_bus'], ['id' => 'dispatch_after_current_bus'],
['id' => 'failed_message_processing_middleware'],
['id' => 'send_message'], ['id' => 'send_message'],
['id' => 'handle_message'], ['id' => 'handle_message'],
], $container->getParameter('messenger.bus.commands.middleware')); ], $container->getParameter('messenger.bus.commands.middleware'));
@ -761,6 +754,7 @@ abstract class FrameworkExtensionTest extends TestCase
$this->assertEquals([ $this->assertEquals([
['id' => 'add_bus_name_stamp_middleware', 'arguments' => ['messenger.bus.events']], ['id' => 'add_bus_name_stamp_middleware', 'arguments' => ['messenger.bus.events']],
['id' => 'dispatch_after_current_bus'], ['id' => 'dispatch_after_current_bus'],
['id' => 'failed_message_processing_middleware'],
['id' => 'with_factory', 'arguments' => ['foo', true, ['bar' => 'baz']]], ['id' => 'with_factory', 'arguments' => ['foo', true, ['bar' => 'baz']]],
['id' => 'send_message'], ['id' => 'send_message'],
['id' => 'handle_message'], ['id' => 'handle_message'],
@ -1363,6 +1357,22 @@ abstract class FrameworkExtensionTest extends TestCase
$this->assertTrue($container->has('property_info')); $this->assertTrue($container->has('property_info'));
} }
public function testPropertyInfoCacheActivated()
{
$container = $this->createContainerFromFile('property_info');
$this->assertTrue($container->hasDefinition('property_info.cache'));
$cache = $container->getDefinition('property_info.cache')->getArgument(1);
$this->assertEquals(new Reference('cache.property_info'), $cache);
}
public function testPropertyInfoCacheDisabled()
{
$container = $this->createContainerFromFile('property_info', ['kernel.debug' => true, 'kernel.container_class' => __CLASS__]);
$this->assertFalse($container->hasDefinition('property_info.cache'));
}
public function testEventDispatcherService() public function testEventDispatcherService()
{ {
$container = $this->createContainer(['kernel.charset' => 'UTF-8', 'kernel.secret' => 'secret']); $container = $this->createContainer(['kernel.charset' => 'UTF-8', 'kernel.secret' => 'secret']);

View File

@ -83,11 +83,13 @@ class ContainerDebugCommandTest extends WebTestCase
public function testDescribeEnvVars() public function testDescribeEnvVars()
{ {
putenv('REAL=value'); putenv('REAL=value');
static::bootKernel(['test_case' => 'ContainerDebug', 'root_config' => 'config.yml']); static::bootKernel(['test_case' => 'ContainerDebug', 'root_config' => 'config.yml', 'debug' => true]);
$application = new Application(static::$kernel); $application = new Application(static::$kernel);
$application->setAutoExit(false); $application->setAutoExit(false);
@unlink(static::$container->getParameter('debug.container.dump'));
$tester = new ApplicationTester($application); $tester = new ApplicationTester($application);
$tester->run(['command' => 'debug:container', '--env-vars' => true], ['decorated' => false]); $tester->run(['command' => 'debug:container', '--env-vars' => true], ['decorated' => false]);
@ -96,13 +98,13 @@ class ContainerDebugCommandTest extends WebTestCase
Symfony Container Environment Variables Symfony Container Environment Variables
======================================= =======================================
--------- ----------------- ------------ --------- ----------------- ------------%w
Name Default value Real value Name Default value Real value%w
--------- ----------------- ------------ --------- ----------------- ------------%w
JSON "[1, "2.5", 3]" n/a JSON "[1, "2.5", 3]" n/a%w
REAL n/a "value" REAL n/a "value"%w
UNKNOWN n/a n/a UNKNOWN n/a n/a%w
--------- ----------------- ------------ --------- ----------------- ------------%w
// Note real values might be different between web and CLI.%w // Note real values might be different between web and CLI.%w
@ -118,35 +120,17 @@ TXT
public function testDescribeEnvVar() public function testDescribeEnvVar()
{ {
static::bootKernel(['test_case' => 'ContainerDebug', 'root_config' => 'config.yml']); static::bootKernel(['test_case' => 'ContainerDebug', 'root_config' => 'config.yml', 'debug' => true]);
$application = new Application(static::$kernel); $application = new Application(static::$kernel);
$application->setAutoExit(false); $application->setAutoExit(false);
@unlink(static::$container->getParameter('debug.container.dump'));
$tester = new ApplicationTester($application); $tester = new ApplicationTester($application);
$tester->run(['command' => 'debug:container', '--env-var' => 'js'], ['decorated' => false]); $tester->run(['command' => 'debug:container', '--env-var' => 'js'], ['decorated' => false]);
$this->assertContains(<<<'TXT' $this->assertContains(file_get_contents(__DIR__.'/Fixtures/describe_env_vars.txt'), $tester->getDisplay(true));
%env(float:key:2:json:JSON)%
----------------------------
----------------- -----------------
Default value "[1, "2.5", 3]"
Real value n/a
Processed value 3.0
----------------- -----------------
%env(int:key:2:json:JSON)%
--------------------------
----------------- -----------------
Default value "[1, "2.5", 3]"
Real value n/a
Processed value 3
----------------- -----------------
TXT
, $tester->getDisplay(true));
} }
public function provideIgnoreBackslashWhenFindingService() public function provideIgnoreBackslashWhenFindingService()

View File

@ -0,0 +1,17 @@
%env(float:key:2:json:JSON)%
----------------------------
----------------- -----------------
Default value "[1, "2.5", 3]"
Real value n/a
Processed value 3.0
----------------- -----------------
%env(int:key:2:json:JSON)%
--------------------------
----------------- -----------------
Default value "[1, "2.5", 3]"
Real value n/a
Processed value 3
----------------- -----------------

View File

@ -6,8 +6,9 @@
<services> <services>
<service id="twig.mailer.message_listener" class="Symfony\Component\Mailer\EventListener\MessageListener"> <service id="twig.mailer.message_listener" class="Symfony\Component\Mailer\EventListener\MessageListener">
<argument /> <argument>null</argument>
<argument type="service" id="twig.mime_body_renderer" /> <argument type="service" id="twig.mime_body_renderer" />
<tag name="kernel.event_subscriber"/>
</service> </service>
<service id="twig.mime_body_renderer" class="Symfony\Bridge\Twig\Mime\BodyRenderer"> <service id="twig.mime_body_renderer" class="Symfony\Bridge\Twig\Mime\BodyRenderer">

View File

@ -164,7 +164,8 @@ abstract class AbstractAdapter implements AdapterInterface, CacheInterface, Logg
$ok = false; $ok = false;
$v = $values[$id]; $v = $values[$id];
$type = \is_object($v) ? \get_class($v) : \gettype($v); $type = \is_object($v) ? \get_class($v) : \gettype($v);
CacheItem::log($this->logger, 'Failed to save key "{key}" ({type})', ['key' => substr($id, \strlen($this->namespace)), 'type' => $type, 'exception' => $e instanceof \Exception ? $e : null]); $message = sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.');
CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->namespace)), 'exception' => $e instanceof \Exception ? $e : null]);
} }
} else { } else {
foreach ($values as $id => $v) { foreach ($values as $id => $v) {
@ -186,7 +187,8 @@ abstract class AbstractAdapter implements AdapterInterface, CacheInterface, Logg
} }
$ok = false; $ok = false;
$type = \is_object($v) ? \get_class($v) : \gettype($v); $type = \is_object($v) ? \get_class($v) : \gettype($v);
CacheItem::log($this->logger, 'Failed to save key "{key}" ({type})', ['key' => substr($id, \strlen($this->namespace)), 'type' => $type, 'exception' => $e instanceof \Exception ? $e : null]); $message = sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.');
CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->namespace)), 'exception' => $e instanceof \Exception ? $e : null]);
} }
} }

View File

@ -178,7 +178,8 @@ abstract class AbstractTagAwareAdapter implements TagAwareAdapterInterface, TagA
$ok = false; $ok = false;
$v = $values[$id]; $v = $values[$id];
$type = \is_object($v) ? \get_class($v) : \gettype($v); $type = \is_object($v) ? \get_class($v) : \gettype($v);
CacheItem::log($this->logger, 'Failed to save key "{key}" ({type})', ['key' => substr($id, \strlen($this->namespace)), 'type' => $type, 'exception' => $e instanceof \Exception ? $e : null]); $message = sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.');
CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->namespace)), 'exception' => $e instanceof \Exception ? $e : null]);
} }
} else { } else {
foreach ($values as $id => $v) { foreach ($values as $id => $v) {
@ -201,7 +202,8 @@ abstract class AbstractTagAwareAdapter implements TagAwareAdapterInterface, TagA
} }
$ok = false; $ok = false;
$type = \is_object($v) ? \get_class($v) : \gettype($v); $type = \is_object($v) ? \get_class($v) : \gettype($v);
CacheItem::log($this->logger, 'Failed to save key "{key}" ({type})', ['key' => substr($id, \strlen($this->namespace)), 'type' => $type, 'exception' => $e instanceof \Exception ? $e : null]); $message = sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.');
CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->namespace)), 'exception' => $e instanceof \Exception ? $e : null]);
} }
} }

View File

@ -128,7 +128,8 @@ trait ArrayTrait
$serialized = serialize($value); $serialized = serialize($value);
} catch (\Exception $e) { } catch (\Exception $e) {
$type = \is_object($value) ? \get_class($value) : \gettype($value); $type = \is_object($value) ? \get_class($value) : \gettype($value);
CacheItem::log($this->logger, 'Failed to save key "{key}" ({type})', ['key' => $key, 'type' => $type, 'exception' => $e]); $message = sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.');
CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->namespace)), 'exception' => $e instanceof \Exception ? $e : null]);
return; return;
} }

View File

@ -21,22 +21,26 @@ class TaggedIteratorArgument extends IteratorArgument
private $tag; private $tag;
private $indexAttribute; private $indexAttribute;
private $defaultIndexMethod; private $defaultIndexMethod;
private $useFqcnAsFallback = false; private $needsIndexes = false;
/** /**
* @param string $tag The name of the tag identifying the target services * @param string $tag The name of the tag identifying the target services
* @param string|null $indexAttribute The name of the attribute that defines the key referencing each service in the tagged collection * @param string|null $indexAttribute The name of the attribute that defines the key referencing each service in the tagged collection
* @param string|null $defaultIndexMethod The static method that should be called to get each service's key when their tag doesn't define the previous attribute * @param string|null $defaultIndexMethod The static method that should be called to get each service's key when their tag doesn't define the previous attribute
* @param bool $useFqcnAsFallback Whether the FQCN of the service should be used as index when neither the attribute nor the method are defined * @param bool $needsIndexes Whether indexes are required and should be generated when computing the map
*/ */
public function __construct(string $tag, string $indexAttribute = null, string $defaultIndexMethod = null, bool $useFqcnAsFallback = false) public function __construct(string $tag, string $indexAttribute = null, string $defaultIndexMethod = null, bool $needsIndexes = false)
{ {
parent::__construct([]); parent::__construct([]);
if (null === $indexAttribute && $needsIndexes) {
$indexAttribute = preg_match('/[^.]++$/', $tag, $m) ? $m[0] : $tag;
}
$this->tag = $tag; $this->tag = $tag;
$this->indexAttribute = $indexAttribute; $this->indexAttribute = $indexAttribute;
$this->defaultIndexMethod = $defaultIndexMethod ?: ('getDefault'.str_replace(' ', '', ucwords(preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', ' ', $indexAttribute ?? ''))).'Name'); $this->defaultIndexMethod = $defaultIndexMethod ?: ('getDefault'.str_replace(' ', '', ucwords(preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', ' ', $indexAttribute ?? ''))).'Name');
$this->useFqcnAsFallback = $useFqcnAsFallback; $this->needsIndexes = $needsIndexes;
} }
public function getTag() public function getTag()
@ -54,8 +58,8 @@ class TaggedIteratorArgument extends IteratorArgument
return $this->defaultIndexMethod; return $this->defaultIndexMethod;
} }
public function useFqcnAsFallback(): bool public function needsIndexes(): bool
{ {
return $this->useFqcnAsFallback; return $this->needsIndexes;
} }
} }

View File

@ -41,12 +41,12 @@ trait PriorityTaggedServiceTrait
*/ */
private function findAndSortTaggedServices($tagName, ContainerBuilder $container) private function findAndSortTaggedServices($tagName, ContainerBuilder $container)
{ {
$indexAttribute = $defaultIndexMethod = $useFqcnAsFallback = null; $indexAttribute = $defaultIndexMethod = $needsIndexes = null;
if ($tagName instanceof TaggedIteratorArgument) { if ($tagName instanceof TaggedIteratorArgument) {
$indexAttribute = $tagName->getIndexAttribute(); $indexAttribute = $tagName->getIndexAttribute();
$defaultIndexMethod = $tagName->getDefaultIndexMethod(); $defaultIndexMethod = $tagName->getDefaultIndexMethod();
$useFqcnAsFallback = $tagName->useFqcnAsFallback(); $needsIndexes = $tagName->needsIndexes();
$tagName = $tagName->getTag(); $tagName = $tagName->getTag();
} }
@ -55,7 +55,7 @@ trait PriorityTaggedServiceTrait
foreach ($container->findTaggedServiceIds($tagName, true) as $serviceId => $attributes) { foreach ($container->findTaggedServiceIds($tagName, true) as $serviceId => $attributes) {
$priority = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0; $priority = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0;
if (null === $indexAttribute && !$useFqcnAsFallback) { if (null === $indexAttribute && !$needsIndexes) {
$services[$priority][] = new Reference($serviceId); $services[$priority][] = new Reference($serviceId);
continue; continue;
@ -77,8 +77,8 @@ trait PriorityTaggedServiceTrait
$class = $r->name; $class = $r->name;
if (!$r->hasMethod($defaultIndexMethod)) { if (!$r->hasMethod($defaultIndexMethod)) {
if ($useFqcnAsFallback) { if ($needsIndexes) {
$services[$priority][$class] = new TypedReference($serviceId, $class); $services[$priority][$serviceId] = new TypedReference($serviceId, $class);
continue; continue;
} }

View File

@ -292,22 +292,22 @@ class IntegrationTest extends TestCase
public function testTaggedServiceLocatorWithIndexAttribute() public function testTaggedServiceLocatorWithIndexAttribute()
{ {
$container = new ContainerBuilder(); $container = new ContainerBuilder();
$container->register(BarTagClass::class) $container->register('bar_tag', BarTagClass::class)
->setPublic(true) ->setPublic(true)
->addTag('foo_bar', ['foo' => 'bar']) ->addTag('foo_bar', ['foo' => 'bar'])
; ;
$container->register(FooTagClass::class) $container->register('foo_tag', FooTagClass::class)
->setPublic(true) ->setPublic(true)
->addTag('foo_bar') ->addTag('foo_bar')
; ;
$container->register(FooBarTaggedClass::class) $container->register('foo_bar_tagged', FooBarTaggedClass::class)
->addArgument(new ServiceLocatorArgument(new TaggedIteratorArgument('foo_bar', 'foo'))) ->addArgument(new ServiceLocatorArgument(new TaggedIteratorArgument('foo_bar', 'foo')))
->setPublic(true) ->setPublic(true)
; ;
$container->compile(); $container->compile();
$s = $container->get(FooBarTaggedClass::class); $s = $container->get('foo_bar_tagged');
/** @var ServiceLocator $serviceLocator */ /** @var ServiceLocator $serviceLocator */
$serviceLocator = $s->getParam(); $serviceLocator = $s->getParam();
@ -317,28 +317,28 @@ class IntegrationTest extends TestCase
'bar' => $serviceLocator->get('bar'), 'bar' => $serviceLocator->get('bar'),
'foo_tag_class' => $serviceLocator->get('foo_tag_class'), 'foo_tag_class' => $serviceLocator->get('foo_tag_class'),
]; ];
$this->assertSame(['bar' => $container->get(BarTagClass::class), 'foo_tag_class' => $container->get(FooTagClass::class)], $same); $this->assertSame(['bar' => $container->get('bar_tag'), 'foo_tag_class' => $container->get('foo_tag')], $same);
} }
public function testTaggedServiceLocatorWithIndexAttributeAndDefaultMethod() public function testTaggedServiceLocatorWithIndexAttributeAndDefaultMethod()
{ {
$container = new ContainerBuilder(); $container = new ContainerBuilder();
$container->register(BarTagClass::class) $container->register('bar_tag', BarTagClass::class)
->setPublic(true) ->setPublic(true)
->addTag('foo_bar') ->addTag('foo_bar')
; ;
$container->register(FooTagClass::class) $container->register('foo_tag', FooTagClass::class)
->setPublic(true) ->setPublic(true)
->addTag('foo_bar', ['foo' => 'foo']) ->addTag('foo_bar', ['foo' => 'foo'])
; ;
$container->register(FooBarTaggedClass::class) $container->register('foo_bar_tagged', FooBarTaggedClass::class)
->addArgument(new ServiceLocatorArgument(new TaggedIteratorArgument('foo_bar', 'foo', 'getFooBar'))) ->addArgument(new ServiceLocatorArgument(new TaggedIteratorArgument('foo_bar', 'foo', 'getFooBar')))
->setPublic(true) ->setPublic(true)
; ;
$container->compile(); $container->compile();
$s = $container->get(FooBarTaggedClass::class); $s = $container->get('foo_bar_tagged');
/** @var ServiceLocator $serviceLocator */ /** @var ServiceLocator $serviceLocator */
$serviceLocator = $s->getParam(); $serviceLocator = $s->getParam();
@ -348,33 +348,59 @@ class IntegrationTest extends TestCase
'bar_tab_class_with_defaultmethod' => $serviceLocator->get('bar_tab_class_with_defaultmethod'), 'bar_tab_class_with_defaultmethod' => $serviceLocator->get('bar_tab_class_with_defaultmethod'),
'foo' => $serviceLocator->get('foo'), 'foo' => $serviceLocator->get('foo'),
]; ];
$this->assertSame(['bar_tab_class_with_defaultmethod' => $container->get(BarTagClass::class), 'foo' => $container->get(FooTagClass::class)], $same); $this->assertSame(['bar_tab_class_with_defaultmethod' => $container->get('bar_tag'), 'foo' => $container->get('foo_tag')], $same);
} }
public function testTaggedServiceLocatorWithFqcnFallback() public function testTaggedServiceLocatorWithFallback()
{ {
$container = new ContainerBuilder(); $container = new ContainerBuilder();
$container->register(BarTagClass::class) $container->register('bar_tag', BarTagClass::class)
->setPublic(true) ->setPublic(true)
->addTag('foo_bar') ->addTag('foo_bar')
; ;
$container->register(FooBarTaggedClass::class) $container->register('foo_bar_tagged', FooBarTaggedClass::class)
->addArgument(new ServiceLocatorArgument(new TaggedIteratorArgument('foo_bar', null, null, true))) ->addArgument(new ServiceLocatorArgument(new TaggedIteratorArgument('foo_bar', null, null, true)))
->setPublic(true) ->setPublic(true)
; ;
$container->compile(); $container->compile();
$s = $container->get(FooBarTaggedClass::class); $s = $container->get('foo_bar_tagged');
/** @var ServiceLocator $serviceLocator */ /** @var ServiceLocator $serviceLocator */
$serviceLocator = $s->getParam(); $serviceLocator = $s->getParam();
$this->assertTrue($s->getParam() instanceof ServiceLocator, sprintf('Wrong instance, should be an instance of ServiceLocator, %s given', \is_object($serviceLocator) ? \get_class($serviceLocator) : \gettype($serviceLocator))); $this->assertTrue($s->getParam() instanceof ServiceLocator, sprintf('Wrong instance, should be an instance of ServiceLocator, %s given', \is_object($serviceLocator) ? \get_class($serviceLocator) : \gettype($serviceLocator)));
$same = [ $expected = [
BarTagClass::class => $serviceLocator->get(BarTagClass::class), 'bar_tag' => $container->get('bar_tag'),
]; ];
$this->assertSame([BarTagClass::class => $container->get(BarTagClass::class)], $same); $this->assertSame($expected, ['bar_tag' => $serviceLocator->get('bar_tag')]);
}
public function testTaggedServiceLocatorWithDefaultIndex()
{
$container = new ContainerBuilder();
$container->register('bar_tag', BarTagClass::class)
->setPublic(true)
->addTag('app.foo_bar', ['foo_bar' => 'baz'])
;
$container->register('foo_bar_tagged', FooBarTaggedClass::class)
->addArgument(new ServiceLocatorArgument(new TaggedIteratorArgument('app.foo_bar', null, null, true)))
->setPublic(true)
;
$container->compile();
$s = $container->get('foo_bar_tagged');
/** @var ServiceLocator $serviceLocator */
$serviceLocator = $s->getParam();
$this->assertTrue($s->getParam() instanceof ServiceLocator, sprintf('Wrong instance, should be an instance of ServiceLocator, %s given', \is_object($serviceLocator) ? \get_class($serviceLocator) : \gettype($serviceLocator)));
$expected = [
'baz' => $container->get('bar_tag'),
];
$this->assertSame($expected, ['baz' => $serviceLocator->get('baz')]);
} }
} }

View File

@ -13,7 +13,7 @@ namespace Symfony\Component\EventDispatcher;
use Psr\EventDispatcher\StoppableEventInterface; use Psr\EventDispatcher\StoppableEventInterface;
use Symfony\Contracts\EventDispatcher\Event as ContractsEvent; use Symfony\Contracts\EventDispatcher\Event as ContractsEvent;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface as ContractsEventDispatcherInterface;
/** /**
* An helper class to provide BC/FC with the legacy signature of EventDispatcherInterface::dispatch(). * An helper class to provide BC/FC with the legacy signature of EventDispatcherInterface::dispatch().
@ -26,7 +26,7 @@ final class LegacyEventDispatcherProxy implements EventDispatcherInterface
{ {
private $dispatcher; private $dispatcher;
public static function decorate(?EventDispatcherInterface $dispatcher): ?EventDispatcherInterface public static function decorate(?ContractsEventDispatcherInterface $dispatcher): ?ContractsEventDispatcherInterface
{ {
if (null === $dispatcher) { if (null === $dispatcher) {
return null; return null;
@ -58,13 +58,13 @@ final class LegacyEventDispatcherProxy implements EventDispatcherInterface
if (\is_object($event)) { if (\is_object($event)) {
$eventName = $eventName ?? \get_class($event); $eventName = $eventName ?? \get_class($event);
} else { } else {
@trigger_error(sprintf('Calling the "%s::dispatch()" method with the event name as first argument is deprecated since Symfony 4.3, pass it second and provide the event object first instead.', EventDispatcherInterface::class), E_USER_DEPRECATED); @trigger_error(sprintf('Calling the "%s::dispatch()" method with the event name as first argument is deprecated since Symfony 4.3, pass it second and provide the event object first instead.', ContractsEventDispatcherInterface::class), E_USER_DEPRECATED);
$swap = $event; $swap = $event;
$event = $eventName ?? new Event(); $event = $eventName ?? new Event();
$eventName = $swap; $eventName = $swap;
if (!$event instanceof Event) { if (!$event instanceof Event) {
throw new \TypeError(sprintf('Argument 1 passed to "%s::dispatch()" must be an instance of %s, %s given.', EventDispatcherInterface::class, Event::class, \is_object($event) ? \get_class($event) : \gettype($event))); throw new \TypeError(sprintf('Argument 1 passed to "%s::dispatch()" must be an instance of %s, %s given.', ContractsEventDispatcherInterface::class, Event::class, \is_object($event) ? \get_class($event) : \gettype($event)));
} }
} }

View File

@ -13,10 +13,12 @@ namespace Symfony\Component\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\ChoiceList\Loader\CallbackChoiceLoader; use Symfony\Component\Form\ChoiceList\Loader\CallbackChoiceLoader;
use Symfony\Component\Form\ChoiceList\Loader\IntlCallbackChoiceLoader;
use Symfony\Component\Form\Exception\LogicException; use Symfony\Component\Form\Exception\LogicException;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeZoneToStringTransformer; use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeZoneToStringTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\IntlTimeZoneToStringTransformer; use Symfony\Component\Form\Extension\Core\DataTransformer\IntlTimeZoneToStringTransformer;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Intl\Timezones;
use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
@ -40,19 +42,41 @@ class TimezoneType extends AbstractType
public function configureOptions(OptionsResolver $resolver) public function configureOptions(OptionsResolver $resolver)
{ {
$resolver->setDefaults([ $resolver->setDefaults([
'intl' => false,
'choice_loader' => function (Options $options) { 'choice_loader' => function (Options $options) {
$regions = $options->offsetGet('regions', false);
$input = $options['input']; $input = $options['input'];
if ($options['intl']) {
$choiceTranslationLocale = $options['choice_translation_locale'];
return new IntlCallbackChoiceLoader(function () use ($input, $choiceTranslationLocale) {
return self::getIntlTimezones($input, $choiceTranslationLocale);
});
}
$regions = $options->offsetGet('regions', false);
return new CallbackChoiceLoader(function () use ($regions, $input) { return new CallbackChoiceLoader(function () use ($regions, $input) {
return self::getTimezones($regions, $input); return self::getPhpTimezones($regions, $input);
}); });
}, },
'choice_translation_domain' => false, 'choice_translation_domain' => false,
'choice_translation_locale' => null,
'input' => 'string', 'input' => 'string',
'regions' => \DateTimeZone::ALL, 'regions' => \DateTimeZone::ALL,
]); ]);
$resolver->setAllowedTypes('intl', ['bool']);
$resolver->setAllowedTypes('choice_translation_locale', ['null', 'string']);
$resolver->setNormalizer('choice_translation_locale', function (Options $options, $value) {
if (null !== $value && !$options['intl']) {
throw new LogicException('The "choice_translation_locale" option can only be used if the "intl" option is set to true.');
}
return $value;
});
$resolver->setAllowedValues('input', ['string', 'datetimezone', 'intltimezone']); $resolver->setAllowedValues('input', ['string', 'datetimezone', 'intltimezone']);
$resolver->setNormalizer('input', function (Options $options, $value) { $resolver->setNormalizer('input', function (Options $options, $value) {
if ('intltimezone' === $value && !class_exists(\IntlTimeZone::class)) { if ('intltimezone' === $value && !class_exists(\IntlTimeZone::class)) {
@ -64,6 +88,13 @@ class TimezoneType extends AbstractType
$resolver->setAllowedTypes('regions', 'int'); $resolver->setAllowedTypes('regions', 'int');
$resolver->setDeprecated('regions', 'The option "%name%" is deprecated since Symfony 4.2.'); $resolver->setDeprecated('regions', 'The option "%name%" is deprecated since Symfony 4.2.');
$resolver->setNormalizer('regions', function (Options $options, $value) {
if ($options['intl'] && \DateTimeZone::ALL !== (\DateTimeZone::ALL & $value)) {
throw new LogicException('The "regions" option can only be used if the "intl" option is set to false.');
}
return $value;
});
} }
/** /**
@ -82,10 +113,7 @@ class TimezoneType extends AbstractType
return 'timezone'; return 'timezone';
} }
/** private static function getPhpTimezones(int $regions, string $input): array
* Returns a normalized array of timezone choices.
*/
private static function getTimezones(int $regions, string $input): array
{ {
$timezones = []; $timezones = [];
@ -99,4 +127,19 @@ class TimezoneType extends AbstractType
return $timezones; return $timezones;
} }
private static function getIntlTimezones(string $input, string $locale = null): array
{
$timezones = array_flip(Timezones::getNames($locale));
if ('intltimezone' === $input) {
foreach ($timezones as $name => $timezone) {
if ('Etc/Unknown' === \IntlTimeZone::createTimeZone($timezone)->getID()) {
unset($timezones[$name]);
}
}
}
return $timezones;
}
} }

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Form\Tests\Extension\Core\Type; namespace Symfony\Component\Form\Tests\Extension\Core\Type;
use Symfony\Component\Form\ChoiceList\View\ChoiceView; use Symfony\Component\Form\ChoiceList\View\ChoiceView;
use Symfony\Component\Intl\Util\IntlTestHelper;
class TimezoneTypeTest extends BaseTypeTest class TimezoneTypeTest extends BaseTypeTest
{ {
@ -83,6 +84,17 @@ class TimezoneTypeTest extends BaseTypeTest
$this->assertContains(new ChoiceView('Europe/Amsterdam', 'Europe/Amsterdam', 'Europe / Amsterdam'), $choices, '', false, false); $this->assertContains(new ChoiceView('Europe/Amsterdam', 'Europe/Amsterdam', 'Europe / Amsterdam'), $choices, '', false, false);
} }
/**
* @group legacy
* @expectedDeprecation The option "regions" is deprecated since Symfony 4.2.
* @expectedException \Symfony\Component\Form\Exception\LogicException
* @expectedExceptionMessage The "regions" option can only be used if the "intl" option is set to false.
*/
public function testFilterByRegionsWithIntl()
{
$this->factory->create(static::TESTED_TYPE, null, ['regions' => \DateTimeZone::EUROPE, 'intl' => true]);
}
/** /**
* @requires extension intl * @requires extension intl
*/ */
@ -116,4 +128,54 @@ class TimezoneTypeTest extends BaseTypeTest
$this->assertNull($form->getData()); $this->assertNull($form->getData());
$this->assertNotContains('Europe/Saratov', $form->getConfig()->getAttribute('choice_list')->getValues()); $this->assertNotContains('Europe/Saratov', $form->getConfig()->getAttribute('choice_list')->getValues());
} }
/**
* @requires extension intl
*/
public function testIntlTimeZoneInputWithBcAndIntl()
{
$form = $this->factory->create(static::TESTED_TYPE, null, ['input' => 'intltimezone', 'intl' => true]);
$form->submit('Europe/Saratov');
$this->assertNull($form->getData());
$this->assertNotContains('Europe/Saratov', $form->getConfig()->getAttribute('choice_list')->getValues());
}
public function testTimezonesAreSelectableWithIntl()
{
IntlTestHelper::requireIntl($this, false);
$choices = $this->factory->create(static::TESTED_TYPE, null, ['intl' => true])
->createView()->vars['choices'];
$this->assertContains(new ChoiceView('Europe/Amsterdam', 'Europe/Amsterdam', 'Central European Time (Amsterdam)'), $choices, '', false, false);
$this->assertContains(new ChoiceView('Etc/UTC', 'Etc/UTC', 'Coordinated Universal Time'), $choices, '', false, false);
}
/**
* @requires extension intl
*/
public function testChoiceTranslationLocaleOptionWithIntl()
{
$choices = $this->factory
->create(static::TESTED_TYPE, null, [
'intl' => true,
'choice_translation_locale' => 'uk',
])
->createView()->vars['choices'];
$this->assertContains(new ChoiceView('Europe/Amsterdam', 'Europe/Amsterdam', 'за центральноєвропейським часом (Амстердам)'), $choices, '', false, false);
$this->assertContains(new ChoiceView('Etc/UTC', 'Etc/UTC', 'за всесвітнім координованим часом'), $choices, '', false, false);
}
/**
* @expectedException \Symfony\Component\Form\Exception\LogicException
* @expectedExceptionMessage The "choice_translation_locale" option can only be used if the "intl" option is set to true.
*/
public function testChoiceTranslationLocaleOptionWithoutIntl()
{
$this->factory->create(static::TESTED_TYPE, null, [
'choice_translation_locale' => 'uk',
]);
}
} }

View File

@ -6,6 +6,7 @@ CHANGELOG
* [BC BREAK] `SendersLocatorInterface` has an additional method: * [BC BREAK] `SendersLocatorInterface` has an additional method:
`getSenderByAlias()`. `getSenderByAlias()`.
* Removed argument `?bool &$handle = false` from `SendersLocatorInterface::getSenders`
* A new `ListableReceiverInterface` was added, which a receiver * A new `ListableReceiverInterface` was added, which a receiver
can implement (when applicable) to enable listing and fetching can implement (when applicable) to enable listing and fetching
individual messages by id (used in the new "Failed Messages" commands). individual messages by id (used in the new "Failed Messages" commands).

View File

@ -15,6 +15,7 @@ use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Dumper; use Symfony\Component\Console\Helper\Dumper;
use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Stamp\RedeliveryStamp;
use Symfony\Component\Messenger\Stamp\SentToFailureTransportStamp; use Symfony\Component\Messenger\Stamp\SentToFailureTransportStamp;
use Symfony\Component\Messenger\Stamp\TransportMessageIdStamp; use Symfony\Component\Messenger\Stamp\TransportMessageIdStamp;
use Symfony\Component\Messenger\Transport\Receiver\MessageCountAwareInterface; use Symfony\Component\Messenger\Transport\Receiver\MessageCountAwareInterface;
@ -59,8 +60,10 @@ abstract class AbstractFailedMessagesCommand extends Command
{ {
$io->title('Failed Message Details'); $io->title('Failed Message Details');
/** @var SentToFailureTransportStamp $sentToFailureTransportStamp */ /** @var SentToFailureTransportStamp|null $sentToFailureTransportStamp */
$sentToFailureTransportStamp = $envelope->last(SentToFailureTransportStamp::class); $sentToFailureTransportStamp = $envelope->last(SentToFailureTransportStamp::class);
/** @var RedeliveryStamp|null $lastRedeliveryStamp */
$lastRedeliveryStamp = $envelope->last(RedeliveryStamp::class);
$rows = [ $rows = [
['Class', \get_class($envelope->getMessage())], ['Class', \get_class($envelope->getMessage())],
@ -70,25 +73,34 @@ abstract class AbstractFailedMessagesCommand extends Command
$rows[] = ['Message Id', $id]; $rows[] = ['Message Id', $id];
} }
$flattenException = null === $lastRedeliveryStamp ? null : $lastRedeliveryStamp->getFlattenException();
if (null === $sentToFailureTransportStamp) { if (null === $sentToFailureTransportStamp) {
$io->warning('Message does not appear to have been sent to this transport after failing'); $io->warning('Message does not appear to have been sent to this transport after failing');
} else { } else {
$rows = array_merge($rows, [ $rows = array_merge($rows, [
['Failed at', $sentToFailureTransportStamp->getSentAt()->format('Y-m-d H:i:s')], ['Failed at', null === $lastRedeliveryStamp ? '' : $lastRedeliveryStamp->getRedeliveredAt()->format('Y-m-d H:i:s')],
['Error', $sentToFailureTransportStamp->getExceptionMessage()], ['Error', null === $lastRedeliveryStamp ? '' : $lastRedeliveryStamp->getExceptionMessage()],
['Error Class', $sentToFailureTransportStamp->getFlattenException() ? $sentToFailureTransportStamp->getFlattenException()->getClass() : '(unknown)'], ['Error Class', null === $flattenException ? '(unknown)' : $flattenException->getClass()],
['Transport', $sentToFailureTransportStamp->getOriginalReceiverName()], ['Transport', $sentToFailureTransportStamp->getOriginalReceiverName()],
]); ]);
} }
$io->table([], $rows); $io->table([], $rows);
/** @var RedeliveryStamp[] $redeliveryStamps */
$redeliveryStamps = $envelope->all(RedeliveryStamp::class);
$io->writeln(' Message history:');
foreach ($redeliveryStamps as $redeliveryStamp) {
$io->writeln(sprintf(' * Message failed and redelivered to the <info>%s</info> transport at <info>%s</info>', $redeliveryStamp->getSenderClassOrAlias(), $redeliveryStamp->getRedeliveredAt()->format('Y-m-d H:i:s')));
}
$io->newLine();
if ($io->isVeryVerbose()) { if ($io->isVeryVerbose()) {
$io->title('Message:'); $io->title('Message:');
$dump = new Dumper($io); $dump = new Dumper($io);
$io->writeln($dump($envelope->getMessage())); $io->writeln($dump($envelope->getMessage()));
$io->title('Exception:'); $io->title('Exception:');
$io->writeln($sentToFailureTransportStamp->getFlattenException()->getTraceAsString()); $io->writeln(null === $flattenException ? '(no data)' : $flattenException->getTraceAsString());
} else { } else {
$io->writeln(' Re-run command with <info>-vv</info> to see more message & error details.'); $io->writeln(' Re-run command with <info>-vv</info> to see more message & error details.');
} }

View File

@ -154,7 +154,7 @@ EOF
} }
// avoid success message if nothing was processed // avoid success message if nothing was processed
if (1 < $count) { if (1 <= $count) {
$io->success('All failed messages have been handled or removed!'); $io->success('All failed messages have been handled or removed!');
} }
} }

View File

@ -18,7 +18,7 @@ use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Messenger\Stamp\SentToFailureTransportStamp; use Symfony\Component\Messenger\Stamp\RedeliveryStamp;
use Symfony\Component\Messenger\Transport\Receiver\ListableReceiverInterface; use Symfony\Component\Messenger\Transport\Receiver\ListableReceiverInterface;
/** /**
@ -83,14 +83,14 @@ EOF
$rows = []; $rows = [];
foreach ($envelopes as $envelope) { foreach ($envelopes as $envelope) {
/** @var SentToFailureTransportStamp $sentToFailureTransportStamp */ /** @var RedeliveryStamp|null $lastRedeliveryStamp */
$sentToFailureTransportStamp = $envelope->last(SentToFailureTransportStamp::class); $lastRedeliveryStamp = $envelope->last(RedeliveryStamp::class);
$rows[] = [ $rows[] = [
$this->getMessageId($envelope), $this->getMessageId($envelope),
\get_class($envelope->getMessage()), \get_class($envelope->getMessage()),
null === $sentToFailureTransportStamp ? '' : $sentToFailureTransportStamp->getSentAt()->format('Y-m-d H:i:s'), null === $lastRedeliveryStamp ? '' : $lastRedeliveryStamp->getRedeliveredAt()->format('Y-m-d H:i:s'),
null === $sentToFailureTransportStamp ? '' : $sentToFailureTransportStamp->getExceptionMessage(), null === $lastRedeliveryStamp ? '' : $lastRedeliveryStamp->getExceptionMessage(),
]; ];
} }

View File

@ -16,9 +16,9 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent; use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
use Symfony\Component\Messenger\Exception\HandlerFailedException; use Symfony\Component\Messenger\Exception\HandlerFailedException;
use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Messenger\Stamp\DelayStamp;
use Symfony\Component\Messenger\Stamp\ReceivedStamp; use Symfony\Component\Messenger\Stamp\ReceivedStamp;
use Symfony\Component\Messenger\Stamp\RedeliveryStamp; use Symfony\Component\Messenger\Stamp\RedeliveryStamp;
use Symfony\Component\Messenger\Stamp\SentStamp;
use Symfony\Component\Messenger\Stamp\SentToFailureTransportStamp; use Symfony\Component\Messenger\Stamp\SentToFailureTransportStamp;
use Symfony\Component\Messenger\Stamp\TransportMessageIdStamp; use Symfony\Component\Messenger\Stamp\TransportMessageIdStamp;
@ -51,11 +51,8 @@ class SendFailedMessageToFailureTransportListener implements EventSubscriberInte
$envelope = $event->getEnvelope(); $envelope = $event->getEnvelope();
// avoid re-sending to the failed sender // avoid re-sending to the failed sender
foreach ($envelope->all(SentStamp::class) as $sentStamp) { if (null !== $envelope->last(SentToFailureTransportStamp::class)) {
/** @var SentStamp $sentStamp */ return;
if ($sentStamp->getSenderAlias() === $this->failureSenderAlias) {
return;
}
} }
// remove the received stamp so it's redelivered // remove the received stamp so it's redelivered
@ -67,8 +64,9 @@ class SendFailedMessageToFailureTransportListener implements EventSubscriberInte
$flattenedException = \class_exists(FlattenException::class) ? FlattenException::createFromThrowable($throwable) : null; $flattenedException = \class_exists(FlattenException::class) ? FlattenException::createFromThrowable($throwable) : null;
$envelope = $envelope->withoutAll(ReceivedStamp::class) $envelope = $envelope->withoutAll(ReceivedStamp::class)
->withoutAll(TransportMessageIdStamp::class) ->withoutAll(TransportMessageIdStamp::class)
->with(new SentToFailureTransportStamp($throwable->getMessage(), $event->getReceiverName(), $flattenedException)) ->with(new SentToFailureTransportStamp($event->getReceiverName()))
->with(new RedeliveryStamp(0, $this->failureSenderAlias)); ->with(new DelayStamp(0))
->with(new RedeliveryStamp(0, $this->failureSenderAlias, $throwable->getMessage(), $flattenedException));
if (null !== $this->logger) { if (null !== $this->logger) {
$this->logger->info('Rejected message {class} will be sent to the failure transport {transport}.', [ $this->logger->info('Rejected message {class} will be sent to the failure transport {transport}.', [

View File

@ -32,13 +32,14 @@ interface MessageSubscriberInterface extends MessageHandlerInterface
* yield FirstMessage::class => ['priority' => 0]; * yield FirstMessage::class => ['priority' => 0];
* yield SecondMessage::class => ['priority => -10]; * yield SecondMessage::class => ['priority => -10];
* *
* It can also specify a method, a priority and/or a bus per message: * It can also specify a method, a priority, a bus and/or a transport per message:
* *
* yield FirstMessage::class => ['method' => 'firstMessageMethod']; * yield FirstMessage::class => ['method' => 'firstMessageMethod'];
* yield SecondMessage::class => [ * yield SecondMessage::class => [
* 'method' => 'secondMessageMethod', * 'method' => 'secondMessageMethod',
* 'priority' => 20, * 'priority' => 20,
* 'bus' => 'my_bus_name', * 'bus' => 'my_bus_name',
* 'from_transport' => 'your_transport_name',
* ]; * ];
* *
* The benefit of using `yield` instead of returning an array is that you can `yield` multiple times the * The benefit of using `yield` instead of returning an array is that you can `yield` multiple times the

View File

@ -0,0 +1,38 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Middleware;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Stamp\ReceivedStamp;
use Symfony\Component\Messenger\Stamp\SentToFailureTransportStamp;
/**
* @author Ryan Weaver <ryan@symfonycasts.com>
*
* @experimental in 4.3
*/
class FailedMessageProcessingMiddleware implements MiddlewareInterface
{
public function handle(Envelope $envelope, StackInterface $stack): Envelope
{
// look for "received" messages decorated with the SentToFailureTransportStamp
/** @var SentToFailureTransportStamp|null $sentToFailureStamp */
$sentToFailureStamp = $envelope->last(SentToFailureTransportStamp::class);
if (null !== $sentToFailureStamp && null !== $envelope->last(ReceivedStamp::class)) {
// mark the message as "received" from the original transport
// this guarantees the same behavior as when originally received
$envelope = $envelope->with(new ReceivedStamp($sentToFailureStamp->getOriginalReceiverName()));
}
return $stack->next()->handle($envelope, $stack);
}
}

View File

@ -52,7 +52,6 @@ class SendMessageMiddleware implements MiddlewareInterface
'class' => \get_class($envelope->getMessage()), 'class' => \get_class($envelope->getMessage()),
]; ];
$handle = false;
$sender = null; $sender = null;
try { try {
@ -65,7 +64,7 @@ class SendMessageMiddleware implements MiddlewareInterface
// dispatch event unless this is a redelivery // dispatch event unless this is a redelivery
$shouldDispatchEvent = null === $redeliveryStamp; $shouldDispatchEvent = null === $redeliveryStamp;
foreach ($this->getSenders($envelope, $handle, $redeliveryStamp) as $alias => $sender) { foreach ($this->getSenders($envelope, $redeliveryStamp) as $alias => $sender) {
if (null !== $this->eventDispatcher && $shouldDispatchEvent) { if (null !== $this->eventDispatcher && $shouldDispatchEvent) {
$event = new SendMessageToTransportsEvent($envelope); $event = new SendMessageToTransportsEvent($envelope);
$this->eventDispatcher->dispatch($event); $this->eventDispatcher->dispatch($event);
@ -76,14 +75,9 @@ class SendMessageMiddleware implements MiddlewareInterface
$this->logger->info('Sending message "{class}" with "{sender}"', $context + ['sender' => \get_class($sender)]); $this->logger->info('Sending message "{class}" with "{sender}"', $context + ['sender' => \get_class($sender)]);
$envelope = $sender->send($envelope->with(new SentStamp(\get_class($sender), \is_string($alias) ? $alias : null))); $envelope = $sender->send($envelope->with(new SentStamp(\get_class($sender), \is_string($alias) ? $alias : null)));
} }
// on a redelivery, only send back to queue: never call local handlers
if (null !== $redeliveryStamp) {
$handle = false;
}
} }
if (null === $sender || $handle) { if (null === $sender) {
return $stack->next()->handle($envelope, $stack); return $stack->next()->handle($envelope, $stack);
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
@ -100,7 +94,7 @@ class SendMessageMiddleware implements MiddlewareInterface
/** /**
* * @return iterable|SenderInterface[] * * @return iterable|SenderInterface[]
*/ */
private function getSenders(Envelope $envelope, &$handle, ?RedeliveryStamp $redeliveryStamp): iterable private function getSenders(Envelope $envelope, ?RedeliveryStamp $redeliveryStamp): iterable
{ {
if (null !== $redeliveryStamp) { if (null !== $redeliveryStamp) {
return [ return [
@ -108,6 +102,6 @@ class SendMessageMiddleware implements MiddlewareInterface
]; ];
} }
return $this->sendersLocator->getSenders($envelope, $handle); return $this->sendersLocator->getSenders($envelope);
} }
} }

View File

@ -28,13 +28,15 @@ use Symfony\Component\Messenger\Stamp\BusNameStamp;
class RoutableMessageBus implements MessageBusInterface class RoutableMessageBus implements MessageBusInterface
{ {
private $busLocator; private $busLocator;
private $fallbackBus;
/** /**
* @param ContainerInterface $busLocator A locator full of MessageBusInterface objects * @param ContainerInterface $busLocator A locator full of MessageBusInterface objects
*/ */
public function __construct(ContainerInterface $busLocator) public function __construct(ContainerInterface $busLocator, MessageBusInterface $fallbackBus = null)
{ {
$this->busLocator = $busLocator; $this->busLocator = $busLocator;
$this->fallbackBus = $fallbackBus;
} }
public function dispatch($envelope, array $stamps = []): Envelope public function dispatch($envelope, array $stamps = []): Envelope
@ -43,14 +45,28 @@ class RoutableMessageBus implements MessageBusInterface
throw new InvalidArgumentException('Messages passed to RoutableMessageBus::dispatch() must be inside an Envelope'); throw new InvalidArgumentException('Messages passed to RoutableMessageBus::dispatch() must be inside an Envelope');
} }
/** @var BusNameStamp $busNameStamp */ return $this->getMessageBus($envelope)->dispatch($envelope, $stamps);
}
private function getMessageBus(Envelope $envelope): MessageBusInterface
{
/** @var BusNameStamp|null $busNameStamp */
$busNameStamp = $envelope->last(BusNameStamp::class); $busNameStamp = $envelope->last(BusNameStamp::class);
$busName = null !== $busNameStamp ? $busNameStamp->getBusName() : MessageBusInterface::class;
if (null === $busNameStamp) {
if (null === $this->fallbackBus) {
throw new InvalidArgumentException(sprintf('Envelope is missing a BusNameStamp and no fallback message bus is configured on RoutableMessageBus.'));
}
return $this->fallbackBus;
}
$busName = $busNameStamp->getBusName();
if (!$this->busLocator->has($busName)) { if (!$this->busLocator->has($busName)) {
throw new InvalidArgumentException(sprintf('Bus named "%s" does not exist.', $busName)); throw new InvalidArgumentException(sprintf('Bus named "%s" does not exist.', $busName));
} }
return $this->busLocator->get($busName)->dispatch($envelope, $stamps); return $this->busLocator->get($busName);
} }
} }

View File

@ -11,6 +11,8 @@
namespace Symfony\Component\Messenger\Stamp; namespace Symfony\Component\Messenger\Stamp;
use Symfony\Component\Debug\Exception\FlattenException;
/** /**
* Stamp applied when a messages needs to be redelivered. * Stamp applied when a messages needs to be redelivered.
* *
@ -20,14 +22,20 @@ class RedeliveryStamp implements StampInterface
{ {
private $retryCount; private $retryCount;
private $senderClassOrAlias; private $senderClassOrAlias;
private $redeliveredAt;
private $exceptionMessage;
private $flattenException;
/** /**
* @param string $senderClassOrAlias Alias from SendersLocator or just the class name * @param string $senderClassOrAlias Alias from SendersLocator or just the class name
*/ */
public function __construct(int $retryCount, string $senderClassOrAlias) public function __construct(int $retryCount, string $senderClassOrAlias, string $exceptionMessage = null, FlattenException $flattenException = null)
{ {
$this->retryCount = $retryCount; $this->retryCount = $retryCount;
$this->senderClassOrAlias = $senderClassOrAlias; $this->senderClassOrAlias = $senderClassOrAlias;
$this->exceptionMessage = $exceptionMessage;
$this->flattenException = $flattenException;
$this->redeliveredAt = new \DateTimeImmutable();
} }
public function getRetryCount(): int public function getRetryCount(): int
@ -36,7 +44,7 @@ class RedeliveryStamp implements StampInterface
} }
/** /**
* Needed for this class to serialize through Symfony's serializer. * The target sender this should be redelivered to.
* *
* @internal * @internal
*/ */
@ -44,4 +52,19 @@ class RedeliveryStamp implements StampInterface
{ {
return $this->senderClassOrAlias; return $this->senderClassOrAlias;
} }
public function getExceptionMessage(): ?string
{
return $this->exceptionMessage;
}
public function getFlattenException(): ?FlattenException
{
return $this->flattenException;
}
public function getRedeliveredAt(): \DateTimeInterface
{
return $this->redeliveredAt;
}
} }

View File

@ -11,8 +11,6 @@
namespace Symfony\Component\Messenger\Stamp; namespace Symfony\Component\Messenger\Stamp;
use Symfony\Component\Debug\Exception\FlattenException;
/** /**
* Stamp applied when a message is sent to the failure transport. * Stamp applied when a message is sent to the failure transport.
* *
@ -22,36 +20,15 @@ use Symfony\Component\Debug\Exception\FlattenException;
*/ */
class SentToFailureTransportStamp implements StampInterface class SentToFailureTransportStamp implements StampInterface
{ {
private $exceptionMessage;
private $originalReceiverName; private $originalReceiverName;
private $flattenException;
private $sentAt;
public function __construct(string $exceptionMessage, string $originalReceiverName, FlattenException $flattenException = null) public function __construct(string $originalReceiverName)
{ {
$this->exceptionMessage = $exceptionMessage;
$this->originalReceiverName = $originalReceiverName; $this->originalReceiverName = $originalReceiverName;
$this->flattenException = $flattenException;
$this->sentAt = new \DateTimeImmutable();
}
public function getExceptionMessage(): string
{
return $this->exceptionMessage;
} }
public function getOriginalReceiverName(): string public function getOriginalReceiverName(): string
{ {
return $this->originalReceiverName; return $this->originalReceiverName;
} }
public function getFlattenException(): ?FlattenException
{
return $this->flattenException;
}
public function getSentAt(): \DateTimeInterface
{
return $this->sentAt;
}
} }

View File

@ -15,6 +15,7 @@ use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\Messenger\Command\FailedMessagesShowCommand; use Symfony\Component\Messenger\Command\FailedMessagesShowCommand;
use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Stamp\RedeliveryStamp;
use Symfony\Component\Messenger\Stamp\SentToFailureTransportStamp; use Symfony\Component\Messenger\Stamp\SentToFailureTransportStamp;
use Symfony\Component\Messenger\Stamp\TransportMessageIdStamp; use Symfony\Component\Messenger\Stamp\TransportMessageIdStamp;
use Symfony\Component\Messenger\Transport\Receiver\ListableReceiverInterface; use Symfony\Component\Messenger\Transport\Receiver\ListableReceiverInterface;
@ -26,10 +27,12 @@ class FailedMessagesShowCommandTest extends TestCase
{ {
public function testBasicRun() public function testBasicRun()
{ {
$sentToFailureStamp = new SentToFailureTransportStamp('Things are bad!', 'async'); $sentToFailureStamp = new SentToFailureTransportStamp('async');
$redeliveryStamp = new RedeliveryStamp(0, 'failure_receiver', 'Things are bad!');
$envelope = new Envelope(new \stdClass(), [ $envelope = new Envelope(new \stdClass(), [
new TransportMessageIdStamp(15), new TransportMessageIdStamp(15),
$sentToFailureStamp, $sentToFailureStamp,
$redeliveryStamp,
]); ]);
$receiver = $this->createMock(ListableReceiverInterface::class); $receiver = $this->createMock(ListableReceiverInterface::class);
$receiver->expects($this->once())->method('find')->with(15)->willReturn($envelope); $receiver->expects($this->once())->method('find')->with(15)->willReturn($envelope);
@ -48,10 +51,11 @@ class FailedMessagesShowCommandTest extends TestCase
Message Id 15 Message Id 15
Failed at %s Failed at %s
Error Things are bad! Error Things are bad!
Error Class (unknown) Error Class (unknown)
Transport async
EOF EOF
, ,
$sentToFailureStamp->getSentAt()->format('Y-m-d H:i:s')), $redeliveryStamp->getRedeliveredAt()->format('Y-m-d H:i:s')),
$tester->getDisplay(true)); $tester->getDisplay(true));
} }
} }

View File

@ -19,7 +19,6 @@ use Symfony\Component\Messenger\Exception\HandlerFailedException;
use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Messenger\Stamp\ReceivedStamp; use Symfony\Component\Messenger\Stamp\ReceivedStamp;
use Symfony\Component\Messenger\Stamp\RedeliveryStamp; use Symfony\Component\Messenger\Stamp\RedeliveryStamp;
use Symfony\Component\Messenger\Stamp\SentStamp;
use Symfony\Component\Messenger\Stamp\SentToFailureTransportStamp; use Symfony\Component\Messenger\Stamp\SentToFailureTransportStamp;
use Symfony\Component\Messenger\Stamp\TransportMessageIdStamp; use Symfony\Component\Messenger\Stamp\TransportMessageIdStamp;
@ -35,13 +34,13 @@ class SendFailedMessageToFailureTransportListenerTest extends TestCase
/** @var SentToFailureTransportStamp $sentToFailureTransportStamp */ /** @var SentToFailureTransportStamp $sentToFailureTransportStamp */
$sentToFailureTransportStamp = $envelope->last(SentToFailureTransportStamp::class); $sentToFailureTransportStamp = $envelope->last(SentToFailureTransportStamp::class);
$this->assertNotNull($sentToFailureTransportStamp); $this->assertNotNull($sentToFailureTransportStamp);
$this->assertSame('no!', $sentToFailureTransportStamp->getExceptionMessage());
$this->assertSame('my_receiver', $sentToFailureTransportStamp->getOriginalReceiverName()); $this->assertSame('my_receiver', $sentToFailureTransportStamp->getOriginalReceiverName());
$this->assertSame('no!', $sentToFailureTransportStamp->getFlattenException()->getMessage());
/** @var RedeliveryStamp $redeliveryStamp */ /** @var RedeliveryStamp $redeliveryStamp */
$redeliveryStamp = $envelope->last(RedeliveryStamp::class); $redeliveryStamp = $envelope->last(RedeliveryStamp::class);
$this->assertSame('failure_sender', $redeliveryStamp->getSenderClassOrAlias()); $this->assertSame('failure_sender', $redeliveryStamp->getSenderClassOrAlias());
$this->assertSame('no!', $redeliveryStamp->getExceptionMessage());
$this->assertSame('no!', $redeliveryStamp->getFlattenException()->getMessage());
$this->assertNull($envelope->last(ReceivedStamp::class)); $this->assertNull($envelope->last(ReceivedStamp::class));
$this->assertNull($envelope->last(TransportMessageIdStamp::class)); $this->assertNull($envelope->last(TransportMessageIdStamp::class));
@ -65,11 +64,11 @@ class SendFailedMessageToFailureTransportListenerTest extends TestCase
$bus = $this->createMock(MessageBusInterface::class); $bus = $this->createMock(MessageBusInterface::class);
$bus->expects($this->once())->method('dispatch')->with($this->callback(function ($envelope) { $bus->expects($this->once())->method('dispatch')->with($this->callback(function ($envelope) {
/** @var Envelope $envelope */ /** @var Envelope $envelope */
/** @var SentToFailureTransportStamp $sentToFailureTransportStamp */ /** @var RedeliveryStamp $redeliveryStamp */
$sentToFailureTransportStamp = $envelope->last(SentToFailureTransportStamp::class); $redeliveryStamp = $envelope->last(RedeliveryStamp::class);
$this->assertNotNull($sentToFailureTransportStamp); $this->assertNotNull($redeliveryStamp);
$this->assertSame('I am inside!', $sentToFailureTransportStamp->getExceptionMessage()); $this->assertSame('I am inside!', $redeliveryStamp->getExceptionMessage());
$this->assertSame('Exception', $sentToFailureTransportStamp->getFlattenException()->getClass()); $this->assertSame('Exception', $redeliveryStamp->getFlattenException()->getClass());
return true; return true;
}))->willReturn(new Envelope(new \stdClass())); }))->willReturn(new Envelope(new \stdClass()));
@ -112,7 +111,7 @@ class SendFailedMessageToFailureTransportListenerTest extends TestCase
); );
$envelope = new Envelope(new \stdClass(), [ $envelope = new Envelope(new \stdClass(), [
new SentStamp('MySender', 'failure_sender'), new SentToFailureTransportStamp('my_receiver'),
]); ]);
$event = new WorkerMessageFailedEvent($envelope, 'my_receiver', new \Exception(''), false); $event = new WorkerMessageFailedEvent($envelope, 'my_receiver', new \Exception(''), false);

View File

@ -0,0 +1,281 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Tests;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
use Symfony\Component\Messenger\EventListener\SendFailedMessageToFailureTransportListener;
use Symfony\Component\Messenger\Exception\HandlerFailedException;
use Symfony\Component\Messenger\Handler\HandlerDescriptor;
use Symfony\Component\Messenger\Handler\HandlersLocator;
use Symfony\Component\Messenger\MessageBus;
use Symfony\Component\Messenger\Middleware\FailedMessageProcessingMiddleware;
use Symfony\Component\Messenger\Middleware\HandleMessageMiddleware;
use Symfony\Component\Messenger\Middleware\SendMessageMiddleware;
use Symfony\Component\Messenger\Retry\MultiplierRetryStrategy;
use Symfony\Component\Messenger\Stamp\RedeliveryStamp;
use Symfony\Component\Messenger\Stamp\SentToFailureTransportStamp;
use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage;
use Symfony\Component\Messenger\Transport\Receiver\ReceiverInterface;
use Symfony\Component\Messenger\Transport\Sender\SenderInterface;
use Symfony\Component\Messenger\Transport\Sender\SendersLocator;
use Symfony\Component\Messenger\Worker;
class FailureIntegrationTest extends TestCase
{
public function testRequeMechanism()
{
$transport1 = new DummyFailureTestSenderAndReceiver();
$transport2 = new DummyFailureTestSenderAndReceiver();
$failureTransport = new DummyFailureTestSenderAndReceiver();
$transports = [
'transport1' => $transport1,
'transport2' => $transport2,
'the_failure_transport' => $failureTransport,
];
$locator = $this->createMock(ContainerInterface::class);
$locator->expects($this->any())
->method('has')
->willReturn(true);
$locator->expects($this->any())
->method('get')
->willReturnCallback(function ($transportName) use ($transports) {
return $transports[$transportName];
});
$senderLocator = new SendersLocator(
[DummyMessage::class => ['transport1', 'transport2']],
$locator
);
// using to so we can lazily get the bus later and avoid circular problem
$transport1HandlerThatFails = new DummyTestHandler(true);
$allTransportHandlerThatWorks = new DummyTestHandler(false);
$transport2HandlerThatWorks = new DummyTestHandler(false);
$container = new Container();
$handlerLocator = new HandlersLocator([
DummyMessage::class => [
new HandlerDescriptor($transport1HandlerThatFails, [
'from_transport' => 'transport1',
'alias' => 'handler_that_fails',
]),
new HandlerDescriptor($allTransportHandlerThatWorks, [
'alias' => 'handler_that_works1',
]),
new HandlerDescriptor($transport2HandlerThatWorks, [
'from_transport' => 'transport2',
'alias' => 'handler_that_works2',
]),
],
]);
$dispatcher = new EventDispatcher();
$bus = new MessageBus([
new FailedMessageProcessingMiddleware(),
new SendMessageMiddleware($senderLocator),
new HandleMessageMiddleware($handlerLocator),
]);
$container->set('bus', $bus);
$dispatcher->addSubscriber(new SendFailedMessageToFailureTransportListener($bus, 'the_failure_transport'));
$runWorker = function (string $transportName, int $maxRetries) use ($transports, $bus, $dispatcher): ?\Throwable {
$throwable = null;
$failedListener = function (WorkerMessageFailedEvent $event) use (&$throwable) {
$throwable = $event->getThrowable();
};
$dispatcher->addListener(WorkerMessageFailedEvent::class, $failedListener);
$worker = new Worker([$transportName => $transports[$transportName]], $bus, [$transportName => new MultiplierRetryStrategy($maxRetries)], $dispatcher);
$worker->run([], function (?Envelope $envelope) use ($worker) {
// handle one envelope, then stop
if (null !== $envelope) {
$worker->stop();
}
});
$dispatcher->removeListener(WorkerMessageFailedEvent::class, $failedListener);
return $throwable;
};
// send the message
$envelope = new Envelope(new DummyMessage('API'));
$bus->dispatch($envelope);
// message has been sent
$this->assertCount(1, $transport1->getMessagesWaitingToBeReceived());
$this->assertCount(1, $transport2->getMessagesWaitingToBeReceived());
$this->assertCount(0, $failureTransport->getMessagesWaitingToBeReceived());
// receive the message - one handler will fail and the message
// will be sent back to transport1 to be retried
/*
* Receive the message from "transport1"
*/
$throwable = $runWorker('transport1', 1);
// make sure this is failing for the reason we think
$this->assertInstanceOf(HandlerFailedException::class, $throwable);
// handler for transport1 and all transports were called
$this->assertSame(1, $transport1HandlerThatFails->getTimesCalled());
$this->assertSame(1, $allTransportHandlerThatWorks->getTimesCalled());
$this->assertSame(0, $transport2HandlerThatWorks->getTimesCalled());
// one handler failed and the message is retried (resent to transport1)
$this->assertCount(1, $transport1->getMessagesWaitingToBeReceived());
$this->assertEmpty($failureTransport->getMessagesWaitingToBeReceived());
/*
* Receive the message for a (final) retry
*/
$runWorker('transport1', 1);
// only the "failed" handler is called a 2nd time
$this->assertSame(2, $transport1HandlerThatFails->getTimesCalled());
$this->assertSame(1, $allTransportHandlerThatWorks->getTimesCalled());
// handling fails again, message is sent to failure transport
$this->assertCount(0, $transport1->getMessagesWaitingToBeReceived());
$this->assertCount(1, $failureTransport->getMessagesWaitingToBeReceived());
/** @var Envelope $failedEnvelope */
$failedEnvelope = $failureTransport->getMessagesWaitingToBeReceived()[0];
/** @var SentToFailureTransportStamp $sentToFailureStamp */
$sentToFailureStamp = $failedEnvelope->last(SentToFailureTransportStamp::class);
$this->assertNotNull($sentToFailureStamp);
/** @var RedeliveryStamp $redeliveryStamp */
$redeliveryStamp = $failedEnvelope->last(RedeliveryStamp::class);
$this->assertNotNull($redeliveryStamp);
$this->assertSame('Failure from call 2', $redeliveryStamp->getExceptionMessage());
/*
* Failed message is handled, fails, and sent for a retry
*/
$throwable = $runWorker('the_failure_transport', 1);
// make sure this is failing for the reason we think
$this->assertInstanceOf(HandlerFailedException::class, $throwable);
// only the "failed" handler is called a 3rd time
$this->assertSame(3, $transport1HandlerThatFails->getTimesCalled());
$this->assertSame(1, $allTransportHandlerThatWorks->getTimesCalled());
// handling fails again, message is retried
$this->assertCount(1, $failureTransport->getMessagesWaitingToBeReceived());
// transport2 still only holds the original message
// a new message was never mistakenly delivered to it
$this->assertCount(1, $transport2->getMessagesWaitingToBeReceived());
/*
* Message is retried on failure transport then discarded
*/
$runWorker('the_failure_transport', 1);
// only the "failed" handler is called a 4th time
$this->assertSame(4, $transport1HandlerThatFails->getTimesCalled());
$this->assertSame(1, $allTransportHandlerThatWorks->getTimesCalled());
// handling fails again, message is discarded
$this->assertCount(0, $failureTransport->getMessagesWaitingToBeReceived());
/*
* Execute handlers on transport2
*/
$runWorker('transport2', 1);
// transport1 handler is not called again
$this->assertSame(4, $transport1HandlerThatFails->getTimesCalled());
// all transport handler is now called again
$this->assertSame(2, $allTransportHandlerThatWorks->getTimesCalled());
// transport1 handler called for the first time
$this->assertSame(1, $transport2HandlerThatWorks->getTimesCalled());
// all transport should be empty
$this->assertEmpty($transport1->getMessagesWaitingToBeReceived());
$this->assertEmpty($transport2->getMessagesWaitingToBeReceived());
$this->assertEmpty($failureTransport->getMessagesWaitingToBeReceived());
/*
* Dispatch the original message again
*/
$bus->dispatch($envelope);
// handle the message, but with no retries
$runWorker('transport1', 0);
// now make the handler work!
$transport1HandlerThatFails->setShouldThrow(false);
$runWorker('the_failure_transport', 1);
// the failure transport is empty because it worked
$this->assertEmpty($failureTransport->getMessagesWaitingToBeReceived());
}
}
class DummyFailureTestSenderAndReceiver implements ReceiverInterface, SenderInterface
{
private $messagesWaiting = [];
public function get(): iterable
{
$message = array_shift($this->messagesWaiting);
if (null === $message) {
return [];
}
return [$message];
}
public function ack(Envelope $envelope): void
{
}
public function reject(Envelope $envelope): void
{
}
public function send(Envelope $envelope): Envelope
{
$this->messagesWaiting[] = $envelope;
return $envelope;
}
/**
* @return Envelope[]
*/
public function getMessagesWaitingToBeReceived(): array
{
return $this->messagesWaiting;
}
}
class DummyTestHandler
{
private $timesCalled = 0;
private $shouldThrow;
public function __construct(bool $shouldThrow)
{
$this->shouldThrow = $shouldThrow;
}
public function __invoke()
{
++$this->timesCalled;
if ($this->shouldThrow) {
throw new \Exception('Failure from call '.$this->timesCalled);
}
}
public function getTimesCalled(): int
{
return $this->timesCalled;
}
public function setShouldThrow(bool $shouldThrow)
{
$this->shouldThrow = $shouldThrow;
}
}

View File

@ -92,11 +92,7 @@ class SendMessageMiddlewareTest extends MiddlewareTestCase
$sendersLocator = $this->createSendersLocator( $sendersLocator = $this->createSendersLocator(
[DummyMessage::class => ['foo', 'bar']], [DummyMessage::class => ['foo', 'bar']],
['foo' => $sender, 'bar' => $sender2], ['foo' => $sender, 'bar' => $sender2]
[
// normally, this class sends and handles (but not on retry)
DummyMessage::class => true,
]
); );
$middleware = new SendMessageMiddleware($sendersLocator); $middleware = new SendMessageMiddleware($sendersLocator);
@ -126,68 +122,46 @@ class SendMessageMiddlewareTest extends MiddlewareTestCase
$middleware->handle($envelope, $this->getStackMock(false)); $middleware->handle($envelope, $this->getStackMock(false));
} }
public function testItAlsoCallsTheNextMiddlewareBasedOnTheMessageClass() public function testItSendsTheMessageBasedOnTheMessageParentClass()
{
$message = new DummyMessage('Hey');
$envelope = new Envelope($message);
$sender = $this->getMockBuilder(SenderInterface::class)->getMock();
$sendersLocator = $this->createSendersLocator(['*' => ['foo_sender']], ['foo_sender' => $sender], [
DummyMessage::class => true,
]);
$middleware = new SendMessageMiddleware($sendersLocator);
$sender->expects($this->once())->method('send')->with($envelope->with(new SentStamp(\get_class($sender), 'foo_sender')))->willReturn($envelope);
$middleware->handle($envelope, $this->getStackMock());
}
public function testItAlsoCallsTheNextMiddlewareBasedOnTheMessageParentClass()
{ {
$message = new ChildDummyMessage('Hey'); $message = new ChildDummyMessage('Hey');
$envelope = new Envelope($message); $envelope = new Envelope($message);
$sender = $this->getMockBuilder(SenderInterface::class)->getMock(); $sender = $this->getMockBuilder(SenderInterface::class)->getMock();
$sendersLocator = $this->createSendersLocator(['*' => ['foo_sender']], ['foo_sender' => $sender], [ $sendersLocator = $this->createSendersLocator([DummyMessage::class => ['foo_sender']], ['foo_sender' => $sender]);
DummyMessage::class => true,
]);
$middleware = new SendMessageMiddleware($sendersLocator); $middleware = new SendMessageMiddleware($sendersLocator);
$sender->expects($this->once())->method('send')->with($envelope->with(new SentStamp(\get_class($sender), 'foo_sender')))->willReturn($envelope); $sender->expects($this->once())->method('send')->with($envelope->with(new SentStamp(\get_class($sender), 'foo_sender')))->willReturn($envelope);
$middleware->handle($envelope, $this->getStackMock()); $middleware->handle($envelope, $this->getStackMock(false));
} }
public function testItAlsoCallsTheNextMiddlewareBasedOnTheMessageInterface() public function testItSendsTheMessageBasedOnTheMessageInterface()
{ {
$message = new DummyMessage('Hey'); $message = new DummyMessage('Hey');
$envelope = new Envelope($message); $envelope = new Envelope($message);
$sender = $this->getMockBuilder(SenderInterface::class)->getMock(); $sender = $this->getMockBuilder(SenderInterface::class)->getMock();
$sendersLocator = $this->createSendersLocator(['*' => ['foo_sender']], ['foo_sender' => $sender], [ $sendersLocator = $this->createSendersLocator([DummyMessageInterface::class => ['foo_sender']], ['foo_sender' => $sender]);
DummyMessageInterface::class => true,
]);
$middleware = new SendMessageMiddleware($sendersLocator); $middleware = new SendMessageMiddleware($sendersLocator);
$sender->expects($this->once())->method('send')->with($envelope->with(new SentStamp(\get_class($sender), 'foo_sender')))->willReturn($envelope); $sender->expects($this->once())->method('send')->with($envelope->with(new SentStamp(\get_class($sender), 'foo_sender')))->willReturn($envelope);
$middleware->handle($envelope, $this->getStackMock()); $middleware->handle($envelope, $this->getStackMock(false));
} }
public function testItAlsoCallsTheNextMiddlewareBasedOnWildcard() public function testItSendsTheMessageBasedOnWildcard()
{ {
$message = new DummyMessage('Hey'); $message = new DummyMessage('Hey');
$envelope = new Envelope($message); $envelope = new Envelope($message);
$sender = $this->getMockBuilder(SenderInterface::class)->getMock(); $sender = $this->getMockBuilder(SenderInterface::class)->getMock();
$sendersLocator = $this->createSendersLocator(['*' => ['foo_sender']], ['foo_sender' => $sender], [ $sendersLocator = $this->createSendersLocator(['*' => ['foo_sender']], ['foo_sender' => $sender]);
'*' => true,
]);
$middleware = new SendMessageMiddleware($sendersLocator); $middleware = new SendMessageMiddleware($sendersLocator);
$sender->expects($this->once())->method('send')->with($envelope->with(new SentStamp(\get_class($sender), 'foo_sender')))->willReturn($envelope); $sender->expects($this->once())->method('send')->with($envelope->with(new SentStamp(\get_class($sender), 'foo_sender')))->willReturn($envelope);
$middleware->handle($envelope, $this->getStackMock()); $middleware->handle($envelope, $this->getStackMock(false));
} }
public function testItCallsTheNextMiddlewareWhenNoSenderForThisMessage() public function testItCallsTheNextMiddlewareWhenNoSenderForThisMessage()
@ -267,7 +241,7 @@ class SendMessageMiddlewareTest extends MiddlewareTestCase
$middleware->handle($envelope, $this->getStackMock(false)); $middleware->handle($envelope, $this->getStackMock(false));
} }
private function createSendersLocator(array $sendersMap, array $senders, array $sendAndHandle = []) private function createSendersLocator(array $sendersMap, array $senders)
{ {
$container = $this->createMock(ContainerInterface::class); $container = $this->createMock(ContainerInterface::class);
$container->expects($this->any()) $container->expects($this->any())
@ -281,6 +255,6 @@ class SendMessageMiddlewareTest extends MiddlewareTestCase
return $senders[$id]; return $senders[$id];
}); });
return new SendersLocator($sendersMap, $container, $sendAndHandle); return new SendersLocator($sendersMap, $container);
} }
} }

View File

@ -38,8 +38,8 @@ class RetryIntegrationTest extends TestCase
$senderLocator->method('get')->with('sender_alias')->willReturn($senderAndReceiver); $senderLocator->method('get')->with('sender_alias')->willReturn($senderAndReceiver);
$senderLocator = new SendersLocator([DummyMessage::class => ['sender_alias']], $senderLocator); $senderLocator = new SendersLocator([DummyMessage::class => ['sender_alias']], $senderLocator);
$handler = new DummyMessageHandlerFailingFirstTimes(0, 'A'); $handler = new DummyMessageHandlerFailingFirstTimes(0);
$throwingHandler = new DummyMessageHandlerFailingFirstTimes(1, 'B'); $throwingHandler = new DummyMessageHandlerFailingFirstTimes(1);
$handlerLocator = new HandlersLocator([ $handlerLocator = new HandlersLocator([
DummyMessage::class => [ DummyMessage::class => [
new HandlerDescriptor($handler, ['alias' => 'first']), new HandlerDescriptor($handler, ['alias' => 'first']),

View File

@ -50,41 +50,34 @@ class RoutableMessageBusTest extends TestCase
->willReturn($envelope); ->willReturn($envelope);
$container = $this->createMock(ContainerInterface::class); $container = $this->createMock(ContainerInterface::class);
$container->expects($this->once())->method('has')->with(MessageBusInterface::class)
->willReturn(true);
$container->expects($this->once())->method('get')->with(MessageBusInterface::class)
->willReturn($defaultBus);
$routableBus = new RoutableMessageBus($container); $routableBus = new RoutableMessageBus($container, $defaultBus);
$this->assertSame($envelope, $routableBus->dispatch($envelope, [$stamp])); $this->assertSame($envelope, $routableBus->dispatch($envelope, [$stamp]));
} }
public function testItExceptionOnDefaultBusNotFound()
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage(sprintf('Bus named "%s" does not exist.', MessageBusInterface::class));
$envelope = new Envelope(new \stdClass());
$container = $this->createMock(ContainerInterface::class);
$container->expects($this->once())->method('has')->with(MessageBusInterface::class)
->willReturn(false);
$routableBus = new RoutableMessageBus($container);
$routableBus->dispatch($envelope);
}
public function testItExceptionOnBusNotFound() public function testItExceptionOnBusNotFound()
{ {
$this->expectException(InvalidArgumentException::class); $this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage(sprintf('Bus named "%s" does not exist.', 'foo_bus')); $this->expectExceptionMessage('Bus named "my_cool_bus" does not exist.');
$envelope = new Envelope(new \stdClass(), [new BusNameStamp('foo_bus')]); $envelope = new Envelope(new \stdClass(), [
new BusNameStamp('my_cool_bus'),
]);
$container = $this->createMock(ContainerInterface::class); $container = $this->createMock(ContainerInterface::class);
$container->expects($this->once())->method('has')->willReturn(false); $routableBus = new RoutableMessageBus($container);
$routableBus->dispatch($envelope);
}
public function testItExceptionOnDefaultBusNotFound()
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Envelope is missing a BusNameStamp and no fallback message bus is configured on RoutableMessageBus.');
$envelope = new Envelope(new \stdClass());
$container = $this->createMock(ContainerInterface::class);
$routableBus = new RoutableMessageBus($container); $routableBus = new RoutableMessageBus($container);
$routableBus->dispatch($envelope); $routableBus->dispatch($envelope);
} }

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Messenger\Tests\Stamp; namespace Symfony\Component\Messenger\Tests\Stamp;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Component\Debug\Exception\FlattenException;
use Symfony\Component\Messenger\Stamp\RedeliveryStamp; use Symfony\Component\Messenger\Stamp\RedeliveryStamp;
class RedeliveryStampTest extends TestCase class RedeliveryStampTest extends TestCase
@ -21,5 +22,16 @@ class RedeliveryStampTest extends TestCase
$stamp = new RedeliveryStamp(10, 'sender_alias'); $stamp = new RedeliveryStamp(10, 'sender_alias');
$this->assertSame(10, $stamp->getRetryCount()); $this->assertSame(10, $stamp->getRetryCount());
$this->assertSame('sender_alias', $stamp->getSenderClassOrAlias()); $this->assertSame('sender_alias', $stamp->getSenderClassOrAlias());
$this->assertInstanceOf(\DateTimeInterface::class, $stamp->getRedeliveredAt());
$this->assertNull($stamp->getExceptionMessage());
$this->assertNull($stamp->getFlattenException());
}
public function testGettersPopulated()
{
$flattenException = new FlattenException();
$stamp = new RedeliveryStamp(10, 'sender_alias', 'exception message', $flattenException);
$this->assertSame('exception message', $stamp->getExceptionMessage());
$this->assertSame($flattenException, $stamp->getFlattenException());
} }
} }

View File

@ -12,22 +12,13 @@
namespace Symfony\Component\Messenger\Tests\Stamp; namespace Symfony\Component\Messenger\Tests\Stamp;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Component\Debug\Exception\FlattenException;
use Symfony\Component\Messenger\Stamp\SentToFailureTransportStamp; use Symfony\Component\Messenger\Stamp\SentToFailureTransportStamp;
class SentToFailureTransportStampTest extends TestCase class SentToFailureTransportStampTest extends TestCase
{ {
public function testGetters() public function testGetOriginalReceiverName()
{ {
$flattenException = new FlattenException(); $stamp = new SentToFailureTransportStamp('original_receiver');
$stamp = new SentToFailureTransportStamp(
'exception message',
'original_receiver',
$flattenException
);
$this->assertSame('exception message', $stamp->getExceptionMessage());
$this->assertSame('original_receiver', $stamp->getOriginalReceiverName()); $this->assertSame('original_receiver', $stamp->getOriginalReceiverName());
$this->assertSame($flattenException, $stamp->getFlattenException());
$this->assertInstanceOf(\DateTimeInterface::class, $stamp->getSentAt());
} }
} }

View File

@ -37,11 +37,8 @@ class DoctrineIntegrationTest extends TestCase
*/ */
public function createConnection() public function createConnection()
{ {
if ($dsn = getenv('MESSENGER_DOCTRINE_DSN')) { $dsn = getenv('MESSENGER_DOCTRINE_DSN') ?: 'sqlite:///'.sys_get_temp_dir().'/symfony.messenger.sqlite';
$this->driverConnection = DriverManager::getConnection(['url' => $dsn]); $this->driverConnection = DriverManager::getConnection(['url' => $dsn]);
} else {
$this->driverConnection = DriverManager::getConnection(['pdo' => new \PDO('sqlite:'.sys_get_temp_dir().'/symfony.messenger.sqlite')]);
}
$this->connection = new Connection([], $this->driverConnection); $this->connection = new Connection([], $this->driverConnection);
// call send to auto-setup the table // call send to auto-setup the table
$this->connection->setup(); $this->connection->setup();

View File

@ -30,14 +30,12 @@ class SendersLocator implements SendersLocatorInterface
private $sendersMap; private $sendersMap;
private $sendersLocator; private $sendersLocator;
private $useLegacyLookup = false; private $useLegacyLookup = false;
private $sendAndHandle;
/** /**
* @param string[][] $sendersMap An array, keyed by "type", set to an array of sender aliases * @param string[][] $sendersMap An array, keyed by "type", set to an array of sender aliases
* @param ContainerInterface $sendersLocator Locator of senders, keyed by sender alias * @param ContainerInterface $sendersLocator Locator of senders, keyed by sender alias
* @param bool[] $sendAndHandle
*/ */
public function __construct(array $sendersMap, /*ContainerInterface*/ $sendersLocator = null, array $sendAndHandle = []) public function __construct(array $sendersMap, /*ContainerInterface*/ $sendersLocator = null)
{ {
$this->sendersMap = $sendersMap; $this->sendersMap = $sendersMap;
@ -45,21 +43,17 @@ class SendersLocator implements SendersLocatorInterface
@trigger_error(sprintf('"%s::__construct()" requires a "%s" as 2nd argument. Not doing so is deprecated since Symfony 4.3 and will be required in 5.0.', __CLASS__, ContainerInterface::class), E_USER_DEPRECATED); @trigger_error(sprintf('"%s::__construct()" requires a "%s" as 2nd argument. Not doing so is deprecated since Symfony 4.3 and will be required in 5.0.', __CLASS__, ContainerInterface::class), E_USER_DEPRECATED);
// "%s" requires a "%s" as 2nd argument. Not doing so is deprecated since Symfony 4.3 and will be required in 5.0.' // "%s" requires a "%s" as 2nd argument. Not doing so is deprecated since Symfony 4.3 and will be required in 5.0.'
$this->sendersLocator = new ServiceLocator([]); $this->sendersLocator = new ServiceLocator([]);
$this->sendAndHandle = $sendersLocator;
$this->useLegacyLookup = true; $this->useLegacyLookup = true;
} else { } else {
$this->sendersLocator = $sendersLocator; $this->sendersLocator = $sendersLocator;
$this->sendAndHandle = $sendAndHandle;
} }
} }
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function getSenders(Envelope $envelope, ?bool &$handle = false): iterable public function getSenders(Envelope $envelope): iterable
{ {
$handle = false;
$sender = null;
$seen = []; $seen = [];
foreach (HandlersLocator::listTypes($envelope) as $type) { foreach (HandlersLocator::listTypes($envelope) as $type) {
@ -71,8 +65,6 @@ class SendersLocator implements SendersLocatorInterface
} }
} }
$handle = $handle ?: $this->sendAndHandle[$type] ?? false;
continue; continue;
} }
@ -87,11 +79,7 @@ class SendersLocator implements SendersLocatorInterface
yield $senderAlias => $sender; yield $senderAlias => $sender;
} }
} }
$handle = $handle ?: $this->sendAndHandle[$type] ?? false;
} }
$handle = $handle || null === $sender;
} }
public function getSenderByAlias(string $alias): SenderInterface public function getSenderByAlias(string $alias): SenderInterface

View File

@ -27,12 +27,9 @@ interface SendersLocatorInterface
/** /**
* Gets the senders for the given message name. * Gets the senders for the given message name.
* *
* @param bool|null &$handle True after calling the method when the next middleware
* should also get the message; false otherwise
*
* @return iterable|SenderInterface[] Indexed by sender alias if available * @return iterable|SenderInterface[] Indexed by sender alias if available
*/ */
public function getSenders(Envelope $envelope, ?bool &$handle = false): iterable; public function getSenders(Envelope $envelope): iterable;
/** /**
* Returns a specific sender by its alias. * Returns a specific sender by its alias.

View File

@ -555,8 +555,12 @@ abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerIn
* *
* @internal * @internal
*/ */
protected function createChildContext(array $parentContext, $attribute/*, string $format = null */) protected function createChildContext(array $parentContext, $attribute/*, ?string $format */)
{ {
if (\func_num_args() < 3) {
@trigger_error(sprintf('Method "%s::%s()" will have a third "?string $format" argument in version 5.0; not defining it is deprecated since Symfony 4.3.', \get_class($this), __FUNCTION__), E_USER_DEPRECATED);
$format = null;
}
if (isset($parentContext[self::ATTRIBUTES][$attribute])) { if (isset($parentContext[self::ATTRIBUTES][$attribute])) {
$parentContext[self::ATTRIBUTES] = $parentContext[self::ATTRIBUTES][$attribute]; $parentContext[self::ATTRIBUTES] = $parentContext[self::ATTRIBUTES][$attribute];
} else { } else {

View File

@ -41,7 +41,7 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
/** /**
* How to track the current depth in the context. * How to track the current depth in the context.
*/ */
private const DEPTH_KEY_PATTERN = 'depth_%s::%s'; public const DEPTH_KEY_PATTERN = 'depth_%s::%s';
/** /**
* While denormalizing, we can verify that types match. * While denormalizing, we can verify that types match.
@ -559,18 +559,19 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
* We must not mix up the attribute cache between parent and children. * We must not mix up the attribute cache between parent and children.
* *
* {@inheritdoc} * {@inheritdoc}
*
* @param string|null $format
*/ */
protected function createChildContext(array $parentContext, $attribute/*, string $format = null */) protected function createChildContext(array $parentContext, $attribute/*, ?string $format */)
{ {
if (\func_num_args() >= 3) { if (\func_num_args() >= 3) {
$format = \func_get_arg(2); $format = \func_get_arg(2);
} else { } else {
// will be deprecated in version 4 @trigger_error(sprintf('Method "%s::%s()" will have a third "?string $format" argument in version 5.0; not defining it is deprecated since Symfony 4.3.', \get_class($this), __FUNCTION__), E_USER_DEPRECATED);
$format = null; $format = null;
} }
$context = parent::createChildContext($parentContext, $attribute, $format); $context = parent::createChildContext($parentContext, $attribute, $format);
// format is already included in the cache_key of the parent.
$context['cache_key'] = $this->getCacheKey($format, $context); $context['cache_key'] = $this->getCacheKey($format, $context);
return $context; return $context;

0
src/Symfony/Component/Validator/Constraints/Json.php Executable file → Normal file
View File

View File

View File

@ -11,6 +11,7 @@
namespace Symfony\Component\Validator\Mapping\Loader; namespace Symfony\Component\Validator\Mapping\Loader;
use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; use Symfony\Component\PropertyInfo\PropertyListExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
use Symfony\Component\PropertyInfo\Type as PropertyInfoType; use Symfony\Component\PropertyInfo\Type as PropertyInfoType;
@ -29,12 +30,14 @@ final class PropertyInfoLoader implements LoaderInterface
{ {
private $listExtractor; private $listExtractor;
private $typeExtractor; private $typeExtractor;
private $accessExtractor;
private $classValidatorRegexp; private $classValidatorRegexp;
public function __construct(PropertyListExtractorInterface $listExtractor, PropertyTypeExtractorInterface $typeExtractor, string $classValidatorRegexp = null) public function __construct(PropertyListExtractorInterface $listExtractor, PropertyTypeExtractorInterface $typeExtractor, PropertyAccessExtractorInterface $accessExtractor, string $classValidatorRegexp = null)
{ {
$this->listExtractor = $listExtractor; $this->listExtractor = $listExtractor;
$this->typeExtractor = $typeExtractor; $this->typeExtractor = $typeExtractor;
$this->accessExtractor = $accessExtractor;
$this->classValidatorRegexp = $classValidatorRegexp; $this->classValidatorRegexp = $classValidatorRegexp;
} }
@ -53,6 +56,10 @@ final class PropertyInfoLoader implements LoaderInterface
} }
foreach ($properties as $property) { foreach ($properties as $property) {
if (false === $this->accessExtractor->isWritable($className, $property)) {
continue;
}
$types = $this->typeExtractor->getTypes($className, $property); $types = $this->typeExtractor->getTypes($className, $property);
if (null === $types) { if (null === $types) {
continue; continue;

View File

@ -46,4 +46,6 @@ class PropertyInfoLoaderEntity
* }) * })
*/ */
public $alreadyPartiallyMappedCollection; public $alreadyPartiallyMappedCollection;
public $readOnly;
} }

View File

@ -45,6 +45,7 @@ class PropertyInfoLoaderTest extends TestCase
'alreadyMappedNotNull', 'alreadyMappedNotNull',
'alreadyMappedNotBlank', 'alreadyMappedNotBlank',
'alreadyPartiallyMappedCollection', 'alreadyPartiallyMappedCollection',
'readOnly',
]) ])
; ;
$propertyInfoStub $propertyInfoStub
@ -58,11 +59,27 @@ class PropertyInfoLoaderTest extends TestCase
[new Type(Type::BUILTIN_TYPE_FLOAT, true)], // The existing constraint is float [new Type(Type::BUILTIN_TYPE_FLOAT, true)], // The existing constraint is float
[new Type(Type::BUILTIN_TYPE_STRING, true)], [new Type(Type::BUILTIN_TYPE_STRING, true)],
[new Type(Type::BUILTIN_TYPE_STRING, true)], [new Type(Type::BUILTIN_TYPE_STRING, true)],
[new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true, null, new Type(Type::BUILTIN_TYPE_FLOAT))] [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true, null, new Type(Type::BUILTIN_TYPE_FLOAT))],
[new Type(Type::BUILTIN_TYPE_STRING)]
))
;
$propertyInfoStub
->method('isWritable')
->will($this->onConsecutiveCalls(
true,
true,
true,
true,
true,
true,
true,
true,
true,
false
)) ))
; ;
$propertyInfoLoader = new PropertyInfoLoader($propertyInfoStub, $propertyInfoStub); $propertyInfoLoader = new PropertyInfoLoader($propertyInfoStub, $propertyInfoStub, $propertyInfoStub);
$validator = Validation::createValidatorBuilder() $validator = Validation::createValidatorBuilder()
->enableAnnotationMapping() ->enableAnnotationMapping()
@ -137,6 +154,9 @@ class PropertyInfoLoaderTest extends TestCase
$this->assertSame('string', $alreadyPartiallyMappedCollectionConstraints[0]->constraints[0]->type); $this->assertSame('string', $alreadyPartiallyMappedCollectionConstraints[0]->constraints[0]->type);
$this->assertInstanceOf(Iban::class, $alreadyPartiallyMappedCollectionConstraints[0]->constraints[1]); $this->assertInstanceOf(Iban::class, $alreadyPartiallyMappedCollectionConstraints[0]->constraints[1]);
$this->assertInstanceOf(NotNull::class, $alreadyPartiallyMappedCollectionConstraints[0]->constraints[2]); $this->assertInstanceOf(NotNull::class, $alreadyPartiallyMappedCollectionConstraints[0]->constraints[2]);
$readOnlyMetadata = $classMetadata->getPropertyMetadata('readOnly');
$this->assertEmpty($readOnlyMetadata);
} }
/** /**
@ -154,7 +174,7 @@ class PropertyInfoLoaderTest extends TestCase
->willReturn([new Type(Type::BUILTIN_TYPE_STRING)]) ->willReturn([new Type(Type::BUILTIN_TYPE_STRING)])
; ;
$propertyInfoLoader = new PropertyInfoLoader($propertyInfoStub, $propertyInfoStub, $classValidatorRegexp); $propertyInfoLoader = new PropertyInfoLoader($propertyInfoStub, $propertyInfoStub, $propertyInfoStub, $classValidatorRegexp);
$classMetadata = new ClassMetadata(PropertyInfoLoaderEntity::class); $classMetadata = new ClassMetadata(PropertyInfoLoaderEntity::class);
$this->assertSame($expected, $propertyInfoLoader->loadClassMetadata($classMetadata)); $this->assertSame($expected, $propertyInfoLoader->loadClassMetadata($classMetadata));

View File

@ -12,7 +12,7 @@
namespace Symfony\Component\Workflow; namespace Symfony\Component\Workflow;
/** /**
* To learn more about how workflow events work check the documentation * To learn more about how workflow events work, check the documentation
* entry at {@link https://symfony.com/doc/current/workflow/usage.html#using-events}. * entry at {@link https://symfony.com/doc/current/workflow/usage.html#using-events}.
*/ */
final class WorkflowEvents final class WorkflowEvents
@ -43,7 +43,7 @@ final class WorkflowEvents
const ENTERED = 'workflow.entered'; const ENTERED = 'workflow.entered';
/** /**
* @Event("Symfony\Component\Workflow\Event\EnteredEvent") * @Event("Symfony\Component\Workflow\Event\LeaveEvent")
*/ */
const LEAVE = 'workflow.leave'; const LEAVE = 'workflow.leave';