Merge branch '4.4'

* 4.4: (33 commits)
  [DI] fix processing of regular parameter bags by MergeExtensionConfigurationPass
  [FrameworkBundle] reset cache pools between requests
  [HttpFoundation] Accept must take the lead for Request::getPreferredFormat()
  [FrameworkBundle] Allow to use the BrowserKit assertions with Panther and API Platform's test client
  Use ConnectionRegistry instead of RegistryInterface.
  Fixes windows error
  Improving the request/response format autodetection
  [Messager] Simplified MessageBus::__construct()
  [WIP][Mailer] Overwrite envelope sender and recipients from config
  [Messenger] Added more test for MessageBus
  [Mime] Updated some PHPDoc contents
  [PropertyAccess] Adds entries to CHANGELOG and UPGRADE
  fixed typo
  [FrameworkBundle] Simplified some code in the DI configuration
  [Filesystem] added missing deprecations to UPGRADE-4.3.md
  [Filesystem] depreacte calling isAbsolutePath with a null
  Fix authentication for redis transport
  only decorate when an event dispatcher was passed
  [Messenger] Added support for auto trimming of redis streams
  [FrmaeworkBundle] More simplifications in the DI configuration
  ...
This commit is contained in:
Nicolas Grekas 2019-07-04 15:54:52 +02:00
commit 155cfb273f
68 changed files with 887 additions and 354 deletions

View File

@ -57,6 +57,12 @@ EventDispatcher
* The signature of the `EventDispatcherInterface::dispatch()` method should be updated to `dispatch($event, string $eventName = null)`, not doing so is deprecated * The signature of the `EventDispatcherInterface::dispatch()` method should be updated to `dispatch($event, string $eventName = null)`, not doing so is deprecated
* The `Event` class has been deprecated, use `Symfony\Contracts\EventDispatcher\Event` instead * The `Event` class has been deprecated, use `Symfony\Contracts\EventDispatcher\Event` instead
Filesystem
----------
* Support for passing arrays to `Filesystem::dumpFile()` is deprecated.
* Support for passing arrays to `Filesystem::appendToFile()` is deprecated.
Form Form
---- ----
@ -71,6 +77,7 @@ Form
FrameworkBundle FrameworkBundle
--------------- ---------------
* Deprecated the `framework.templating` option, use Twig instead.
* Not passing the project directory to the constructor of the `AssetsInstallCommand` is deprecated. This argument will * Not passing the project directory to the constructor of the `AssetsInstallCommand` is deprecated. This argument will
be mandatory in 5.0. be mandatory in 5.0.
* Deprecated the "Psr\SimpleCache\CacheInterface" / "cache.app.simple" service, use "Symfony\Contracts\Cache\CacheInterface" / "cache.app" instead. * Deprecated the "Psr\SimpleCache\CacheInterface" / "cache.app.simple" service, use "Symfony\Contracts\Cache\CacheInterface" / "cache.app" instead.

View File

@ -26,7 +26,7 @@ DependencyInjection
services: services:
App\Handler: App\Handler:
tags: ['app.handler'] tags: ['app.handler']
App\HandlerCollection: App\HandlerCollection:
arguments: [!tagged app.handler] arguments: [!tagged app.handler]
``` ```
@ -36,11 +36,16 @@ DependencyInjection
services: services:
App\Handler: App\Handler:
tags: ['app.handler'] tags: ['app.handler']
App\HandlerCollection: App\HandlerCollection:
arguments: [!tagged_iterator app.handler] arguments: [!tagged_iterator app.handler]
``` ```
Filesystem
----------
* Support for passing a `null` value to `Filesystem::isAbsolutePath()` is deprecated.
Form Form
---- ----
@ -60,6 +65,11 @@ HttpClient
* Added method `cancel()` to `ResponseInterface` * Added method `cancel()` to `ResponseInterface`
HttpFoundation
--------------
* `ApacheRequest` is deprecated, use `Request` class instead.
HttpKernel HttpKernel
---------- ----------
@ -76,6 +86,11 @@ MonologBridge
* The `RouteProcessor` has been marked final. * The `RouteProcessor` has been marked final.
PropertyAccess
--------------
* Deprecated passing `null` as 2nd argument of `PropertyAccessor::createCache()` method (`$defaultLifetime`), pass `0` instead.
Security Security
-------- --------
@ -84,11 +99,19 @@ Security
TwigBridge TwigBridge
---------- ----------
* Deprecated to pass `$rootDir` and `$fileLinkFormatter` as 5th and 6th argument respectively to the * Deprecated to pass `$rootDir` and `$fileLinkFormatter` as 5th and 6th argument respectively to the
`DebugCommand::__construct()` method, swap the variables position. `DebugCommand::__construct()` method, swap the variables position.
Validator Validator
--------- ---------
* Deprecated passing an `ExpressionLanguage` instance as the second argument of `ExpressionValidator::__construct()`.
* Deprecated using anything else than a `string` as the code of a `ConstraintViolation`, a `string` type-hint will
be added to the constructor of the `ConstraintViolation` class and to the `ConstraintViolationBuilder::setCode()`
method in 5.0.
* Deprecated passing an `ExpressionLanguage` instance as the second argument of `ExpressionValidator::__construct()`. * Deprecated passing an `ExpressionLanguage` instance as the second argument of `ExpressionValidator::__construct()`.
Pass it as the first argument instead. Pass it as the first argument instead.
* The `Length` constraint expects the `allowEmptyString` option to be defined
when the `min` option is used.
Set it to `true` to keep the current behavior and `false` to reject empty strings.
In 5.0, it'll become optional and will default to `false`.

View File

@ -101,7 +101,7 @@ DependencyInjection
services: services:
App\Handler: App\Handler:
tags: ['app.handler'] tags: ['app.handler']
App\HandlerCollection: App\HandlerCollection:
arguments: [!tagged_iterator app.handler] arguments: [!tagged_iterator app.handler]
``` ```
@ -114,7 +114,6 @@ DoctrineBridge
* Passing an `IdReader` to the `DoctrineChoiceLoader` when the query cannot be optimized with single id field will throw an exception, pass `null` instead * Passing an `IdReader` to the `DoctrineChoiceLoader` when the query cannot be optimized with single id field will throw an exception, pass `null` instead
* Not passing an `IdReader` to the `DoctrineChoiceLoader` when the query can be optimized with single id field will not apply any optimization * Not passing an `IdReader` to the `DoctrineChoiceLoader` when the query can be optimized with single id field will not apply any optimization
DomCrawler DomCrawler
---------- ----------
@ -135,6 +134,7 @@ EventDispatcher
Filesystem Filesystem
---------- ----------
* The `Filesystem::isAbsolutePath()` method no longer supports `null` in the `$file` argument.
* The `Filesystem::dumpFile()` method no longer supports arrays in the `$content` argument. * The `Filesystem::dumpFile()` method no longer supports arrays in the `$content` argument.
* The `Filesystem::appendToFile()` method no longer supports arrays in the `$content` argument. * The `Filesystem::appendToFile()` method no longer supports arrays in the `$content` argument.
@ -207,8 +207,8 @@ Form
FrameworkBundle FrameworkBundle
--------------- ---------------
* Removed the `framework.templating` option, use Twig instead.
* The project dir argument of the constructor of `AssetsInstallCommand` is required. * The project dir argument of the constructor of `AssetsInstallCommand` is required.
* Removed support for `bundle:controller:action` syntax to reference controllers. Use `serviceOrFqcn::method` * Removed support for `bundle:controller:action` syntax to reference controllers. Use `serviceOrFqcn::method`
instead where `serviceOrFqcn` is either the service ID when using controllers as services or the FQCN of the controller. instead where `serviceOrFqcn` is either the service ID when using controllers as services or the FQCN of the controller.
@ -269,6 +269,7 @@ HttpFoundation
use `Symfony\Component\Mime\FileBinaryMimeTypeGuesser` instead. use `Symfony\Component\Mime\FileBinaryMimeTypeGuesser` instead.
* The `FileinfoMimeTypeGuesser` class has been removed, * The `FileinfoMimeTypeGuesser` class has been removed,
use `Symfony\Component\Mime\FileinfoMimeTypeGuesser` instead. use `Symfony\Component\Mime\FileinfoMimeTypeGuesser` instead.
* `ApacheRequest` has been removed, use the `Request` class instead.
HttpKernel HttpKernel
---------- ----------
@ -476,6 +477,8 @@ TwigBridge
Validator Validator
-------- --------
* Removed support for non-string codes of a `ConstraintViolation`. A `string` type-hint was added to the constructor of
the `ConstraintViolation` class and to the `ConstraintViolationBuilder::setCode()` method.
* An `ExpressionLanguage` instance or null must be passed as the first argument of `ExpressionValidator::__construct()` * An `ExpressionLanguage` instance or null must be passed as the first argument of `ExpressionValidator::__construct()`
* The `checkMX` and `checkHost` options of the `Email` constraint were removed * The `checkMX` and `checkHost` options of the `Email` constraint were removed
* The `Email::__construct()` 'strict' property has been removed. Use 'mode'=>"strict" instead. * The `Email::__construct()` 'strict' property has been removed. Use 'mode'=>"strict" instead.
@ -540,7 +543,6 @@ Workflow
property: state property: state
``` ```
* Support for using a workflow with a single state marking is dropped. Use a state machine instead. * Support for using a workflow with a single state marking is dropped. Use a state machine instead.
Before: Before:

View File

@ -2,6 +2,9 @@
namespace Symfony\Bridge\Doctrine\Tests\Fixtures; namespace Symfony\Bridge\Doctrine\Tests\Fixtures;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Mapping\ClassMetadata;
/** /**
* Class BaseUser. * Class BaseUser.
*/ */
@ -46,4 +49,15 @@ class BaseUser
{ {
return $this->username; return $this->username;
} }
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
$allowEmptyString = property_exists(Assert\Length::class, 'allowEmptyString') ? ['allowEmptyString' => true] : [];
$metadata->addPropertyConstraint('username', new Assert\Length([
'min' => 2,
'max' => 120,
'groups' => ['Registration'],
] + $allowEmptyString));
}
} }

View File

@ -14,6 +14,7 @@ namespace Symfony\Bridge\Doctrine\Tests\Fixtures;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Mapping\ClassMetadata;
/** /**
* @ORM\Entity * @ORM\Entity
@ -36,13 +37,11 @@ class DoctrineLoaderEntity extends DoctrineLoaderParentEntity
/** /**
* @ORM\Column(length=20) * @ORM\Column(length=20)
* @Assert\Length(min=5)
*/ */
public $mergedMaxLength; public $mergedMaxLength;
/** /**
* @ORM\Column(length=20) * @ORM\Column(length=20)
* @Assert\Length(min=1, max=10)
*/ */
public $alreadyMappedMaxLength; public $alreadyMappedMaxLength;
@ -69,4 +68,12 @@ class DoctrineLoaderEntity extends DoctrineLoaderParentEntity
/** @ORM\Column(type="simple_array", length=100) */ /** @ORM\Column(type="simple_array", length=100) */
public $simpleArrayField = []; public $simpleArrayField = [];
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
$allowEmptyString = property_exists(Assert\Length::class, 'allowEmptyString') ? ['allowEmptyString' => true] : [];
$metadata->addPropertyConstraint('mergedMaxLength', new Assert\Length(['min' => 5] + $allowEmptyString));
$metadata->addPropertyConstraint('alreadyMappedMaxLength', new Assert\Length(['min' => 1, 'max' => 10] + $allowEmptyString));
}
} }

View File

@ -9,11 +9,6 @@
<constraint name="NotBlank"> <constraint name="NotBlank">
<option name="groups">Registration</option> <option name="groups">Registration</option>
</constraint> </constraint>
<constraint name="Length">
<option name="min">2</option>
<option name="max">120</option>
<option name="groups">Registration</option>
</constraint>
</property> </property>
</class> </class>
</constraint-mapping> </constraint-mapping>

View File

