Merge branch '4.3' into 4.4
* 4.3: [Cache] replace getNsSeparator by NS_SEPARATOR on AbstractTrait [Cache] fix versioning with SimpleCacheAdapter [Messenger] fix AMQP delay queue to be per exchange Fix expired lock not cleaned [HttpClient] throw DecodingExceptionInterface when toArray() fails because of content-type error [HttpFoundation] Fix SA/phpdoc JsonResponse [DI] Show the right class autowired when providing a non-existing class in constructor SimpleCacheAdapter fails to cache any item if a namespace is used validate composite constraints in all groups [Serializer] Handle true and false appropriately in CSV encoder [Messenger] improve logs [Messenger] fix delay delivery for non-fanout exchanges Parameterize Mailgun's region Fix binary operation `+`, `-` or `*` on string [VarDumper] fix dumping objects that implement __debugInfo() [HttpClient] Don't use CurlHttpClient on Windows when curl.cainfo is not set Add statement to fileLink to ignore href code when no fileLink. [Routing] fix absolute url generation when scheme is not known
This commit is contained in:
commit
651de173df
@ -557,8 +557,11 @@ class TextDescriptor extends Descriptor
|
||||
}
|
||||
|
||||
$fileLink = $this->fileLinkFormatter->format($r->getFileName(), $r->getStartLine());
|
||||
if ($fileLink) {
|
||||
return sprintf('<href=%s>%s</>', $fileLink, $anchorText);
|
||||
}
|
||||
|
||||
return sprintf('<href=%s>%s</>', $fileLink, $anchorText);
|
||||
return $anchorText;
|
||||
}
|
||||
|
||||
private function formatCallable($callable): string
|
||||
|
@ -26,6 +26,11 @@ use Symfony\Contracts\Cache\CacheInterface;
|
||||
*/
|
||||
abstract class AbstractAdapter implements AdapterInterface, CacheInterface, LoggerAwareInterface, ResettableInterface
|
||||
{
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected const NS_SEPARATOR = ':';
|
||||
|
||||
use AbstractAdapterTrait;
|
||||
use ContractsTrait;
|
||||
|
||||
@ -34,7 +39,7 @@ abstract class AbstractAdapter implements AdapterInterface, CacheInterface, Logg
|
||||
|
||||
protected function __construct(string $namespace = '', int $defaultLifetime = 0)
|
||||
{
|
||||
$this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace).':';
|
||||
$this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace).static::NS_SEPARATOR;
|
||||
if (null !== $this->maxIdLength && \strlen($namespace) > $this->maxIdLength - 24) {
|
||||
throw new InvalidArgumentException(sprintf('Namespace must be %d chars max, %d given ("%s")', $this->maxIdLength - 24, \strlen($namespace), $namespace));
|
||||
}
|
||||
|
@ -23,6 +23,11 @@ use Symfony\Component\Cache\Traits\ProxyTrait;
|
||||
*/
|
||||
class Psr16Adapter extends AbstractAdapter implements PruneableInterface, ResettableInterface
|
||||
{
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected const NS_SEPARATOR = '_';
|
||||
|
||||
use ProxyTrait;
|
||||
|
||||
private $miss;
|
||||
|
@ -27,6 +27,11 @@ use Symfony\Contracts\Cache\CacheInterface;
|
||||
*/
|
||||
abstract class AbstractCache implements Psr16CacheInterface, LoggerAwareInterface, ResettableInterface
|
||||
{
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected const NS_SEPARATOR = ':';
|
||||
|
||||
use AbstractTrait {
|
||||
deleteItems as private;
|
||||
AbstractTrait::deleteItem as delete;
|
||||
|
@ -56,7 +56,7 @@ class MaxIdLengthAdapterTest extends TestCase
|
||||
$reflectionProperty->setValue($cache, true);
|
||||
|
||||
// Versioning enabled
|
||||
$this->assertEquals('--------------------------:1/------------', $reflectionMethod->invokeArgs($cache, [str_repeat('-', 12)]));
|
||||
$this->assertEquals('--------------------------:1:------------', $reflectionMethod->invokeArgs($cache, [str_repeat('-', 12)]));
|
||||
$this->assertLessThanOrEqual(50, \strlen($reflectionMethod->invokeArgs($cache, [str_repeat('-', 12)])));
|
||||
$this->assertLessThanOrEqual(50, \strlen($reflectionMethod->invokeArgs($cache, [str_repeat('-', 23)])));
|
||||
$this->assertLessThanOrEqual(50, \strlen($reflectionMethod->invokeArgs($cache, [str_repeat('-', 40)])));
|
||||
|
@ -11,6 +11,7 @@
|
||||
|
||||
namespace Symfony\Component\Cache\Tests\Adapter;
|
||||
|
||||
use Symfony\Component\Cache\Adapter\ArrayAdapter;
|
||||
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
|
||||
use Symfony\Component\Cache\Adapter\Psr16Adapter;
|
||||
use Symfony\Component\Cache\Psr16Cache;
|
||||
@ -28,4 +29,14 @@ class Psr16AdapterTest extends AdapterTestCase
|
||||
{
|
||||
return new Psr16Adapter(new Psr16Cache(new FilesystemAdapter()), '', $defaultLifetime);
|
||||
}
|
||||
|
||||
public function testValidCacheKeyWithNamespace()
|
||||
{
|
||||
$cache = new Psr16Adapter(new Psr16Cache(new ArrayAdapter()), 'some_namespace', 0);
|
||||
$item = $cache->getItem('my_key');
|
||||
$item->set('someValue');
|
||||
$cache->save($item);
|
||||
|
||||
$this->assertTrue($cache->getItem('my_key')->isHit(), 'Stored item is successfully retrieved.');
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ namespace Symfony\Component\Cache\Tests\Adapter;
|
||||
|
||||
use Symfony\Component\Cache\Adapter\SimpleCacheAdapter;
|
||||
use Symfony\Component\Cache\Simple\FilesystemCache;
|
||||
use Symfony\Component\Cache\Simple\ArrayCache;
|
||||
|
||||
/**
|
||||
* @group time-sensitive
|
||||
@ -28,4 +29,14 @@ class SimpleCacheAdapterTest extends AdapterTestCase
|
||||
{
|
||||
return new SimpleCacheAdapter(new FilesystemCache(), '', $defaultLifetime);
|
||||
}
|
||||
|
||||
public function testValidCacheKeyWithNamespace()
|
||||
{
|
||||
$cache = new SimpleCacheAdapter(new ArrayCache(), 'some_namespace', 0);
|
||||
$item = $cache->getItem('my_key');
|
||||
$item->set('someValue');
|
||||
$cache->save($item);
|
||||
|
||||
$this->assertTrue($cache->getItem('my_key')->isHit(), 'Stored item is successfully retrieved.');
|
||||
}
|
||||
}
|
||||
|
@ -107,9 +107,9 @@ trait AbstractTrait
|
||||
{
|
||||
$this->deferred = [];
|
||||
if ($cleared = $this->versioningIsEnabled) {
|
||||
$namespaceVersion = substr_replace(base64_encode(pack('V', mt_rand())), ':', 5);
|
||||
$namespaceVersion = substr_replace(base64_encode(pack('V', mt_rand())), static::NS_SEPARATOR, 5);
|
||||
try {
|
||||
$cleared = $this->doSave(['/'.$this->namespace => $namespaceVersion], 0);
|
||||
$cleared = $this->doSave([static::NS_SEPARATOR.$this->namespace => $namespaceVersion], 0);
|
||||
} catch (\Exception $e) {
|
||||
$cleared = false;
|
||||
}
|
||||
@ -243,14 +243,14 @@ trait AbstractTrait
|
||||
{
|
||||
if ($this->versioningIsEnabled && '' === $this->namespaceVersion) {
|
||||
$this->ids = [];
|
||||
$this->namespaceVersion = '1/';
|
||||
$this->namespaceVersion = '1'.static::NS_SEPARATOR;
|
||||
try {
|
||||
foreach ($this->doFetch(['/'.$this->namespace]) as $v) {
|
||||
foreach ($this->doFetch([static::NS_SEPARATOR.$this->namespace]) as $v) {
|
||||
$this->namespaceVersion = $v;
|
||||
}
|
||||
if ('1:' === $this->namespaceVersion) {
|
||||
$this->namespaceVersion = substr_replace(base64_encode(pack('V', time())), ':', 5);
|
||||
$this->doSave(['@'.$this->namespace => $this->namespaceVersion], 0);
|
||||
if ('1'.static::NS_SEPARATOR === $this->namespaceVersion) {
|
||||
$this->namespaceVersion = substr_replace(base64_encode(pack('V', time())), static::NS_SEPARATOR, 5);
|
||||
$this->doSave([static::NS_SEPARATOR.$this->namespace => $this->namespaceVersion], 0);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
@ -267,7 +267,7 @@ trait AbstractTrait
|
||||
}
|
||||
if (\strlen($id = $this->namespace.$this->namespaceVersion.$key) > $this->maxIdLength) {
|
||||
// Use MD5 to favor speed over security, which is not an issue here
|
||||
$this->ids[$key] = $id = substr_replace(base64_encode(hash('md5', $key, true)), ':', -(\strlen($this->namespaceVersion) + 2));
|
||||
$this->ids[$key] = $id = substr_replace(base64_encode(hash('md5', $key, true)), static::NS_SEPARATOR, -(\strlen($this->namespaceVersion) + 2));
|
||||
$id = $this->namespace.$this->namespaceVersion.$id;
|
||||
}
|
||||
|
||||
|
@ -379,13 +379,14 @@ class AutowirePass extends AbstractRecursivePass
|
||||
$container->setAliases($this->container->getAliases());
|
||||
$container->setDefinitions($this->container->getDefinitions());
|
||||
$container->setResourceTracking(false);
|
||||
$currentId = $this->currentId;
|
||||
|
||||
return function () use ($container, $reference, $label) {
|
||||
return $this->createTypeNotFoundMessage($container, $reference, $label);
|
||||
return function () use ($container, $reference, $label, $currentId) {
|
||||
return $this->createTypeNotFoundMessage($container, $reference, $label, $currentId);
|
||||
};
|
||||
}
|
||||
|
||||
private function createTypeNotFoundMessage(ContainerBuilder $container, TypedReference $reference, $label)
|
||||
private function createTypeNotFoundMessage(ContainerBuilder $container, TypedReference $reference, $label, string $currentId)
|
||||
{
|
||||
if (!$r = $container->getReflectionClass($type = $reference->getType(), false)) {
|
||||
// either $type does not exist or a parent class does not exist
|
||||
@ -409,7 +410,7 @@ class AutowirePass extends AbstractRecursivePass
|
||||
}
|
||||
}
|
||||
|
||||
$message = sprintf('Cannot autowire service "%s": %s %s', $this->currentId, $label, $message);
|
||||
$message = sprintf('Cannot autowire service "%s": %s %s', $currentId, $label, $message);
|
||||
|
||||
if (null !== $this->lastFailure) {
|
||||
$message = $this->lastFailure."\n".$message;
|
||||
|
@ -50,6 +50,22 @@ class AutowirePassTest extends TestCase
|
||||
$this->assertEquals(Foo::class, (string) $container->getDefinition('bar')->getArgument(0));
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\DependencyInjection\Exception\AutowiringFailedException
|
||||
* @expectedExceptionMessage Cannot autowire service "Symfony\Component\DependencyInjection\Tests\CompilerEslaAction": argument "$notExisting" of method "Symfony\Component\DependencyInjection\Tests\Compiler\ElsaAction::__construct()" has type "Symfony\Component\DependencyInjection\Tests\Compiler\NotExisting" but this class was not found.
|
||||
*/
|
||||
public function testProcessNotExistingActionParam()
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
|
||||
$container->register(Foo::class);
|
||||
$barDefinition = $container->register(__NAMESPACE__.'EslaAction', __NAMESPACE__.'\ElsaAction');
|
||||
$barDefinition->setAutowired(true);
|
||||
|
||||
(new ResolveClassPass())->process($container);
|
||||
(new AutowirePass())->process($container);
|
||||
}
|
||||
|
||||
public function testProcessVariadic()
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
|
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Component\DependencyInjection\Tests\Fixtures;
|
||||
|
||||
class ConstructNotExists
|
||||
{
|
||||
public function __construct(NotExist $notExist)
|
||||
{
|
||||
}
|
||||
}
|
@ -419,3 +419,10 @@ class NonAutowirableDecorator implements DecoratorInterface
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
final class ElsaAction
|
||||
{
|
||||
public function __construct(NotExisting $notExisting)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,7 @@
|
||||
services:
|
||||
_defaults:
|
||||
public: true
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
|
||||
Symfony\Component\DependencyInjection\Tests\Fixtures\ConstructNotExists: ~
|
@ -810,6 +810,18 @@ class YamlFileLoaderTest extends TestCase
|
||||
], array_map(function (BoundArgument $v) { return $v->getValues()[0]; }, $definition->getBindings()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException
|
||||
* @expectedExceptionMessage Cannot autowire service "Symfony\Component\DependencyInjection\Tests\Fixtures\ConstructNotExists": argument "$notExist" of method "__construct()" has type "Symfony\Component\DependencyInjection\Tests\Fixtures\NotExist" but this class was not found.
|
||||
*/
|
||||
public function testProcessNotExistingActionParam()
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
$loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml'));
|
||||
$loader->load('services_not_existing.yml');
|
||||
$container->compile();
|
||||
}
|
||||
|
||||
public function testFqcnLazyProxy()
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
|
@ -21,7 +21,7 @@ class BirthdayType extends AbstractType
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefault('years', range(date('Y') - 120, date('Y')));
|
||||
$resolver->setDefault('years', range((int) date('Y') - 120, date('Y')));
|
||||
|
||||
$resolver->setAllowedTypes('years', 'array');
|
||||
}
|
||||
|
@ -259,7 +259,7 @@ class DateType extends AbstractType
|
||||
};
|
||||
|
||||
$resolver->setDefaults([
|
||||
'years' => range(date('Y') - 5, date('Y') + 5),
|
||||
'years' => range((int) date('Y') - 5, (int) date('Y') + 5),
|
||||
'months' => range(1, 12),
|
||||
'days' => range(1, 31),
|
||||
'widget' => 'choice',
|
||||
|
@ -13,6 +13,7 @@ namespace Symfony\Component\Form\Extension\Validator\Constraints;
|
||||
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\Constraints\Composite;
|
||||
use Symfony\Component\Validator\Constraints\GroupSequence;
|
||||
use Symfony\Component\Validator\Constraints\Valid;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
@ -90,7 +91,9 @@ class FormValidator extends ConstraintValidator
|
||||
$validator->atPath('data')->validate($form->getData(), $constraint, $group);
|
||||
|
||||
// Prevent duplicate validation
|
||||
continue 2;
|
||||
if (!$constraint instanceof Composite) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\SubmitButtonBuilder;
|
||||
use Symfony\Component\Translation\IdentityTranslator;
|
||||
use Symfony\Component\Validator\Constraints\Collection;
|
||||
use Symfony\Component\Validator\Constraints\GroupSequence;
|
||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
use Symfony\Component\Validator\Constraints\NotNull;
|
||||
@ -714,6 +715,63 @@ class FormValidatorTest extends ConstraintValidatorTestCase
|
||||
$this->assertSame($constraint, $context->getViolations()->get(0)->getConstraint());
|
||||
}
|
||||
|
||||
public function testNonCompositeConstraintValidatedOnce()
|
||||
{
|
||||
$form = $this
|
||||
->getBuilder('form', null, [
|
||||
'constraints' => [new NotBlank(['groups' => ['foo', 'bar']])],
|
||||
'validation_groups' => ['foo', 'bar'],
|
||||
])
|
||||
->setCompound(false)
|
||||
->getForm();
|
||||
$form->submit('');
|
||||
|
||||
$context = new ExecutionContext(Validation::createValidator(), $form, new IdentityTranslator());
|
||||
$this->validator->initialize($context);
|
||||
$this->validator->validate($form, new Form());
|
||||
|
||||
$this->assertCount(1, $context->getViolations());
|
||||
$this->assertSame('This value should not be blank.', $context->getViolations()[0]->getMessage());
|
||||
$this->assertSame('data', $context->getViolations()[0]->getPropertyPath());
|
||||
}
|
||||
|
||||
public function testCompositeConstraintValidatedInEachGroup()
|
||||
{
|
||||
$form = $this->getBuilder('form', null, [
|
||||
'constraints' => [
|
||||
new Collection([
|
||||
'field1' => new NotBlank([
|
||||
'groups' => ['field1'],
|
||||
]),
|
||||
'field2' => new NotBlank([
|
||||
'groups' => ['field2'],
|
||||
]),
|
||||
]),
|
||||
],
|
||||
'validation_groups' => ['field1', 'field2'],
|
||||
])
|
||||
->setData([])
|
||||
->setCompound(true)
|
||||
->setDataMapper(new PropertyPathMapper())
|
||||
->getForm();
|
||||
$form->add($this->getForm('field1'));
|
||||
$form->add($this->getForm('field2'));
|
||||
$form->submit([
|
||||
'field1' => '',
|
||||
'field2' => '',
|
||||
]);
|
||||
|
||||
$context = new ExecutionContext(Validation::createValidator(), $form, new IdentityTranslator());
|
||||
$this->validator->initialize($context);
|
||||
$this->validator->validate($form, new Form());
|
||||
|
||||
$this->assertCount(2, $context->getViolations());
|
||||
$this->assertSame('This value should not be blank.', $context->getViolations()[0]->getMessage());
|
||||
$this->assertSame('data[field1]', $context->getViolations()[0]->getPropertyPath());
|
||||
$this->assertSame('This value should not be blank.', $context->getViolations()[1]->getMessage());
|
||||
$this->assertSame('data[field2]', $context->getViolations()[1]->getPropertyPath());
|
||||
}
|
||||
|
||||
protected function createValidator()
|
||||
{
|
||||
return new FormValidator();
|
||||
|
@ -11,7 +11,7 @@
|
||||
|
||||
namespace Symfony\Component\HttpClient\Exception;
|
||||
|
||||
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
|
||||
|
||||
/**
|
||||
* Thrown by responses' toArray() method when their content cannot be JSON-decoded.
|
||||
@ -20,6 +20,6 @@ use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||
*
|
||||
* @experimental in 4.3
|
||||
*/
|
||||
final class JsonException extends \JsonException implements TransportExceptionInterface
|
||||
final class JsonException extends \JsonException implements DecodingExceptionInterface
|
||||
{
|
||||
}
|
||||
|
@ -32,7 +32,11 @@ final class HttpClient
|
||||
public static function create(array $defaultOptions = [], int $maxHostConnections = 6, int $maxPendingPushes = 50): HttpClientInterface
|
||||
{
|
||||
if (\extension_loaded('curl')) {
|
||||
return new CurlHttpClient($defaultOptions, $maxHostConnections, $maxPendingPushes);
|
||||
if ('\\' !== \DIRECTORY_SEPARATOR || ini_get('curl.cainfo') || ini_get('openssl.cafile') || ini_get('openssl.capath')) {
|
||||
return new CurlHttpClient($defaultOptions, $maxHostConnections, $maxPendingPushes);
|
||||
}
|
||||
|
||||
@trigger_error('Configure the "curl.cainfo", "openssl.cafile" or "openssl.capath" php.ini setting to enable the CurlHttpClient', E_USER_WARNING);
|
||||
}
|
||||
|
||||
return new NativeHttpClient($defaultOptions, $maxHostConnections);
|
||||
|
@ -336,7 +336,7 @@ final class CurlResponse implements ResponseInterface
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($certinfo = curl_getinfo($ch, CURLINFO_CERTINFO)) {
|
||||
if (\function_exists('openssl_x509_read') && $certinfo = curl_getinfo($ch, CURLINFO_CERTINFO)) {
|
||||
$info['peer_certificate_chain'] = array_map('openssl_x509_read', array_column($certinfo, 'Cert'));
|
||||
}
|
||||
|
||||
|
@ -22,7 +22,7 @@
|
||||
"require": {
|
||||
"php": "^7.1.3",
|
||||
"psr/log": "^1.0",
|
||||
"symfony/http-client-contracts": "^1.1.3",
|
||||
"symfony/http-client-contracts": "^1.1.4",
|
||||
"symfony/polyfill-php73": "^1.11"
|
||||
},
|
||||
"require-dev": {
|
||||
|
@ -55,10 +55,10 @@ class JsonResponse extends Response
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* return JsonResponse::create($data, 200)
|
||||
* return JsonResponse::create(['key' => 'value'])
|
||||
* ->setSharedMaxAge(300);
|
||||
*
|
||||
* @param mixed $data The json response data
|
||||
* @param mixed $data The JSON response data
|
||||
* @param int $status The response status code
|
||||
* @param array $headers An array of response headers
|
||||
*
|
||||
@ -70,7 +70,18 @@ class JsonResponse extends Response
|
||||
}
|
||||
|
||||
/**
|
||||
* Make easier the creation of JsonResponse from raw json.
|
||||
* Factory method for chainability.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* return JsonResponse::fromJsonString('{"key": "value"}')
|
||||
* ->setSharedMaxAge(300);
|
||||
*
|
||||
* @param string|null $data The JSON response string
|
||||
* @param int $status The response status code
|
||||
* @param array $headers An array of response headers
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public static function fromJsonString($data = null, $status = 200, $headers = [])
|
||||
{
|
||||
|
@ -684,7 +684,7 @@ class Response
|
||||
return (int) $age;
|
||||
}
|
||||
|
||||
return max(time() - $this->getDate()->format('U'), 0);
|
||||
return max(time() - (int) $this->getDate()->format('U'), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -764,7 +764,7 @@ class Response
|
||||
}
|
||||
|
||||
if (null !== $this->getExpires()) {
|
||||
return (int) ($this->getExpires()->format('U') - $this->getDate()->format('U'));
|
||||
return (int) $this->getExpires()->format('U') - (int) $this->getDate()->format('U');
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -85,7 +85,7 @@ class ResponseCacheStrategy implements ResponseCacheStrategyInterface
|
||||
$this->storeRelativeAgeDirective('s-maxage', $response->headers->getCacheControlDirective('s-maxage') ?: $response->headers->getCacheControlDirective('max-age'), $age);
|
||||
|
||||
$expires = $response->getExpires();
|
||||
$expires = null !== $expires ? $expires->format('U') - $response->getDate()->format('U') : null;
|
||||
$expires = null !== $expires ? (int) $expires->format('U') - (int) $response->getDate()->format('U') : null;
|
||||
$this->storeRelativeAgeDirective('expires', $expires >= 0 ? $expires : null, 0);
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,7 @@ class DayOfYearTransformer extends Transformer
|
||||
*/
|
||||
public function format(\DateTime $dateTime, int $length): string
|
||||
{
|
||||
$dayOfYear = $dateTime->format('z') + 1;
|
||||
$dayOfYear = (int) $dateTime->format('z') + 1;
|
||||
|
||||
return $this->padLeft($dayOfYear, $length);
|
||||
}
|
||||
|
@ -315,7 +315,7 @@ class FullTransformer
|
||||
preg_match_all($this->regExp, $this->pattern, $matches);
|
||||
if (\in_array('yy', $matches[0])) {
|
||||
$dateTime->setTimestamp(time());
|
||||
$year = $year > $dateTime->format('y') + 20 ? 1900 + $year : 2000 + $year;
|
||||
$year = $year > (int) $dateTime->format('y') + 20 ? 1900 + $year : 2000 + $year;
|
||||
}
|
||||
|
||||
$dateTime->setDate($year, $month, $day);
|
||||
|
@ -83,6 +83,11 @@ final class Lock implements LockInterface, LoggerAwareInterface
|
||||
}
|
||||
|
||||
if ($this->key->isExpired()) {
|
||||
try {
|
||||
$this->release();
|
||||
} catch (\Exception $e) {
|
||||
// swallow exception to not hide the original issue
|
||||
}
|
||||
throw new LockExpiredException(sprintf('Failed to store the "%s" lock.', $this->key));
|
||||
}
|
||||
|
||||
@ -120,6 +125,11 @@ final class Lock implements LockInterface, LoggerAwareInterface
|
||||
$this->dirty = true;
|
||||
|
||||
if ($this->key->isExpired()) {
|
||||
try {
|
||||
$this->release();
|
||||
} catch (\Exception $e) {
|
||||
// swallow exception to not hide the original issue
|
||||
}
|
||||
throw new LockExpiredException(sprintf('Failed to put off the expiration of the "%s" lock within the specified time.', $this->key));
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,6 @@ use Psr\Log\LoggerAwareTrait;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\Lock\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Lock\Exception\LockConflictedException;
|
||||
use Symfony\Component\Lock\Exception\LockExpiredException;
|
||||
use Symfony\Component\Lock\Exception\NotSupportedException;
|
||||
use Symfony\Component\Lock\Key;
|
||||
use Symfony\Component\Lock\StoreInterface;
|
||||
@ -30,6 +29,7 @@ use Symfony\Component\Lock\Strategy\StrategyInterface;
|
||||
class CombinedStore implements StoreInterface, LoggerAwareInterface
|
||||
{
|
||||
use LoggerAwareTrait;
|
||||
use ExpiringStoreTrait;
|
||||
|
||||
/** @var StoreInterface[] */
|
||||
private $stores;
|
||||
@ -78,6 +78,8 @@ class CombinedStore implements StoreInterface, LoggerAwareInterface
|
||||
}
|
||||
}
|
||||
|
||||
$this->checkNotExpired($key);
|
||||
|
||||
if ($this->strategy->isMet($successCount, $storesCount)) {
|
||||
return;
|
||||
}
|
||||
@ -125,9 +127,7 @@ class CombinedStore implements StoreInterface, LoggerAwareInterface
|
||||
}
|
||||
}
|
||||
|
||||
if ($key->isExpired()) {
|
||||
throw new LockExpiredException(sprintf('Failed to put off the expiration of the "%s" lock within the specified time.', $key));
|
||||
}
|
||||
$this->checkNotExpired($key);
|
||||
|
||||
if ($this->strategy->isMet($successCount, $storesCount)) {
|
||||
return;
|
||||
|
30
src/Symfony/Component/Lock/Store/ExpiringStoreTrait.php
Normal file
30
src/Symfony/Component/Lock/Store/ExpiringStoreTrait.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?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\Lock\Store;
|
||||
|
||||
use Symfony\Component\Lock\Exception\LockExpiredException;
|
||||
use Symfony\Component\Lock\Key;
|
||||
|
||||
trait ExpiringStoreTrait
|
||||
{
|
||||
private function checkNotExpired(Key $key)
|
||||
{
|
||||
if ($key->isExpired()) {
|
||||
try {
|
||||
$this->delete($key);
|
||||
} catch (\Exception $e) {
|
||||
// swallow exception to not hide the original issue
|
||||
}
|
||||
throw new LockExpiredException(sprintf('Failed to store the "%s" lock.', $key));
|
||||
}
|
||||
}
|
||||
}
|
@ -13,7 +13,6 @@ namespace Symfony\Component\Lock\Store;
|
||||
|
||||
use Symfony\Component\Lock\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Lock\Exception\LockConflictedException;
|
||||
use Symfony\Component\Lock\Exception\LockExpiredException;
|
||||
use Symfony\Component\Lock\Key;
|
||||
use Symfony\Component\Lock\StoreInterface;
|
||||
|
||||
@ -24,6 +23,8 @@ use Symfony\Component\Lock\StoreInterface;
|
||||
*/
|
||||
class MemcachedStore implements StoreInterface
|
||||
{
|
||||
use ExpiringStoreTrait;
|
||||
|
||||
private $memcached;
|
||||
private $initialTtl;
|
||||
/** @var bool */
|
||||
@ -64,9 +65,7 @@ class MemcachedStore implements StoreInterface
|
||||
$this->putOffExpiration($key, $this->initialTtl);
|
||||
}
|
||||
|
||||
if ($key->isExpired()) {
|
||||
throw new LockExpiredException(sprintf('Failed to store the "%s" lock.', $key));
|
||||
}
|
||||
$this->checkNotExpired($key);
|
||||
}
|
||||
|
||||
public function waitAndSave(Key $key)
|
||||
@ -110,9 +109,7 @@ class MemcachedStore implements StoreInterface
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
|
||||
if ($key->isExpired()) {
|
||||
throw new LockExpiredException(sprintf('Failed to put off the expiration of the "%s" lock within the specified time.', $key));
|
||||
}
|
||||
$this->checkNotExpired($key);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -15,7 +15,6 @@ use Symfony\Component\Cache\Traits\RedisClusterProxy;
|
||||
use Symfony\Component\Cache\Traits\RedisProxy;
|
||||
use Symfony\Component\Lock\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Lock\Exception\LockConflictedException;
|
||||
use Symfony\Component\Lock\Exception\LockExpiredException;
|
||||
use Symfony\Component\Lock\Key;
|
||||
use Symfony\Component\Lock\StoreInterface;
|
||||
|
||||
@ -26,6 +25,8 @@ use Symfony\Component\Lock\StoreInterface;
|
||||
*/
|
||||
class RedisStore implements StoreInterface
|
||||
{
|
||||
use ExpiringStoreTrait;
|
||||
|
||||
private $redis;
|
||||
private $initialTtl;
|
||||
|
||||
@ -67,9 +68,7 @@ class RedisStore implements StoreInterface
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
|
||||
if ($key->isExpired()) {
|
||||
throw new LockExpiredException(sprintf('Failed to store the "%s" lock.', $key));
|
||||
}
|
||||
$this->checkNotExpired($key);
|
||||
}
|
||||
|
||||
public function waitAndSave(Key $key)
|
||||
@ -95,9 +94,7 @@ class RedisStore implements StoreInterface
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
|
||||
if ($key->isExpired()) {
|
||||
throw new LockExpiredException(sprintf('Failed to put off the expiration of the "%s" lock within the specified time.', $key));
|
||||
}
|
||||
$this->checkNotExpired($key);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -11,6 +11,7 @@
|
||||
|
||||
namespace Symfony\Component\Lock\Tests\Store;
|
||||
|
||||
use Symfony\Component\Lock\Exception\LockExpiredException;
|
||||
use Symfony\Component\Lock\Key;
|
||||
use Symfony\Component\Lock\StoreInterface;
|
||||
|
||||
@ -105,4 +106,28 @@ trait ExpiringStoreTestTrait
|
||||
$this->assertGreaterThanOrEqual(0, $key->getRemainingLifetime());
|
||||
$this->assertLessThanOrEqual(1, $key->getRemainingLifetime());
|
||||
}
|
||||
|
||||
public function testExpiredLockCleaned()
|
||||
{
|
||||
$resource = uniqid(__METHOD__, true);
|
||||
|
||||
$key1 = new Key($resource);
|
||||
$key2 = new Key($resource);
|
||||
|
||||
/** @var StoreInterface $store */
|
||||
$store = $this->getStore();
|
||||
$key1->reduceLifetime(0);
|
||||
|
||||
$this->assertTrue($key1->isExpired());
|
||||
try {
|
||||
$store->save($key1);
|
||||
$this->fail('The store shouldn\'t have save an expired key');
|
||||
} catch (LockExpiredException $e) {
|
||||
}
|
||||
|
||||
$this->assertFalse($store->exists($key1));
|
||||
|
||||
$store->save($key2);
|
||||
$this->assertTrue($store->exists($key2));
|
||||
}
|
||||
}
|
||||
|
@ -27,15 +27,17 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
*/
|
||||
class MailgunTransport extends AbstractApiTransport
|
||||
{
|
||||
private const ENDPOINT = 'https://api.mailgun.net/v3/%domain%/messages';
|
||||
private const ENDPOINT = 'https://api.%region_dot%mailgun.net/v3/%domain%/messages';
|
||||
|
||||
private $key;
|
||||
private $domain;
|
||||
private $region;
|
||||
|
||||
public function __construct(string $key, string $domain, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
|
||||
public function __construct(string $key, string $domain, string $region = null, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
|
||||
{
|
||||
$this->key = $key;
|
||||
$this->domain = $domain;
|
||||
$this->region = $region;
|
||||
|
||||
parent::__construct($client, $dispatcher, $logger);
|
||||
}
|
||||
@ -48,7 +50,7 @@ class MailgunTransport extends AbstractApiTransport
|
||||
$headers[] = $header->toString();
|
||||
}
|
||||
|
||||
$endpoint = str_replace('%domain%', urlencode($this->domain), self::ENDPOINT);
|
||||
$endpoint = str_replace(['%domain%', '%region_dot%'], [urlencode($this->domain), 'us' !== ($this->region ?: 'us') ? $this->region.'.' : ''], self::ENDPOINT);
|
||||
$response = $this->client->request('POST', $endpoint, [
|
||||
'auth_basic' => 'api:'.$this->key,
|
||||
'headers' => $headers,
|
||||
|
@ -27,14 +27,16 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
*/
|
||||
class MailgunTransport extends AbstractHttpTransport
|
||||
{
|
||||
private const ENDPOINT = 'https://api.mailgun.net/v3/%domain%/messages.mime';
|
||||
private const ENDPOINT = 'https://api.%region_dot%mailgun.net/v3/%domain%/messages.mime';
|
||||
private $key;
|
||||
private $domain;
|
||||
private $region;
|
||||
|
||||
public function __construct(string $key, string $domain, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
|
||||
public function __construct(string $key, string $domain, string $region = null, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
|
||||
{
|
||||
$this->key = $key;
|
||||
$this->domain = $domain;
|
||||
$this->region = $region;
|
||||
|
||||
parent::__construct($client, $dispatcher, $logger);
|
||||
}
|
||||
@ -49,7 +51,7 @@ class MailgunTransport extends AbstractHttpTransport
|
||||
foreach ($body->getPreparedHeaders()->getAll() as $header) {
|
||||
$headers[] = $header->toString();
|
||||
}
|
||||
$endpoint = str_replace('%domain%', urlencode($this->domain), self::ENDPOINT);
|
||||
$endpoint = str_replace(['%domain%', '%region_dot%'], [urlencode($this->domain), 'us' !== ($this->region ?: 'us') ? $this->region.'.' : ''], self::ENDPOINT);
|
||||
$response = $this->client->request('POST', $endpoint, [
|
||||
'auth_basic' => 'api:'.$this->key,
|
||||
'headers' => $headers,
|
||||
|
@ -22,9 +22,9 @@ use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
*/
|
||||
class MailgunTransport extends EsmtpTransport
|
||||
{
|
||||
public function __construct(string $username, string $password, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
|
||||
public function __construct(string $username, string $password, string $region = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
|
||||
{
|
||||
parent::__construct('smtp.mailgun.org', 465, 'ssl', null, $dispatcher, $logger);
|
||||
parent::__construct('us' !== ($region ?: 'us') ? sprintf('smtp.%s.mailgun.org', $region) : 'smtp.mailgun.org', 465, 'ssl', null, $dispatcher, $logger);
|
||||
|
||||
$this->setUsername($username);
|
||||
$this->setPassword($password);
|
||||
|
@ -22,8 +22,10 @@ use Symfony\Component\Mailer\Bridge\Sendgrid;
|
||||
use Symfony\Component\Mailer\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Mailer\Exception\LogicException;
|
||||
use Symfony\Component\Mailer\Transport;
|
||||
use Symfony\Component\Mime\Email;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
|
||||
class TransportTest extends TestCase
|
||||
{
|
||||
@ -106,6 +108,15 @@ class TransportTest extends TestCase
|
||||
$this->assertEquals('pa$s', $transport->getPassword());
|
||||
$this->assertProperties($transport, $dispatcher, $logger);
|
||||
|
||||
$transport = Transport::fromDsn('smtp://'.urlencode('u$er').':'.urlencode('pa$s').'@mailgun', $dispatcher, null, $logger);
|
||||
$this->assertEquals('smtp.mailgun.org', $transport->getStream()->getHost());
|
||||
|
||||
$transport = Transport::fromDsn('smtp://'.urlencode('u$er').':'.urlencode('pa$s').'@mailgun?region=eu', $dispatcher, null, $logger);
|
||||
$this->assertEquals('smtp.eu.mailgun.org', $transport->getStream()->getHost());
|
||||
|
||||
$transport = Transport::fromDsn('smtp://'.urlencode('u$er').':'.urlencode('pa$s').'@mailgun?region=us', $dispatcher, null, $logger);
|
||||
$this->assertEquals('smtp.mailgun.org', $transport->getStream()->getHost());
|
||||
|
||||
$client = $this->createMock(HttpClientInterface::class);
|
||||
$transport = Transport::fromDsn('http://'.urlencode('u$er').':'.urlencode('pa$s').'@mailgun', $dispatcher, $client, $logger);
|
||||
$this->assertInstanceOf(Mailgun\Http\MailgunTransport::class, $transport);
|
||||
@ -115,6 +126,25 @@ class TransportTest extends TestCase
|
||||
'client' => $client,
|
||||
]);
|
||||
|
||||
$response = $this->createMock(ResponseInterface::class);
|
||||
$response->expects($this->any())->method('getStatusCode')->willReturn(200);
|
||||
$message = (new Email())->from('me@me.com')->to('you@you.com')->subject('hello')->text('Hello you');
|
||||
|
||||
$client = $this->createMock(HttpClientInterface::class);
|
||||
$client->expects($this->once())->method('request')->with('POST', 'https://api.mailgun.net/v3/pa%24s/messages.mime')->willReturn($response);
|
||||
$transport = Transport::fromDsn('http://'.urlencode('u$er').':'.urlencode('pa$s').'@mailgun', $dispatcher, $client, $logger);
|
||||
$transport->send($message);
|
||||
|
||||
$client = $this->createMock(HttpClientInterface::class);
|
||||
$client->expects($this->once())->method('request')->with('POST', 'https://api.eu.mailgun.net/v3/pa%24s/messages.mime')->willReturn($response);
|
||||
$transport = Transport::fromDsn('http://'.urlencode('u$er').':'.urlencode('pa$s').'@mailgun?region=eu', $dispatcher, $client, $logger);
|
||||
$transport->send($message);
|
||||
|
||||
$client = $this->createMock(HttpClientInterface::class);
|
||||
$client->expects($this->once())->method('request')->with('POST', 'https://api.mailgun.net/v3/pa%24s/messages.mime')->willReturn($response);
|
||||
$transport = Transport::fromDsn('http://'.urlencode('u$er').':'.urlencode('pa$s').'@mailgun?region=us', $dispatcher, $client, $logger);
|
||||
$transport->send($message);
|
||||
|
||||
$transport = Transport::fromDsn('api://'.urlencode('u$er').':'.urlencode('pa$s').'@mailgun', $dispatcher, $client, $logger);
|
||||
$this->assertInstanceOf(Mailgun\Http\Api\MailgunTransport::class, $transport);
|
||||
$this->assertProperties($transport, $dispatcher, $logger, [
|
||||
@ -123,6 +153,21 @@ class TransportTest extends TestCase
|
||||
'client' => $client,
|
||||
]);
|
||||
|
||||
$client = $this->createMock(HttpClientInterface::class);
|
||||
$client->expects($this->once())->method('request')->with('POST', 'https://api.mailgun.net/v3/pa%24s/messages')->willReturn($response);
|
||||
$transport = Transport::fromDsn('api://'.urlencode('u$er').':'.urlencode('pa$s').'@mailgun', $dispatcher, $client, $logger);
|
||||
$transport->send($message);
|
||||
|
||||
$client = $this->createMock(HttpClientInterface::class);
|
||||
$client->expects($this->once())->method('request')->with('POST', 'https://api.eu.mailgun.net/v3/pa%24s/messages')->willReturn($response);
|
||||
$transport = Transport::fromDsn('api://'.urlencode('u$er').':'.urlencode('pa$s').'@mailgun?region=eu', $dispatcher, $client, $logger);
|
||||
$transport->send($message);
|
||||
|
||||
$client = $this->createMock(HttpClientInterface::class);
|
||||
$client->expects($this->once())->method('request')->with('POST', 'https://api.mailgun.net/v3/pa%24s/messages')->willReturn($response);
|
||||
$transport = Transport::fromDsn('api://'.urlencode('u$er').':'.urlencode('pa$s').'@mailgun?region=us', $dispatcher, $client, $logger);
|
||||
$transport->send($message);
|
||||
|
||||
$this->expectException(LogicException::class);
|
||||
Transport::fromDsn('foo://mailgun');
|
||||
}
|
||||
|
@ -101,13 +101,13 @@ class Transport
|
||||
}
|
||||
|
||||
if ('smtp' === $parsedDsn['scheme']) {
|
||||
return new Mailgun\Smtp\MailgunTransport($user, $pass, $dispatcher, $logger);
|
||||
return new Mailgun\Smtp\MailgunTransport($user, $pass, $query['region'] ?? null, $dispatcher, $logger);
|
||||
}
|
||||
if ('http' === $parsedDsn['scheme']) {
|
||||
return new Mailgun\Http\MailgunTransport($user, $pass, $client, $dispatcher, $logger);
|
||||
return new Mailgun\Http\MailgunTransport($user, $pass, $query['region'] ?? null, $client, $dispatcher, $logger);
|
||||
}
|
||||
if ('api' === $parsedDsn['scheme']) {
|
||||
return new Mailgun\Http\Api\MailgunTransport($user, $pass, $client, $dispatcher, $logger);
|
||||
return new Mailgun\Http\Api\MailgunTransport($user, $pass, $query['region'] ?? null, $client, $dispatcher, $logger);
|
||||
}
|
||||
|
||||
throw new LogicException(sprintf('The "%s" scheme is not supported for mailer "%s".', $parsedDsn['scheme'], $parsedDsn['host']));
|
||||
|
@ -64,7 +64,7 @@ class HandleMessageMiddleware implements MiddlewareInterface
|
||||
$handler = $handlerDescriptor->getHandler();
|
||||
$handledStamp = HandledStamp::fromDescriptor($handlerDescriptor, $handler($message));
|
||||
$envelope = $envelope->with($handledStamp);
|
||||
$this->logger->info('Message "{class}" handled by "{handler}"', $context + ['handler' => $handledStamp->getHandlerName()]);
|
||||
$this->logger->info('Message {class} handled by {handler}', $context + ['handler' => $handledStamp->getHandlerName()]);
|
||||
} catch (\Throwable $e) {
|
||||
$exceptions[] = $e;
|
||||
}
|
||||
@ -75,7 +75,7 @@ class HandleMessageMiddleware implements MiddlewareInterface
|
||||
throw new NoHandlerForMessageException(sprintf('No handler for message "%s".', $context['class']));
|
||||
}
|
||||
|
||||
$this->logger->info('No handler for message "{class}"', $context);
|
||||
$this->logger->info('No handler for message {class}', $context);
|
||||
}
|
||||
|
||||
if (\count($exceptions)) {
|
||||
|
@ -54,37 +54,30 @@ class SendMessageMiddleware implements MiddlewareInterface
|
||||
|
||||
$sender = null;
|
||||
|
||||
try {
|
||||
if ($envelope->all(ReceivedStamp::class)) {
|
||||
// it's a received message, do not send it back
|
||||
$this->logger->info('Received message "{class}"', $context);
|
||||
} else {
|
||||
/** @var RedeliveryStamp|null $redeliveryStamp */
|
||||
$redeliveryStamp = $envelope->last(RedeliveryStamp::class);
|
||||
if ($envelope->all(ReceivedStamp::class)) {
|
||||
// it's a received message, do not send it back
|
||||
$this->logger->info('Received message {class}', $context);
|
||||
} else {
|
||||
/** @var RedeliveryStamp|null $redeliveryStamp */
|
||||
$redeliveryStamp = $envelope->last(RedeliveryStamp::class);
|
||||
|
||||
// dispatch event unless this is a redelivery
|
||||
$shouldDispatchEvent = null === $redeliveryStamp;
|
||||
foreach ($this->getSenders($envelope, $redeliveryStamp) as $alias => $sender) {
|
||||
if (null !== $this->eventDispatcher && $shouldDispatchEvent) {
|
||||
$event = new SendMessageToTransportsEvent($envelope);
|
||||
$this->eventDispatcher->dispatch($event);
|
||||
$envelope = $event->getEnvelope();
|
||||
$shouldDispatchEvent = false;
|
||||
}
|
||||
|
||||
$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)));
|
||||
// dispatch event unless this is a redelivery
|
||||
$shouldDispatchEvent = null === $redeliveryStamp;
|
||||
foreach ($this->getSenders($envelope, $redeliveryStamp) as $alias => $sender) {
|
||||
if (null !== $this->eventDispatcher && $shouldDispatchEvent) {
|
||||
$event = new SendMessageToTransportsEvent($envelope);
|
||||
$this->eventDispatcher->dispatch($event);
|
||||
$envelope = $event->getEnvelope();
|
||||
$shouldDispatchEvent = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (null === $sender) {
|
||||
return $stack->next()->handle($envelope, $stack);
|
||||
$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)));
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$context['exception'] = $e;
|
||||
$this->logger->warning('An exception occurred while handling message "{class}": '.$e->getMessage(), $context);
|
||||
}
|
||||
|
||||
throw $e;
|
||||
if (null === $sender) {
|
||||
return $stack->next()->handle($envelope, $stack);
|
||||
}
|
||||
|
||||
// message should only be sent and not be handled by the next middleware
|
||||
|
@ -70,14 +70,14 @@ class MultiplierRetryStrategy implements RetryStrategyInterface
|
||||
return true;
|
||||
}
|
||||
|
||||
$retries = $this->getCurrentRetryCount($message);
|
||||
$retries = RedeliveryStamp::getRetryCountFromEnvelope($message);
|
||||
|
||||
return $retries < $this->maxRetries;
|
||||
}
|
||||
|
||||
public function getWaitingTime(Envelope $message): int
|
||||
{
|
||||
$retries = $this->getCurrentRetryCount($message);
|
||||
$retries = RedeliveryStamp::getRetryCountFromEnvelope($message);
|
||||
|
||||
$delay = $this->delayMilliseconds * pow($this->multiplier, $retries);
|
||||
|
||||
@ -87,12 +87,4 @@ class MultiplierRetryStrategy implements RetryStrategyInterface
|
||||
|
||||
return $delay;
|
||||
}
|
||||
|
||||
private function getCurrentRetryCount(Envelope $message): int
|
||||
{
|
||||
/** @var RedeliveryStamp|null $retryMessageStamp */
|
||||
$retryMessageStamp = $message->last(RedeliveryStamp::class);
|
||||
|
||||
return $retryMessageStamp ? $retryMessageStamp->getRetryCount() : 0;
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@
|
||||
namespace Symfony\Component\Messenger\Stamp;
|
||||
|
||||
use Symfony\Component\Debug\Exception\FlattenException;
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
|
||||
/**
|
||||
* Stamp applied when a messages needs to be redelivered.
|
||||
@ -38,6 +39,14 @@ class RedeliveryStamp implements StampInterface
|
||||
$this->redeliveredAt = new \DateTimeImmutable();
|
||||
}
|
||||
|
||||
public static function getRetryCountFromEnvelope(Envelope $envelope): int
|
||||
{
|
||||
/** @var self|null $retryMessageStamp */
|
||||
$retryMessageStamp = $envelope->last(self::class);
|
||||
|
||||
return $retryMessageStamp ? $retryMessageStamp->getRetryCount() : 0;
|
||||
}
|
||||
|
||||
public function getRetryCount(): int
|
||||
{
|
||||
return $this->retryCount;
|
||||
|
@ -23,16 +23,18 @@ use Symfony\Component\Messenger\Transport\AmqpExt\Connection;
|
||||
*/
|
||||
class ConnectionTest extends TestCase
|
||||
{
|
||||
private const DEFAULT_EXCHANGE_NAME = 'messages';
|
||||
|
||||
/**
|
||||
* @expectedException \InvalidArgumentException
|
||||
* @expectedExceptionMessage The given AMQP DSN "amqp://" is invalid.
|
||||
* @expectedExceptionMessage The given AMQP DSN "amqp://:" is invalid.
|
||||
*/
|
||||
public function testItCannotBeConstructedWithAWrongDsn()
|
||||
{
|
||||
Connection::fromDsn('amqp://');
|
||||
Connection::fromDsn('amqp://:');
|
||||
}
|
||||
|
||||
public function testItGetsParametersFromTheDsn()
|
||||
public function testItCanBeConstructedWithDefaults()
|
||||
{
|
||||
$this->assertEquals(
|
||||
new Connection([
|
||||
@ -40,11 +42,27 @@ class ConnectionTest extends TestCase
|
||||
'port' => 5672,
|
||||
'vhost' => '/',
|
||||
], [
|
||||
'name' => 'messages',
|
||||
'name' => self::DEFAULT_EXCHANGE_NAME,
|
||||
], [
|
||||
'messages' => [],
|
||||
self::DEFAULT_EXCHANGE_NAME => [],
|
||||
]),
|
||||
Connection::fromDsn('amqp://localhost/%2f/messages')
|
||||
Connection::fromDsn('amqp://')
|
||||
);
|
||||
}
|
||||
|
||||
public function testItGetsParametersFromTheDsn()
|
||||
{
|
||||
$this->assertEquals(
|
||||
new Connection([
|
||||
'host' => 'host',
|
||||
'port' => 5672,
|
||||
'vhost' => '/',
|
||||
], [
|
||||
'name' => 'custom',
|
||||
], [
|
||||
'custom' => [],
|
||||
]),
|
||||
Connection::fromDsn('amqp://host/%2f/custom')
|
||||
);
|
||||
}
|
||||
|
||||
@ -52,9 +70,9 @@ class ConnectionTest extends TestCase
|
||||
{
|
||||
$this->assertEquals(
|
||||
new Connection([
|
||||
'host' => 'redis',
|
||||
'host' => 'localhost',
|
||||
'port' => 1234,
|
||||
'vhost' => '/',
|
||||
'vhost' => 'vhost',
|
||||
'login' => 'guest',
|
||||
'password' => 'password',
|
||||
], [
|
||||
@ -62,7 +80,7 @@ class ConnectionTest extends TestCase
|
||||
], [
|
||||
'queueName' => [],
|
||||
]),
|
||||
Connection::fromDsn('amqp://guest:password@redis:1234/%2f/queue?exchange[name]=exchangeName&queues[queueName]')
|
||||
Connection::fromDsn('amqp://guest:password@localhost:1234/vhost/queue?exchange[name]=exchangeName&queues[queueName]')
|
||||
);
|
||||
}
|
||||
|
||||
@ -70,18 +88,16 @@ class ConnectionTest extends TestCase
|
||||
{
|
||||
$this->assertEquals(
|
||||
new Connection([
|
||||
'host' => 'redis',
|
||||
'port' => 1234,
|
||||
'host' => 'localhost',
|
||||
'port' => 5672,
|
||||
'vhost' => '/',
|
||||
'login' => 'guest',
|
||||
'password' => 'password',
|
||||
'persistent' => 'true',
|
||||
], [
|
||||
'name' => 'exchangeName',
|
||||
], [
|
||||
'queueName' => [],
|
||||
]),
|
||||
Connection::fromDsn('amqp://guest:password@redis:1234/%2f/queue?exchange[name]=exchangeName&queues[queueName]', [
|
||||
Connection::fromDsn('amqp://localhost/%2f/queue?exchange[name]=exchangeName&queues[queueName]', [
|
||||
'persistent' => 'true',
|
||||
'exchange' => ['name' => 'toBeOverwritten'],
|
||||
])
|
||||
@ -182,7 +198,7 @@ class ConnectionTest extends TestCase
|
||||
$amqpChannel->expects($this->once())->method('isConnected')->willReturn(true);
|
||||
$amqpConnection->expects($this->once())->method('connect');
|
||||
|
||||
$connection = Connection::fromDsn('amqp://localhost/%2f/messages', [], $factory);
|
||||
$connection = Connection::fromDsn('amqp://localhost', [], $factory);
|
||||
$connection->publish('body');
|
||||
}
|
||||
|
||||
@ -199,7 +215,7 @@ class ConnectionTest extends TestCase
|
||||
$amqpChannel->expects($this->once())->method('isConnected')->willReturn(true);
|
||||
$amqpConnection->expects($this->once())->method('pconnect');
|
||||
|
||||
$connection = Connection::fromDsn('amqp://localhost/%2f/messages?persistent=true', [], $factory);
|
||||
$connection = Connection::fromDsn('amqp://localhost?persistent=true', [], $factory);
|
||||
$connection->publish('body');
|
||||
}
|
||||
|
||||
@ -212,13 +228,12 @@ class ConnectionTest extends TestCase
|
||||
$amqpExchange = $this->createMock(\AMQPExchange::class)
|
||||
);
|
||||
|
||||
$amqpExchange->method('getName')->willReturn('exchange_name');
|
||||
$amqpExchange->expects($this->once())->method('declareExchange');
|
||||
$amqpExchange->expects($this->once())->method('publish')->with('body', null, AMQP_NOPARAM, ['headers' => []]);
|
||||
$amqpQueue->expects($this->once())->method('declareQueue');
|
||||
$amqpQueue->expects($this->once())->method('bind')->with('exchange_name', null);
|
||||
$amqpQueue->expects($this->once())->method('bind')->with(self::DEFAULT_EXCHANGE_NAME, null);
|
||||
|
||||
$connection = Connection::fromDsn('amqp://localhost/%2f/messages', [], $factory);
|
||||
$connection = Connection::fromDsn('amqp://localhost', [], $factory);
|
||||
$connection->publish('body');
|
||||
}
|
||||
|
||||
@ -236,21 +251,20 @@ class ConnectionTest extends TestCase
|
||||
$factory->method('createExchange')->willReturn($amqpExchange);
|
||||
$factory->method('createQueue')->will($this->onConsecutiveCalls($amqpQueue0, $amqpQueue1));
|
||||
|
||||
$amqpExchange->method('getName')->willReturn('exchange_name');
|
||||
$amqpExchange->expects($this->once())->method('declareExchange');
|
||||
$amqpExchange->expects($this->once())->method('publish')->with('body', 'routing_key', AMQP_NOPARAM, ['headers' => []]);
|
||||
$amqpQueue0->expects($this->once())->method('declareQueue');
|
||||
$amqpQueue0->expects($this->exactly(2))->method('bind')->withConsecutive(
|
||||
['exchange_name', 'binding_key0'],
|
||||
['exchange_name', 'binding_key1']
|
||||
[self::DEFAULT_EXCHANGE_NAME, 'binding_key0'],
|
||||
[self::DEFAULT_EXCHANGE_NAME, 'binding_key1']
|
||||
);
|
||||
$amqpQueue1->expects($this->once())->method('declareQueue');
|
||||
$amqpQueue1->expects($this->exactly(2))->method('bind')->withConsecutive(
|
||||
['exchange_name', 'binding_key2'],
|
||||
['exchange_name', 'binding_key3']
|
||||
[self::DEFAULT_EXCHANGE_NAME, 'binding_key2'],
|
||||
[self::DEFAULT_EXCHANGE_NAME, 'binding_key3']
|
||||
);
|
||||
|
||||
$dsn = 'amqp://localhost/%2f/messages?'.
|
||||
$dsn = 'amqp://localhost?'.
|
||||
'exchange[default_publish_routing_key]=routing_key&'.
|
||||
'queues[queue0][binding_keys][0]=binding_key0&'.
|
||||
'queues[queue0][binding_keys][1]=binding_key1&'.
|
||||
@ -270,18 +284,17 @@ class ConnectionTest extends TestCase
|
||||
$amqpExchange = $this->createMock(\AMQPExchange::class)
|
||||
);
|
||||
|
||||
$amqpExchange->method('getName')->willReturn('exchange_name');
|
||||
$amqpExchange->expects($this->never())->method('declareExchange');
|
||||
$amqpQueue->expects($this->never())->method('declareQueue');
|
||||
$amqpQueue->expects($this->never())->method('bind');
|
||||
|
||||
$connection = Connection::fromDsn('amqp://localhost/%2f/messages', ['auto_setup' => 'false'], $factory);
|
||||
$connection = Connection::fromDsn('amqp://localhost', ['auto_setup' => 'false'], $factory);
|
||||
$connection->publish('body');
|
||||
|
||||
$connection = Connection::fromDsn('amqp://localhost/%2f/messages', ['auto_setup' => false], $factory);
|
||||
$connection = Connection::fromDsn('amqp://localhost', ['auto_setup' => false], $factory);
|
||||
$connection->publish('body');
|
||||
|
||||
$connection = Connection::fromDsn('amqp://localhost/%2f/messages?auto_setup=false', [], $factory);
|
||||
$connection = Connection::fromDsn('amqp://localhost?auto_setup=false', [], $factory);
|
||||
$connection->publish('body');
|
||||
}
|
||||
|
||||
@ -298,9 +311,9 @@ class ConnectionTest extends TestCase
|
||||
$amqpChannel->expects($this->exactly(2))->method('isConnected')->willReturn(true);
|
||||
|
||||
$amqpChannel->expects($this->exactly(2))->method('setPrefetchCount')->with(2);
|
||||
$connection = Connection::fromDsn('amqp://localhost/%2f/messages?prefetch_count=2', [], $factory);
|
||||
$connection = Connection::fromDsn('amqp://localhost?prefetch_count=2', [], $factory);
|
||||
$connection->setup();
|
||||
$connection = Connection::fromDsn('amqp://localhost/%2f/messages', ['prefetch_count' => 2], $factory);
|
||||
$connection = Connection::fromDsn('amqp://localhost', ['prefetch_count' => 2], $factory);
|
||||
$connection->setup();
|
||||
}
|
||||
|
||||
@ -315,29 +328,29 @@ class ConnectionTest extends TestCase
|
||||
$factory->method('createChannel')->willReturn($amqpChannel);
|
||||
$factory->method('createQueue')->willReturn($delayQueue);
|
||||
$factory->method('createExchange')->will($this->onConsecutiveCalls(
|
||||
$delayExchange = $this->getMockBuilder(\AMQPExchange::class)->disableOriginalConstructor()->getMock(),
|
||||
$amqpExchange = $this->getMockBuilder(\AMQPExchange::class)->disableOriginalConstructor()->getMock()
|
||||
$amqpExchange = $this->getMockBuilder(\AMQPExchange::class)->disableOriginalConstructor()->getMock(),
|
||||
$delayExchange = $this->getMockBuilder(\AMQPExchange::class)->disableOriginalConstructor()->getMock()
|
||||
));
|
||||
|
||||
$amqpExchange->expects($this->once())->method('setName')->with('messages');
|
||||
$amqpExchange->method('getName')->willReturn('messages');
|
||||
$amqpExchange->expects($this->once())->method('setName')->with(self::DEFAULT_EXCHANGE_NAME);
|
||||
$amqpExchange->expects($this->once())->method('declareExchange');
|
||||
|
||||
$delayExchange->expects($this->once())->method('setName')->with('delay');
|
||||
$delayExchange->expects($this->once())->method('declareExchange');
|
||||
$delayExchange->method('getName')->willReturn('delay');
|
||||
|
||||
$delayQueue->expects($this->once())->method('setName')->with('delay_queue__5000');
|
||||
$delayQueue->expects($this->once())->method('setName')->with('delay_queue_messages__5000');
|
||||
$delayQueue->expects($this->once())->method('setArguments')->with([
|
||||
'x-message-ttl' => 5000,
|
||||
'x-dead-letter-exchange' => 'messages',
|
||||
'x-dead-letter-exchange' => self::DEFAULT_EXCHANGE_NAME,
|
||||
'x-dead-letter-routing-key' => '',
|
||||
]);
|
||||
|
||||
$delayQueue->expects($this->once())->method('declareQueue');
|
||||
$delayQueue->expects($this->once())->method('bind')->with('delay', 'delay__5000');
|
||||
$delayQueue->expects($this->once())->method('bind')->with('delay', 'delay_messages__5000');
|
||||
|
||||
$delayExchange->expects($this->once())->method('publish')->with('{}', 'delay__5000', AMQP_NOPARAM, ['headers' => ['x-some-headers' => 'foo']]);
|
||||
$delayExchange->expects($this->once())->method('publish')->with('{}', 'delay_messages__5000', AMQP_NOPARAM, ['headers' => ['x-some-headers' => 'foo']]);
|
||||
|
||||
$connection = Connection::fromDsn('amqp://localhost/%2f/messages', [], $factory);
|
||||
$connection = Connection::fromDsn('amqp://localhost', [], $factory);
|
||||
$connection->publish('{}', ['x-some-headers' => 'foo'], 5000);
|
||||
}
|
||||
|
||||
@ -352,16 +365,15 @@ class ConnectionTest extends TestCase
|
||||
$factory->method('createChannel')->willReturn($amqpChannel);
|
||||
$factory->method('createQueue')->willReturn($delayQueue);
|
||||
$factory->method('createExchange')->will($this->onConsecutiveCalls(
|
||||
$delayExchange = $this->getMockBuilder(\AMQPExchange::class)->disableOriginalConstructor()->getMock(),
|
||||
$amqpExchange = $this->getMockBuilder(\AMQPExchange::class)->disableOriginalConstructor()->getMock()
|
||||
$amqpExchange = $this->getMockBuilder(\AMQPExchange::class)->disableOriginalConstructor()->getMock(),
|
||||
$delayExchange = $this->getMockBuilder(\AMQPExchange::class)->disableOriginalConstructor()->getMock()
|
||||
));
|
||||
|
||||
$amqpExchange->expects($this->once())->method('setName')->with('messages');
|
||||
$amqpExchange->method('getName')->willReturn('messages');
|
||||
$amqpExchange->expects($this->once())->method('setName')->with(self::DEFAULT_EXCHANGE_NAME);
|
||||
$amqpExchange->expects($this->once())->method('declareExchange');
|
||||
|
||||
$delayExchange->expects($this->once())->method('setName')->with('delay');
|
||||
$delayExchange->expects($this->once())->method('declareExchange');
|
||||
$delayExchange->method('getName')->willReturn('delay');
|
||||
|
||||
$connectionOptions = [
|
||||
'retry' => [
|
||||
@ -369,24 +381,25 @@ class ConnectionTest extends TestCase
|
||||
],
|
||||
];
|
||||
|
||||
$connection = Connection::fromDsn('amqp://localhost/%2f/messages', $connectionOptions, $factory);
|
||||
$connection = Connection::fromDsn('amqp://localhost', $connectionOptions, $factory);
|
||||
|
||||
$delayQueue->expects($this->once())->method('setName')->with('delay_queue__120000');
|
||||
$delayQueue->expects($this->once())->method('setName')->with('delay_queue_messages__120000');
|
||||
$delayQueue->expects($this->once())->method('setArguments')->with([
|
||||
'x-message-ttl' => 120000,
|
||||
'x-dead-letter-exchange' => 'messages',
|
||||
'x-dead-letter-exchange' => self::DEFAULT_EXCHANGE_NAME,
|
||||
'x-dead-letter-routing-key' => '',
|
||||
]);
|
||||
|
||||
$delayQueue->expects($this->once())->method('declareQueue');
|
||||
$delayQueue->expects($this->once())->method('bind')->with('delay', 'delay__120000');
|
||||
$delayQueue->expects($this->once())->method('bind')->with('delay', 'delay_messages__120000');
|
||||
|
||||
$delayExchange->expects($this->once())->method('publish')->with('{}', 'delay__120000', AMQP_NOPARAM, ['headers' => []]);
|
||||
$delayExchange->expects($this->once())->method('publish')->with('{}', 'delay_messages__120000', AMQP_NOPARAM, ['headers' => []]);
|
||||
$connection->publish('{}', [], 120000);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \AMQPException
|
||||
* @expectedExceptionMessage Could not connect to the AMQP server. Please verify the provided DSN. ({"delay":{"routing_key_pattern":"delay_%routing_key%_%delay%","exchange_name":"delay","queue_name_pattern":"delay_queue_%routing_key%_%delay%"},"host":"localhost","port":5672,"vhost":"\/","login":"user","password":"********"})
|
||||
* @expectedExceptionMessage Could not connect to the AMQP server. Please verify the provided DSN. ({"host":"localhost","port":5672,"vhost":"\/","login":"user","password":"********"})
|
||||
*/
|
||||
public function testObfuscatePasswordInDsn()
|
||||
{
|
||||
@ -401,7 +414,7 @@ class ConnectionTest extends TestCase
|
||||
new \AMQPConnectionException('Oups.')
|
||||
);
|
||||
|
||||
$connection = Connection::fromDsn('amqp://user:secretpassword@localhost/%2f/messages', [], $factory);
|
||||
$connection = Connection::fromDsn('amqp://user:secretpassword@localhost', [], $factory);
|
||||
$connection->channel();
|
||||
}
|
||||
|
||||
@ -416,7 +429,7 @@ class ConnectionTest extends TestCase
|
||||
|
||||
$amqpExchange->expects($this->once())->method('publish')->with('body', 'routing_key');
|
||||
|
||||
$connection = Connection::fromDsn('amqp://localhost/%2f/messages?exchange[default_publish_routing_key]=routing_key', [], $factory);
|
||||
$connection = Connection::fromDsn('amqp://localhost?exchange[default_publish_routing_key]=routing_key', [], $factory);
|
||||
$connection->publish('body');
|
||||
}
|
||||
|
||||
@ -431,7 +444,7 @@ class ConnectionTest extends TestCase
|
||||
|
||||
$amqpExchange->expects($this->once())->method('publish')->with('body', 'routing_key');
|
||||
|
||||
$connection = Connection::fromDsn('amqp://localhost/%2f/messages?exchange[default_publish_routing_key]=default_routing_key', [], $factory);
|
||||
$connection = Connection::fromDsn('amqp://localhost?exchange[default_publish_routing_key]=default_routing_key', [], $factory);
|
||||
$connection->publish('body', [], 0, new AmqpStamp('routing_key'));
|
||||
}
|
||||
|
||||
@ -446,16 +459,15 @@ class ConnectionTest extends TestCase
|
||||
$factory->method('createChannel')->willReturn($amqpChannel);
|
||||
$factory->method('createQueue')->willReturn($delayQueue);
|
||||
$factory->method('createExchange')->will($this->onConsecutiveCalls(
|
||||
$delayExchange = $this->createMock(\AMQPExchange::class),
|
||||
$amqpExchange = $this->createMock(\AMQPExchange::class)
|
||||
$amqpExchange = $this->createMock(\AMQPExchange::class),
|
||||
$delayExchange = $this->createMock(\AMQPExchange::class)
|
||||
));
|
||||
|
||||
$amqpExchange->expects($this->once())->method('setName')->with('messages');
|
||||
$amqpExchange->method('getName')->willReturn('messages');
|
||||
$amqpExchange->expects($this->once())->method('setName')->with(self::DEFAULT_EXCHANGE_NAME);
|
||||
$amqpExchange->expects($this->once())->method('declareExchange');
|
||||
|
||||
$delayExchange->expects($this->once())->method('setName')->with('delay');
|
||||
$delayExchange->expects($this->once())->method('declareExchange');
|
||||
$delayExchange->method('getName')->willReturn('delay');
|
||||
|
||||
$connectionOptions = [
|
||||
'retry' => [
|
||||
@ -463,22 +475,19 @@ class ConnectionTest extends TestCase
|
||||
],
|
||||
];
|
||||
|
||||
$connection = Connection::fromDsn('amqp://localhost/%2f/messages', $connectionOptions, $factory);
|
||||
$connection = Connection::fromDsn('amqp://localhost', $connectionOptions, $factory);
|
||||
|
||||
$delayQueue->expects($this->once())->method('setName')->with('delay_queue_routing_key_120000');
|
||||
$delayQueue->expects($this->once())->method('setName')->with('delay_queue_messages_routing_key_120000');
|
||||
$delayQueue->expects($this->once())->method('setArguments')->with([
|
||||
'x-message-ttl' => 120000,
|
||||
'x-dead-letter-exchange' => 'messages',
|
||||
'x-dead-letter-exchange' => self::DEFAULT_EXCHANGE_NAME,
|
||||
'x-dead-letter-routing-key' => 'routing_key',
|
||||
]);
|
||||
$delayQueue->expects($this->once())->method('setArgument')->with(
|
||||
'x-dead-letter-routing-key',
|
||||
'routing_key'
|
||||
);
|
||||
|
||||
$delayQueue->expects($this->once())->method('declareQueue');
|
||||
$delayQueue->expects($this->once())->method('bind')->with('delay', 'delay_routing_key_120000');
|
||||
$delayQueue->expects($this->once())->method('bind')->with('delay', 'delay_messages_routing_key_120000');
|
||||
|
||||
$delayExchange->expects($this->once())->method('publish')->with('{}', 'delay_routing_key_120000', AMQP_NOPARAM, ['headers' => []]);
|
||||
$delayExchange->expects($this->once())->method('publish')->with('{}', 'delay_messages_routing_key_120000', AMQP_NOPARAM, ['headers' => []]);
|
||||
$connection->publish('{}', [], 120000, new AmqpStamp('routing_key'));
|
||||
}
|
||||
|
||||
@ -498,7 +507,7 @@ class ConnectionTest extends TestCase
|
||||
['delivery_mode' => 2, 'headers' => ['type' => DummyMessage::class]]
|
||||
);
|
||||
|
||||
$connection = Connection::fromDsn('amqp://localhost/%2f/messages', [], $factory);
|
||||
$connection = Connection::fromDsn('amqp://localhost', [], $factory);
|
||||
$connection->publish('body', ['type' => DummyMessage::class], 0, new AmqpStamp('routing_key', AMQP_IMMEDIATE, ['delivery_mode' => 2]));
|
||||
}
|
||||
}
|
||||
|
@ -58,8 +58,22 @@ class Connection
|
||||
*/
|
||||
private $amqpDelayExchange;
|
||||
|
||||
public function __construct(array $connectionOptions, array $exchangeOptions, array $queuesOptions, AmqpFactory $amqpFactory = null)
|
||||
{
|
||||
$this->connectionOptions = array_replace_recursive([
|
||||
'delay' => [
|
||||
'routing_key_pattern' => 'delay_%exchange_name%_%routing_key%_%delay%',
|
||||
'exchange_name' => 'delay',
|
||||
'queue_name_pattern' => 'delay_queue_%exchange_name%_%routing_key%_%delay%',
|
||||
],
|
||||
], $connectionOptions);
|
||||
$this->exchangeOptions = $exchangeOptions;
|
||||
$this->queuesOptions = $queuesOptions;
|
||||
$this->amqpFactory = $amqpFactory ?: new AmqpFactory();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* Creates a connection based on the DSN and options.
|
||||
*
|
||||
* Available options:
|
||||
*
|
||||
@ -79,31 +93,21 @@ class Connection
|
||||
* * flags: Exchange flags (Default: AMQP_DURABLE)
|
||||
* * arguments: Extra arguments
|
||||
* * delay:
|
||||
* * routing_key_pattern: The pattern of the routing key (Default: "delay_%routing_key%_%delay%")
|
||||
* * queue_name_pattern: Pattern to use to create the queues (Default: "delay_queue_%routing_key%_%delay%")
|
||||
* * exchange_name: Name of the exchange to be used for the retried messages (Default: "retry")
|
||||
* * routing_key_pattern: The pattern of the routing key (Default: "delay_%exchange_name%_%routing_key%_%delay%")
|
||||
* * queue_name_pattern: Pattern to use to create the queues (Default: "delay_queue_%exchange_name%_%routing_key%_%delay%")
|
||||
* * exchange_name: Name of the exchange to be used for the retried messages (Default: "delay")
|
||||
* * auto_setup: Enable or not the auto-setup of queues and exchanges (Default: true)
|
||||
* * loop_sleep: Amount of micro-seconds to wait if no message are available (Default: 200000)
|
||||
* * prefetch_count: set channel prefetch count
|
||||
*/
|
||||
public function __construct(array $connectionOptions, array $exchangeOptions, array $queuesOptions, AmqpFactory $amqpFactory = null)
|
||||
{
|
||||
$this->connectionOptions = array_replace_recursive([
|
||||
'delay' => [
|
||||
'routing_key_pattern' => 'delay_%routing_key%_%delay%',
|
||||
'exchange_name' => 'delay',
|
||||
'queue_name_pattern' => 'delay_queue_%routing_key%_%delay%',
|
||||
],
|
||||
], $connectionOptions);
|
||||
$this->exchangeOptions = $exchangeOptions;
|
||||
$this->queuesOptions = $queuesOptions;
|
||||
$this->amqpFactory = $amqpFactory ?: new AmqpFactory();
|
||||
}
|
||||
|
||||
public static function fromDsn(string $dsn, array $options = [], AmqpFactory $amqpFactory = null): self
|
||||
{
|
||||
if (false === $parsedUrl = parse_url($dsn)) {
|
||||
throw new InvalidArgumentException(sprintf('The given AMQP DSN "%s" is invalid.', $dsn));
|
||||
// this is a valid URI that parse_url cannot handle when you want to pass all parameters as options
|
||||
if ('amqp://' !== $dsn) {
|
||||
throw new InvalidArgumentException(sprintf('The given AMQP DSN "%s" is invalid.', $dsn));
|
||||
}
|
||||
|
||||
$parsedUrl = [];
|
||||
}
|
||||
|
||||
$pathParts = isset($parsedUrl['path']) ? explode('/', trim($parsedUrl['path'], '/')) : [];
|
||||
@ -242,12 +246,12 @@ class Connection
|
||||
$this->clear();
|
||||
}
|
||||
|
||||
$exchange = $this->getDelayExchange();
|
||||
$exchange->declareExchange();
|
||||
$this->exchange()->declareExchange(); // setup normal exchange for delay queue to DLX messages to
|
||||
$this->getDelayExchange()->declareExchange();
|
||||
|
||||
$queue = $this->createDelayQueue($delay, $routingKey);
|
||||
$queue->declareQueue();
|
||||
$queue->bind($exchange->getName(), $this->getRoutingKeyForDelay($delay, $routingKey));
|
||||
$queue->bind($this->connectionOptions['delay']['exchange_name'], $this->getRoutingKeyForDelay($delay, $routingKey));
|
||||
}
|
||||
|
||||
private function getDelayExchange(): \AMQPExchange
|
||||
@ -274,28 +278,26 @@ class Connection
|
||||
{
|
||||
$queue = $this->amqpFactory->createQueue($this->channel());
|
||||
$queue->setName(str_replace(
|
||||
['%delay%', '%routing_key%'],
|
||||
[$delay, $routingKey ?: ''],
|
||||
['%delay%', '%exchange_name%', '%routing_key%'],
|
||||
[$delay, $this->exchangeOptions['name'], $routingKey ?? ''],
|
||||
$this->connectionOptions['delay']['queue_name_pattern']
|
||||
));
|
||||
));
|
||||
$queue->setArguments([
|
||||
'x-message-ttl' => $delay,
|
||||
'x-dead-letter-exchange' => $this->exchange()->getName(),
|
||||
'x-dead-letter-exchange' => $this->exchangeOptions['name'],
|
||||
// after being released from to DLX, make sure the original routing key will be used
|
||||
// we must use an empty string instead of null for the argument to be picked up
|
||||
'x-dead-letter-routing-key' => $routingKey ?? '',
|
||||
]);
|
||||
|
||||
if (null !== $routingKey) {
|
||||
// after being released from to DLX, this routing key will be used
|
||||
$queue->setArgument('x-dead-letter-routing-key', $routingKey);
|
||||
}
|
||||
|
||||
return $queue;
|
||||
}
|
||||
|
||||
private function getRoutingKeyForDelay(int $delay, ?string $finalRoutingKey): string
|
||||
{
|
||||
return str_replace(
|
||||
['%delay%', '%routing_key%'],
|
||||
[$delay, $finalRoutingKey ?: ''],
|
||||
['%delay%', '%exchange_name%', '%routing_key%'],
|
||||
[$delay, $this->exchangeOptions['name'], $finalRoutingKey ?? ''],
|
||||
$this->connectionOptions['delay']['routing_key_pattern']
|
||||
);
|
||||
}
|
||||
@ -350,7 +352,7 @@ class Connection
|
||||
foreach ($this->queuesOptions as $queueName => $queueConfig) {
|
||||
$this->queue($queueName)->declareQueue();
|
||||
foreach ($queueConfig['binding_keys'] ?? [null] as $bindingKey) {
|
||||
$this->queue($queueName)->bind($this->exchange()->getName(), $bindingKey);
|
||||
$this->queue($queueName)->bind($this->exchangeOptions['name'], $bindingKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -374,6 +376,7 @@ class Connection
|
||||
} catch (\AMQPConnectionException $e) {
|
||||
$credentials = $this->connectionOptions;
|
||||
$credentials['password'] = '********';
|
||||
unset($credentials['delay']);
|
||||
|
||||
throw new \AMQPException(sprintf('Could not connect to the AMQP server. Please verify the provided DSN. (%s)', json_encode($credentials)), 0, $e);
|
||||
}
|
||||
|
@ -140,14 +140,16 @@ class Worker implements WorkerInterface
|
||||
|
||||
$this->dispatchEvent(new WorkerMessageFailedEvent($envelope, $transportName, $throwable, $shouldRetry));
|
||||
|
||||
$retryCount = RedeliveryStamp::getRetryCountFromEnvelope($envelope);
|
||||
if ($shouldRetry) {
|
||||
$retryCount = $this->getRetryCount($envelope) + 1;
|
||||
++$retryCount;
|
||||
$delay = $retryStrategy->getWaitingTime($envelope);
|
||||
if (null !== $this->logger) {
|
||||
$this->logger->error('Retrying {class} - retry #{retryCount}.', $context + ['retryCount' => $retryCount, 'error' => $throwable]);
|
||||
$this->logger->error('Error thrown while handling message {class}. Dispatching for retry #{retryCount} using {delay} ms delay. Error: "{error}"', $context + ['retryCount' => $retryCount, 'delay' => $delay, 'error' => $throwable->getMessage(), 'exception' => $throwable]);
|
||||
}
|
||||
|
||||
// add the delay and retry stamp info + remove ReceivedStamp
|
||||
$retryEnvelope = $envelope->with(new DelayStamp($retryStrategy->getWaitingTime($envelope)))
|
||||
$retryEnvelope = $envelope->with(new DelayStamp($delay))
|
||||
->with(new RedeliveryStamp($retryCount, $this->getSenderClassOrAlias($envelope)))
|
||||
->withoutAll(ReceivedStamp::class);
|
||||
|
||||
@ -157,7 +159,7 @@ class Worker implements WorkerInterface
|
||||
$receiver->ack($envelope);
|
||||
} else {
|
||||
if (null !== $this->logger) {
|
||||
$this->logger->critical('Rejecting {class} (removing from transport).', $context + ['error' => $throwable]);
|
||||
$this->logger->critical('Error thrown while handling message {class}. Removing from transport after {retryCount} retries. Error: "{error}"', $context + ['retryCount' => $retryCount, 'error' => $throwable->getMessage(), 'exception' => $throwable]);
|
||||
}
|
||||
|
||||
$receiver->reject($envelope);
|
||||
@ -207,14 +209,6 @@ class Worker implements WorkerInterface
|
||||
return $retryStrategy->isRetryable($envelope);
|
||||
}
|
||||
|
||||
private function getRetryCount(Envelope $envelope): int
|
||||
{
|
||||
/** @var RedeliveryStamp|null $retryMessageStamp */
|
||||
$retryMessageStamp = $envelope->last(RedeliveryStamp::class);
|
||||
|
||||
return $retryMessageStamp ? $retryMessageStamp->getRetryCount() : 0;
|
||||
}
|
||||
|
||||
private function getSenderClassOrAlias(Envelope $envelope): string
|
||||
{
|
||||
/** @var SentStamp|null $sentStamp */
|
||||
|
@ -257,16 +257,18 @@ class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInt
|
||||
}
|
||||
}
|
||||
|
||||
if ((self::ABSOLUTE_URL === $referenceType || self::NETWORK_PATH === $referenceType) && !empty($host)) {
|
||||
$port = '';
|
||||
if ('http' === $scheme && 80 != $this->context->getHttpPort()) {
|
||||
$port = ':'.$this->context->getHttpPort();
|
||||
} elseif ('https' === $scheme && 443 != $this->context->getHttpsPort()) {
|
||||
$port = ':'.$this->context->getHttpsPort();
|
||||
}
|
||||
if (self::ABSOLUTE_URL === $referenceType || self::NETWORK_PATH === $referenceType) {
|
||||
if ('' !== $host || ('' !== $scheme && 'http' !== $scheme && 'https' !== $scheme)) {
|
||||
$port = '';
|
||||
if ('http' === $scheme && 80 !== $this->context->getHttpPort()) {
|
||||
$port = ':'.$this->context->getHttpPort();
|
||||
} elseif ('https' === $scheme && 443 !== $this->context->getHttpsPort()) {
|
||||
$port = ':'.$this->context->getHttpsPort();
|
||||
}
|
||||
|
||||
$schemeAuthority = self::NETWORK_PATH === $referenceType ? '//' : "$scheme://";
|
||||
$schemeAuthority .= $host.$port;
|
||||
$schemeAuthority = self::NETWORK_PATH === $referenceType || '' === $scheme ? '//' : "$scheme://";
|
||||
$schemeAuthority .= $host.$port;
|
||||
}
|
||||
}
|
||||
|
||||
if (self::RELATIVE_PATH === $referenceType) {
|
||||
|
@ -600,28 +600,27 @@ class UrlGeneratorTest extends TestCase
|
||||
|
||||
public function testDefaultHostIsUsedWhenContextHostIsEmpty()
|
||||
{
|
||||
$routes = $this->getRoutes('test', new Route('/route', ['domain' => 'my.fallback.host'], ['domain' => '.+'], [], '{domain}', ['http']));
|
||||
$routes = $this->getRoutes('test', new Route('/path', ['domain' => 'my.fallback.host'], ['domain' => '.+'], [], '{domain}'));
|
||||
|
||||
$generator = $this->getGenerator($routes);
|
||||
$generator->getContext()->setHost('');
|
||||
|
||||
$this->assertSame('http://my.fallback.host/app.php/route', $generator->generate('test', [], UrlGeneratorInterface::ABSOLUTE_URL));
|
||||
$this->assertSame('http://my.fallback.host/app.php/path', $generator->generate('test', [], UrlGeneratorInterface::ABSOLUTE_URL));
|
||||
}
|
||||
|
||||
public function testDefaultHostIsUsedWhenContextHostIsEmptyAndSchemeIsNot()
|
||||
public function testDefaultHostIsUsedWhenContextHostIsEmptyAndPathReferenceType()
|
||||
{
|
||||
$routes = $this->getRoutes('test', new Route('/route', ['domain' => 'my.fallback.host'], ['domain' => '.+'], [], '{domain}', ['http', 'https']));
|
||||
$routes = $this->getRoutes('test', new Route('/path', ['domain' => 'my.fallback.host'], ['domain' => '.+'], [], '{domain}'));
|
||||
|
||||
$generator = $this->getGenerator($routes);
|
||||
$generator->getContext()->setHost('');
|
||||
$generator->getContext()->setScheme('https');
|
||||
|
||||
$this->assertSame('https://my.fallback.host/app.php/route', $generator->generate('test', [], UrlGeneratorInterface::ABSOLUTE_URL));
|
||||
$this->assertSame('//my.fallback.host/app.php/path', $generator->generate('test', [], UrlGeneratorInterface::ABSOLUTE_PATH));
|
||||
}
|
||||
|
||||
public function testAbsoluteUrlFallbackToRelativeIfHostIsEmptyAndSchemeIsNot()
|
||||
public function testAbsoluteUrlFallbackToPathIfHostIsEmptyAndSchemeIsHttp()
|
||||
{
|
||||
$routes = $this->getRoutes('test', new Route('/route', [], [], [], '', ['http', 'https']));
|
||||
$routes = $this->getRoutes('test', new Route('/route'));
|
||||
|
||||
$generator = $this->getGenerator($routes);
|
||||
$generator->getContext()->setHost('');
|
||||
@ -630,6 +629,39 @@ class UrlGeneratorTest extends TestCase
|
||||
$this->assertSame('/app.php/route', $generator->generate('test', [], UrlGeneratorInterface::ABSOLUTE_URL));
|
||||
}
|
||||
|
||||
public function testAbsoluteUrlFallbackToNetworkIfSchemeIsEmptyAndHostIsNot()
|
||||
{
|
||||
$routes = $this->getRoutes('test', new Route('/path'));
|
||||
|
||||
$generator = $this->getGenerator($routes);
|
||||
$generator->getContext()->setHost('example.com');
|
||||
$generator->getContext()->setScheme('');
|
||||
|
||||
$this->assertSame('//example.com/app.php/path', $generator->generate('test', [], UrlGeneratorInterface::ABSOLUTE_URL));
|
||||
}
|
||||
|
||||
public function testAbsoluteUrlFallbackToPathIfSchemeAndHostAreEmpty()
|
||||
{
|
||||
$routes = $this->getRoutes('test', new Route('/path'));
|
||||
|
||||
$generator = $this->getGenerator($routes);
|
||||
$generator->getContext()->setHost('');
|
||||
$generator->getContext()->setScheme('');
|
||||
|
||||
$this->assertSame('/app.php/path', $generator->generate('test', [], UrlGeneratorInterface::ABSOLUTE_URL));
|
||||
}
|
||||
|
||||
public function testAbsoluteUrlWithNonHttpSchemeAndEmptyHost()
|
||||
{
|
||||
$routes = $this->getRoutes('test', new Route('/path', [], [], [], '', ['file']));
|
||||
|
||||
$generator = $this->getGenerator($routes);
|
||||
$generator->getContext()->setBaseUrl('');
|
||||
$generator->getContext()->setHost('');
|
||||
|
||||
$this->assertSame('file:///path', $generator->generate('test', [], UrlGeneratorInterface::ABSOLUTE_URL));
|
||||
}
|
||||
|
||||
public function testGenerateNetworkPath()
|
||||
{
|
||||
$routes = $this->getRoutes('test', new Route('/{name}', [], [], [], '{locale}.example.com', ['http']));
|
||||
|
@ -216,10 +216,11 @@ class CsvEncoder implements EncoderInterface, DecoderInterface
|
||||
if (\is_array($value)) {
|
||||
$this->flatten($value, $result, $keySeparator, $parentKey.$key.$keySeparator, $escapeFormulas);
|
||||
} else {
|
||||
if ($escapeFormulas && \in_array(substr($value, 0, 1), $this->formulasStartCharacters, true)) {
|
||||
if ($escapeFormulas && \in_array(substr((string) $value, 0, 1), $this->formulasStartCharacters, true)) {
|
||||
$result[$parentKey.$key] = "\t".$value;
|
||||
} else {
|
||||
$result[$parentKey.$key] = $value;
|
||||
// Ensures an actual value is used when dealing with true and false
|
||||
$result[$parentKey.$key] = false === $value ? 0 : (true === $value ? 1 : $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,24 @@ class CsvEncoderTest extends TestCase
|
||||
$this->encoder = new CsvEncoder();
|
||||
}
|
||||
|
||||
public function testTrueFalseValues()
|
||||
{
|
||||
$data = [
|
||||
'string' => 'foo',
|
||||
'int' => 2,
|
||||
'false' => false,
|
||||
'true' => true,
|
||||
];
|
||||
|
||||
// Check that true and false are appropriately handled
|
||||
$this->assertEquals(<<<'CSV'
|
||||
string,int,false,true
|
||||
foo,2,0,1
|
||||
|
||||
CSV
|
||||
, $this->encoder->encode($data, 'csv'));
|
||||
}
|
||||
|
||||
public function testSupportEncoding()
|
||||
{
|
||||
$this->assertTrue($this->encoder->supportsEncoding('csv'));
|
||||
|
@ -121,7 +121,7 @@ class IssnValidator extends ConstraintValidator
|
||||
|
||||
for ($i = 0; $i < 7; ++$i) {
|
||||
// Multiply the first digit by 8, the second by 7, etc.
|
||||
$checkSum += (8 - $i) * $canonical[$i];
|
||||
$checkSum += (8 - $i) * (int) $canonical[$i];
|
||||
}
|
||||
|
||||
if (0 !== $checkSum % 11) {
|
||||
|
@ -84,7 +84,7 @@ class LuhnValidator extends ConstraintValidator
|
||||
// ^ ^ ^ ^ ^
|
||||
// = 1+8 + 4 + 6 + 1+6 + 2
|
||||
for ($i = $length - 2; $i >= 0; $i -= 2) {
|
||||
$checkSum += array_sum(str_split($value[$i] * 2));
|
||||
$checkSum += array_sum(str_split((int) $value[$i] * 2));
|
||||
}
|
||||
|
||||
if (0 === $checkSum || 0 !== $checkSum % 10) {
|
||||
|
@ -48,13 +48,8 @@ class Caster
|
||||
*/
|
||||
public static function castObject($obj, $class, $hasDebugInfo = false)
|
||||
{
|
||||
if ($hasDebugInfo) {
|
||||
$a = $obj->__debugInfo();
|
||||
} elseif ($obj instanceof \Closure) {
|
||||
$a = [];
|
||||
} else {
|
||||
$a = (array) $obj;
|
||||
}
|
||||
$a = $obj instanceof \Closure ? [] : (array) $obj;
|
||||
|
||||
if ($obj instanceof \__PHP_Incomplete_Class) {
|
||||
return $a;
|
||||
}
|
||||
@ -88,6 +83,17 @@ class Caster
|
||||
}
|
||||
}
|
||||
|
||||
if ($hasDebugInfo && \is_array($debugInfo = $obj->__debugInfo())) {
|
||||
foreach ($debugInfo as $k => $v) {
|
||||
if (!isset($k[0]) || "\0" !== $k[0]) {
|
||||
$k = self::PREFIX_VIRTUAL.$k;
|
||||
}
|
||||
|
||||
unset($a[$k]);
|
||||
$a[$k] = $v;
|
||||
}
|
||||
}
|
||||
|
||||
return $a;
|
||||
}
|
||||
|
||||
|
@ -88,7 +88,7 @@ class DateCaster
|
||||
if (self::PERIOD_LIMIT === $i) {
|
||||
$now = new \DateTimeImmutable();
|
||||
$dates[] = sprintf('%s more', ($end = $p->getEndDate())
|
||||
? ceil(($end->format('U.u') - $d->format('U.u')) / ($now->add($p->getDateInterval())->format('U.u') - $now->format('U.u')))
|
||||
? ceil(($end->format('U.u') - $d->format('U.u')) / ((int) $now->add($p->getDateInterval())->format('U.u') - (int) $now->format('U.u')))
|
||||
: $p->recurrences - $i
|
||||
);
|
||||
break;
|
||||
|
@ -0,0 +1,23 @@
|
||||
<?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\Contracts\HttpClient\Exception;
|
||||
|
||||
/**
|
||||
* When a content-type cannot be decoded to the expected representation.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*
|
||||
* @experimental in 1.1
|
||||
*/
|
||||
interface DecodingExceptionInterface extends ExceptionInterface
|
||||
{
|
||||
}
|
@ -12,6 +12,7 @@
|
||||
namespace Symfony\Contracts\HttpClient;
|
||||
|
||||
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
|
||||
@ -64,7 +65,8 @@ interface ResponseInterface
|
||||
*
|
||||
* @param bool $throw Whether an exception should be thrown on 3/4/5xx status codes
|
||||
*
|
||||
* @throws TransportExceptionInterface When the body cannot be decoded or when a network error occurs
|
||||
* @throws DecodingExceptionInterface When the body cannot be decoded to an array
|
||||
* @throws TransportExceptionInterface When a network error occurs
|
||||
* @throws RedirectionExceptionInterface On a 3xx when $throw is true and the "max_redirects" option has been reached
|
||||
* @throws ClientExceptionInterface On a 4xx when $throw is true
|
||||
* @throws ServerExceptionInterface On a 5xx when $throw is true
|
||||
|
Reference in New Issue
Block a user