diff --git a/UPGRADE-4.3.md b/UPGRADE-4.3.md
index 20b970c15a..b85e09dadc 100644
--- a/UPGRADE-4.3.md
+++ b/UPGRADE-4.3.md
@@ -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 `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
----
@@ -71,6 +77,7 @@ Form
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
be mandatory in 5.0.
* Deprecated the "Psr\SimpleCache\CacheInterface" / "cache.app.simple" service, use "Symfony\Contracts\Cache\CacheInterface" / "cache.app" instead.
diff --git a/UPGRADE-4.4.md b/UPGRADE-4.4.md
index 96c170675c..37b9ef22a1 100644
--- a/UPGRADE-4.4.md
+++ b/UPGRADE-4.4.md
@@ -26,7 +26,7 @@ DependencyInjection
services:
App\Handler:
tags: ['app.handler']
-
+
App\HandlerCollection:
arguments: [!tagged app.handler]
```
@@ -36,11 +36,16 @@ DependencyInjection
services:
App\Handler:
tags: ['app.handler']
-
+
App\HandlerCollection:
arguments: [!tagged_iterator app.handler]
```
+Filesystem
+----------
+
+ * Support for passing a `null` value to `Filesystem::isAbsolutePath()` is deprecated.
+
Form
----
@@ -60,6 +65,11 @@ HttpClient
* Added method `cancel()` to `ResponseInterface`
+HttpFoundation
+--------------
+
+ * `ApacheRequest` is deprecated, use `Request` class instead.
+
HttpKernel
----------
@@ -76,6 +86,11 @@ MonologBridge
* The `RouteProcessor` has been marked final.
+PropertyAccess
+--------------
+
+ * Deprecated passing `null` as 2nd argument of `PropertyAccessor::createCache()` method (`$defaultLifetime`), pass `0` instead.
+
Security
--------
@@ -84,11 +99,19 @@ Security
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.
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()`.
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`.
diff --git a/UPGRADE-5.0.md b/UPGRADE-5.0.md
index f4cc77b915..20bd88bc09 100644
--- a/UPGRADE-5.0.md
+++ b/UPGRADE-5.0.md
@@ -101,7 +101,7 @@ DependencyInjection
services:
App\Handler:
tags: ['app.handler']
-
+
App\HandlerCollection:
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
* Not passing an `IdReader` to the `DoctrineChoiceLoader` when the query can be optimized with single id field will not apply any optimization
-
DomCrawler
----------
@@ -135,6 +134,7 @@ EventDispatcher
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::appendToFile()` method no longer supports arrays in the `$content` argument.
@@ -207,8 +207,8 @@ Form
FrameworkBundle
---------------
+ * Removed the `framework.templating` option, use Twig instead.
* The project dir argument of the constructor of `AssetsInstallCommand` is required.
-
* 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.
@@ -269,6 +269,7 @@ HttpFoundation
use `Symfony\Component\Mime\FileBinaryMimeTypeGuesser` instead.
* The `FileinfoMimeTypeGuesser` class has been removed,
use `Symfony\Component\Mime\FileinfoMimeTypeGuesser` instead.
+ * `ApacheRequest` has been removed, use the `Request` class instead.
HttpKernel
----------
@@ -476,6 +477,8 @@ TwigBridge
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()`
* The `checkMX` and `checkHost` options of the `Email` constraint were removed
* The `Email::__construct()` 'strict' property has been removed. Use 'mode'=>"strict" instead.
@@ -540,7 +543,6 @@ Workflow
property: state
```
-
* Support for using a workflow with a single state marking is dropped. Use a state machine instead.
Before:
diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/BaseUser.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/BaseUser.php
index abf8819a4c..50b5845581 100644
--- a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/BaseUser.php
+++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/BaseUser.php
@@ -2,6 +2,9 @@
namespace Symfony\Bridge\Doctrine\Tests\Fixtures;
+use Symfony\Component\Validator\Constraints as Assert;
+use Symfony\Component\Validator\Mapping\ClassMetadata;
+
/**
* Class BaseUser.
*/
@@ -46,4 +49,15 @@ class BaseUser
{
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));
+ }
}
diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/DoctrineLoaderEntity.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/DoctrineLoaderEntity.php
index dc06d37fa3..9a2111f2b9 100644
--- a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/DoctrineLoaderEntity.php
+++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/DoctrineLoaderEntity.php
@@ -14,6 +14,7 @@ namespace Symfony\Bridge\Doctrine\Tests\Fixtures;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert;
+use Symfony\Component\Validator\Mapping\ClassMetadata;
/**
* @ORM\Entity
@@ -36,13 +37,11 @@ class DoctrineLoaderEntity extends DoctrineLoaderParentEntity
/**
* @ORM\Column(length=20)
- * @Assert\Length(min=5)
*/
public $mergedMaxLength;
/**
* @ORM\Column(length=20)
- * @Assert\Length(min=1, max=10)
*/
public $alreadyMappedMaxLength;
@@ -69,4 +68,12 @@ class DoctrineLoaderEntity extends DoctrineLoaderParentEntity
/** @ORM\Column(type="simple_array", length=100) */
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));
+ }
}
diff --git a/src/Symfony/Bridge/Doctrine/Tests/Resources/validator/BaseUser.xml b/src/Symfony/Bridge/Doctrine/Tests/Resources/validator/BaseUser.xml
index bf64b92ca4..ddb8a13bc1 100644
--- a/src/Symfony/Bridge/Doctrine/Tests/Resources/validator/BaseUser.xml
+++ b/src/Symfony/Bridge/Doctrine/Tests/Resources/validator/BaseUser.xml
@@ -9,11 +9,6 @@
-
-
-
-
-
diff --git a/src/Symfony/Bridge/Doctrine/Tests/Validator/DoctrineLoaderTest.php b/src/Symfony/Bridge/Doctrine/Tests/Validator/DoctrineLoaderTest.php
index 2dcab2533d..45cae2da41 100644
--- a/src/Symfony/Bridge/Doctrine/Tests/Validator/DoctrineLoaderTest.php
+++ b/src/Symfony/Bridge/Doctrine/Tests/Validator/DoctrineLoaderTest.php
@@ -40,6 +40,7 @@ class DoctrineLoaderTest extends TestCase
}
$validator = Validation::createValidatorBuilder()
+ ->addMethodMapping('loadValidatorMetadata')
->enableAnnotationMapping()
->addLoader(new DoctrineLoader(DoctrineTestHelper::createTestEntityManager(), '{^Symfony\\\\Bridge\\\\Doctrine\\\\Tests\\\\Fixtures\\\\DoctrineLoader}'))
->getValidator()
@@ -142,6 +143,7 @@ class DoctrineLoaderTest extends TestCase
}
$validator = Validation::createValidatorBuilder()
+ ->addMethodMapping('loadValidatorMetadata')
->enableAnnotationMapping()
->addXmlMappings([__DIR__.'/../Resources/validator/BaseUser.xml'])
->addLoader(
diff --git a/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php b/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php
index db530fe884..203797b5e9 100644
--- a/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php
+++ b/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php
@@ -131,7 +131,7 @@ if (!file_exists("$PHPUNIT_DIR/phpunit-$PHPUNIT_VERSION/phpunit") || md5_file(__
$prevRoot = getenv('COMPOSER_ROOT_VERSION');
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
- $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 : ''));
if ($exit) {
exit($exit);
diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
index c61311e7c8..050714d630 100644
--- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
+++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
@@ -29,6 +29,7 @@ CHANGELOG
4.3.0
-----
+ * Deprecated the `framework.templating` option, use Twig instead.
* Added `WebTestAssertionsTrait` (included by default in `WebTestCase`)
* Renamed `Client` to `KernelBrowser`
* Not passing the project directory to the constructor of the `AssetsInstallCommand` is deprecated. This argument will
diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
index fe5106dd5c..1e9a2dbc6c 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
@@ -297,10 +297,7 @@ class Configuration implements ConfigurationInterface
->cannotBeEmpty()
->end()
->arrayNode('initial_marking')
- ->beforeNormalization()
- ->ifTrue(function ($v) { return !\is_array($v); })
- ->then(function ($v) { return [$v]; })
- ->end()
+ ->beforeNormalization()->castToArray()->end()
->defaultValue([])
->prototype('scalar')->end()
->end()
@@ -533,10 +530,7 @@ class Configuration implements ConfigurationInterface
->ifTrue(function ($v) { return \is_array($v) && isset($v['mime_type']); })
->then(function ($v) { return $v['mime_type']; })
->end()
- ->beforeNormalization()
- ->ifTrue(function ($v) { return !\is_array($v); })
- ->then(function ($v) { return [$v]; })
- ->end()
+ ->beforeNormalization()->castToArray()->end()
->prototype('scalar')->end()
->end()
->end()
@@ -562,10 +556,7 @@ class Configuration implements ConfigurationInterface
->scalarNode('base_path')->defaultValue('')->end()
->arrayNode('base_urls')
->requiresAtLeastOneElement()
- ->beforeNormalization()
- ->ifTrue(function ($v) { return !\is_array($v); })
- ->then(function ($v) { return [$v]; })
- ->end()
+ ->beforeNormalization()->castToArray()->end()
->prototype('scalar')->end()
->end()
->end()
@@ -607,10 +598,7 @@ class Configuration implements ConfigurationInterface
->scalarNode('base_path')->defaultValue('')->end()
->arrayNode('base_urls')
->requiresAtLeastOneElement()
- ->beforeNormalization()
- ->ifTrue(function ($v) { return !\is_array($v); })
- ->then(function ($v) { return [$v]; })
- ->end()
+ ->beforeNormalization()->castToArray()->end()
->prototype('scalar')->end()
->end()
->end()
@@ -684,10 +672,7 @@ class Configuration implements ConfigurationInterface
->defaultValue(['loadValidatorMetadata'])
->prototype('scalar')->end()
->treatFalseLike([])
- ->validate()
- ->ifTrue(function ($v) { return !\is_array($v); })
- ->then(function ($v) { return (array) $v; })
- ->end()
+ ->validate()->castToArray()->end()
->end()
->scalarNode('translation_domain')->defaultValue('validators')->end()
->enumNode('email_validation_mode')->values(['html5', 'loose', 'strict'])->end()
@@ -1061,9 +1046,14 @@ class Configuration implements ConfigurationInterface
->end()
->arrayNode('retry_strategy')
->addDefaultsIfNotSet()
- ->validate()
- ->ifTrue(function ($v) { return null !== $v['service'] && (isset($v['max_retries']) || isset($v['delay']) || isset($v['multiplier']) || isset($v['max_delay'])); })
- ->thenInvalid('"service" cannot be used along with the other retry_strategy options.')
+ ->beforeNormalization()
+ ->always(function ($v) {
+ 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()
->children()
->scalarNode('service')->defaultNull()->info('Service id to override the retry strategy entirely')->end()
@@ -1280,6 +1270,9 @@ class Configuration implements ConfigurationInterface
->scalarNode('auth_bearer')
->info('A token enabling HTTP Bearer authorization.')
->end()
+ ->scalarNode('auth_ntlm')
+ ->info('A "username:password" pair to use Microsoft NTLM authentication (requires the cURL extension).')
+ ->end()
->arrayNode('query')
->info('Associative array of query string values merged with the base URI.')
->useAttributeAsKey('key')
@@ -1391,6 +1384,22 @@ class Configuration implements ConfigurationInterface
->{!class_exists(FullStack::class) && class_exists(Mailer::class) ? 'canBeDisabled' : 'canBeEnabled'}()
->children()
->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()
diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
index be20076fe3..a0efbc9b08 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
@@ -1763,6 +1763,13 @@ class FrameworkExtension extends Extension
$loader->load('mailer.xml');
$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);
}
/**
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml
index b24b25c8c0..9959debfa9 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml
@@ -8,7 +8,7 @@
-
+
@@ -41,7 +41,7 @@
-
+
0
@@ -51,7 +51,7 @@
-
+
0
@@ -62,7 +62,7 @@
-
+
@@ -73,7 +73,7 @@
-
+
0
@@ -85,14 +85,14 @@
-
+
0
-
+
@@ -129,7 +129,7 @@
-
+
0
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.xml
index 1f567a6c93..cfe98f21dc 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.xml
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.xml
@@ -25,5 +25,11 @@
+
+
+
+
+
+
diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/BrowserKitAssertionsTrait.php b/src/Symfony/Bundle/FrameworkBundle/Test/BrowserKitAssertionsTrait.php
new file mode 100644
index 0000000000..086d83e8ad
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Test/BrowserKitAssertionsTrait.php
@@ -0,0 +1,159 @@
+
+ *
+ * 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;
+ }
+}
diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/DomCrawlerAssertionsTrait.php b/src/Symfony/Bundle/FrameworkBundle/Test/DomCrawlerAssertionsTrait.php
new file mode 100644
index 0000000000..465c265f69
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Test/DomCrawlerAssertionsTrait.php
@@ -0,0 +1,94 @@
+
+ *
+ * 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;
+ }
+}
diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/WebTestAssertionsTrait.php b/src/Symfony/Bundle/FrameworkBundle/Test/WebTestAssertionsTrait.php
index f820e8d080..197f2131bd 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Test/WebTestAssertionsTrait.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Test/WebTestAssertionsTrait.php
@@ -11,16 +11,6 @@
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.
*
@@ -28,203 +18,6 @@ use Symfony\Component\HttpFoundation\Test\Constraint as ResponseConstraint;
*/
trait WebTestAssertionsTrait
{
- 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 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;
- }
+ use BrowserKitAssertionsTrait;
+ use DomCrawlerAssertionsTrait;
}
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php
index d5c397e652..b8cb60b157 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php
@@ -20,7 +20,6 @@ use Symfony\Bundle\FullStack;
use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Component\Cache\Adapter\ApcuAdapter;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
-use Symfony\Component\Cache\Adapter\ChainAdapter;
use Symfony\Component\Cache\Adapter\DoctrineAdapter;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Component\Cache\Adapter\ProxyAdapter;
@@ -1459,10 +1458,6 @@ abstract class FrameworkExtensionTest extends TestCase
$this->assertSame(DoctrineAdapter::class, $parentDefinition->getClass());
break;
case 'cache.app':
- if (ChainAdapter::class === $parentDefinition->getClass()) {
- break;
- }
- // no break
case 'cache.adapter.filesystem':
$this->assertSame(FilesystemAdapter::class, $parentDefinition->getClass());
break;
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/MailerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/MailerTest.php
new file mode 100644
index 0000000000..85987fe28f
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/MailerTest.php
@@ -0,0 +1,62 @@
+ '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);
+ }
+}
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Mailer/bundles.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Mailer/bundles.php
new file mode 100644
index 0000000000..15ff182c6f
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Mailer/bundles.php
@@ -0,0 +1,18 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
+use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\TestBundle;
+
+return [
+ new FrameworkBundle(),
+ new TestBundle(),
+];
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Mailer/config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Mailer/config.yml
new file mode 100644
index 0000000000..196869945e
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Mailer/config.yml
@@ -0,0 +1,9 @@
+imports:
+ - { resource: ../config/default.yml }
+
+framework:
+ mailer:
+ envelope:
+ sender: sender@example.org
+ recipients:
+ - redirected@example.org
diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AnalyzeServiceReferencesPass.php b/src/Symfony/Component/DependencyInjection/Compiler/AnalyzeServiceReferencesPass.php
index 83f568b706..224331c9e4 100644
--- a/src/Symfony/Component/DependencyInjection/Compiler/AnalyzeServiceReferencesPass.php
+++ b/src/Symfony/Component/DependencyInjection/Compiler/AnalyzeServiceReferencesPass.php
@@ -34,7 +34,6 @@ class AnalyzeServiceReferencesPass extends AbstractRecursivePass
private $onlyConstructorArguments;
private $hasProxyDumper;
private $lazy;
- private $expressionLanguage;
private $byConstructor;
private $definitions;
private $aliases;
diff --git a/src/Symfony/Component/DependencyInjection/Compiler/MergeExtensionConfigurationPass.php b/src/Symfony/Component/DependencyInjection/Compiler/MergeExtensionConfigurationPass.php
index 822dfe795c..76e6d35a79 100644
--- a/src/Symfony/Component/DependencyInjection/Compiler/MergeExtensionConfigurationPass.php
+++ b/src/Symfony/Component/DependencyInjection/Compiler/MergeExtensionConfigurationPass.php
@@ -200,6 +200,10 @@ class MergeExtensionConfigurationContainerBuilder extends ContainerBuilder
$bag = $this->getParameterBag();
$value = $bag->resolveValue($value);
+ if (!$bag instanceof EnvPlaceholderParameterBag) {
+ return parent::resolveEnvPlaceholders($value, $format, $usedEnvs);
+ }
+
foreach ($bag->getEnvPlaceholders() as $env => $placeholders) {
if (false === strpos($env, ':')) {
continue;
diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ServiceReferenceGraphNode.php b/src/Symfony/Component/DependencyInjection/Compiler/ServiceReferenceGraphNode.php
index f0b9bcfe20..fec142426e 100644
--- a/src/Symfony/Component/DependencyInjection/Compiler/ServiceReferenceGraphNode.php
+++ b/src/Symfony/Component/DependencyInjection/Compiler/ServiceReferenceGraphNode.php
@@ -81,7 +81,7 @@ class ServiceReferenceGraphNode
/**
* Returns the in edges.
*
- * @return array The in ServiceReferenceGraphEdge array
+ * @return ServiceReferenceGraphEdge[]
*/
public function getInEdges()
{
@@ -91,7 +91,7 @@ class ServiceReferenceGraphNode
/**
* Returns the out edges.
*
- * @return array The out ServiceReferenceGraphEdge array
+ * @return ServiceReferenceGraphEdge[]
*/
public function getOutEdges()
{
diff --git a/src/Symfony/Component/DomCrawler/Test/Constraint/CrawlerSelectorAttributeValueSame.php b/src/Symfony/Component/DomCrawler/Test/Constraint/CrawlerSelectorAttributeValueSame.php
index 962b6bf0b1..7008779e14 100644
--- a/src/Symfony/Component/DomCrawler/Test/Constraint/CrawlerSelectorAttributeValueSame.php
+++ b/src/Symfony/Component/DomCrawler/Test/Constraint/CrawlerSelectorAttributeValueSame.php
@@ -47,7 +47,7 @@ final class CrawlerSelectorAttributeValueSame extends Constraint
return false;
}
- return $this->expectedText === trim($crawler->getNode(0)->getAttribute($this->attribute));
+ return $this->expectedText === trim($crawler->attr($this->attribute));
}
/**
diff --git a/src/Symfony/Component/Filesystem/CHANGELOG.md b/src/Symfony/Component/Filesystem/CHANGELOG.md
index 66b2790e67..d103bc2c57 100644
--- a/src/Symfony/Component/Filesystem/CHANGELOG.md
+++ b/src/Symfony/Component/Filesystem/CHANGELOG.md
@@ -6,6 +6,11 @@ CHANGELOG
* `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
-----
diff --git a/src/Symfony/Component/Filesystem/Filesystem.php b/src/Symfony/Component/Filesystem/Filesystem.php
index 652089c8d0..fcc423805e 100644
--- a/src/Symfony/Component/Filesystem/Filesystem.php
+++ b/src/Symfony/Component/Filesystem/Filesystem.php
@@ -600,6 +600,10 @@ class Filesystem
*/
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)
|| (\strlen($file) > 3 && ctype_alpha($file[0])
&& ':' === $file[1]
diff --git a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php
index 55397f5f3a..23ec44bbe8 100644
--- a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php
+++ b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php
@@ -1397,10 +1397,18 @@ class FilesystemTest extends FilesystemTestCase
['var/lib', false],
['../var/lib', 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()
{
$dirname = $this->workspace;
diff --git a/src/Symfony/Component/Form/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php b/src/Symfony/Component/Form/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php
index 57f92b6574..a920e3be5b 100644
--- a/src/Symfony/Component/Form/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php
+++ b/src/Symfony/Component/Form/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php
@@ -57,13 +57,15 @@ class FormTypeValidatorExtensionTest extends BaseValidatorExtensionTest
public function testGroupSequenceWithConstraintsOption()
{
+ $allowEmptyString = property_exists(Length::class, 'allowEmptyString') ? ['allowEmptyString' => true] : [];
+
$form = Forms::createFormFactoryBuilder()
->addExtension(new ValidatorExtension(Validation::createValidator()))
->getFormFactory()
->create(FormTypeTest::TESTED_TYPE, null, (['validation_groups' => new GroupSequence(['First', 'Second'])]))
->add('field', TextTypeTest::TESTED_TYPE, [
'constraints' => [
- new Length(['min' => 10, 'groups' => ['First']]),
+ new Length(['min' => 10, 'groups' => ['First']] + $allowEmptyString),
new Email(['groups' => ['Second']]),
],
])
diff --git a/src/Symfony/Component/Form/Tests/Extension/Validator/ValidatorTypeGuesserTest.php b/src/Symfony/Component/Form/Tests/Extension/Validator/ValidatorTypeGuesserTest.php
index 878bbfad21..fd11342bea 100644
--- a/src/Symfony/Component/Form/Tests/Extension/Validator/ValidatorTypeGuesserTest.php
+++ b/src/Symfony/Component/Form/Tests/Extension/Validator/ValidatorTypeGuesserTest.php
@@ -61,11 +61,13 @@ class ValidatorTypeGuesserTest extends TestCase
public function guessRequiredProvider()
{
+ $allowEmptyString = property_exists(Length::class, 'allowEmptyString') ? ['allowEmptyString' => true] : [];
+
return [
[new NotNull(), new ValueGuess(true, Guess::HIGH_CONFIDENCE)],
[new NotBlank(), 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)],
];
}
@@ -101,7 +103,9 @@ class ValidatorTypeGuesserTest extends TestCase
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);
$this->assertNull($result);
diff --git a/src/Symfony/Component/HttpClient/CHANGELOG.md b/src/Symfony/Component/HttpClient/CHANGELOG.md
index b2f900bf39..5348259f63 100644
--- a/src/Symfony/Component/HttpClient/CHANGELOG.md
+++ b/src/Symfony/Component/HttpClient/CHANGELOG.md
@@ -6,6 +6,7 @@ CHANGELOG
* made `Psr18Client` implement relevant PSR-17 factories
* added `HttplugClient`
+ * added support for NTLM authentication
4.3.0
-----
diff --git a/src/Symfony/Component/HttpClient/CurlHttpClient.php b/src/Symfony/Component/HttpClient/CurlHttpClient.php
index 258462261e..bedd6e8889 100644
--- a/src/Symfony/Component/HttpClient/CurlHttpClient.php
+++ b/src/Symfony/Component/HttpClient/CurlHttpClient.php
@@ -37,7 +37,10 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface
use HttpClientTrait;
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.
@@ -150,6 +153,25 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface
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) {
$curlopts[CURLOPT_DNS_USE_GLOBAL_CACHE] = false;
}
diff --git a/src/Symfony/Component/HttpClient/HttpClientTrait.php b/src/Symfony/Component/HttpClient/HttpClientTrait.php
index 75458fa4dd..35af628dcc 100644
--- a/src/Symfony/Component/HttpClient/HttpClientTrait.php
+++ b/src/Symfony/Component/HttpClient/HttpClientTrait.php
@@ -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))));
}
diff --git a/src/Symfony/Component/HttpFoundation/CHANGELOG.md b/src/Symfony/Component/HttpFoundation/CHANGELOG.md
index efe5035792..7b7002f084 100644
--- a/src/Symfony/Component/HttpFoundation/CHANGELOG.md
+++ b/src/Symfony/Component/HttpFoundation/CHANGELOG.md
@@ -15,6 +15,7 @@ CHANGELOG
-----
* passing arguments to `Request::isMethodSafe()` is deprecated.
+ * `ApacheRequest` is deprecated, use the `Request` class instead.
4.3.0
-----
diff --git a/src/Symfony/Component/HttpFoundation/Request.php b/src/Symfony/Component/HttpFoundation/Request.php
index 9f1bd31267..73ccfc3276 100644
--- a/src/Symfony/Component/HttpFoundation/Request.php
+++ b/src/Symfony/Component/HttpFoundation/Request.php
@@ -192,6 +192,10 @@ class Request
protected static $requestFactory;
+ /**
+ * @var string|null
+ */
+ private $preferredFormat;
private $isHostValid = true;
private $isForwardedValid = true;
@@ -1342,6 +1346,8 @@ class Request
* * _format request attribute
* * $default
*
+ * @see getPreferredFormat
+ *
* @param string|null $default The default 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');
}
+ /**
+ * 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.
*
diff --git a/src/Symfony/Component/HttpFoundation/Response.php b/src/Symfony/Component/HttpFoundation/Response.php
index 72b31a4b8d..3bbac462b5 100644
--- a/src/Symfony/Component/HttpFoundation/Response.php
+++ b/src/Symfony/Component/HttpFoundation/Response.php
@@ -265,7 +265,7 @@ class Response
} else {
// Content-type based on the Request
if (!$headers->has('Content-Type')) {
- $format = $request->getRequestFormat();
+ $format = $request->getPreferredFormat();
if (null !== $format && $mimeType = $request->getMimeType($format)) {
$headers->set('Content-Type', $mimeType);
}
diff --git a/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php b/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php
index ea3496a8c9..e1ff8991c3 100644
--- a/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php
+++ b/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php
@@ -399,6 +399,32 @@ class RequestTest extends TestCase
$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
*/
diff --git a/src/Symfony/Component/HttpFoundation/Tests/ResponseTest.php b/src/Symfony/Component/HttpFoundation/Tests/ResponseTest.php
index 7856a77c03..550d7ce0b0 100644
--- a/src/Symfony/Component/HttpFoundation/Tests/ResponseTest.php
+++ b/src/Symfony/Component/HttpFoundation/Tests/ResponseTest.php
@@ -504,6 +504,7 @@ class ResponseTest extends ResponseTestCase
$response = new Response('foo');
$request = Request::create('/');
$request->setRequestFormat('json');
+ $request->headers->remove('accept');
$response->prepare($request);
diff --git a/src/Symfony/Component/HttpKernel/Controller/ContainerControllerResolver.php b/src/Symfony/Component/HttpKernel/Controller/ContainerControllerResolver.php
index 4f80921cf5..b9e8de7893 100644
--- a/src/Symfony/Component/HttpKernel/Controller/ContainerControllerResolver.php
+++ b/src/Symfony/Component/HttpKernel/Controller/ContainerControllerResolver.php
@@ -59,7 +59,7 @@ class ContainerControllerResolver extends ControllerResolver
$this->throwExceptionIfControllerWasRemoved($class, $e);
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);
diff --git a/src/Symfony/Component/HttpKernel/EventListener/DebugHandlersListener.php b/src/Symfony/Component/HttpKernel/EventListener/DebugHandlersListener.php
index 3df54c0639..dbb75e821a 100644
--- a/src/Symfony/Component/HttpKernel/EventListener/DebugHandlersListener.php
+++ b/src/Symfony/Component/HttpKernel/EventListener/DebugHandlersListener.php
@@ -170,7 +170,7 @@ class DebugHandlersListener implements EventSubscriberInterface
$e = $request->attributes->get('exception');
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 $_) {
return new Response($this->errorFormatter->render($e), $e->getStatusCode(), $e->getHeaders());
}
diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ContainerControllerResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ContainerControllerResolverTest.php
index 27f35b64c6..2e856d97e4 100644
--- a/src/Symfony/Component/HttpKernel/Tests/Controller/ContainerControllerResolverTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ContainerControllerResolverTest.php
@@ -184,16 +184,16 @@ class ContainerControllerResolverTest extends ControllerResolverTest
$tests[] = [
[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?',
+ '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[] = [
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[] = [
InvokableControllerService::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;
diff --git a/src/Symfony/Component/Ldap/Adapter/ConnectionInterface.php b/src/Symfony/Component/Ldap/Adapter/ConnectionInterface.php
index aa0b5d8571..56829dc4ea 100644
--- a/src/Symfony/Component/Ldap/Adapter/ConnectionInterface.php
+++ b/src/Symfony/Component/Ldap/Adapter/ConnectionInterface.php
@@ -11,6 +11,10 @@
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
*/
@@ -25,6 +29,10 @@ interface ConnectionInterface
/**
* 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);
}
diff --git a/src/Symfony/Component/Lock/Store/ZookeeperStore.php b/src/Symfony/Component/Lock/Store/ZookeeperStore.php
index ce3cb4bab7..10987c5f72 100644
--- a/src/Symfony/Component/Lock/Store/ZookeeperStore.php
+++ b/src/Symfony/Component/Lock/Store/ZookeeperStore.php
@@ -95,7 +95,7 @@ class ZookeeperStore implements StoreInterface
*/
public function putOffExpiration(Key $key, $ttl)
{
- throw new NotSupportedException();
+ // do nothing, zookeeper locks forever.
}
/**
diff --git a/src/Symfony/Component/Lock/StoreInterface.php b/src/Symfony/Component/Lock/StoreInterface.php
index 519c3e4731..e5a1dfa588 100644
--- a/src/Symfony/Component/Lock/StoreInterface.php
+++ b/src/Symfony/Component/Lock/StoreInterface.php
@@ -49,7 +49,6 @@ interface StoreInterface
* @param float $ttl amount of seconds to keep the lock in the store
*
* @throws LockConflictedException
- * @throws NotSupportedException
*/
public function putOffExpiration(Key $key, $ttl);
diff --git a/src/Symfony/Component/Messenger/CHANGELOG.md b/src/Symfony/Component/Messenger/CHANGELOG.md
index 8bbf0c0773..bfba2d126a 100644
--- a/src/Symfony/Component/Messenger/CHANGELOG.md
+++ b/src/Symfony/Component/Messenger/CHANGELOG.md
@@ -12,6 +12,7 @@ CHANGELOG
* Deprecated passing a `ContainerInterface` instance as first argument of the `ConsumeMessagesCommand` constructor,
pass a `RoutableMessageBus` instance instead.
+ * Added support for auto trimming of Redis streams.
4.3.0
-----
diff --git a/src/Symfony/Component/Messenger/MessageBus.php b/src/Symfony/Component/Messenger/MessageBus.php
index aadeacfff2..5809a1fcbe 100644
--- a/src/Symfony/Component/Messenger/MessageBus.php
+++ b/src/Symfony/Component/Messenger/MessageBus.php
@@ -33,17 +33,26 @@ class MessageBus implements MessageBusInterface
} elseif (\is_array($middlewareHandlers)) {
$this->middlewareAggregate = new \ArrayObject($middlewareHandlers);
} else {
- $this->middlewareAggregate = new class() {
- public $aggregate;
- public $iterator;
+ // $this->middlewareAggregate should be an instance of IteratorAggregate.
+ // When $middlewareHandlers is an Iterator, we wrap it to ensure it is lazy-loaded and can be rewound.
+ $this->middlewareAggregate = new class($middlewareHandlers) implements \IteratorAggregate {
+ private $middlewareHandlers;
+ private $cachedIterator;
+
+ public function __construct($middlewareHandlers)
+ {
+ $this->middlewareHandlers = $middlewareHandlers;
+ }
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;
}
}
diff --git a/src/Symfony/Component/Messenger/Tests/MessageBusTest.php b/src/Symfony/Component/Messenger/Tests/MessageBusTest.php
index bcddcffd8a..c8644623b7 100644
--- a/src/Symfony/Component/Messenger/Tests/MessageBusTest.php
+++ b/src/Symfony/Component/Messenger/Tests/MessageBusTest.php
@@ -16,6 +16,7 @@ use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBus;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Messenger\Middleware\MiddlewareInterface;
+use Symfony\Component\Messenger\Middleware\StackInterface;
use Symfony\Component\Messenger\Stamp\BusNameStamp;
use Symfony\Component\Messenger\Stamp\DelayStamp;
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')]);
$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;
+ }
}
diff --git a/src/Symfony/Component/Messenger/Tests/Transport/Doctrine/DoctrineTransportFactoryTest.php b/src/Symfony/Component/Messenger/Tests/Transport/Doctrine/DoctrineTransportFactoryTest.php
index 9129ac6299..cdde9284d7 100644
--- a/src/Symfony/Component/Messenger/Tests/Transport/Doctrine/DoctrineTransportFactoryTest.php
+++ b/src/Symfony/Component/Messenger/Tests/Transport/Doctrine/DoctrineTransportFactoryTest.php
@@ -11,8 +11,8 @@
namespace Symfony\Component\Messenger\Tests\Transport\Doctrine;
+use Doctrine\Common\Persistence\ConnectionRegistry;
use PHPUnit\Framework\TestCase;
-use Symfony\Bridge\Doctrine\RegistryInterface;
use Symfony\Component\Messenger\Transport\Doctrine\Connection;
use Symfony\Component\Messenger\Transport\Doctrine\DoctrineTransport;
use Symfony\Component\Messenger\Transport\Doctrine\DoctrineTransportFactory;
@@ -23,7 +23,7 @@ class DoctrineTransportFactoryTest extends TestCase
public function testSupports()
{
$factory = new DoctrineTransportFactory(
- $this->getMockBuilder(RegistryInterface::class)->getMock()
+ $this->getMockBuilder(ConnectionRegistry::class)->getMock()
);
$this->assertTrue($factory->supports('doctrine://default', []));
@@ -35,7 +35,7 @@ class DoctrineTransportFactoryTest extends TestCase
$connection = $this->getMockBuilder(\Doctrine\DBAL\Connection::class)
->disableOriginalConstructor()
->getMock();
- $registry = $this->getMockBuilder(RegistryInterface::class)->getMock();
+ $registry = $this->getMockBuilder(ConnectionRegistry::class)->getMock();
$registry->expects($this->once())
->method('getConnection')
->willReturn($connection);
@@ -55,7 +55,7 @@ class DoctrineTransportFactoryTest extends TestCase
*/
public function testCreateTransportMustThrowAnExceptionIfManagerIsNotFound()
{
- $registry = $this->getMockBuilder(RegistryInterface::class)->getMock();
+ $registry = $this->getMockBuilder(ConnectionRegistry::class)->getMock();
$registry->expects($this->once())
->method('getConnection')
->willReturnCallback(function () {
diff --git a/src/Symfony/Component/Messenger/Tests/Transport/RedisExt/ConnectionTest.php b/src/Symfony/Component/Messenger/Tests/Transport/RedisExt/ConnectionTest.php
index 177f6b9038..07acf67891 100644
--- a/src/Symfony/Component/Messenger/Tests/Transport/RedisExt/ConnectionTest.php
+++ b/src/Symfony/Component/Messenger/Tests/Transport/RedisExt/ConnectionTest.php
@@ -42,13 +42,13 @@ class ConnectionTest extends TestCase
public function testFromDsnWithOptions()
{
$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',
'port' => 6379,
], [
'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());
}
+ 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()
{
$redis = $this->getMockBuilder(\Redis::class)->disableOriginalConstructor()->getMock();
@@ -142,4 +152,16 @@ class ConnectionTest extends TestCase
$connection->reject($message['id']);
$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', []);
+ }
}
diff --git a/src/Symfony/Component/Messenger/Transport/Doctrine/DoctrineTransportFactory.php b/src/Symfony/Component/Messenger/Transport/Doctrine/DoctrineTransportFactory.php
index 54ca901eb2..9d7676353d 100644
--- a/src/Symfony/Component/Messenger/Transport/Doctrine/DoctrineTransportFactory.php
+++ b/src/Symfony/Component/Messenger/Transport/Doctrine/DoctrineTransportFactory.php
@@ -11,7 +11,7 @@
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\Transport\Serialization\SerializerInterface;
use Symfony\Component\Messenger\Transport\TransportFactoryInterface;
@@ -24,7 +24,7 @@ class DoctrineTransportFactory implements TransportFactoryInterface
{
private $registry;
- public function __construct(RegistryInterface $registry)
+ public function __construct(ConnectionRegistry $registry)
{
$this->registry = $registry;
}
diff --git a/src/Symfony/Component/Messenger/Transport/RedisExt/Connection.php b/src/Symfony/Component/Messenger/Transport/RedisExt/Connection.php
index 9cc96762c8..8e5a5d88ee 100644
--- a/src/Symfony/Component/Messenger/Transport/RedisExt/Connection.php
+++ b/src/Symfony/Component/Messenger/Transport/RedisExt/Connection.php
@@ -31,6 +31,7 @@ class Connection
'group' => 'symfony',
'consumer' => 'consumer',
'auto_setup' => true,
+ 'stream_max_entries' => 0, // any value higher than 0 defines an approximate maximum number of stream entries
];
private $connection;
@@ -38,6 +39,7 @@ class Connection
private $group;
private $consumer;
private $autoSetup;
+ private $maxEntries;
private $couldHavePendingMessages = true;
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->connect($connectionCredentials['host'] ?? '127.0.0.1', $connectionCredentials['port'] ?? 6379);
$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->group = $configuration['group'] ?? self::DEFAULT_OPTIONS['group'];
$this->consumer = $configuration['consumer'] ?? self::DEFAULT_OPTIONS['consumer'];
$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
@@ -70,6 +78,7 @@ class Connection
$connectionCredentials = [
'host' => $parsedUrl['host'] ?? '127.0.0.1',
'port' => $parsedUrl['port'] ?? 6379,
+ 'auth' => $parsedUrl['pass'] ?? $parsedUrl['user'] ?? null,
];
if (isset($parsedUrl['query'])) {
@@ -82,7 +91,19 @@ class Connection
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
@@ -169,9 +190,15 @@ class Connection
$e = null;
try {
- $added = $this->connection->xadd($this->stream, '*', ['message' => json_encode(
- ['body' => $body, 'headers' => $headers]
- )]);
+ if ($this->maxEntries) {
+ $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) {
}
diff --git a/src/Symfony/Component/Mime/Crypto/SMimeEncrypter.php b/src/Symfony/Component/Mime/Crypto/SMimeEncrypter.php
index 72f59ce1bc..d4c4a42aae 100644
--- a/src/Symfony/Component/Mime/Crypto/SMimeEncrypter.php
+++ b/src/Symfony/Component/Mime/Crypto/SMimeEncrypter.php
@@ -23,7 +23,8 @@ final class SMimeEncrypter extends SMime
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)
{
diff --git a/src/Symfony/Component/Mime/Crypto/SMimeSigner.php b/src/Symfony/Component/Mime/Crypto/SMimeSigner.php
index 71ce5df962..ef4375cc1e 100644
--- a/src/Symfony/Component/Mime/Crypto/SMimeSigner.php
+++ b/src/Symfony/Component/Mime/Crypto/SMimeSigner.php
@@ -30,13 +30,11 @@ final class SMimeSigner extends SMime
private $privateKeyPassphrase;
/**
- * @see https://secure.php.net/manual/en/openssl.pkcs7.flags.php
- *
- * @param string $certificate
- * @param string $privateKey A file containing the private key (in PEM format)
+ * @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|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 int $signOptions Bitwise operator options for openssl_pkcs7_sign()
+ * @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() (@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)
{
diff --git a/src/Symfony/Component/Mime/Message.php b/src/Symfony/Component/Mime/Message.php
index fc8aa85bb3..d141cb525e 100644
--- a/src/Symfony/Component/Mime/Message.php
+++ b/src/Symfony/Component/Mime/Message.php
@@ -128,17 +128,11 @@ class Message extends RawMessage
return bin2hex(random_bytes(16)).strstr($email, '@');
}
- /**
- * @internal
- */
public function __serialize(): array
{
return [$this->headers, $this->body];
}
- /**
- * @internal
- */
public function __unserialize(array $data): void
{
[$this->headers, $this->body] = $data;
diff --git a/src/Symfony/Component/Mime/RawMessage.php b/src/Symfony/Component/Mime/RawMessage.php
index 7d7301eda5..8a01049e51 100644
--- a/src/Symfony/Component/Mime/RawMessage.php
+++ b/src/Symfony/Component/Mime/RawMessage.php
@@ -67,17 +67,11 @@ class RawMessage implements \Serializable
$this->__unserialize(unserialize($serialized));
}
- /**
- * @internal
- */
public function __serialize(): array
{
return [$this->message];
}
- /**
- * @internal
- */
public function __unserialize(array $data): void
{
[$this->message] = $data;
diff --git a/src/Symfony/Component/PropertyAccess/CHANGELOG.md b/src/Symfony/Component/PropertyAccess/CHANGELOG.md
index 0a012bb476..d733c41481 100644
--- a/src/Symfony/Component/PropertyAccess/CHANGELOG.md
+++ b/src/Symfony/Component/PropertyAccess/CHANGELOG.md
@@ -1,6 +1,12 @@
CHANGELOG
=========
+4.4.0
+-----
+
+ * deprecated passing `null` as `$defaultLifetime` 2nd argument of `PropertyAccessor::createCache()` method,
+ pass `0` instead
+
4.3.0
-----
diff --git a/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php
index 69ee61a2ce..7dbee7078e 100644
--- a/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php
+++ b/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php
@@ -14,6 +14,7 @@ namespace Symfony\Component\PropertyInfo\Extractor;
use phpDocumentor\Reflection\DocBlock;
use phpDocumentor\Reflection\DocBlockFactory;
use phpDocumentor\Reflection\DocBlockFactoryInterface;
+use phpDocumentor\Reflection\Types\Context;
use phpDocumentor\Reflection\Types\ContextFactory;
use Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
@@ -38,6 +39,11 @@ class PhpDocExtractor implements PropertyDescriptionExtractorInterface, Property
*/
private $docBlocks = [];
+ /**
+ * @var Context[]
+ */
+ private $contexts = [];
+
private $docBlockFactory;
private $contextFactory;
private $phpDocTypeHelper;
@@ -191,7 +197,7 @@ class PhpDocExtractor implements PropertyDescriptionExtractorInterface, Property
}
try {
- return $this->docBlockFactory->create($reflectionProperty, $this->contextFactory->createFromReflector($reflectionProperty->getDeclaringClass()));
+ return $this->docBlockFactory->create($reflectionProperty, $this->createFromReflector($reflectionProperty->getDeclaringClass()));
} catch (\InvalidArgumentException $e) {
return null;
}
@@ -227,9 +233,25 @@ class PhpDocExtractor implements PropertyDescriptionExtractorInterface, Property
}
try {
- return [$this->docBlockFactory->create($reflectionMethod, $this->contextFactory->createFromReflector($reflectionMethod)), $prefix];
+ return [$this->docBlockFactory->create($reflectionMethod, $this->createFromReflector($reflectionMethod->getDeclaringClass())), $prefix];
} catch (\InvalidArgumentException $e) {
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];
+ }
}
diff --git a/src/Symfony/Component/Validator/CHANGELOG.md b/src/Symfony/Component/Validator/CHANGELOG.md
index 423de6404a..4313467f3a 100644
--- a/src/Symfony/Component/Validator/CHANGELOG.md
+++ b/src/Symfony/Component/Validator/CHANGELOG.md
@@ -18,10 +18,14 @@ CHANGELOG
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.
* added the `compared_value_path` parameter in violations when using any
comparison constraint with the `propertyPath` option.
* 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
-----
diff --git a/src/Symfony/Component/Validator/ConstraintViolation.php b/src/Symfony/Component/Validator/ConstraintViolation.php
index a0ce00e334..7eec001587 100644
--- a/src/Symfony/Component/Validator/ConstraintViolation.php
+++ b/src/Symfony/Component/Validator/ConstraintViolation.php
@@ -49,7 +49,7 @@ class ConstraintViolation implements ConstraintViolationInterface
* caused 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->messageTemplate = $messageTemplate;
@@ -79,13 +79,12 @@ class ConstraintViolation implements ConstraintViolationInterface
}
$propertyPath = (string) $this->propertyPath;
- $code = (string) $this->code;
if ('' !== $propertyPath && '[' !== $propertyPath[0] && '' !== $class) {
$class .= '.';
}
- if ('' !== $code) {
+ if ('' !== $code = $this->code) {
$code = ' (code '.$code.')';
}
diff --git a/src/Symfony/Component/Validator/Constraints/FileValidator.php b/src/Symfony/Component/Validator/Constraints/FileValidator.php
index 7266669213..ac23f6fb8e 100644
--- a/src/Symfony/Component/Validator/Constraints/FileValidator.php
+++ b/src/Symfony/Component/Validator/Constraints/FileValidator.php
@@ -65,49 +65,49 @@ class FileValidator extends ConstraintValidator
$this->context->buildViolation($constraint->uploadIniSizeErrorMessage)
->setParameter('{{ limit }}', $limitAsString)
->setParameter('{{ suffix }}', $suffix)
- ->setCode(UPLOAD_ERR_INI_SIZE)
+ ->setCode((string) UPLOAD_ERR_INI_SIZE)
->addViolation();
return;
case UPLOAD_ERR_FORM_SIZE:
$this->context->buildViolation($constraint->uploadFormSizeErrorMessage)
- ->setCode(UPLOAD_ERR_FORM_SIZE)
+ ->setCode((string) UPLOAD_ERR_FORM_SIZE)
->addViolation();
return;
case UPLOAD_ERR_PARTIAL:
$this->context->buildViolation($constraint->uploadPartialErrorMessage)
- ->setCode(UPLOAD_ERR_PARTIAL)
+ ->setCode((string) UPLOAD_ERR_PARTIAL)
->addViolation();
return;
case UPLOAD_ERR_NO_FILE:
$this->context->buildViolation($constraint->uploadNoFileErrorMessage)
- ->setCode(UPLOAD_ERR_NO_FILE)
+ ->setCode((string) UPLOAD_ERR_NO_FILE)
->addViolation();
return;
case UPLOAD_ERR_NO_TMP_DIR:
$this->context->buildViolation($constraint->uploadNoTmpDirErrorMessage)
- ->setCode(UPLOAD_ERR_NO_TMP_DIR)
+ ->setCode((string) UPLOAD_ERR_NO_TMP_DIR)
->addViolation();
return;
case UPLOAD_ERR_CANT_WRITE:
$this->context->buildViolation($constraint->uploadCantWriteErrorMessage)
- ->setCode(UPLOAD_ERR_CANT_WRITE)
+ ->setCode((string) UPLOAD_ERR_CANT_WRITE)
->addViolation();
return;
case UPLOAD_ERR_EXTENSION:
$this->context->buildViolation($constraint->uploadExtensionErrorMessage)
- ->setCode(UPLOAD_ERR_EXTENSION)
+ ->setCode((string) UPLOAD_ERR_EXTENSION)
->addViolation();
return;
default:
$this->context->buildViolation($constraint->uploadErrorMessage)
- ->setCode($value->getError())
+ ->setCode((string) $value->getError())
->addViolation();
return;
diff --git a/src/Symfony/Component/Validator/Constraints/Length.php b/src/Symfony/Component/Validator/Constraints/Length.php
index 0edd0e97e0..d9b0d1f1c5 100644
--- a/src/Symfony/Component/Validator/Constraints/Length.php
+++ b/src/Symfony/Component/Validator/Constraints/Length.php
@@ -41,6 +41,7 @@ class Length extends Constraint
public $min;
public $charset = 'UTF-8';
public $normalizer;
+ public $allowEmptyString;
public function __construct($options = null)
{
@@ -56,6 +57,13 @@ class Length extends Constraint
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) {
throw new MissingOptionsException(sprintf('Either option "min" or "max" must be given for constraint %s', __CLASS__), ['min', 'max']);
}
diff --git a/src/Symfony/Component/Validator/Constraints/LengthValidator.php b/src/Symfony/Component/Validator/Constraints/LengthValidator.php
index f3cf245cf4..b1b5d7c770 100644
--- a/src/Symfony/Component/Validator/Constraints/LengthValidator.php
+++ b/src/Symfony/Component/Validator/Constraints/LengthValidator.php
@@ -30,7 +30,7 @@ class LengthValidator extends ConstraintValidator
throw new UnexpectedTypeException($constraint, __NAMESPACE__.'\Length');
}
- if (null === $value || '' === $value) {
+ if (null === $value || ('' === $value && $constraint->allowEmptyString)) {
return;
}
diff --git a/src/Symfony/Component/Validator/Tests/ConstraintViolationTest.php b/src/Symfony/Component/Validator/Tests/ConstraintViolationTest.php
index b43e51f273..d38431b640 100644
--- a/src/Symfony/Component/Validator/Tests/ConstraintViolationTest.php
+++ b/src/Symfony/Component/Validator/Tests/ConstraintViolationTest.php
@@ -64,7 +64,7 @@ EOF;
'some_value',
null,
null,
- 0
+ '0'
);
$expected = <<<'EOF'
@@ -108,4 +108,24 @@ EOF;
$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());
+ }
}
diff --git a/src/Symfony/Component/Validator/Tests/Constraints/FileValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/FileValidatorTest.php
index c5105c20cf..7e75f8151f 100644
--- a/src/Symfony/Component/Validator/Tests/Constraints/FileValidatorTest.php
+++ b/src/Symfony/Component/Validator/Tests/Constraints/FileValidatorTest.php
@@ -435,23 +435,23 @@ abstract class FileValidatorTest extends ConstraintValidatorTestCase
public function uploadedFileErrorProvider()
{
$tests = [
- [UPLOAD_ERR_FORM_SIZE, 'uploadFormSizeErrorMessage'],
- [UPLOAD_ERR_PARTIAL, 'uploadPartialErrorMessage'],
- [UPLOAD_ERR_NO_FILE, 'uploadNoFileErrorMessage'],
- [UPLOAD_ERR_NO_TMP_DIR, 'uploadNoTmpDirErrorMessage'],
- [UPLOAD_ERR_CANT_WRITE, 'uploadCantWriteErrorMessage'],
- [UPLOAD_ERR_EXTENSION, 'uploadExtensionErrorMessage'],
+ [(string) UPLOAD_ERR_FORM_SIZE, 'uploadFormSizeErrorMessage'],
+ [(string) UPLOAD_ERR_PARTIAL, 'uploadPartialErrorMessage'],
+ [(string) UPLOAD_ERR_NO_FILE, 'uploadNoFileErrorMessage'],
+ [(string) UPLOAD_ERR_NO_TMP_DIR, 'uploadNoTmpDirErrorMessage'],
+ [(string) UPLOAD_ERR_CANT_WRITE, 'uploadCantWriteErrorMessage'],
+ [(string) UPLOAD_ERR_EXTENSION, 'uploadExtensionErrorMessage'],
];
if (class_exists('Symfony\Component\HttpFoundation\File\UploadedFile')) {
// 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,
'{{ suffix }}' => 'MiB',
]];
// 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,
'{{ suffix }}' => 'bytes',
], '1'];
@@ -464,14 +464,14 @@ abstract class FileValidatorTest extends ConstraintValidatorTestCase
// it correctly parses the maxSize option and not only uses simple string comparison
// 1000M should be bigger than the ini value
- $tests[] = [UPLOAD_ERR_INI_SIZE, 'uploadIniSizeErrorMessage', [
+ $tests[] = [(string) UPLOAD_ERR_INI_SIZE, 'uploadIniSizeErrorMessage', [
'{{ limit }}' => $limit,
'{{ suffix }}' => $suffix,
], '1000M'];
// it correctly parses the maxSize option and not only uses simple string comparison
// 1000M should be bigger than the ini value
- $tests[] = [UPLOAD_ERR_INI_SIZE, 'uploadIniSizeErrorMessage', [
+ $tests[] = [(string) UPLOAD_ERR_INI_SIZE, 'uploadIniSizeErrorMessage', [
'{{ limit }}' => '0.1',
'{{ suffix }}' => 'MB',
], '100K'];
diff --git a/src/Symfony/Component/Validator/Tests/Constraints/LengthTest.php b/src/Symfony/Component/Validator/Tests/Constraints/LengthTest.php
index 6a20ff541f..b7aa2339aa 100644
--- a/src/Symfony/Component/Validator/Tests/Constraints/LengthTest.php
+++ b/src/Symfony/Component/Validator/Tests/Constraints/LengthTest.php
@@ -21,7 +21,7 @@ class LengthTest extends TestCase
{
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);
}
@@ -32,7 +32,7 @@ class LengthTest extends TestCase
*/
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()
{
- new Length(['min' => 0, 'max' => 10, 'normalizer' => new \stdClass()]);
+ new Length(['min' => 0, 'max' => 10, 'normalizer' => new \stdClass(), 'allowEmptyString' => false]);
}
}
diff --git a/src/Symfony/Component/Validator/Tests/Constraints/LengthValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/LengthValidatorTest.php
index 96c388ae5b..6e94a0233e 100644
--- a/src/Symfony/Component/Validator/Tests/Constraints/LengthValidatorTest.php
+++ b/src/Symfony/Component/Validator/Tests/Constraints/LengthValidatorTest.php
@@ -22,26 +22,47 @@ class LengthValidatorTest extends ConstraintValidatorTestCase
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();
}
- 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->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
*/
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()
@@ -109,7 +130,7 @@ class LengthValidatorTest extends ConstraintValidatorTestCase
*/
public function testValidValuesMin($value)
{
- $constraint = new Length(['min' => 5]);
+ $constraint = new Length(['min' => 5, 'allowEmptyString' => false]);
$this->validator->validate($value, $constraint);
$this->assertNoViolation();
@@ -131,7 +152,7 @@ class LengthValidatorTest extends ConstraintValidatorTestCase
*/
public function testValidValuesExact($value)
{
- $constraint = new Length(4);
+ $constraint = new Length(['value' => 4, 'allowEmptyString' => false]);
$this->validator->validate($value, $constraint);
$this->assertNoViolation();
@@ -142,7 +163,7 @@ class LengthValidatorTest extends ConstraintValidatorTestCase
*/
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->assertNoViolation();
@@ -156,6 +177,7 @@ class LengthValidatorTest extends ConstraintValidatorTestCase
$constraint = new Length([
'min' => 4,
'minMessage' => 'myMessage',
+ 'allowEmptyString' => false,
]);
$this->validator->validate($value, $constraint);
@@ -199,6 +221,7 @@ class LengthValidatorTest extends ConstraintValidatorTestCase
'min' => 4,
'max' => 4,
'exactMessage' => 'myMessage',
+ 'allowEmptyString' => false,
]);
$this->validator->validate($value, $constraint);
@@ -221,6 +244,7 @@ class LengthValidatorTest extends ConstraintValidatorTestCase
'min' => 4,
'max' => 4,
'exactMessage' => 'myMessage',
+ 'allowEmptyString' => false,
]);
$this->validator->validate($value, $constraint);
@@ -244,6 +268,7 @@ class LengthValidatorTest extends ConstraintValidatorTestCase
'max' => 1,
'charset' => $charset,
'charsetMessage' => 'myMessage',
+ 'allowEmptyString' => false,
]);
$this->validator->validate($value, $constraint);
@@ -262,7 +287,7 @@ class LengthValidatorTest extends ConstraintValidatorTestCase
public function testConstraintDefaultOption()
{
- $constraint = new Length(5);
+ $constraint = new Length(['value' => 5, 'allowEmptyString' => false]);
$this->assertEquals(5, $constraint->min);
$this->assertEquals(5, $constraint->max);
@@ -270,7 +295,7 @@ class LengthValidatorTest extends ConstraintValidatorTestCase
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->max);
diff --git a/src/Symfony/Component/Validator/Tests/Validator/RecursiveValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/RecursiveValidatorTest.php
index 8109b6b9bf..c0c7c3e96d 100644
--- a/src/Symfony/Component/Validator/Tests/Validator/RecursiveValidatorTest.php
+++ b/src/Symfony/Component/Validator/Tests/Validator/RecursiveValidatorTest.php
@@ -103,7 +103,7 @@ class RecursiveValidatorTest extends AbstractTest
public function testCollectionConstraintValidateAllGroupsForNestedConstraints()
{
$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'])],
]]));
@@ -121,7 +121,7 @@ class RecursiveValidatorTest extends AbstractTest
{
$this->metadata->addPropertyConstraint('data', new All(['constraints' => [
new NotBlank(['groups' => 'one']),
- new Length(['min' => 2, 'groups' => 'two']),
+ new Length(['min' => 2, 'groups' => 'two', 'allowEmptyString' => false]),
]]));
$entity = new Entity();
@@ -129,8 +129,9 @@ class RecursiveValidatorTest extends AbstractTest
$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(Length::class, $violations->get(1)->getConstraint());
+ $this->assertInstanceOf(Length::class, $violations->get(2)->getConstraint());
}
}
diff --git a/src/Symfony/Component/Validator/Tests/Violation/ConstraintViolationBuilderTest.php b/src/Symfony/Component/Validator/Tests/Violation/ConstraintViolationBuilderTest.php
new file mode 100644
index 0000000000..622bcbd8c9
--- /dev/null
+++ b/src/Symfony/Component/Validator/Tests/Violation/ConstraintViolationBuilderTest.php
@@ -0,0 +1,36 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\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());
+ }
+}
diff --git a/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilder.php b/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilder.php
index 2291132050..19a965b0dc 100644
--- a/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilder.php
+++ b/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilder.php
@@ -121,6 +121,10 @@ class ConstraintViolationBuilder implements ConstraintViolationBuilderInterface
*/
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;
return $this;