@ -40,6 +40,7 @@ class DoctrineLoaderTest extends TestCase
} }
$validator = Validation::createValidatorBuilder() $validator = Validation::createValidatorBuilder()
->addMethodMapping('loadValidatorMetadata')
->enableAnnotationMapping() ->enableAnnotationMapping()
->addLoader(new DoctrineLoader(DoctrineTestHelper::createTestEntityManager(), '{^Symfony\\\\Bridge\\\\Doctrine\\\\Tests\\\\Fixtures\\\\DoctrineLoader}')) ->addLoader(new DoctrineLoader(DoctrineTestHelper::createTestEntityManager(), '{^Symfony\\\\Bridge\\\\Doctrine\\\\Tests\\\\Fixtures\\\\DoctrineLoader}'))
->getValidator() ->getValidator()
@ -142,6 +143,7 @@ class DoctrineLoaderTest extends TestCase
} }
$validator = Validation::createValidatorBuilder() $validator = Validation::createValidatorBuilder()
->addMethodMapping('loadValidatorMetadata')
->enableAnnotationMapping() ->enableAnnotationMapping()
->addXmlMappings([__DIR__.'/../Resources/validator/BaseUser.xml']) ->addXmlMappings([__DIR__.'/../Resources/validator/BaseUser.xml'])
->addLoader( ->addLoader(

View File

@ -131,7 +131,7 @@ if (!file_exists("$PHPUNIT_DIR/phpunit-$PHPUNIT_VERSION/phpunit") || md5_file(__
$prevRoot = getenv('COMPOSER_ROOT_VERSION'); $prevRoot = getenv('COMPOSER_ROOT_VERSION');
putenv("COMPOSER_ROOT_VERSION=$PHPUNIT_VERSION.99"); putenv("COMPOSER_ROOT_VERSION=$PHPUNIT_VERSION.99");
// --no-suggest is not in the list to keep compat with composer 1.0, which is shipped with Ubuntu 16.04LTS // --no-suggest is not in the list to keep compat with composer 1.0, which is shipped with Ubuntu 16.04LTS
$exit = proc_close(proc_open("$COMPOSER install --no-dev --prefer-dist --no-progress --ansi", array(), $p, getcwd(), null, array('bypass_shell' => true))); $exit = proc_close(proc_open("$COMPOSER install --no-dev --prefer-dist --no-progress --ansi", array(), $p));
putenv('COMPOSER_ROOT_VERSION'.(false !== $prevRoot ? '='.$prevRoot : '')); putenv('COMPOSER_ROOT_VERSION'.(false !== $prevRoot ? '='.$prevRoot : ''));
if ($exit) { if ($exit) {
exit($exit); exit($exit);

View File

@ -29,6 +29,7 @@ CHANGELOG
4.3.0 4.3.0
----- -----
* Deprecated the `framework.templating` option, use Twig instead.
* Added `WebTestAssertionsTrait` (included by default in `WebTestCase`) * Added `WebTestAssertionsTrait` (included by default in `WebTestCase`)
* Renamed `Client` to `KernelBrowser` * Renamed `Client` to `KernelBrowser`
* Not passing the project directory to the constructor of the `AssetsInstallCommand` is deprecated. This argument will * Not passing the project directory to the constructor of the `AssetsInstallCommand` is deprecated. This argument will

View File

@ -297,10 +297,7 @@ class Configuration implements ConfigurationInterface
->cannotBeEmpty() ->cannotBeEmpty()
->end() ->end()
->arrayNode('initial_marking') ->arrayNode('initial_marking')
->beforeNormalization() ->beforeNormalization()->castToArray()->end()
->ifTrue(function ($v) { return !\is_array($v); })
->then(function ($v) { return [$v]; })
->end()
->defaultValue([]) ->defaultValue([])
->prototype('scalar')->end() ->prototype('scalar')->end()
->end() ->end()
@ -533,10 +530,7 @@ class Configuration implements ConfigurationInterface
->ifTrue(function ($v) { return \is_array($v) && isset($v['mime_type']); }) ->ifTrue(function ($v) { return \is_array($v) && isset($v['mime_type']); })
->then(function ($v) { return $v['mime_type']; }) ->then(function ($v) { return $v['mime_type']; })
->end() ->end()
->beforeNormalization() ->beforeNormalization()->castToArray()->end()
->ifTrue(function ($v) { return !\is_array($v); })
->then(function ($v) { return [$v]; })
->end()
->prototype('scalar')->end() ->prototype('scalar')->end()
->end() ->end()
->end() ->end()
@ -562,10 +556,7 @@ class Configuration implements ConfigurationInterface
->scalarNode('base_path')->defaultValue('')->end() ->scalarNode('base_path')->defaultValue('')->end()
->arrayNode('base_urls') ->arrayNode('base_urls')
->requiresAtLeastOneElement() ->requiresAtLeastOneElement()
->beforeNormalization() ->beforeNormalization()->castToArray()->end()
->ifTrue(function ($v) { return !\is_array($v); })
->then(function ($v) { return [$v]; })
->end()
->prototype('scalar')->end() ->prototype('scalar')->end()
->end() ->end()
->end() ->end()
@ -607,10 +598,7 @@ class Configuration implements ConfigurationInterface
->scalarNode('base_path')->defaultValue('')->end() ->scalarNode('base_path')->defaultValue('')->end()
->arrayNode('base_urls') ->arrayNode('base_urls')
->requiresAtLeastOneElement() ->requiresAtLeastOneElement()
->beforeNormalization() ->beforeNormalization()->castToArray()->end()
->ifTrue(function ($v) { return !\is_array($v); })
->then(function ($v) { return [$v]; })
->end()
->prototype('scalar')->end() ->prototype('scalar')->end()
->end() ->end()
->end() ->end()
@ -684,10 +672,7 @@ class Configuration implements ConfigurationInterface
->defaultValue(['loadValidatorMetadata']) ->defaultValue(['loadValidatorMetadata'])
->prototype('scalar')->end() ->prototype('scalar')->end()
->treatFalseLike([]) ->treatFalseLike([])
->validate() ->validate()->castToArray()->end()
->ifTrue(function ($v) { return !\is_array($v); })
->then(function ($v) { return (array) $v; })
->end()
->end() ->end()
->scalarNode('translation_domain')->defaultValue('validators')->end() ->scalarNode('translation_domain')->defaultValue('validators')->end()
->enumNode('email_validation_mode')->values(['html5', 'loose', 'strict'])->end() ->enumNode('email_validation_mode')->values(['html5', 'loose', 'strict'])->end()
@ -1061,9 +1046,14 @@ class Configuration implements ConfigurationInterface
->end() ->end()
->arrayNode('retry_strategy') ->arrayNode('retry_strategy')
->addDefaultsIfNotSet() ->addDefaultsIfNotSet()
->validate() ->beforeNormalization()
->ifTrue(function ($v) { return null !== $v['service'] && (isset($v['max_retries']) || isset($v['delay']) || isset($v['multiplier']) || isset($v['max_delay'])); }) ->always(function ($v) {
->thenInvalid('"service" cannot be used along with the other retry_strategy options.') if (isset($v['service']) && (isset($v['max_retries']) || isset($v['delay']) || isset($v['multiplier']) || isset($v['max_delay']))) {
throw new \InvalidArgumentException('The "service" cannot be used along with the other "retry_strategy" options.');
}
return $v;
})
->end() ->end()
->children() ->children()
->scalarNode('service')->defaultNull()->info('Service id to override the retry strategy entirely')->end() ->scalarNode('service')->defaultNull()->info('Service id to override the retry strategy entirely')->end()
@ -1280,6 +1270,9 @@ class Configuration implements ConfigurationInterface
->scalarNode('auth_bearer') ->scalarNode('auth_bearer')
->info('A token enabling HTTP Bearer authorization.') ->info('A token enabling HTTP Bearer authorization.')
->end() ->end()
->scalarNode('auth_ntlm')
->info('A "username:password" pair to use Microsoft NTLM authentication (requires the cURL extension).')
->end()
->arrayNode('query') ->arrayNode('query')
->info('Associative array of query string values merged with the base URI.') ->info('Associative array of query string values merged with the base URI.')
->useAttributeAsKey('key') ->useAttributeAsKey('key')
@ -1391,6 +1384,22 @@ class Configuration implements ConfigurationInterface
->{!class_exists(FullStack::class) && class_exists(Mailer::class) ? 'canBeDisabled' : 'canBeEnabled'}() ->{!class_exists(FullStack::class) && class_exists(Mailer::class) ? 'canBeDisabled' : 'canBeEnabled'}()
->children() ->children()
->scalarNode('dsn')->defaultValue('smtp://null')->end() ->scalarNode('dsn')->defaultValue('smtp://null')->end()
->arrayNode('envelope')
->info('Mailer Envelope configuration')
->children()
->scalarNode('sender')->end()
->arrayNode('recipients')
->performNoDeepMerging()
->beforeNormalization()
->ifArray()
->then(function ($v) {
return array_filter(array_values($v));
})
->end()
->prototype('scalar')->end()
->end()
->end()
->end()
->end() ->end()
->end() ->end()
->end() ->end()

View File

@ -1763,6 +1763,13 @@ class FrameworkExtension extends Extension
$loader->load('mailer.xml'); $loader->load('mailer.xml');
$container->getDefinition('mailer.default_transport')->setArgument(0, $config['dsn']); $container->getDefinition('mailer.default_transport')->setArgument(0, $config['dsn']);
$recipients = $config['envelope']['recipients'] ?? null;
$sender = $config['envelope']['sender'] ?? null;
$envelopeListener = $container->getDefinition('mailer.envelope_listener');
$envelopeListener->setArgument(0, $sender);
$envelopeListener->setArgument(1, $recipients);
} }
/** /**

View File

@ -8,7 +8,7 @@
<defaults public="false" /> <defaults public="false" />
<service id="cache.app" parent="cache.adapter.filesystem" public="true"> <service id="cache.app" parent="cache.adapter.filesystem" public="true">
<tag name="cache.pool" clearer="cache.app_clearer" reset="reset" /> <tag name="cache.pool" clearer="cache.app_clearer" />
</service> </service>
<service id="cache.app.taggable" class="Symfony\Component\Cache\Adapter\TagAwareAdapter"> <service id="cache.app.taggable" class="Symfony\Component\Cache\Adapter\TagAwareAdapter">
@ -41,7 +41,7 @@
<service id="cache.adapter.system" class="Symfony\Component\Cache\Adapter\AdapterInterface" abstract="true"> <service id="cache.adapter.system" class="Symfony\Component\Cache\Adapter\AdapterInterface" abstract="true">
<factory class="Symfony\Component\Cache\Adapter\AbstractAdapter" method="createSystemCache" /> <factory class="Symfony\Component\Cache\Adapter\AbstractAdapter" method="createSystemCache" />
<tag name="cache.pool" clearer="cache.system_clearer" /> <tag name="cache.pool" clearer="cache.system_clearer" reset="reset" />
<tag name="monolog.logger" channel="cache" /> <tag name="monolog.logger" channel="cache" />
<argument /> <!-- namespace --> <argument /> <!-- namespace -->
<argument>0</argument> <!-- default lifetime --> <argument>0</argument> <!-- default lifetime -->
@ -51,7 +51,7 @@
</service> </service>
<service id="cache.adapter.apcu" class="Symfony\Component\Cache\Adapter\ApcuAdapter" abstract="true"> <service id="cache.adapter.apcu" class="Symfony\Component\Cache\Adapter\ApcuAdapter" abstract="true">
<tag name="cache.pool" clearer="cache.default_clearer" /> <tag name="cache.pool" clearer="cache.default_clearer" reset="reset" />
<tag name="monolog.logger" channel="cache" /> <tag name="monolog.logger" channel="cache" />
<argument /> <!-- namespace --> <argument /> <!-- namespace -->
<argument>0</argument> <!-- default lifetime --> <argument>0</argument> <!-- default lifetime -->
@ -62,7 +62,7 @@
</service> </service>
<service id="cache.adapter.doctrine" class="Symfony\Component\Cache\Adapter\DoctrineAdapter" abstract="true"> <service id="cache.adapter.doctrine" class="Symfony\Component\Cache\Adapter\DoctrineAdapter" abstract="true">
<tag name="cache.pool" provider="cache.default_doctrine_provider" clearer="cache.default_clearer" /> <tag name="cache.pool" provider="cache.default_doctrine_provider" clearer="cache.default_clearer" reset="reset" />
<tag name="monolog.logger" channel="cache" /> <tag name="monolog.logger" channel="cache" />
<argument /> <!-- Doctrine provider service --> <argument /> <!-- Doctrine provider service -->
<argument /> <!-- namespace --> <argument /> <!-- namespace -->
@ -73,7 +73,7 @@
</service> </service>
<service id="cache.adapter.filesystem" class="Symfony\Component\Cache\Adapter\FilesystemAdapter" abstract="true"> <service id="cache.adapter.filesystem" class="Symfony\Component\Cache\Adapter\FilesystemAdapter" abstract="true">
<tag name="cache.pool" clearer="cache.default_clearer" /> <tag name="cache.pool" clearer="cache.default_clearer" reset="reset" />
<tag name="monolog.logger" channel="cache" /> <tag name="monolog.logger" channel="cache" />
<argument /> <!-- namespace --> <argument /> <!-- namespace -->
<argument>0</argument> <!-- default lifetime --> <argument>0</argument> <!-- default lifetime -->
@ -85,14 +85,14 @@
</service> </service>
<service id="cache.adapter.psr6" class="Symfony\Component\Cache\Adapter\ProxyAdapter" abstract="true"> <service id="cache.adapter.psr6" class="Symfony\Component\Cache\Adapter\ProxyAdapter" abstract="true">
<tag name="cache.pool" provider="cache.default_psr6_provider" clearer="cache.default_clearer" /> <tag name="cache.pool" provider="cache.default_psr6_provider" clearer="cache.default_clearer" reset="reset" />
<argument /> <!-- PSR-6 provider service --> <argument /> <!-- PSR-6 provider service -->
<argument /> <!-- namespace --> <argument /> <!-- namespace -->
<argument>0</argument> <!-- default lifetime --> <argument>0</argument> <!-- default lifetime -->
</service> </service>
<service id="cache.adapter.redis" class="Symfony\Component\Cache\Adapter\RedisAdapter" abstract="true"> <service id="cache.adapter.redis" class="Symfony\Component\Cache\Adapter\RedisAdapter" abstract="true">
<tag name="cache.pool" provider="cache.default_redis_provider" clearer="cache.default_clearer" /> <tag name="cache.pool" provider="cache.default_redis_provider" clearer="cache.default_clearer" reset="reset" />
<tag name="monolog.logger" channel="cache" /> <tag name="monolog.logger" channel="cache" />
<argument /> <!-- Redis connection service --> <argument /> <!-- Redis connection service -->
<argument /> <!-- namespace --> <argument /> <!-- namespace -->
@ -129,7 +129,7 @@
</service> </service>
<service id="cache.adapter.array" class="Symfony\Component\Cache\Adapter\ArrayAdapter" abstract="true"> <service id="cache.adapter.array" class="Symfony\Component\Cache\Adapter\ArrayAdapter" abstract="true">
<tag name="cache.pool" clearer="cache.default_clearer" /> <tag name="cache.pool" clearer="cache.default_clearer" reset="reset" />
<tag name="monolog.logger" channel="cache" /> <tag name="monolog.logger" channel="cache" />
<argument>0</argument> <!-- default lifetime --> <argument>0</argument> <!-- default lifetime -->
<call method="setLogger"> <call method="setLogger">

View File

@ -25,5 +25,11 @@
<argument type="service" id="mailer.default_transport" /> <argument type="service" id="mailer.default_transport" />
<tag name="messenger.message_handler" /> <tag name="messenger.message_handler" />
</service> </service>
<service id="mailer.envelope_listener" class="Symfony\Component\Mailer\EventListener\EnvelopeListener">
<argument /> <!-- sender -->
<argument /> <!-- recipients -->
<tag name="kernel.event_subscriber"/>
</service>
</services> </services>
</container> </container>

View File

@ -0,0 +1,159 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\FrameworkBundle\Test;
use PHPUnit\Framework\Constraint\LogicalAnd;
use PHPUnit\Framework\Constraint\LogicalNot;
use Symfony\Component\BrowserKit\AbstractBrowser;
use Symfony\Component\BrowserKit\Test\Constraint as BrowserKitConstraint;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Test\Constraint as ResponseConstraint;
/**
* Ideas borrowed from Laravel Dusk's assertions.
*
* @see https://laravel.com/docs/5.7/dusk#available-assertions
*/
trait BrowserKitAssertionsTrait
{
public static function assertResponseIsSuccessful(string $message = ''): void
{
self::assertThat(self::getResponse(), new ResponseConstraint\ResponseIsSuccessful(), $message);
}
public static function assertResponseStatusCodeSame(int $expectedCode, string $message = ''): void
{
self::assertThat(self::getResponse(), new ResponseConstraint\ResponseStatusCodeSame($expectedCode), $message);
}
public static function assertResponseRedirects(string $expectedLocation = null, int $expectedCode = null, string $message = ''): void
{
$constraint = new ResponseConstraint\ResponseIsRedirected();
if ($expectedLocation) {
$constraint = LogicalAnd::fromConstraints($constraint, new ResponseConstraint\ResponseHeaderSame('Location', $expectedLocation));
}
if ($expectedCode) {
$constraint = LogicalAnd::fromConstraints($constraint, new ResponseConstraint\ResponseStatusCodeSame($expectedCode));
}
self::assertThat(self::getResponse(), $constraint, $message);
}
public static function assertResponseHasHeader(string $headerName, string $message = ''): void
{
self::assertThat(self::getResponse(), new ResponseConstraint\ResponseHasHeader($headerName), $message);
}
public static function assertResponseNotHasHeader(string $headerName, string $message = ''): void
{
self::assertThat(self::getResponse(), new LogicalNot(new ResponseConstraint\ResponseHasHeader($headerName)), $message);
}
public static function assertResponseHeaderSame(string $headerName, string $expectedValue, string $message = ''): void
{
self::assertThat(self::getResponse(), new ResponseConstraint\ResponseHeaderSame($headerName, $expectedValue), $message);
}
public static function assertResponseHeaderNotSame(string $headerName, string $expectedValue, string $message = ''): void
{
self::assertThat(self::getResponse(), new LogicalNot(new ResponseConstraint\ResponseHeaderSame($headerName, $expectedValue)), $message);
}
public static function assertResponseHasCookie(string $name, string $path = '/', string $domain = null, string $message = ''): void
{
self::assertThat(self::getResponse(), new ResponseConstraint\ResponseHasCookie($name, $path, $domain), $message);
}
public static function assertResponseNotHasCookie(string $name, string $path = '/', string $domain = null, string $message = ''): void
{
self::assertThat(self::getResponse(), new LogicalNot(new ResponseConstraint\ResponseHasCookie($name, $path, $domain)), $message);
}
public static function assertResponseCookieValueSame(string $name, string $expectedValue, string $path = '/', string $domain = null, string $message = ''): void
{
self::assertThat(self::getResponse(), LogicalAnd::fromConstraints(
new ResponseConstraint\ResponseHasCookie($name, $path, $domain),
new ResponseConstraint\ResponseCookieValueSame($name, $expectedValue, $path, $domain)
), $message);
}
public static function assertBrowserHasCookie(string $name, string $path = '/', string $domain = null, string $message = ''): void
{
self::assertThat(self::getClient(), new BrowserKitConstraint\BrowserHasCookie($name, $path, $domain), $message);
}
public static function assertBrowserNotHasCookie(string $name, string $path = '/', string $domain = null, string $message = ''): void
{
self::assertThat(self::getClient(), new LogicalNot(new BrowserKitConstraint\BrowserHasCookie($name, $path, $domain)), $message);
}
public static function assertBrowserCookieValueSame(string $name, string $expectedValue, bool $raw = false, string $path = '/', string $domain = null, string $message = ''): void
{
self::assertThat(self::getClient(), LogicalAnd::fromConstraints(
new BrowserKitConstraint\BrowserHasCookie($name, $path, $domain),
new BrowserKitConstraint\BrowserCookieValueSame($name, $expectedValue, $raw, $path, $domain)
), $message);
}
public static function assertRequestAttributeValueSame(string $name, string $expectedValue, string $message = ''): void
{
self::assertThat(self::getRequest(), new ResponseConstraint\RequestAttributeValueSame($name, $expectedValue), $message);
}
public static function assertRouteSame($expectedRoute, array $parameters = [], string $message = ''): void
{
$constraint = new ResponseConstraint\RequestAttributeValueSame('_route', $expectedRoute);
$constraints = [];
foreach ($parameters as $key => $value) {
$constraints[] = new ResponseConstraint\RequestAttributeValueSame($key, $value);
}
if ($constraints) {
$constraint = LogicalAnd::fromConstraints($constraint, ...$constraints);
}
self::assertThat(self::getRequest(), $constraint, $message);
}
private static function getClient(AbstractBrowser $newClient = null): ?AbstractBrowser
{
static $client;
if (0 < \func_num_args()) {
return $client = $newClient;
}
if (!$client instanceof AbstractBrowser) {
static::fail(sprintf('A client must be set to make assertions on it. Did you forget to call "%s::createClient()"?', __CLASS__));
}
return $client;
}
private static function getResponse(): Response
{
if (!$response = self::getClient()->getResponse()) {
static::fail('A client must have an HTTP Response to make assertions. Did you forget to make an HTTP request?');
}
return $response;
}
private static function getRequest(): Request
{
if (!$request = self::getClient()->getRequest()) {
static::fail('A client must have an HTTP Request to make assertions. Did you forget to make an HTTP request?');
}
return $request;
}
}

View File

@ -0,0 +1,94 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\FrameworkBundle\Test;
use PHPUnit\Framework\Constraint\LogicalAnd;
use PHPUnit\Framework\Constraint\LogicalNot;
use Symfony\Component\DomCrawler\Crawler;
use Symfony\Component\DomCrawler\Test\Constraint as DomCrawlerConstraint;
/**
* Ideas borrowed from Laravel Dusk's assertions.
*
* @see https://laravel.com/docs/5.7/dusk#available-assertions
*/
trait DomCrawlerAssertionsTrait
{
public static function assertSelectorExists(string $selector, string $message = ''): void
{
self::assertThat(self::getCrawler(), new DomCrawlerConstraint\CrawlerSelectorExists($selector), $message);
}
public static function assertSelectorNotExists(string $selector, string $message = ''): void
{
self::assertThat(self::getCrawler(), new LogicalNot(new DomCrawlerConstraint\CrawlerSelectorExists($selector)), $message);
}
public static function assertSelectorTextContains(string $selector, string $text, string $message = ''): void
{
self::assertThat(self::getCrawler(), LogicalAnd::fromConstraints(
new DomCrawlerConstraint\CrawlerSelectorExists($selector),
new DomCrawlerConstraint\CrawlerSelectorTextContains($selector, $text)
), $message);
}
public static function assertSelectorTextSame(string $selector, string $text, string $message = ''): void
{
self::assertThat(self::getCrawler(), LogicalAnd::fromConstraints(
new DomCrawlerConstraint\CrawlerSelectorExists($selector),
new DomCrawlerConstraint\CrawlerSelectorTextSame($selector, $text)
), $message);
}
public static function assertSelectorTextNotContains(string $selector, string $text, string $message = ''): void
{
self::assertThat(self::getCrawler(), LogicalAnd::fromConstraints(
new DomCrawlerConstraint\CrawlerSelectorExists($selector),
new LogicalNot(new DomCrawlerConstraint\CrawlerSelectorTextContains($selector, $text))
), $message);
}
public static function assertPageTitleSame(string $expectedTitle, string $message = ''): void
{
self::assertSelectorTextSame('title', $expectedTitle, $message);
}
public static function assertPageTitleContains(string $expectedTitle, string $message = ''): void
{
self::assertSelectorTextContains('title', $expectedTitle, $message);
}
public static function assertInputValueSame(string $fieldName, string $expectedValue, string $message = ''): void
{
self::assertThat(self::getCrawler(), LogicalAnd::fromConstraints(
new DomCrawlerConstraint\CrawlerSelectorExists("input[name=\"$fieldName\"]"),
new DomCrawlerConstraint\CrawlerSelectorAttributeValueSame("input[name=\"$fieldName\"]", 'value', $expectedValue)
), $message);
}
public static function assertInputValueNotSame(string $fieldName, string $expectedValue, string $message = ''): void
{
self::assertThat(self::getCrawler(), LogicalAnd::fromConstraints(
new DomCrawlerConstraint\CrawlerSelectorExists("input[name=\"$fieldName\"]"),
new LogicalNot(new DomCrawlerConstraint\CrawlerSelectorAttributeValueSame("input[name=\"$fieldName\"]", 'value', $expectedValue))
), $message);
}
private static function getCrawler(): Crawler
{
if (!$crawler = self::getClient()->getCrawler()) {
static::fail('A client must have a crawler to make assertions. Did you forget to make an HTTP request?');
}
return $crawler;
}
}

View File

@ -11,16 +11,6 @@
namespace Symfony\Bundle\FrameworkBundle\Test; namespace Symfony\Bundle\FrameworkBundle\Test;
use PHPUnit\Framework\Constraint\LogicalAnd;
use PHPUnit\Framework\Constraint\LogicalNot;
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Component\BrowserKit\Test\Constraint as BrowserKitConstraint;
use Symfony\Component\DomCrawler\Crawler;
use Symfony\Component\DomCrawler\Test\Constraint as DomCrawlerConstraint;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Test\Constraint as ResponseConstraint;
/** /**
* Ideas borrowed from Laravel Dusk's assertions. * Ideas borrowed from Laravel Dusk's assertions.
* *
@ -28,203 +18,6 @@ use Symfony\Component\HttpFoundation\Test\Constraint as ResponseConstraint;
*/ */
trait WebTestAssertionsTrait trait WebTestAssertionsTrait
{ {
public static function assertResponseIsSuccessful(string $message = ''): void use BrowserKitAssertionsTrait;
{ use DomCrawlerAssertionsTrait;
self::assertThat(self::getResponse(), new ResponseConstraint\ResponseIsSuccessful(), $message);
}
public static function assertResponseStatusCodeSame(int $expectedCode, string $message = ''): void
{
self::assertThat(self::getResponse(), new ResponseConstraint\ResponseStatusCodeSame($expectedCode), $message);
}
public static function assertResponseRedirects(string $expectedLocation = null, int $expectedCode = null, string $message = ''): void
{
$constraint = new ResponseConstraint\ResponseIsRedirected();
if ($expectedLocation) {
$constraint = LogicalAnd::fromConstraints($constraint, new ResponseConstraint\ResponseHeaderSame('Location', $expectedLocation));
}
if ($expectedCode) {
$constraint = LogicalAnd::fromConstraints($constraint, new ResponseConstraint\ResponseStatusCodeSame($expectedCode));
}
self::assertThat(self::getResponse(), $constraint, $message);
}
public static function assertResponseHasHeader(string $headerName, string $message = ''): void
{
self::assertThat(self::getResponse(), new ResponseConstraint\ResponseHasHeader($headerName), $message);
}
public static function assertResponseNotHasHeader(string $headerName, string $message = ''): void
{
self::assertThat(self::getResponse(), new LogicalNot(new ResponseConstraint\ResponseHasHeader($headerName)), $message);
}
public static function assertResponseHeaderSame(string $headerName, string $expectedValue, string $message = ''): void
{
self::assertThat(self::getResponse(), new ResponseConstraint\ResponseHeaderSame($headerName, $expectedValue), $message);
}
public static function assertResponseHeaderNotSame(string $headerName, string $expectedValue, string $message = ''): void
{
self::assertThat(self::getResponse(), new LogicalNot(new ResponseConstraint\ResponseHeaderSame($headerName, $expectedValue)), $message);
}
public static function assertResponseHasCookie(string $name, string $path = '/', string $domain = null, string $message = ''): void
{
self::assertThat(self::getResponse(), new ResponseConstraint\ResponseHasCookie($name, $path, $domain), $message);
}
public static function assertResponseNotHasCookie(string $name, string $path = '/', string $domain = null, string $message = ''): void
{
self::assertThat(self::getResponse(), new LogicalNot(new ResponseConstraint\ResponseHasCookie($name, $path, $domain)), $message);
}
public static function assertResponseCookieValueSame(string $name, string $expectedValue, string $path = '/', string $domain = null, string $message = ''): void
{
self::assertThat(self::getResponse(), LogicalAnd::fromConstraints(
new ResponseConstraint\ResponseHasCookie($name, $path, $domain),
new ResponseConstraint\ResponseCookieValueSame($name, $expectedValue, $path, $domain)
), $message);
}
public static function assertBrowserHasCookie(string $name, string $path = '/', string $domain = null, string $message = ''): void
{
self::assertThat(self::getClient(), new BrowserKitConstraint\BrowserHasCookie($name, $path, $domain), $message);
}
public static function assertBrowserNotHasCookie(string $name, string $path = '/', string $domain = null, string $message = ''): void
{
self::assertThat(self::getClient(), new LogicalNot(new BrowserKitConstraint\BrowserHasCookie($name, $path, $domain)), $message);
}
public static function assertBrowserCookieValueSame(string $name, string $expectedValue, bool $raw = false, string $path = '/', string $domain = null, string $message = ''): void
{
self::assertThat(self::getClient(), LogicalAnd::fromConstraints(
new BrowserKitConstraint\BrowserHasCookie($name, $path, $domain),
new BrowserKitConstraint\BrowserCookieValueSame($name, $expectedValue, $raw, $path, $domain)
), $message);
}
public static function assertSelectorExists(string $selector, string $message = ''): void
{
self::assertThat(self::getCrawler(), new DomCrawlerConstraint\CrawlerSelectorExists($selector), $message);
}
public static function assertSelectorNotExists(string $selector, string $message = ''): void
{
self::assertThat(self::getCrawler(), new LogicalNot(new DomCrawlerConstraint\CrawlerSelectorExists($selector)), $message);
}
public static function assertSelectorTextContains(string $selector, string $text, string $message = ''): void
{
self::assertThat(self::getCrawler(), LogicalAnd::fromConstraints(
new DomCrawlerConstraint\CrawlerSelectorExists($selector),
new DomCrawlerConstraint\CrawlerSelectorTextContains($selector, $text)
), $message);
}
public static function assertSelectorTextSame(string $selector, string $text, string $message = ''): void
{
self::assertThat(self::getCrawler(), LogicalAnd::fromConstraints(
new DomCrawlerConstraint\CrawlerSelectorExists($selector),
new DomCrawlerConstraint\CrawlerSelectorTextSame($selector, $text)
), $message);
}
public static function assertSelectorTextNotContains(string $selector, string $text, string $message = ''): void
{
self::assertThat(self::getCrawler(), LogicalAnd::fromConstraints(
new DomCrawlerConstraint\CrawlerSelectorExists($selector),
new LogicalNot(new DomCrawlerConstraint\CrawlerSelectorTextContains($selector, $text))
), $message);
}
public static function assertPageTitleSame(string $expectedTitle, string $message = ''): void
{
self::assertSelectorTextSame('title', $expectedTitle, $message);
}
public static function assertPageTitleContains(string $expectedTitle, string $message = ''): void
{
self::assertSelectorTextContains('title', $expectedTitle, $message);
}
public static function assertInputValueSame(string $fieldName, string $expectedValue, string $message = ''): void
{
self::assertThat(self::getCrawler(), LogicalAnd::fromConstraints(
new DomCrawlerConstraint\CrawlerSelectorExists("input[name=\"$fieldName\"]"),
new DomCrawlerConstraint\CrawlerSelectorAttributeValueSame("input[name=\"$fieldName\"]", 'value', $expectedValue)
), $message);
}
public static function assertInputValueNotSame(string $fieldName, string $expectedValue, string $message = ''): void
{
self::assertThat(self::getCrawler(), LogicalAnd::fromConstraints(
new DomCrawlerConstraint\CrawlerSelectorExists("input[name=\"$fieldName\"]"),
new LogicalNot(new DomCrawlerConstraint\CrawlerSelectorAttributeValueSame("input[name=\"$fieldName\"]", 'value', $expectedValue))
), $message);
}
public static function assertRequestAttributeValueSame(string $name, string $expectedValue, string $message = ''): void
{
self::assertThat(self::getRequest(), new ResponseConstraint\RequestAttributeValueSame($name, $expectedValue), $message);
}
public static function assertRouteSame($expectedRoute, array $parameters = [], string $message = ''): void
{
$constraint = new ResponseConstraint\RequestAttributeValueSame('_route', $expectedRoute);
$constraints = [];
foreach ($parameters as $key => $value) {
$constraints[] = new ResponseConstraint\RequestAttributeValueSame($key, $value);
}
if ($constraints) {
$constraint = LogicalAnd::fromConstraints($constraint, ...$constraints);
}
self::assertThat(self::getRequest(), $constraint, $message);
}
private static function getClient(KernelBrowser $newClient = null): ?KernelBrowser
{
static $client;
if (0 < \func_num_args()) {
return $client = $newClient;
}
if (!$client instanceof KernelBrowser) {
static::fail(sprintf('A client must be set to make assertions on it. Did you forget to call "%s::createClient()"?', __CLASS__));
}
return $client;
}
private static function getCrawler(): Crawler
{
if (!$crawler = self::getClient()->getCrawler()) {
static::fail('A client must have a crawler to make assertions. Did you forget to make an HTTP request?');
}
return $crawler;
}
private static function getResponse(): Response
{
if (!$response = self::getClient()->getResponse()) {
static::fail('A client must have an HTTP Response to make assertions. Did you forget to make an HTTP request?');
}
return $response;
}
private static function getRequest(): Request
{
if (!$request = self::getClient()->getRequest()) {
static::fail('A client must have an HTTP Request to make assertions. Did you forget to make an HTTP request?');
}
return $request;
}
} }

View File

@ -20,7 +20,6 @@ use Symfony\Bundle\FullStack;
use Symfony\Component\Cache\Adapter\AdapterInterface; use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Component\Cache\Adapter\ApcuAdapter; use Symfony\Component\Cache\Adapter\ApcuAdapter;
use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Cache\Adapter\ChainAdapter;
use Symfony\Component\Cache\Adapter\DoctrineAdapter; use Symfony\Component\Cache\Adapter\DoctrineAdapter;
use Symfony\Component\Cache\Adapter\FilesystemAdapter; use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Component\Cache\Adapter\ProxyAdapter; use Symfony\Component\Cache\Adapter\ProxyAdapter;
@ -1459,10 +1458,6 @@ abstract class FrameworkExtensionTest extends TestCase
$this->assertSame(DoctrineAdapter::class, $parentDefinition->getClass()); $this->assertSame(DoctrineAdapter::class, $parentDefinition->getClass());
break; break;
case 'cache.app': case 'cache.app':
if (ChainAdapter::class === $parentDefinition->getClass()) {
break;
}
// no break
case 'cache.adapter.filesystem': case 'cache.adapter.filesystem':
$this->assertSame(FilesystemAdapter::class, $parentDefinition->getClass()); $this->assertSame(FilesystemAdapter::class, $parentDefinition->getClass());
break; break;

View File

@ -0,0 +1,62 @@
<?php
namespace Symfony\Bundle\FrameworkBundle\Tests\Functional;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Mailer\SentMessage;
use Symfony\Component\Mailer\Transport\AbstractTransport;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email;
class MailerTest extends WebTestCase
{
public function testEnvelopeListener()
{
self::bootKernel(['test_case' => 'Mailer']);
$onDoSend = function (SentMessage $message) {
$envelope = $message->getEnvelope();
$this->assertEquals(
[new Address('redirected@example.org')],
$envelope->getRecipients()
);
$this->assertEquals('sender@example.org', $envelope->getSender()->getAddress());
};
$eventDispatcher = self::$container->get(EventDispatcherInterface::class);
$logger = self::$container->get('logger');
$testTransport = new class($eventDispatcher, $logger, $onDoSend) extends AbstractTransport {
/**
* @var callable
*/
private $onDoSend;
public function __construct(EventDispatcherInterface $eventDispatcher, LoggerInterface $logger, callable $onDoSend)
{
parent::__construct($eventDispatcher, $logger);
$this->onDoSend = $onDoSend;
}
protected function doSend(SentMessage $message): void
{
$onDoSend = $this->onDoSend;
$onDoSend($message);
}
};
$mailer = new Mailer($testTransport, null);
$message = (new Email())
->subject('Test subject')
->text('Hello world')
->from('from@example.org')
->to('to@example.org');
$mailer->send($message);
}
}

View File

@ -0,0 +1,18 @@
<?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.
*/
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\TestBundle;
return [
new FrameworkBundle(),
new TestBundle(),
];

View File

@ -0,0 +1,9 @@
imports:
- { resource: ../config/default.yml }
framework:
mailer:
envelope:
sender: sender@example.org
recipients:
- redirected@example.org

View File

@ -34,7 +34,6 @@ class AnalyzeServiceReferencesPass extends AbstractRecursivePass
private $onlyConstructorArguments; private $onlyConstructorArguments;
private $hasProxyDumper; private $hasProxyDumper;
private $lazy; private $lazy;
private $expressionLanguage;
private $byConstructor; private $byConstructor;
private $definitions; private $definitions;
private $aliases; private $aliases;

View File

@ -200,6 +200,10 @@ class MergeExtensionConfigurationContainerBuilder extends ContainerBuilder
$bag = $this->getParameterBag(); $bag = $this->getParameterBag();
$value = $bag->resolveValue($value); $value = $bag->resolveValue($value);
if (!$bag instanceof EnvPlaceholderParameterBag) {
return parent::resolveEnvPlaceholders($value, $format, $usedEnvs);
}
foreach ($bag->getEnvPlaceholders() as $env => $placeholders) { foreach ($bag->getEnvPlaceholders() as $env => $placeholders) {
if (false === strpos($env, ':')) { if (false === strpos($env, ':')) {
continue; continue;

View File

@ -81,7 +81,7 @@ class ServiceReferenceGraphNode
/** /**
* Returns the in edges. * Returns the in edges.
* *
* @return array The in ServiceReferenceGraphEdge array * @return ServiceReferenceGraphEdge[]
*/ */
public function getInEdges() public function getInEdges()
{ {
@ -91,7 +91,7 @@ class ServiceReferenceGraphNode
/** /**
* Returns the out edges. * Returns the out edges.
* *
* @return array The out ServiceReferenceGraphEdge array * @return ServiceReferenceGraphEdge[]
*/ */
public function getOutEdges() public function getOutEdges()
{ {

View File

@ -47,7 +47,7 @@ final class CrawlerSelectorAttributeValueSame extends Constraint
return false; return false;
} }
return $this->expectedText === trim($crawler->getNode(0)->getAttribute($this->attribute)); return $this->expectedText === trim($crawler->attr($this->attribute));
} }
/** /**

View File

@ -6,6 +6,11 @@ CHANGELOG
* `Filesystem::dumpFile()` and `appendToFile()` don't accept arrays anymore * `Filesystem::dumpFile()` and `appendToFile()` don't accept arrays anymore
4.4.0
-----
* support for passing a `null` value to `Filesystem::isAbsolutePath()` is deprecated and will be removed in 5.0
4.3.0 4.3.0
----- -----

View File

@ -600,6 +600,10 @@ class Filesystem
*/ */
public function isAbsolutePath($file) public function isAbsolutePath($file)
{ {
if (null === $file) {
@trigger_error(sprintf('Calling "%s()" with a null in the $file argument is deprecated since Symfony 4.4.', __METHOD__), E_USER_DEPRECATED);
}
return strspn($file, '/\\', 0, 1) return strspn($file, '/\\', 0, 1)
|| (\strlen($file) > 3 && ctype_alpha($file[0]) || (\strlen($file) > 3 && ctype_alpha($file[0])
&& ':' === $file[1] && ':' === $file[1]

View File

@ -1397,10 +1397,18 @@ class FilesystemTest extends FilesystemTestCase
['var/lib', false], ['var/lib', false],
['../var/lib', false], ['../var/lib', false],
['', false], ['', false],
[null, false],
]; ];
} }
/**
* @group legacy
* @expectedDeprecation Calling "Symfony\Component\Filesystem\Filesystem::isAbsolutePath()" with a null in the $file argument is deprecated since Symfony 4.4.
*/
public function testIsAbsolutePathWithNull()
{
$this->assertFalse($this->filesystem->isAbsolutePath(null));
}
public function testTempnam() public function testTempnam()
{ {
$dirname = $this->workspace; $dirname = $this->workspace;

View File

@ -57,13 +57,15 @@ class FormTypeValidatorExtensionTest extends BaseValidatorExtensionTest
public function testGroupSequenceWithConstraintsOption() public function testGroupSequenceWithConstraintsOption()
{ {
$allowEmptyString = property_exists(Length::class, 'allowEmptyString') ? ['allowEmptyString' => true] : [];
$form = Forms::createFormFactoryBuilder() $form = Forms::createFormFactoryBuilder()
->addExtension(new ValidatorExtension(Validation::createValidator())) ->addExtension(new ValidatorExtension(Validation::createValidator()))
->getFormFactory() ->getFormFactory()
->create(FormTypeTest::TESTED_TYPE, null, (['validation_groups' => new GroupSequence(['First', 'Second'])])) ->create(FormTypeTest::TESTED_TYPE, null, (['validation_groups' => new GroupSequence(['First', 'Second'])]))
->add('field', TextTypeTest::TESTED_TYPE, [ ->add('field', TextTypeTest::TESTED_TYPE, [
'constraints' => [ 'constraints' => [
new Length(['min' => 10, 'groups' => ['First']]), new Length(['min' => 10, 'groups' => ['First']] + $allowEmptyString),
new Email(['groups' => ['Second']]), new Email(['groups' => ['Second']]),
], ],
]) ])

View File

@ -61,11 +61,13 @@ class ValidatorTypeGuesserTest extends TestCase
public function guessRequiredProvider() public function guessRequiredProvider()
{ {
$allowEmptyString = property_exists(Length::class, 'allowEmptyString') ? ['allowEmptyString' => true] : [];
return [ return [
[new NotNull(), new ValueGuess(true, Guess::HIGH_CONFIDENCE)], [new NotNull(), new ValueGuess(true, Guess::HIGH_CONFIDENCE)],
[new NotBlank(), new ValueGuess(true, Guess::HIGH_CONFIDENCE)], [new NotBlank(), new ValueGuess(true, Guess::HIGH_CONFIDENCE)],
[new IsTrue(), new ValueGuess(true, Guess::HIGH_CONFIDENCE)], [new IsTrue(), new ValueGuess(true, Guess::HIGH_CONFIDENCE)],
[new Length(10), new ValueGuess(false, Guess::LOW_CONFIDENCE)], [new Length(['min' => 10, 'max' => 10] + $allowEmptyString), new ValueGuess(false, Guess::LOW_CONFIDENCE)],
[new Range(['min' => 1, 'max' => 20]), new ValueGuess(false, Guess::LOW_CONFIDENCE)], [new Range(['min' => 1, 'max' => 20]), new ValueGuess(false, Guess::LOW_CONFIDENCE)],
]; ];
} }
@ -101,7 +103,9 @@ class ValidatorTypeGuesserTest extends TestCase
public function testGuessMaxLengthForConstraintWithMinValue() public function testGuessMaxLengthForConstraintWithMinValue()
{ {
$constraint = new Length(['min' => '2']); $allowEmptyString = property_exists(Length::class, 'allowEmptyString') ? ['allowEmptyString' => true] : [];
$constraint = new Length(['min' => '2'] + $allowEmptyString);
$result = $this->guesser->guessMaxLengthForConstraint($constraint); $result = $this->guesser->guessMaxLengthForConstraint($constraint);
$this->assertNull($result); $this->assertNull($result);

View File

@ -6,6 +6,7 @@ CHANGELOG
* made `Psr18Client` implement relevant PSR-17 factories * made `Psr18Client` implement relevant PSR-17 factories
* added `HttplugClient` * added `HttplugClient`
* added support for NTLM authentication
4.3.0 4.3.0
----- -----

View File

@ -37,7 +37,10 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface
use HttpClientTrait; use HttpClientTrait;
use LoggerAwareTrait; use LoggerAwareTrait;
private $defaultOptions = self::OPTIONS_DEFAULTS; private $defaultOptions = self::OPTIONS_DEFAULTS + [
'auth_ntlm' => null, // array|string - an array containing the username as first value, and optionally the
// password as the second one; or string like username:password - enabling NTLM auth
];
/** /**
* An internal object to share state between the client and its responses. * An internal object to share state between the client and its responses.
@ -150,6 +153,25 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface
CURLOPT_CERTINFO => $options['capture_peer_cert_chain'], CURLOPT_CERTINFO => $options['capture_peer_cert_chain'],
]; ];
if (isset($options['auth_ntlm'])) {
$curlopts[CURLOPT_HTTPAUTH] = CURLAUTH_NTLM;
if (\is_array($options['auth_ntlm'])) {
$count = \count($options['auth_ntlm']);
if ($count <= 0 || $count > 2) {
throw new InvalidArgumentException(sprintf('Option "auth_ntlm" must contain 1 or 2 elements, %s given.', $count));
}
$options['auth_ntlm'] = implode(':', $options['auth_ntlm']);
}
if (!\is_string($options['auth_ntlm'])) {
throw new InvalidArgumentException(sprintf('Option "auth_ntlm" must be string or an array, %s given.', \gettype($options['auth_ntlm'])));
}
$curlopts[CURLOPT_USERPWD] = $options['auth_ntlm'];
}
if (!ZEND_THREAD_SAFE) { if (!ZEND_THREAD_SAFE) {
$curlopts[CURLOPT_DNS_USE_GLOBAL_CACHE] = false; $curlopts[CURLOPT_DNS_USE_GLOBAL_CACHE] = false;
} }

View File

@ -179,6 +179,10 @@ trait HttpClientTrait
} }
} }
if ('auth_ntlm' === $name) {
throw new InvalidArgumentException(sprintf('Option "%s" is not supported by %s, try using CurlHttpClient instead.', __CLASS__));
}
throw new InvalidArgumentException(sprintf('Unsupported option "%s" passed to %s, did you mean "%s"?', $name, __CLASS__, implode('", "', $alternatives ?: array_keys($defaultOptions)))); throw new InvalidArgumentException(sprintf('Unsupported option "%s" passed to %s, did you mean "%s"?', $name, __CLASS__, implode('", "', $alternatives ?: array_keys($defaultOptions))));
} }

View File

@ -15,6 +15,7 @@ CHANGELOG
----- -----
* passing arguments to `Request::isMethodSafe()` is deprecated. * passing arguments to `Request::isMethodSafe()` is deprecated.
* `ApacheRequest` is deprecated, use the `Request` class instead.
4.3.0 4.3.0
----- -----

View File

@ -192,6 +192,10 @@ class Request
protected static $requestFactory; protected static $requestFactory;
/**
* @var string|null
*/
private $preferredFormat;
private $isHostValid = true; private $isHostValid = true;
private $isForwardedValid = true; private $isForwardedValid = true;
@ -1342,6 +1346,8 @@ class Request
* * _format request attribute * * _format request attribute
* * $default * * $default
* *
* @see getPreferredFormat
*
* @param string|null $default The default format * @param string|null $default The default format
* *
* @return string|null The request format * @return string|null The request format
@ -1554,6 +1560,30 @@ class Request
return $this->headers->hasCacheControlDirective('no-cache') || 'no-cache' == $this->headers->get('Pragma'); return $this->headers->hasCacheControlDirective('no-cache') || 'no-cache' == $this->headers->get('Pragma');
} }
/**
* Gets the preferred format for the response by inspecting, in the following order:
* * the request format set using setRequestFormat
* * the values of the Accept HTTP header
* * the content type of the body of the request.
*/
public function getPreferredFormat(?string $default = 'html'): ?string
{
if (null !== $this->preferredFormat) {
return $this->preferredFormat;
}
$preferredFormat = null;
foreach ($this->getAcceptableContentTypes() as $contentType) {
if ($preferredFormat = $this->getFormat($contentType)) {
break;
}
}
$this->preferredFormat = $this->getRequestFormat($preferredFormat ?: $this->getContentType());
return $this->preferredFormat ?: $default;
}
/** /**
* Returns the preferred language. * Returns the preferred language.
* *

View File

@ -265,7 +265,7 @@ class Response
} else { } else {
// Content-type based on the Request // Content-type based on the Request
if (!$headers->has('Content-Type')) { if (!$headers->has('Content-Type')) {
$format = $request->getRequestFormat(); $format = $request->getPreferredFormat();
if (null !== $format && $mimeType = $request->getMimeType($format)) { if (null !== $format && $mimeType = $request->getMimeType($format)) {
$headers->set('Content-Type', $mimeType); $headers->set('Content-Type', $mimeType);
} }

View File

@ -399,6 +399,32 @@ class RequestTest extends TestCase
$this->assertEquals('xml', $dup->getRequestFormat()); $this->assertEquals('xml', $dup->getRequestFormat());
} }
public function testGetPreferredFormat()
{
$request = new Request();
$this->assertNull($request->getPreferredFormat(null));
$this->assertSame('html', $request->getPreferredFormat());
$this->assertSame('json', $request->getPreferredFormat('json'));
$request->setRequestFormat('atom');
$request->headers->set('Accept', 'application/ld+json');
$request->headers->set('Content-Type', 'application/merge-patch+json');
$this->assertSame('atom', $request->getPreferredFormat());
$request = new Request();
$request->headers->set('Accept', 'application/xml');
$request->headers->set('Content-Type', 'application/json');
$this->assertSame('xml', $request->getPreferredFormat());
$request = new Request();
$request->headers->set('Accept', 'application/xml');
$this->assertSame('xml', $request->getPreferredFormat());
$request = new Request();
$request->headers->set('Accept', 'application/json;q=0.8,application/xml;q=0.9');
$this->assertSame('xml', $request->getPreferredFormat());
}
/** /**
* @dataProvider getFormatToMimeTypeMapProviderWithAdditionalNullFormat * @dataProvider getFormatToMimeTypeMapProviderWithAdditionalNullFormat
*/ */

View File

@ -504,6 +504,7 @@ class ResponseTest extends ResponseTestCase
$response = new Response('foo'); $response = new Response('foo');
$request = Request::create('/'); $request = Request::create('/');
$request->setRequestFormat('json'); $request->setRequestFormat('json');
$request->headers->remove('accept');
$response->prepare($request); $response->prepare($request);

View File

@ -59,7 +59,7 @@ class ContainerControllerResolver extends ControllerResolver
$this->throwExceptionIfControllerWasRemoved($class, $e); $this->throwExceptionIfControllerWasRemoved($class, $e);
if ($e instanceof \ArgumentCountError) { if ($e instanceof \ArgumentCountError) {
throw new \InvalidArgumentException(sprintf('Controller "%s" has required constructor arguments and does not exist in the container. Did you forget to define such a service?', $class), 0, $e); throw new \InvalidArgumentException(sprintf('Controller "%s" has required constructor arguments and does not exist in the container. Did you forget to define the controller as a service?', $class), 0, $e);
} }
throw new \InvalidArgumentException(sprintf('Controller "%s" does neither exist as service nor as class', $class), 0, $e); throw new \InvalidArgumentException(sprintf('Controller "%s" does neither exist as service nor as class', $class), 0, $e);

View File

@ -170,7 +170,7 @@ class DebugHandlersListener implements EventSubscriberInterface
$e = $request->attributes->get('exception'); $e = $request->attributes->get('exception');
try { try {
return new Response($this->errorFormatter->render($e, $request->getRequestFormat()), $e->getStatusCode(), $e->getHeaders()); return new Response($this->errorFormatter->render($e, $request->getPreferredFormat()), $e->getStatusCode(), $e->getHeaders());
} catch (ErrorRendererNotFoundException $_) { } catch (ErrorRendererNotFoundException $_) {
return new Response($this->errorFormatter->render($e), $e->getStatusCode(), $e->getHeaders()); return new Response($this->errorFormatter->render($e), $e->getStatusCode(), $e->getHeaders());
} }

View File

@ -184,16 +184,16 @@ class ContainerControllerResolverTest extends ControllerResolverTest
$tests[] = [ $tests[] = [
[ControllerTestService::class, 'action'], [ControllerTestService::class, 'action'],
\InvalidArgumentException::class, \InvalidArgumentException::class,
'Controller "Symfony\Component\HttpKernel\Tests\Controller\ControllerTestService" has required constructor arguments and does not exist in the container. Did you forget to define such a service?', 'Controller "Symfony\Component\HttpKernel\Tests\Controller\ControllerTestService" has required constructor arguments and does not exist in the container. Did you forget to define the controller as a service?',
]; ];
$tests[] = [ $tests[] = [
ControllerTestService::class.'::action', ControllerTestService::class.'::action',
\InvalidArgumentException::class, 'Controller "Symfony\Component\HttpKernel\Tests\Controller\ControllerTestService" has required constructor arguments and does not exist in the container. Did you forget to define such a service?', \InvalidArgumentException::class, 'Controller "Symfony\Component\HttpKernel\Tests\Controller\ControllerTestService" has required constructor arguments and does not exist in the container. Did you forget to define the controller as a service?',
]; ];
$tests[] = [ $tests[] = [
InvokableControllerService::class, InvokableControllerService::class,
\InvalidArgumentException::class, \InvalidArgumentException::class,
'Controller "Symfony\Component\HttpKernel\Tests\Controller\InvokableControllerService" has required constructor arguments and does not exist in the container. Did you forget to define such a service?', 'Controller "Symfony\Component\HttpKernel\Tests\Controller\InvokableControllerService" has required constructor arguments and does not exist in the container. Did you forget to define the controller as a service?',
]; ];
return $tests; return $tests;

View File

@ -11,6 +11,10 @@
namespace Symfony\Component\Ldap\Adapter; namespace Symfony\Component\Ldap\Adapter;
use Symfony\Component\Ldap\Exception\AlreadyExistsException;
use Symfony\Component\Ldap\Exception\ConnectionTimeoutException;
use Symfony\Component\Ldap\Exception\InvalidCredentialsException;
/** /**
* @author Charles Sarrazin <charles@sarraz.in> * @author Charles Sarrazin <charles@sarraz.in>
*/ */
@ -25,6 +29,10 @@ interface ConnectionInterface
/** /**
* Binds the connection against a user's DN and password. * Binds the connection against a user's DN and password.
*
* @throws AlreadyExistsException When the connection can't be created because of an LDAP_ALREADY_EXISTS error
* @throws ConnectionTimeoutException When the connection can't be created because of an LDAP_TIMEOUT error
* @throws InvalidCredentialsException When the connection can't be created because of an LDAP_INVALID_CREDENTIALS error
*/ */
public function bind(string $dn = null, string $password = null); public function bind(string $dn = null, string $password = null);
} }

View File

@ -95,7 +95,7 @@ class ZookeeperStore implements StoreInterface
*/ */
public function putOffExpiration(Key $key, $ttl) public function putOffExpiration(Key $key, $ttl)
{ {
throw new NotSupportedException(); // do nothing, zookeeper locks forever.
} }
/** /**

View File

@ -49,7 +49,6 @@ interface StoreInterface
* @param float $ttl amount of seconds to keep the lock in the store * @param float $ttl amount of seconds to keep the lock in the store
* *
* @throws LockConflictedException * @throws LockConflictedException
* @throws NotSupportedException
*/ */
public function putOffExpiration(Key $key, $ttl); public function putOffExpiration(Key $key, $ttl);

View File

@ -12,6 +12,7 @@ CHANGELOG
* Deprecated passing a `ContainerInterface` instance as first argument of the `ConsumeMessagesCommand` constructor, * Deprecated passing a `ContainerInterface` instance as first argument of the `ConsumeMessagesCommand` constructor,
pass a `RoutableMessageBus` instance instead. pass a `RoutableMessageBus` instance instead.
* Added support for auto trimming of Redis streams.
4.3.0 4.3.0
----- -----

View File

@ -33,17 +33,26 @@ class MessageBus implements MessageBusInterface
} elseif (\is_array($middlewareHandlers)) { } elseif (\is_array($middlewareHandlers)) {
$this->middlewareAggregate = new \ArrayObject($middlewareHandlers); $this->middlewareAggregate = new \ArrayObject($middlewareHandlers);
} else { } else {
$this->middlewareAggregate = new class() { // $this->middlewareAggregate should be an instance of IteratorAggregate.
public $aggregate; // When $middlewareHandlers is an Iterator, we wrap it to ensure it is lazy-loaded and can be rewound.
public $iterator; $this->middlewareAggregate = new class($middlewareHandlers) implements \IteratorAggregate {
private $middlewareHandlers;
private $cachedIterator;
public function __construct($middlewareHandlers)
{
$this->middlewareHandlers = $middlewareHandlers;
}
public function getIterator() public function getIterator()
{ {
return $this->aggregate = new \ArrayObject(iterator_to_array($this->iterator, false)); if (null === $this->cachedIterator) {
$this->cachedIterator = new \ArrayObject(iterator_to_array($this->middlewareHandlers, false));
}
return $this->cachedIterator;
} }
}; };
$this->middlewareAggregate->aggregate = &$this->middlewareAggregate;
$this->middlewareAggregate->iterator = $middlewareHandlers;
} }
} }

View File

@ -16,6 +16,7 @@ use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBus; use Symfony\Component\Messenger\MessageBus;
use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Messenger\Middleware\MiddlewareInterface; use Symfony\Component\Messenger\Middleware\MiddlewareInterface;
use Symfony\Component\Messenger\Middleware\StackInterface;
use Symfony\Component\Messenger\Stamp\BusNameStamp; use Symfony\Component\Messenger\Stamp\BusNameStamp;
use Symfony\Component\Messenger\Stamp\DelayStamp; use Symfony\Component\Messenger\Stamp\DelayStamp;
use Symfony\Component\Messenger\Stamp\ReceivedStamp; use Symfony\Component\Messenger\Stamp\ReceivedStamp;
@ -148,4 +149,44 @@ class MessageBusTest extends TestCase
$finalEnvelope = (new MessageBus())->dispatch(new Envelope(new \stdClass()), [new DelayStamp(5), new BusNameStamp('bar')]); $finalEnvelope = (new MessageBus())->dispatch(new Envelope(new \stdClass()), [new DelayStamp(5), new BusNameStamp('bar')]);
$this->assertCount(2, $finalEnvelope->all()); $this->assertCount(2, $finalEnvelope->all());
} }
public function provideConstructorDataStucture()
{
yield 'iterator' => [new \ArrayObject([
new SimpleMiddleware(),
new SimpleMiddleware(),
])];
yield 'array' => [[
new SimpleMiddleware(),
new SimpleMiddleware(),
]];
yield 'generator' => [(function (): \Generator {
yield new SimpleMiddleware();
yield new SimpleMiddleware();
})()];
}
/** @dataProvider provideConstructorDataStucture */
public function testConstructDataStructure($dataStructure)
{
$bus = new MessageBus($dataStructure);
$envelope = new Envelope(new DummyMessage('Hello'));
$newEnvelope = $bus->dispatch($envelope);
$this->assertSame($envelope->getMessage(), $newEnvelope->getMessage());
// Test rewindable capacity
$envelope = new Envelope(new DummyMessage('Hello'));
$newEnvelope = $bus->dispatch($envelope);
$this->assertSame($envelope->getMessage(), $newEnvelope->getMessage());
}
}
class SimpleMiddleware implements MiddlewareInterface
{
public function handle(Envelope $envelope, StackInterface $stack): Envelope
{
return $envelope;
}
} }

View File

@ -11,8 +11,8 @@
namespace Symfony\Component\Messenger\Tests\Transport\Doctrine; namespace Symfony\Component\Messenger\Tests\Transport\Doctrine;
use Doctrine\Common\Persistence\ConnectionRegistry;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Bridge\Doctrine\RegistryInterface;
use Symfony\Component\Messenger\Transport\Doctrine\Connection; use Symfony\Component\Messenger\Transport\Doctrine\Connection;
use Symfony\Component\Messenger\Transport\Doctrine\DoctrineTransport; use Symfony\Component\Messenger\Transport\Doctrine\DoctrineTransport;
use Symfony\Component\Messenger\Transport\Doctrine\DoctrineTransportFactory; use Symfony\Component\Messenger\Transport\Doctrine\DoctrineTransportFactory;
@ -23,7 +23,7 @@ class DoctrineTransportFactoryTest extends TestCase
public function testSupports() public function testSupports()
{ {
$factory = new DoctrineTransportFactory( $factory = new DoctrineTransportFactory(
$this->getMockBuilder(RegistryInterface::class)->getMock() $this->getMockBuilder(ConnectionRegistry::class)->getMock()
); );
$this->assertTrue($factory->supports('doctrine://default', [])); $this->assertTrue($factory->supports('doctrine://default', []));
@ -35,7 +35,7 @@ class DoctrineTransportFactoryTest extends TestCase
$connection = $this->getMockBuilder(\Doctrine\DBAL\Connection::class) $connection = $this->getMockBuilder(\Doctrine\DBAL\Connection::class)
->disableOriginalConstructor() ->disableOriginalConstructor()
->getMock(); ->getMock();
$registry = $this->getMockBuilder(RegistryInterface::class)->getMock(); $registry = $this->getMockBuilder(ConnectionRegistry::class)->getMock();
$registry->expects($this->once()) $registry->expects($this->once())
->method('getConnection') ->method('getConnection')
->willReturn($connection); ->willReturn($connection);
@ -55,7 +55,7 @@ class DoctrineTransportFactoryTest extends TestCase
*/ */
public function testCreateTransportMustThrowAnExceptionIfManagerIsNotFound() public function testCreateTransportMustThrowAnExceptionIfManagerIsNotFound()
{ {
$registry = $this->getMockBuilder(RegistryInterface::class)->getMock(); $registry = $this->getMockBuilder(ConnectionRegistry::class)->getMock();
$registry->expects($this->once()) $registry->expects($this->once())
->method('getConnection') ->method('getConnection')
->willReturnCallback(function () { ->willReturnCallback(function () {

View File

@ -42,13 +42,13 @@ class ConnectionTest extends TestCase
public function testFromDsnWithOptions() public function testFromDsnWithOptions()
{ {
$this->assertEquals( $this->assertEquals(
new Connection(['stream' => 'queue', 'group' => 'group1', 'consumer' => 'consumer1', 'auto_setup' => false], [ new Connection(['stream' => 'queue', 'group' => 'group1', 'consumer' => 'consumer1', 'auto_setup' => false, 'stream_max_entries' => 20000], [
'host' => 'localhost', 'host' => 'localhost',
'port' => 6379, 'port' => 6379,
], [ ], [
'serializer' => 2, 'serializer' => 2,
]), ]),
Connection::fromDsn('redis://localhost/queue/group1/consumer1', ['serializer' => 2, 'auto_setup' => false]) Connection::fromDsn('redis://localhost/queue/group1/consumer1', ['serializer' => 2, 'auto_setup' => false, 'stream_max_entries' => 20000])
); );
} }
@ -79,6 +79,16 @@ class ConnectionTest extends TestCase
$this->assertNotNull($connection->get()); $this->assertNotNull($connection->get());
} }
public function testAuth()
{
$redis = $this->getMockBuilder(\Redis::class)->disableOriginalConstructor()->getMock();
$redis->expects($this->exactly(1))->method('auth')
->with('password');
Connection::fromDsn('redis://password@localhost/queue', [], $redis);
}
public function testFirstGetPendingMessagesThenNewMessages() public function testFirstGetPendingMessagesThenNewMessages()
{ {
$redis = $this->getMockBuilder(\Redis::class)->disableOriginalConstructor()->getMock(); $redis = $this->getMockBuilder(\Redis::class)->disableOriginalConstructor()->getMock();
@ -142,4 +152,16 @@ class ConnectionTest extends TestCase
$connection->reject($message['id']); $connection->reject($message['id']);
$redis->del('messenger-getnonblocking'); $redis->del('messenger-getnonblocking');
} }
public function testMaxEntries()
{
$redis = $this->getMockBuilder(\Redis::class)->disableOriginalConstructor()->getMock();
$redis->expects($this->exactly(1))->method('xadd')
->with('queue', '*', ['message' => '{"body":"1","headers":[]}'], 20000, true)
->willReturn(1);
$connection = Connection::fromDsn('redis://localhost/queue?stream_max_entries=20000', [], $redis); // 1 = always
$connection->add('1', []);
}
} }

View File

@ -11,7 +11,7 @@
namespace Symfony\Component\Messenger\Transport\Doctrine; namespace Symfony\Component\Messenger\Transport\Doctrine;
use Symfony\Bridge\Doctrine\RegistryInterface; use Doctrine\Common\Persistence\ConnectionRegistry;
use Symfony\Component\Messenger\Exception\TransportException; use Symfony\Component\Messenger\Exception\TransportException;
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface; use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
use Symfony\Component\Messenger\Transport\TransportFactoryInterface; use Symfony\Component\Messenger\Transport\TransportFactoryInterface;
@ -24,7 +24,7 @@ class DoctrineTransportFactory implements TransportFactoryInterface
{ {
private $registry; private $registry;
public function __construct(RegistryInterface $registry) public function __construct(ConnectionRegistry $registry)
{ {
$this->registry = $registry; $this->registry = $registry;
} }

View File

@ -31,6 +31,7 @@ class Connection
'group' => 'symfony', 'group' => 'symfony',
'consumer' => 'consumer', 'consumer' => 'consumer',
'auto_setup' => true, 'auto_setup' => true,
'stream_max_entries' => 0, // any value higher than 0 defines an approximate maximum number of stream entries
]; ];
private $connection; private $connection;
@ -38,6 +39,7 @@ class Connection
private $group; private $group;
private $consumer; private $consumer;
private $autoSetup; private $autoSetup;
private $maxEntries;
private $couldHavePendingMessages = true; private $couldHavePendingMessages = true;
public function __construct(array $configuration, array $connectionCredentials = [], array $redisOptions = [], \Redis $redis = null) public function __construct(array $configuration, array $connectionCredentials = [], array $redisOptions = [], \Redis $redis = null)
@ -49,10 +51,16 @@ class Connection
$this->connection = $redis ?: new \Redis(); $this->connection = $redis ?: new \Redis();
$this->connection->connect($connectionCredentials['host'] ?? '127.0.0.1', $connectionCredentials['port'] ?? 6379); $this->connection->connect($connectionCredentials['host'] ?? '127.0.0.1', $connectionCredentials['port'] ?? 6379);
$this->connection->setOption(\Redis::OPT_SERIALIZER, $redisOptions['serializer'] ?? \Redis::SERIALIZER_PHP); $this->connection->setOption(\Redis::OPT_SERIALIZER, $redisOptions['serializer'] ?? \Redis::SERIALIZER_PHP);
if (isset($connectionCredentials['auth'])) {
$this->connection->auth($connectionCredentials['auth']);
}
$this->stream = $configuration['stream'] ?? self::DEFAULT_OPTIONS['stream']; $this->stream = $configuration['stream'] ?? self::DEFAULT_OPTIONS['stream'];
$this->group = $configuration['group'] ?? self::DEFAULT_OPTIONS['group']; $this->group = $configuration['group'] ?? self::DEFAULT_OPTIONS['group'];
$this->consumer = $configuration['consumer'] ?? self::DEFAULT_OPTIONS['consumer']; $this->consumer = $configuration['consumer'] ?? self::DEFAULT_OPTIONS['consumer'];
$this->autoSetup = $configuration['auto_setup'] ?? self::DEFAULT_OPTIONS['auto_setup']; $this->autoSetup = $configuration['auto_setup'] ?? self::DEFAULT_OPTIONS['auto_setup'];
$this->maxEntries = $configuration['stream_max_entries'] ?? self::DEFAULT_OPTIONS['stream_max_entries'];
} }
public static function fromDsn(string $dsn, array $redisOptions = [], \Redis $redis = null): self public static function fromDsn(string $dsn, array $redisOptions = [], \Redis $redis = null): self
@ -70,6 +78,7 @@ class Connection
$connectionCredentials = [ $connectionCredentials = [
'host' => $parsedUrl['host'] ?? '127.0.0.1', 'host' => $parsedUrl['host'] ?? '127.0.0.1',
'port' => $parsedUrl['port'] ?? 6379, 'port' => $parsedUrl['port'] ?? 6379,
'auth' => $parsedUrl['pass'] ?? $parsedUrl['user'] ?? null,
]; ];
if (isset($parsedUrl['query'])) { if (isset($parsedUrl['query'])) {
@ -82,7 +91,19 @@ class Connection
unset($redisOptions['auto_setup']); unset($redisOptions['auto_setup']);
} }
return new self(['stream' => $stream, 'group' => $group, 'consumer' => $consumer, 'auto_setup' => $autoSetup], $connectionCredentials, $redisOptions, $redis); $maxEntries = null;
if (\array_key_exists('stream_max_entries', $redisOptions)) {
$maxEntries = filter_var($redisOptions['stream_max_entries'], FILTER_VALIDATE_INT);
unset($redisOptions['stream_max_entries']);
}
return new self([
'stream' => $stream,
'group' => $group,
'consumer' => $consumer,
'auto_setup' => $autoSetup,
'stream_max_entries' => $maxEntries,
], $connectionCredentials, $redisOptions, $redis);
} }
public function get(): ?array public function get(): ?array
@ -169,9 +190,15 @@ class Connection
$e = null; $e = null;
try { try {
$added = $this->connection->xadd($this->stream, '*', ['message' => json_encode( if ($this->maxEntries) {
['body' => $body, 'headers' => $headers] $added = $this->connection->xadd($this->stream, '*', ['message' => json_encode(
)]); ['body' => $body, 'headers' => $headers]
)], $this->maxEntries, true);
} else {
$added = $this->connection->xadd($this->stream, '*', ['message' => json_encode(
['body' => $body, 'headers' => $headers]
)]);
}
} catch (\RedisException $e) { } catch (\RedisException $e) {
} }

View File

@ -23,7 +23,8 @@ final class SMimeEncrypter extends SMime
private $cipher; private $cipher;
/** /**
* @param string|string[] $certificate Either a lone X.509 certificate, or an array of X.509 certificates * @param string|string[] $certificate The path (or array of paths) of the file(s) containing the X.509 certificate(s)
* @param int $cipher A set of algorithms used to encrypt the message. Must be one of these PHP constants: https://www.php.net/manual/en/openssl.ciphers.php
*/ */
public function __construct($certificate, int $cipher = OPENSSL_CIPHER_AES_256_CBC) public function __construct($certificate, int $cipher = OPENSSL_CIPHER_AES_256_CBC)
{ {

View File

@ -30,13 +30,11 @@ final class SMimeSigner extends SMime
private $privateKeyPassphrase; private $privateKeyPassphrase;
/** /**
* @see https://secure.php.net/manual/en/openssl.pkcs7.flags.php * @param string $certificate The path of the file containing the signing certificate (in PEM format)
* * @param string $privateKey The path of the file containing the private key (in PEM format)
* @param string $certificate
* @param string $privateKey A file containing the private key (in PEM format)
* @param string|null $privateKeyPassphrase A passphrase of the private key (if any) * @param string|null $privateKeyPassphrase A passphrase of the private key (if any)
* @param string $extraCerts A file containing intermediate certificates (in PEM format) needed by the signing certificate * @param string|null $extraCerts The path of the file containing intermediate certificates (in PEM format) needed by the signing certificate
* @param int $signOptions Bitwise operator options for openssl_pkcs7_sign() * @param int $signOptions Bitwise operator options for openssl_pkcs7_sign() (@see https://secure.php.net/manual/en/openssl.pkcs7.flags.php)
*/ */
public function __construct(string $certificate, string $privateKey, ?string $privateKeyPassphrase = null, ?string $extraCerts = null, int $signOptions = PKCS7_DETACHED) public function __construct(string $certificate, string $privateKey, ?string $privateKeyPassphrase = null, ?string $extraCerts = null, int $signOptions = PKCS7_DETACHED)
{ {

View File

@ -128,17 +128,11 @@ class Message extends RawMessage
return bin2hex(random_bytes(16)).strstr($email, '@'); return bin2hex(random_bytes(16)).strstr($email, '@');
} }
/**
* @internal
*/
public function __serialize(): array public function __serialize(): array
{ {
return [$this->headers, $this->body]; return [$this->headers, $this->body];
} }
/**
* @internal
*/
public function __unserialize(array $data): void public function __unserialize(array $data): void
{ {
[$this->headers, $this->body] = $data; [$this->headers, $this->body] = $data;

View File

@ -67,17 +67,11 @@ class RawMessage implements \Serializable
$this->__unserialize(unserialize($serialized)); $this->__unserialize(unserialize($serialized));
} }
/**
* @internal
*/
public function __serialize(): array public function __serialize(): array
{ {
return [$this->message]; return [$this->message];
} }
/**
* @internal
*/
public function __unserialize(array $data): void public function __unserialize(array $data): void
{ {
[$this->message] = $data; [$this->message] = $data;

View File

@ -1,6 +1,12 @@
CHANGELOG CHANGELOG
========= =========
4.4.0
-----
* deprecated passing `null` as `$defaultLifetime` 2nd argument of `PropertyAccessor::createCache()` method,
pass `0` instead
4.3.0 4.3.0
----- -----

View File

@ -14,6 +14,7 @@ namespace Symfony\Component\PropertyInfo\Extractor;
use phpDocumentor\Reflection\DocBlock; use phpDocumentor\Reflection\DocBlock;
use phpDocumentor\Reflection\DocBlockFactory; use phpDocumentor\Reflection\DocBlockFactory;
use phpDocumentor\Reflection\DocBlockFactoryInterface; use phpDocumentor\Reflection\DocBlockFactoryInterface;
use phpDocumentor\Reflection\Types\Context;
use phpDocumentor\Reflection\Types\ContextFactory; use phpDocumentor\Reflection\Types\ContextFactory;
use Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface; use Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
@ -38,6 +39,11 @@ class PhpDocExtractor implements PropertyDescriptionExtractorInterface, Property
*/ */
private $docBlocks = []; private $docBlocks = [];
/**
* @var Context[]
*/
private $contexts = [];
private $docBlockFactory; private $docBlockFactory;
private $contextFactory; private $contextFactory;
private $phpDocTypeHelper; private $phpDocTypeHelper;
@ -191,7 +197,7 @@ class PhpDocExtractor implements PropertyDescriptionExtractorInterface, Property
} }
try { try {
return $this->docBlockFactory->create($reflectionProperty, $this->contextFactory->createFromReflector($reflectionProperty->getDeclaringClass())); return $this->docBlockFactory->create($reflectionProperty, $this->createFromReflector($reflectionProperty->getDeclaringClass()));
} catch (\InvalidArgumentException $e) { } catch (\InvalidArgumentException $e) {
return null; return null;
} }
@ -227,9 +233,25 @@ class PhpDocExtractor implements PropertyDescriptionExtractorInterface, Property
} }
try { try {
return [$this->docBlockFactory->create($reflectionMethod, $this->contextFactory->createFromReflector($reflectionMethod)), $prefix]; return [$this->docBlockFactory->create($reflectionMethod, $this->createFromReflector($reflectionMethod->getDeclaringClass())), $prefix];
} catch (\InvalidArgumentException $e) { } catch (\InvalidArgumentException $e) {
return null; return null;
} }
} }
/**
* Prevents a lot of redundant calls to ContextFactory::createForNamespace().
*/
private function createFromReflector(\ReflectionClass $reflector): Context
{
$cacheKey = $reflector->getNamespaceName().':'.$reflector->getFileName();
if (isset($this->contexts[$cacheKey])) {
return $this->contexts[$cacheKey];
}
$this->contexts[$cacheKey] = $this->contextFactory->createFromReflector($reflector);
return $this->contexts[$cacheKey];
}
} }

