Merge branch '4.4'

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

View File

@ -57,6 +57,12 @@ EventDispatcher
* The signature of the `EventDispatcherInterface::dispatch()` method should be updated to `dispatch($event, string $eventName = null)`, not doing so is deprecated
* The `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.

View File

@ -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`.

View File

@ -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:

View File

@ -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));
}
}

View File

@ -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));
}
}

View File

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

View File

@ -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(

View File

@ -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);

View File

@ -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

View File

@ -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()

View File

@ -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);
}
/**

View File

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

View File

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

View File

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

View File

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

View File

@ -11,16 +11,6 @@
namespace Symfony\Bundle\FrameworkBundle\Test;
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;
}

View File

@ -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;

View File

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

View File

@ -0,0 +1,18 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\TestBundle;
return [
new FrameworkBundle(),
new TestBundle(),
];

View File

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

View File

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

View File

@ -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;

View File

@ -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()
{

View File

@ -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));
}
/**

View File

@ -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
-----

View File

@ -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]

View File

@ -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;

View File

@ -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']]),
],
])

View File

@ -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);

View File

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

View File

@ -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;
}

View File

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

View File

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

View File

@ -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.
*

View File

@ -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);
}

View File

@ -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
*/

View File

@ -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);

View File

@ -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);

View File

@ -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());
}

View File

@ -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;

View File

@ -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 <charles@sarraz.in>
*/
@ -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);
}

View File

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

View File

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

View File

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

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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 () {

View File

@ -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', []);
}
}

View File

@ -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;
}

View File

@ -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) {
}

View File

@ -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)
{

View File

@ -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)
{

View File

@ -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;

View File

@ -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;

View File

@ -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
-----

View File

@ -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];
}
}

View File

@ -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
-----

View File

@ -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.')';
}

View File

@ -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;

View File

@ -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']);
}

View File

@ -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;
}

View File

@ -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());
}
}

View File

@ -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'];

View File

@ -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]);
}
}

View File

@ -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);

View File

@ -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());
}
}

View File

@ -0,0 +1,36 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Validator\Tests\Violation;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Translation\IdentityTranslator;
use Symfony\Component\Validator\ConstraintViolationList;
use Symfony\Component\Validator\Tests\Fixtures\ConstraintA;
use Symfony\Component\Validator\Violation\ConstraintViolationBuilder;
class ConstraintViolationBuilderTest extends TestCase
{
/**
* @group legacy
* @expectedDeprecation Not using a string as the error code in Symfony\Component\Validator\Violation\ConstraintViolationBuilder::setCode() is deprecated since Symfony 4.4. A type-hint will be added in 5.0.
* @expectedDeprecation Not using a string as the error code in Symfony\Component\Validator\ConstraintViolation::__construct() is deprecated since Symfony 4.4. A type-hint will be added in 5.0.
*/
public function testNonStringCode()
{
$constraintViolationList = new ConstraintViolationList();
(new ConstraintViolationBuilder($constraintViolationList, new ConstraintA(), 'invalid message', [], null, 'foo', 'baz', new IdentityTranslator()))
->setCode(42)
->addViolation();
self::assertSame(42, $constraintViolationList->get(0)->getCode());
}
}

View File

@ -121,6 +121,10 @@ class ConstraintViolationBuilder implements ConstraintViolationBuilderInterface
*/
public function setCode(?string $code)
{
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;