View File

@ -18,10 +18,14 @@ CHANGELOG
4.4.0 4.4.0
----- -----
* using anything else than a `string` as the code of a `ConstraintViolation` is deprecated, a `string` type-hint will
be added to the constructor of the `ConstraintViolation` class and to the `ConstraintViolationBuilder::setCode()`
method in 5.0
* deprecated passing an `ExpressionLanguage` instance as the second argument of `ExpressionValidator::__construct()`. Pass it as the first argument instead. * deprecated passing an `ExpressionLanguage` instance as the second argument of `ExpressionValidator::__construct()`. Pass it as the first argument instead.
* added the `compared_value_path` parameter in violations when using any * added the `compared_value_path` parameter in violations when using any
comparison constraint with the `propertyPath` option. comparison constraint with the `propertyPath` option.
* added support for checking an array of types in `TypeValidator` * added support for checking an array of types in `TypeValidator`
* added a new `allowEmptyString` option to the `Length` constraint to allow rejecting empty strings when `min` is set, by setting it to `false`.
4.3.0 4.3.0
----- -----

View File

@ -49,7 +49,7 @@ class ConstraintViolation implements ConstraintViolationInterface
* caused the violation * caused the violation
* @param mixed $cause The cause of the violation * @param mixed $cause The cause of the violation
*/ */
public function __construct(string $message, ?string $messageTemplate, array $parameters, $root, ?string $propertyPath, $invalidValue, int $plural = null, $code = null, Constraint $constraint = null, $cause = null) public function __construct(string $message, ?string $messageTemplate, array $parameters, $root, ?string $propertyPath, $invalidValue, int $plural = null, string $code = null, Constraint $constraint = null, $cause = null)
{ {
$this->message = $message; $this->message = $message;
$this->messageTemplate = $messageTemplate; $this->messageTemplate = $messageTemplate;
@ -79,13 +79,12 @@ class ConstraintViolation implements ConstraintViolationInterface
} }
$propertyPath = (string) $this->propertyPath; $propertyPath = (string) $this->propertyPath;
$code = (string) $this->code;
if ('' !== $propertyPath && '[' !== $propertyPath[0] && '' !== $class) { if ('' !== $propertyPath && '[' !== $propertyPath[0] && '' !== $class) {
$class .= '.'; $class .= '.';
} }
if ('' !== $code) { if ('' !== $code = $this->code) {
$code = ' (code '.$code.')'; $code = ' (code '.$code.')';
} }

View File

@ -65,49 +65,49 @@ class FileValidator extends ConstraintValidator
$this->context->buildViolation($constraint->uploadIniSizeErrorMessage) $this->context->buildViolation($constraint->uploadIniSizeErrorMessage)
->setParameter('{{ limit }}', $limitAsString) ->setParameter('{{ limit }}', $limitAsString)
->setParameter('{{ suffix }}', $suffix) ->setParameter('{{ suffix }}', $suffix)
->setCode(UPLOAD_ERR_INI_SIZE) ->setCode((string) UPLOAD_ERR_INI_SIZE)
->addViolation(); ->addViolation();
return; return;
case UPLOAD_ERR_FORM_SIZE: case UPLOAD_ERR_FORM_SIZE:
$this->context->buildViolation($constraint->uploadFormSizeErrorMessage) $this->context->buildViolation($constraint->uploadFormSizeErrorMessage)
->setCode(UPLOAD_ERR_FORM_SIZE) ->setCode((string) UPLOAD_ERR_FORM_SIZE)
->addViolation(); ->addViolation();
return; return;
case UPLOAD_ERR_PARTIAL: case UPLOAD_ERR_PARTIAL:
$this->context->buildViolation($constraint->uploadPartialErrorMessage) $this->context->buildViolation($constraint->uploadPartialErrorMessage)
->setCode(UPLOAD_ERR_PARTIAL) ->setCode((string) UPLOAD_ERR_PARTIAL)
->addViolation(); ->addViolation();
return; return;
case UPLOAD_ERR_NO_FILE: case UPLOAD_ERR_NO_FILE:
$this->context->buildViolation($constraint->uploadNoFileErrorMessage) $this->context->buildViolation($constraint->uploadNoFileErrorMessage)
->setCode(UPLOAD_ERR_NO_FILE) ->setCode((string) UPLOAD_ERR_NO_FILE)
->addViolation(); ->addViolation();
return; return;
case UPLOAD_ERR_NO_TMP_DIR: case UPLOAD_ERR_NO_TMP_DIR:
$this->context->buildViolation($constraint->uploadNoTmpDirErrorMessage) $this->context->buildViolation($constraint->uploadNoTmpDirErrorMessage)
->setCode(UPLOAD_ERR_NO_TMP_DIR) ->setCode((string) UPLOAD_ERR_NO_TMP_DIR)
->addViolation(); ->addViolation();
return; return;
case UPLOAD_ERR_CANT_WRITE: case UPLOAD_ERR_CANT_WRITE:
$this->context->buildViolation($constraint->uploadCantWriteErrorMessage) $this->context->buildViolation($constraint->uploadCantWriteErrorMessage)
->setCode(UPLOAD_ERR_CANT_WRITE) ->setCode((string) UPLOAD_ERR_CANT_WRITE)
->addViolation(); ->addViolation();
return; return;
case UPLOAD_ERR_EXTENSION: case UPLOAD_ERR_EXTENSION:
$this->context->buildViolation($constraint->uploadExtensionErrorMessage) $this->context->buildViolation($constraint->uploadExtensionErrorMessage)
->setCode(UPLOAD_ERR_EXTENSION) ->setCode((string) UPLOAD_ERR_EXTENSION)
->addViolation(); ->addViolation();
return; return;
default: default:
$this->context->buildViolation($constraint->uploadErrorMessage) $this->context->buildViolation($constraint->uploadErrorMessage)
->setCode($value->getError()) ->setCode((string) $value->getError())
->addViolation(); ->addViolation();
return; return;

View File

@ -41,6 +41,7 @@ class Length extends Constraint
public $min; public $min;
public $charset = 'UTF-8'; public $charset = 'UTF-8';
public $normalizer; public $normalizer;
public $allowEmptyString;
public function __construct($options = null) public function __construct($options = null)
{ {
@ -56,6 +57,13 @@ class Length extends Constraint
parent::__construct($options); parent::__construct($options);
if (null === $this->allowEmptyString) {
$this->allowEmptyString = true;
if (null !== $this->min) {
@trigger_error(sprintf('Using the "%s" constraint with the "min" option without setting the "allowEmptyString" one is deprecated and defaults to true. In 5.0, it will become optional and default to false.', self::class), E_USER_DEPRECATED);
}
}
if (null === $this->min && null === $this->max) { if (null === $this->min && null === $this->max) {
throw new MissingOptionsException(sprintf('Either option "min" or "max" must be given for constraint %s', __CLASS__), ['min', 'max']); throw new MissingOptionsException(sprintf('Either option "min" or "max" must be given for constraint %s', __CLASS__), ['min', 'max']);
} }

View File

@ -30,7 +30,7 @@ class LengthValidator extends ConstraintValidator
throw new UnexpectedTypeException($constraint, __NAMESPACE__.'\Length'); throw new UnexpectedTypeException($constraint, __NAMESPACE__.'\Length');
} }
if (null === $value || '' === $value) { if (null === $value || ('' === $value && $constraint->allowEmptyString)) {
return; return;
} }

View File

@ -64,7 +64,7 @@ EOF;
'some_value', 'some_value',
null, null,
null, null,
0 '0'
); );
$expected = <<<'EOF' $expected = <<<'EOF'
@ -108,4 +108,24 @@ EOF;
$this->assertSame($expected, (string) $violation); $this->assertSame($expected, (string) $violation);
} }
/**
* @group legacy
* @expectedDeprecation Not using a string as the error code in Symfony\Component\Validator\ConstraintViolation::__construct() is deprecated since Symfony 4.4. A type-hint will be added in 5.0.
*/
public function testNonStringCode()
{
$violation = new ConstraintViolation(
'42 cannot be used here',
'this is the message template',
[],
['some_value' => 42],
'some_value',
null,
null,
42
);
self::assertSame(42, $violation->getCode());
}
} }

View File

@ -435,23 +435,23 @@ abstract class FileValidatorTest extends ConstraintValidatorTestCase
public function uploadedFileErrorProvider() public function uploadedFileErrorProvider()
{ {
$tests = [ $tests = [
[UPLOAD_ERR_FORM_SIZE, 'uploadFormSizeErrorMessage'], [(string) UPLOAD_ERR_FORM_SIZE, 'uploadFormSizeErrorMessage'],
[UPLOAD_ERR_PARTIAL, 'uploadPartialErrorMessage'], [(string) UPLOAD_ERR_PARTIAL, 'uploadPartialErrorMessage'],
[UPLOAD_ERR_NO_FILE, 'uploadNoFileErrorMessage'], [(string) UPLOAD_ERR_NO_FILE, 'uploadNoFileErrorMessage'],
[UPLOAD_ERR_NO_TMP_DIR, 'uploadNoTmpDirErrorMessage'], [(string) UPLOAD_ERR_NO_TMP_DIR, 'uploadNoTmpDirErrorMessage'],
[UPLOAD_ERR_CANT_WRITE, 'uploadCantWriteErrorMessage'], [(string) UPLOAD_ERR_CANT_WRITE, 'uploadCantWriteErrorMessage'],
[UPLOAD_ERR_EXTENSION, 'uploadExtensionErrorMessage'], [(string) UPLOAD_ERR_EXTENSION, 'uploadExtensionErrorMessage'],
]; ];
if (class_exists('Symfony\Component\HttpFoundation\File\UploadedFile')) { if (class_exists('Symfony\Component\HttpFoundation\File\UploadedFile')) {
// when no maxSize is specified on constraint, it should use the ini value // when no maxSize is specified on constraint, it should use the ini value
$tests[] = [UPLOAD_ERR_INI_SIZE, 'uploadIniSizeErrorMessage', [ $tests[] = [(string) UPLOAD_ERR_INI_SIZE, 'uploadIniSizeErrorMessage', [
'{{ limit }}' => UploadedFile::getMaxFilesize() / 1048576, '{{ limit }}' => UploadedFile::getMaxFilesize() / 1048576,
'{{ suffix }}' => 'MiB', '{{ suffix }}' => 'MiB',
]]; ]];
// it should use the smaller limitation (maxSize option in this case) // it should use the smaller limitation (maxSize option in this case)
$tests[] = [UPLOAD_ERR_INI_SIZE, 'uploadIniSizeErrorMessage', [ $tests[] = [(string) UPLOAD_ERR_INI_SIZE, 'uploadIniSizeErrorMessage', [
'{{ limit }}' => 1, '{{ limit }}' => 1,
'{{ suffix }}' => 'bytes', '{{ suffix }}' => 'bytes',
], '1']; ], '1'];
@ -464,14 +464,14 @@ abstract class FileValidatorTest extends ConstraintValidatorTestCase
// it correctly parses the maxSize option and not only uses simple string comparison // it correctly parses the maxSize option and not only uses simple string comparison
// 1000M should be bigger than the ini value // 1000M should be bigger than the ini value
$tests[] = [UPLOAD_ERR_INI_SIZE, 'uploadIniSizeErrorMessage', [ $tests[] = [(string) UPLOAD_ERR_INI_SIZE, 'uploadIniSizeErrorMessage', [
'{{ limit }}' => $limit, '{{ limit }}' => $limit,
'{{ suffix }}' => $suffix, '{{ suffix }}' => $suffix,
], '1000M']; ], '1000M'];
// it correctly parses the maxSize option and not only uses simple string comparison // it correctly parses the maxSize option and not only uses simple string comparison
// 1000M should be bigger than the ini value // 1000M should be bigger than the ini value
$tests[] = [UPLOAD_ERR_INI_SIZE, 'uploadIniSizeErrorMessage', [ $tests[] = [(string) UPLOAD_ERR_INI_SIZE, 'uploadIniSizeErrorMessage', [
'{{ limit }}' => '0.1', '{{ limit }}' => '0.1',
'{{ suffix }}' => 'MB', '{{ suffix }}' => 'MB',
], '100K']; ], '100K'];

View File

@ -21,7 +21,7 @@ class LengthTest extends TestCase
{ {
public function testNormalizerCanBeSet() public function testNormalizerCanBeSet()
{ {
$length = new Length(['min' => 0, 'max' => 10, 'normalizer' => 'trim']); $length = new Length(['min' => 0, 'max' => 10, 'normalizer' => 'trim', 'allowEmptyString' => false]);
$this->assertEquals('trim', $length->normalizer); $this->assertEquals('trim', $length->normalizer);
} }
@ -32,7 +32,7 @@ class LengthTest extends TestCase
*/ */
public function testInvalidNormalizerThrowsException() public function testInvalidNormalizerThrowsException()
{ {
new Length(['min' => 0, 'max' => 10, 'normalizer' => 'Unknown Callable']); new Length(['min' => 0, 'max' => 10, 'normalizer' => 'Unknown Callable', 'allowEmptyString' => false]);
} }
/** /**
@ -41,6 +41,6 @@ class LengthTest extends TestCase
*/ */
public function testInvalidNormalizerObjectThrowsException() public function testInvalidNormalizerObjectThrowsException()
{ {
new Length(['min' => 0, 'max' => 10, 'normalizer' => new \stdClass()]); new Length(['min' => 0, 'max' => 10, 'normalizer' => new \stdClass(), 'allowEmptyString' => false]);
} }
} }

View File

@ -22,26 +22,47 @@ class LengthValidatorTest extends ConstraintValidatorTestCase
return new LengthValidator(); return new LengthValidator();
} }
public function testNullIsValid() public function testLegacyNullIsValid()
{ {
$this->validator->validate(null, new Length(6)); $this->validator->validate(null, new Length(['value' => 6, 'allowEmptyString' => false]));
$this->assertNoViolation(); $this->assertNoViolation();
} }
public function testEmptyStringIsValid() /**
* @group legacy
* @expectedDeprecation Using the "Symfony\Component\Validator\Constraints\Length" constraint with the "min" option without setting the "allowEmptyString" one is deprecated and defaults to true. In 5.0, it will become optional and default to false.
*/
public function testLegacyEmptyStringIsValid()
{ {
$this->validator->validate('', new Length(6)); $this->validator->validate('', new Length(6));
$this->assertNoViolation(); $this->assertNoViolation();
} }
public function testEmptyStringIsInvalid()
{
$this->validator->validate('', new Length([
'value' => $limit = 6,
'allowEmptyString' => false,
'exactMessage' => 'myMessage',
]));
$this->buildViolation('myMessage')
->setParameter('{{ value }}', '""')
->setParameter('{{ limit }}', $limit)
->setInvalidValue('')
->setPlural($limit)
->setCode(Length::TOO_SHORT_ERROR)
->assertRaised();
}
/** /**
* @expectedException \Symfony\Component\Validator\Exception\UnexpectedValueException * @expectedException \Symfony\Component\Validator\Exception\UnexpectedValueException
*/ */
public function testExpectsStringCompatibleType() public function testExpectsStringCompatibleType()
{ {
$this->validator->validate(new \stdClass(), new Length(5)); $this->validator->validate(new \stdClass(), new Length(['value' => 5, 'allowEmptyString' => false]));
} }
public function getThreeOrLessCharacters() public function getThreeOrLessCharacters()
@ -109,7 +130,7 @@ class LengthValidatorTest extends ConstraintValidatorTestCase
*/ */
public function testValidValuesMin($value) public function testValidValuesMin($value)
{ {
$constraint = new Length(['min' => 5]); $constraint = new Length(['min' => 5, 'allowEmptyString' => false]);
$this->validator->validate($value, $constraint); $this->validator->validate($value, $constraint);
$this->assertNoViolation(); $this->assertNoViolation();
@ -131,7 +152,7 @@ class LengthValidatorTest extends ConstraintValidatorTestCase
*/ */
public function testValidValuesExact($value) public function testValidValuesExact($value)
{ {
$constraint = new Length(4); $constraint = new Length(['value' => 4, 'allowEmptyString' => false]);
$this->validator->validate($value, $constraint); $this->validator->validate($value, $constraint);
$this->assertNoViolation(); $this->assertNoViolation();
@ -142,7 +163,7 @@ class LengthValidatorTest extends ConstraintValidatorTestCase
*/ */
public function testValidNormalizedValues($value) public function testValidNormalizedValues($value)
{ {
$constraint = new Length(['min' => 3, 'max' => 3, 'normalizer' => 'trim']); $constraint = new Length(['min' => 3, 'max' => 3, 'normalizer' => 'trim', 'allowEmptyString' => false]);
$this->validator->validate($value, $constraint); $this->validator->validate($value, $constraint);
$this->assertNoViolation(); $this->assertNoViolation();
@ -156,6 +177,7 @@ class LengthValidatorTest extends ConstraintValidatorTestCase
$constraint = new Length([ $constraint = new Length([
'min' => 4, 'min' => 4,
'minMessage' => 'myMessage', 'minMessage' => 'myMessage',
'allowEmptyString' => false,
]); ]);
$this->validator->validate($value, $constraint); $this->validator->validate($value, $constraint);
@ -199,6 +221,7 @@ class LengthValidatorTest extends ConstraintValidatorTestCase
'min' => 4, 'min' => 4,
'max' => 4, 'max' => 4,
'exactMessage' => 'myMessage', 'exactMessage' => 'myMessage',
'allowEmptyString' => false,
]); ]);
$this->validator->validate($value, $constraint); $this->validator->validate($value, $constraint);
@ -221,6 +244,7 @@ class LengthValidatorTest extends ConstraintValidatorTestCase
'min' => 4, 'min' => 4,
'max' => 4, 'max' => 4,
'exactMessage' => 'myMessage', 'exactMessage' => 'myMessage',
'allowEmptyString' => false,
]); ]);
$this->validator->validate($value, $constraint); $this->validator->validate($value, $constraint);
@ -244,6 +268,7 @@ class LengthValidatorTest extends ConstraintValidatorTestCase
'max' => 1, 'max' => 1,
'charset' => $charset, 'charset' => $charset,
'charsetMessage' => 'myMessage', 'charsetMessage' => 'myMessage',
'allowEmptyString' => false,
]); ]);
$this->validator->validate($value, $constraint); $this->validator->validate($value, $constraint);
@ -262,7 +287,7 @@ class LengthValidatorTest extends ConstraintValidatorTestCase
public function testConstraintDefaultOption() public function testConstraintDefaultOption()
{ {
$constraint = new Length(5); $constraint = new Length(['value' => 5, 'allowEmptyString' => false]);
$this->assertEquals(5, $constraint->min); $this->assertEquals(5, $constraint->min);
$this->assertEquals(5, $constraint->max); $this->assertEquals(5, $constraint->max);
@ -270,7 +295,7 @@ class LengthValidatorTest extends ConstraintValidatorTestCase
public function testConstraintAnnotationDefaultOption() public function testConstraintAnnotationDefaultOption()
{ {
$constraint = new Length(['value' => 5, 'exactMessage' => 'message']); $constraint = new Length(['value' => 5, 'exactMessage' => 'message', 'allowEmptyString' => false]);
$this->assertEquals(5, $constraint->min); $this->assertEquals(5, $constraint->min);
$this->assertEquals(5, $constraint->max); $this->assertEquals(5, $constraint->max);

View File

@ -103,7 +103,7 @@ class RecursiveValidatorTest extends AbstractTest
public function testCollectionConstraintValidateAllGroupsForNestedConstraints() public function testCollectionConstraintValidateAllGroupsForNestedConstraints()
{ {
$this->metadata->addPropertyConstraint('data', new Collection(['fields' => [ $this->metadata->addPropertyConstraint('data', new Collection(['fields' => [
'one' => [new NotBlank(['groups' => 'one']), new Length(['min' => 2, 'groups' => 'two'])], 'one' => [new NotBlank(['groups' => 'one']), new Length(['min' => 2, 'groups' => 'two', 'allowEmptyString' => false])],
'two' => [new NotBlank(['groups' => 'two'])], 'two' => [new NotBlank(['groups' => 'two'])],
]])); ]]));
@ -121,7 +121,7 @@ class RecursiveValidatorTest extends AbstractTest
{ {
$this->metadata->addPropertyConstraint('data', new All(['constraints' => [ $this->metadata->addPropertyConstraint('data', new All(['constraints' => [
new NotBlank(['groups' => 'one']), new NotBlank(['groups' => 'one']),
new Length(['min' => 2, 'groups' => 'two']), new Length(['min' => 2, 'groups' => 'two', 'allowEmptyString' => false]),
]])); ]]));
$entity = new Entity(); $entity = new Entity();
@ -129,8 +129,9 @@ class RecursiveValidatorTest extends AbstractTest
$violations = $this->validator->validate($entity, null, ['one', 'two']); $violations = $this->validator->validate($entity, null, ['one', 'two']);
$this->assertCount(2, $violations); $this->assertCount(3, $violations);
$this->assertInstanceOf(NotBlank::class, $violations->get(0)->getConstraint()); $this->assertInstanceOf(NotBlank::class, $violations->get(0)->getConstraint());
$this->assertInstanceOf(Length::class, $violations->get(1)->getConstraint()); $this->assertInstanceOf(Length::class, $violations->get(1)->getConstraint());
$this->assertInstanceOf(Length::class, $violations->get(2)->getConstraint());
} }
} }

View File

@ -0,0 +1,36 @@
<?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\Validator\Tests\Violation;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Translation\IdentityTranslator;
use Symfony\Component\Validator\ConstraintViolationList;
use Symfony\Component\Validator\Tests\Fixtures\ConstraintA;
use Symfony\Component\Validator\Violation\ConstraintViolationBuilder;
class ConstraintViolationBuilderTest extends TestCase
{
/**
* @group legacy
* @expectedDeprecation Not using a string as the error code in Symfony\Component\Validator\Violation\ConstraintViolationBuilder::setCode() is deprecated since Symfony 4.4. A type-hint will be added in 5.0.
* @expectedDeprecation Not using a string as the error code in Symfony\Component\Validator\ConstraintViolation::__construct() is deprecated since Symfony 4.4. A type-hint will be added in 5.0.
*/
public function testNonStringCode()
{
$constraintViolationList = new ConstraintViolationList();
(new ConstraintViolationBuilder($constraintViolationList, new ConstraintA(), 'invalid message', [], null, 'foo', 'baz', new IdentityTranslator()))
->setCode(42)
->addViolation();
self::assertSame(42, $constraintViolationList->get(0)->getCode());
}
}

View File

@ -121,6 +121,10 @@ class ConstraintViolationBuilder implements ConstraintViolationBuilderInterface
*/ */
public function setCode(?string $code) public function setCode(?string $code)
{ {
if (null !== $code && !\is_string($code)) {
@trigger_error(sprintf('Not using a string as the error code in %s() is deprecated since Symfony 4.4. A type-hint will be added in 5.0.', __METHOD__), E_USER_DEPRECATED);
}
$this->code = $code; $this->code = $code;
return $this; return $this;