The redirect was intercepted by the Symfony Web Debug toolbar to help debugging.
diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/Twig/WebProfilerExtensionTest.php b/src/Symfony/Bundle/WebProfilerBundle/Tests/Twig/WebProfilerExtensionTest.php
index 1bb1296b09903..37438ed560206 100644
--- a/src/Symfony/Bundle/WebProfilerBundle/Tests/Twig/WebProfilerExtensionTest.php
+++ b/src/Symfony/Bundle/WebProfilerBundle/Tests/Twig/WebProfilerExtensionTest.php
@@ -15,8 +15,6 @@
use Symfony\Bundle\WebProfilerBundle\Twig\WebProfilerExtension;
use Symfony\Component\VarDumper\Cloner\VarCloner;
use Twig\Environment;
-use Twig\Extension\CoreExtension;
-use Twig\Extension\EscaperExtension;
class WebProfilerExtensionTest extends TestCase
{
@@ -25,9 +23,6 @@ class WebProfilerExtensionTest extends TestCase
*/
public function testDumpHeaderIsDisplayed(string $message, array $context, bool $dump1HasHeader, bool $dump2HasHeader)
{
- class_exists(CoreExtension::class); // Load twig_convert_encoding()
- class_exists(EscaperExtension::class); // Load twig_escape_filter()
-
$twigEnvironment = $this->mockTwigEnvironment();
$varCloner = new VarCloner();
diff --git a/src/Symfony/Bundle/WebProfilerBundle/Twig/WebProfilerExtension.php b/src/Symfony/Bundle/WebProfilerBundle/Twig/WebProfilerExtension.php
index 9d7ebfcfb91eb..390fe49f58afa 100644
--- a/src/Symfony/Bundle/WebProfilerBundle/Twig/WebProfilerExtension.php
+++ b/src/Symfony/Bundle/WebProfilerBundle/Twig/WebProfilerExtension.php
@@ -14,6 +14,7 @@
use Symfony\Component\VarDumper\Cloner\Data;
use Symfony\Component\VarDumper\Dumper\HtmlDumper;
use Twig\Environment;
+use Twig\Extension\EscaperExtension;
use Twig\Extension\ProfilerExtension;
use Twig\Profiler\Profile;
use Twig\TwigFunction;
@@ -84,12 +85,12 @@ public function dumpData(Environment $env, Data $data, int $maxDepth = 0): strin
public function dumpLog(Environment $env, string $message, Data $context = null): string
{
- $message = twig_escape_filter($env, $message);
+ $message = self::escape($env, $message);
$message = preg_replace('/"(.*?)"/', '"$1"', $message);
$replacements = [];
foreach ($context ?? [] as $k => $v) {
- $k = '{'.twig_escape_filter($env, $k).'}';
+ $k = '{'.self::escape($env, $k).'}';
if (str_contains($message, $k)) {
$replacements[$k] = $v;
}
@@ -110,4 +111,14 @@ public function getName(): string
{
return 'profiler';
}
+
+ private static function escape(Environment $env, string $s): string
+ {
+ if (method_exists(EscaperExtension::class, 'escape')) {
+ return EscaperExtension::escape($env, $s);
+ }
+
+ // to be removed when support for Twig 3 is dropped
+ return twig_escape_filter($env, $s);
+ }
}
diff --git a/src/Symfony/Component/Cache/Adapter/CouchbaseCollectionAdapter.php b/src/Symfony/Component/Cache/Adapter/CouchbaseCollectionAdapter.php
index 19c9e075db95b..317d7c345abbd 100644
--- a/src/Symfony/Component/Cache/Adapter/CouchbaseCollectionAdapter.php
+++ b/src/Symfony/Component/Cache/Adapter/CouchbaseCollectionAdapter.php
@@ -35,7 +35,7 @@ class CouchbaseCollectionAdapter extends AbstractAdapter
public function __construct(Collection $connection, string $namespace = '', int $defaultLifetime = 0, MarshallerInterface $marshaller = null)
{
if (!static::isSupported()) {
- throw new CacheException('Couchbase >= 3.0.0 < 4.0.0 is required.');
+ throw new CacheException('Couchbase >= 3.0.5 < 4.0.0 is required.');
}
$this->maxIdLength = static::MAX_KEY_LENGTH;
@@ -54,7 +54,7 @@ public static function createConnection(#[\SensitiveParameter] array|string $dsn
}
if (!static::isSupported()) {
- throw new CacheException('Couchbase >= 3.0.0 < 4.0.0 is required.');
+ throw new CacheException('Couchbase >= 3.0.5 < 4.0.0 is required.');
}
set_error_handler(function ($type, $msg, $file, $line): bool { throw new \ErrorException($msg, 0, $type, $file, $line); });
@@ -183,7 +183,7 @@ protected function doSave(array $values, $lifetime): array|bool
}
$upsertOptions = new UpsertOptions();
- $upsertOptions->expiry($lifetime);
+ $upsertOptions->expiry(\DateTimeImmutable::createFromFormat('U', time() + $lifetime));
$ko = [];
foreach ($values as $key => $value) {
diff --git a/src/Symfony/Component/Cache/Tests/Adapter/CouchbaseBucketAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/CouchbaseBucketAdapterTest.php
index 11ca665c38cf8..d0d513621f9c8 100644
--- a/src/Symfony/Component/Cache/Tests/Adapter/CouchbaseBucketAdapterTest.php
+++ b/src/Symfony/Component/Cache/Tests/Adapter/CouchbaseBucketAdapterTest.php
@@ -33,7 +33,7 @@ class CouchbaseBucketAdapterTest extends AdapterTestCase
/** @var \CouchbaseBucket */
protected static $client;
- public static function setupBeforeClass(): void
+ public static function setUpBeforeClass(): void
{
if (!CouchbaseBucketAdapter::isSupported()) {
throw new SkippedTestSuiteError('Couchbase >= 2.6.0 < 3.0.0 is required.');
diff --git a/src/Symfony/Component/Cache/Tests/Adapter/CouchbaseCollectionAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/CouchbaseCollectionAdapterTest.php
index 192bc00e2c516..a7c33e2358b0b 100644
--- a/src/Symfony/Component/Cache/Tests/Adapter/CouchbaseCollectionAdapterTest.php
+++ b/src/Symfony/Component/Cache/Tests/Adapter/CouchbaseCollectionAdapterTest.php
@@ -18,7 +18,7 @@
/**
* @requires extension couchbase <4.0.0
- * @requires extension couchbase >=3.0.0
+ * @requires extension couchbase >=3.0.5
*
* @group integration
*
@@ -33,10 +33,10 @@ class CouchbaseCollectionAdapterTest extends AdapterTestCase
/** @var Collection */
protected static $client;
- public static function setupBeforeClass(): void
+ public static function setUpBeforeClass(): void
{
if (!CouchbaseCollectionAdapter::isSupported()) {
- self::markTestSkipped('Couchbase >= 3.0.0 < 4.0.0 is required.');
+ self::markTestSkipped('Couchbase >= 3.0.5 < 4.0.0 is required.');
}
self::$client = AbstractAdapter::createConnection('couchbase://'.getenv('COUCHBASE_HOST').'/cache',
@@ -47,7 +47,7 @@ public static function setupBeforeClass(): void
public function createCachePool($defaultLifetime = 0): CacheItemPoolInterface
{
if (!CouchbaseCollectionAdapter::isSupported()) {
- self::markTestSkipped('Couchbase >= 3.0.0 < 4.0.0 is required.');
+ self::markTestSkipped('Couchbase >= 3.0.5 < 4.0.0 is required.');
}
$client = $defaultLifetime
diff --git a/src/Symfony/Component/Cache/Tests/Adapter/RedisArrayAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/RedisArrayAdapterTest.php
index 58ca31441f5fb..6323dbd3beabc 100644
--- a/src/Symfony/Component/Cache/Tests/Adapter/RedisArrayAdapterTest.php
+++ b/src/Symfony/Component/Cache/Tests/Adapter/RedisArrayAdapterTest.php
@@ -20,7 +20,7 @@ class RedisArrayAdapterTest extends AbstractRedisAdapterTestCase
{
public static function setUpBeforeClass(): void
{
- parent::setupBeforeClass();
+ parent::setUpBeforeClass();
if (!class_exists(\RedisArray::class)) {
throw new SkippedTestSuiteError('The RedisArray class is required.');
}
diff --git a/src/Symfony/Component/Clock/Test/ClockSensitiveTrait.php b/src/Symfony/Component/Clock/Test/ClockSensitiveTrait.php
index 68c69e398acd5..f7e2244b2fa16 100644
--- a/src/Symfony/Component/Clock/Test/ClockSensitiveTrait.php
+++ b/src/Symfony/Component/Clock/Test/ClockSensitiveTrait.php
@@ -42,14 +42,20 @@ public static function mockTime(string|\DateTimeImmutable|bool $when = true): Cl
}
/**
+ * @beforeClass
+ *
* @before
*
* @internal
*/
- protected static function saveClockBeforeTest(bool $save = true): ClockInterface
+ public static function saveClockBeforeTest(bool $save = true): ClockInterface
{
static $originalClock;
+ if ($save && $originalClock) {
+ self::restoreClockAfterTest();
+ }
+
return $save ? $originalClock = Clock::get() : $originalClock;
}
diff --git a/src/Symfony/Component/Clock/Tests/ClockBeforeClassTest.php b/src/Symfony/Component/Clock/Tests/ClockBeforeClassTest.php
new file mode 100644
index 0000000000000..bd207550ec3b6
--- /dev/null
+++ b/src/Symfony/Component/Clock/Tests/ClockBeforeClassTest.php
@@ -0,0 +1,60 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Clock\Tests;
+
+use PHPUnit\Framework\TestCase;
+use Psr\Clock\ClockInterface;
+use Symfony\Component\Clock\Clock;
+use Symfony\Component\Clock\MockClock;
+use Symfony\Component\Clock\NativeClock;
+use Symfony\Component\Clock\Test\ClockSensitiveTrait;
+
+class ClockBeforeClassTest extends TestCase
+{
+ use ClockSensitiveTrait;
+
+ private static ?ClockInterface $clock = null;
+
+ public static function setUpBeforeClass(): void
+ {
+ self::$clock = self::mockTime();
+ }
+
+ public static function tearDownAfterClass(): void
+ {
+ self::$clock = null;
+ }
+
+ public function testMockClock()
+ {
+ $this->assertInstanceOf(MockClock::class, self::$clock);
+ $this->assertInstanceOf(NativeClock::class, Clock::get());
+
+ $clock = self::mockTime();
+ $this->assertInstanceOf(MockClock::class, Clock::get());
+ $this->assertSame(Clock::get(), $clock);
+
+ $this->assertNotSame($clock, self::$clock);
+
+ self::restoreClockAfterTest();
+ self::saveClockBeforeTest();
+
+ $this->assertInstanceOf(MockClock::class, self::$clock);
+ $this->assertInstanceOf(NativeClock::class, Clock::get());
+
+ $clock = self::mockTime();
+ $this->assertInstanceOf(MockClock::class, Clock::get());
+ $this->assertSame(Clock::get(), $clock);
+
+ $this->assertNotSame($clock, self::$clock);
+ }
+}
diff --git a/src/Symfony/Component/Console/Output/StreamOutput.php b/src/Symfony/Component/Console/Output/StreamOutput.php
index 155066ea0e1e0..b390ac941135c 100644
--- a/src/Symfony/Component/Console/Output/StreamOutput.php
+++ b/src/Symfony/Component/Console/Output/StreamOutput.php
@@ -96,18 +96,17 @@ protected function hasColorSupport(): bool
return false;
}
- if ('Hyper' === getenv('TERM_PROGRAM')) {
+ if (\DIRECTORY_SEPARATOR === '\\'
+ && \function_exists('sapi_windows_vt100_support')
+ && @sapi_windows_vt100_support($this->stream)
+ ) {
return true;
}
- if (\DIRECTORY_SEPARATOR === '\\') {
- return (\function_exists('sapi_windows_vt100_support')
- && @sapi_windows_vt100_support($this->stream))
- || false !== getenv('ANSICON')
- || 'ON' === getenv('ConEmuANSI')
- || 'xterm' === getenv('TERM');
- }
-
- return stream_isatty($this->stream);
+ return 'Hyper' === getenv('TERM_PROGRAM')
+ || false !== getenv('ANSICON')
+ || 'ON' === getenv('ConEmuANSI')
+ || str_starts_with((string) getenv('TERM'), 'xterm')
+ || stream_isatty($this->stream);
}
}
diff --git a/src/Symfony/Component/DependencyInjection/EnvVarProcessorInterface.php b/src/Symfony/Component/DependencyInjection/EnvVarProcessorInterface.php
index fecd474076e37..3cda63934bf71 100644
--- a/src/Symfony/Component/DependencyInjection/EnvVarProcessorInterface.php
+++ b/src/Symfony/Component/DependencyInjection/EnvVarProcessorInterface.php
@@ -23,7 +23,6 @@ interface EnvVarProcessorInterface
/**
* Returns the value of the given variable as managed by the current instance.
*
- * @param string $prefix The namespace of the variable
* @param string $prefix The namespace of the variable; when the empty string is passed, null values should be kept as is
* @param string $name The name of the variable within the namespace
* @param \Closure(string): mixed $getEnv A closure that allows fetching more env vars
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Dumper/YamlDumperTest.php b/src/Symfony/Component/DependencyInjection/Tests/Dumper/YamlDumperTest.php
index 0b5c125be8c9a..3cf0f3d971573 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Dumper/YamlDumperTest.php
+++ b/src/Symfony/Component/DependencyInjection/Tests/Dumper/YamlDumperTest.php
@@ -167,8 +167,6 @@ public function testDumpHandlesEnumeration()
}
/**
- * @requires PHP 8.1
- *
* @dataProvider provideDefaultClasses
*/
public function testDumpHandlesDefaultAttribute($class, $expectedFile)
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container8.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container8.php
index 0caa0fe3ef2b6..18c78746e4ab6 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container8.php
+++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container8.php
@@ -12,7 +12,7 @@
'utf-8 valid string' => "\u{021b}\u{1b56}\ttest",
'binary' => "\xf0\xf0\xf0\xf0",
'binary-control-char' => "This is a Bell char \x07",
- 'console banner' => "\e[37;44m#StandWith\e[30;43mUkraine\e[0m",
+ 'console banner' => "\e[37;44mHello\e[30;43mWorld\e[0m",
'null string' => 'null',
'string of digits' => '123',
'string of digits prefixed with minus character' => '-123',
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services8.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services8.php
index 65aded752be4e..c5778bddbcbb6 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services8.php
+++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services8.php
@@ -98,7 +98,7 @@ protected function getDefaultParameters(): array
'utf-8 valid string' => 'ț᭖ test',
'binary' => '',
'binary-control-char' => 'This is a Bell char ',
- 'console banner' => '[37;44m#StandWith[30;43mUkraine[0m',
+ 'console banner' => '[37;44mHello[30;43mWorld[0m',
'null string' => 'null',
'string of digits' => '123',
'string of digits prefixed with minus character' => '-123',
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services8.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services8.xml
index e5655d5b0c11d..92a5f4279f4a6 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services8.xml
+++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services8.xml
@@ -21,7 +21,7 @@
ț᭖ test8PDw8A==VGhpcyBpcyBhIEJlbGwgY2hhciAH
- G1szNzs0NG0jU3RhbmRXaXRoG1szMDs0M21Va3JhaW5lG1swbQ==
+ G1szNzs0NG1IZWxsbxtbMzA7NDNtV29ybGQbWzBtnull123-123
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services8.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services8.yml
index 7374092036409..739b86971eab2 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services8.yml
+++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services8.yml
@@ -7,7 +7,7 @@ parameters:
utf-8 valid string: "ț᭖\ttest"
binary: !!binary 8PDw8A==
binary-control-char: !!binary VGhpcyBpcyBhIEJlbGwgY2hhciAH
- console banner: "\e[37;44m#StandWith\e[30;43mUkraine\e[0m"
+ console banner: "\e[37;44mHello\e[30;43mWorld\e[0m"
null string: 'null'
string of digits: '123'
string of digits prefixed with minus character: '-123'
diff --git a/src/Symfony/Component/Dotenv/Command/DebugCommand.php b/src/Symfony/Component/Dotenv/Command/DebugCommand.php
index 85cca991ca760..eb4a089fd097a 100644
--- a/src/Symfony/Component/Dotenv/Command/DebugCommand.php
+++ b/src/Symfony/Component/Dotenv/Command/DebugCommand.php
@@ -151,7 +151,13 @@ private function getVariables(array $envFiles, ?string $nameFilter): array
private function getAvailableVars(): array
{
- $vars = explode(',', $_SERVER['SYMFONY_DOTENV_VARS'] ?? '');
+ $dotenvVars = $_SERVER['SYMFONY_DOTENV_VARS'] ?? '';
+
+ if ('' === $dotenvVars) {
+ return [];
+ }
+
+ $vars = explode(',', $dotenvVars);
sort($vars);
return $vars;
diff --git a/src/Symfony/Component/Dotenv/Dotenv.php b/src/Symfony/Component/Dotenv/Dotenv.php
index 6e693ac28b329..9b905f31c2f23 100644
--- a/src/Symfony/Component/Dotenv/Dotenv.php
+++ b/src/Symfony/Component/Dotenv/Dotenv.php
@@ -25,7 +25,7 @@
*/
final class Dotenv
{
- public const VARNAME_REGEX = '(?i:[A-Z][A-Z0-9_]*+)';
+ public const VARNAME_REGEX = '(?i:_?[A-Z][A-Z0-9_]*+)';
public const STATE_VARNAME = 0;
public const STATE_VALUE = 1;
@@ -341,8 +341,8 @@ private function lexValue(): string
++$this->cursor;
$value = str_replace(['\\"', '\r', '\n'], ['"', "\r", "\n"], $value);
$resolvedValue = $value;
- $resolvedValue = $this->resolveVariables($resolvedValue, $loadedVars);
$resolvedValue = $this->resolveCommands($resolvedValue, $loadedVars);
+ $resolvedValue = $this->resolveVariables($resolvedValue, $loadedVars);
$resolvedValue = str_replace('\\\\', '\\', $resolvedValue);
$v .= $resolvedValue;
} else {
@@ -364,8 +364,8 @@ private function lexValue(): string
}
$value = rtrim($value);
$resolvedValue = $value;
- $resolvedValue = $this->resolveVariables($resolvedValue, $loadedVars);
$resolvedValue = $this->resolveCommands($resolvedValue, $loadedVars);
+ $resolvedValue = $this->resolveVariables($resolvedValue, $loadedVars);
$resolvedValue = str_replace('\\\\', '\\', $resolvedValue);
if ($resolvedValue === $value && preg_match('/\s+/', $value)) {
diff --git a/src/Symfony/Component/Dotenv/Tests/Command/DebugCommandTest.php b/src/Symfony/Component/Dotenv/Tests/Command/DebugCommandTest.php
index 8bf787336574b..22e656d97cd43 100644
--- a/src/Symfony/Component/Dotenv/Tests/Command/DebugCommandTest.php
+++ b/src/Symfony/Component/Dotenv/Tests/Command/DebugCommandTest.php
@@ -27,6 +27,8 @@ class DebugCommandTest extends TestCase
*/
public function testErrorOnUninitializedDotenv()
{
+ unset($_SERVER['SYMFONY_DOTENV_VARS']);
+
$command = new DebugCommand('dev', __DIR__.'/Fixtures/Scenario1');
$command->setHelperSet(new HelperSet([new FormatterHelper()]));
$tester = new CommandTester($command);
@@ -36,6 +38,30 @@ public function testErrorOnUninitializedDotenv()
$this->assertStringContainsString('[ERROR] Dotenv component is not initialized', $output);
}
+ /**
+ * @runInSeparateProcess
+ */
+ public function testEmptyDotEnvVarsList()
+ {
+ $_SERVER['SYMFONY_DOTENV_VARS'] = '';
+
+ $command = new DebugCommand('dev', __DIR__.'/Fixtures/Scenario1');
+ $command->setHelperSet(new HelperSet([new FormatterHelper()]));
+ $tester = new CommandTester($command);
+ $tester->execute([]);
+ $expectedFormat = <<<'OUTPUT'
+%a
+ ---------- ------- ------------ ------%S
+ Variable Value .env.local .env%S
+ ---------- ------- ------------ ------%S
+
+ // Note that values might be different between web and CLI.%S
+%a
+OUTPUT;
+
+ $this->assertStringMatchesFormat($expectedFormat, $tester->getDisplay());
+ }
+
public function testScenario1InDevEnv()
{
$output = $this->executeCommand(__DIR__.'/Fixtures/Scenario1', 'dev');
diff --git a/src/Symfony/Component/Dotenv/Tests/DotenvTest.php b/src/Symfony/Component/Dotenv/Tests/DotenvTest.php
index 2089e4bca336c..72d0d5630ec9a 100644
--- a/src/Symfony/Component/Dotenv/Tests/DotenvTest.php
+++ b/src/Symfony/Component/Dotenv/Tests/DotenvTest.php
@@ -53,6 +53,7 @@ public static function getEnvDataWithFormatErrors()
["FOO=\nBAR=\${FOO:-\'a{a}a}", "Unsupported character \"'\" found in the default value of variable \"\$FOO\". in \".env\" at line 2.\n...\\nBAR=\${FOO:-\'a{a}a}...\n ^ line 2 offset 24"],
["FOO=\nBAR=\${FOO:-a\$a}", "Unsupported character \"\$\" found in the default value of variable \"\$FOO\". in \".env\" at line 2.\n...FOO=\\nBAR=\${FOO:-a\$a}...\n ^ line 2 offset 20"],
["FOO=\nBAR=\${FOO:-a\"a}", "Unclosed braces on variable expansion in \".env\" at line 2.\n...FOO=\\nBAR=\${FOO:-a\"a}...\n ^ line 2 offset 17"],
+ ['_=FOO', "Invalid character in variable name in \".env\" at line 1.\n..._=FOO...\n ^ line 1 offset 0"],
];
if ('\\' !== \DIRECTORY_SEPARATOR) {
@@ -175,6 +176,10 @@ public static function getEnvData()
["FOO=\nBAR=\${FOO:=TEST}", ['FOO' => 'TEST', 'BAR' => 'TEST']],
["FOO=\nBAR=\$FOO:=TEST}", ['FOO' => 'TEST', 'BAR' => 'TEST}']],
["FOO=foo\nFOOBAR=\${FOO}\${BAR}", ['FOO' => 'foo', 'FOOBAR' => 'foo']],
+
+ // underscores
+ ['_FOO=BAR', ['_FOO' => 'BAR']],
+ ['_FOO_BAR=FOOBAR', ['_FOO_BAR' => 'FOOBAR']],
];
if ('\\' !== \DIRECTORY_SEPARATOR) {
diff --git a/src/Symfony/Component/EventDispatcher/GenericEvent.php b/src/Symfony/Component/EventDispatcher/GenericEvent.php
index 68a20306334c3..0ccbbd81045c9 100644
--- a/src/Symfony/Component/EventDispatcher/GenericEvent.php
+++ b/src/Symfony/Component/EventDispatcher/GenericEvent.php
@@ -29,7 +29,7 @@ class GenericEvent extends Event implements \ArrayAccess, \IteratorAggregate
protected $arguments;
/**
- * Encapsulate an event with $subject and $args.
+ * Encapsulate an event with $subject and $arguments.
*
* @param mixed $subject The subject of the event, usually an object or a callable
* @param array $arguments Arguments to store in the event
diff --git a/src/Symfony/Component/ExpressionLanguage/Node/NullCoalesceNode.php b/src/Symfony/Component/ExpressionLanguage/Node/NullCoalesceNode.php
index 1cc5eb058e0cc..025bb1a42418e 100644
--- a/src/Symfony/Component/ExpressionLanguage/Node/NullCoalesceNode.php
+++ b/src/Symfony/Component/ExpressionLanguage/Node/NullCoalesceNode.php
@@ -39,7 +39,7 @@ public function compile(Compiler $compiler): void
public function evaluate(array $functions, array $values): mixed
{
if ($this->nodes['expr1'] instanceof GetAttrNode) {
- $this->nodes['expr1']->attributes['is_null_coalesce'] = true;
+ $this->addNullCoalesceAttributeToGetAttrNodes($this->nodes['expr1']);
}
return $this->nodes['expr1']->evaluate($functions, $values) ?? $this->nodes['expr2']->evaluate($functions, $values);
@@ -49,4 +49,17 @@ public function toArray(): array
{
return ['(', $this->nodes['expr1'], ') ?? (', $this->nodes['expr2'], ')'];
}
+
+ private function addNullCoalesceAttributeToGetAttrNodes(Node $node): void
+ {
+ if (!$node instanceof GetAttrNode) {
+ return;
+ }
+
+ $node->attributes['is_null_coalesce'] = true;
+
+ foreach ($node->nodes as $node) {
+ $this->addNullCoalesceAttributeToGetAttrNodes($node);
+ }
+ }
}
diff --git a/src/Symfony/Component/ExpressionLanguage/Tests/ExpressionLanguageTest.php b/src/Symfony/Component/ExpressionLanguage/Tests/ExpressionLanguageTest.php
index bef2395e859c6..0e2e964f448d5 100644
--- a/src/Symfony/Component/ExpressionLanguage/Tests/ExpressionLanguageTest.php
+++ b/src/Symfony/Component/ExpressionLanguage/Tests/ExpressionLanguageTest.php
@@ -424,6 +424,9 @@ public function bar()
yield ['foo["bar"]["baz"] ?? "default"', ['bar' => null]];
yield ['foo["bar"].baz ?? "default"', ['bar' => null]];
yield ['foo.bar().baz ?? "default"', $foo];
+ yield ['foo.bar.baz.bam ?? "default"', (object) ['bar' => null]];
+ yield ['foo?.bar?.baz?.qux ?? "default"', (object) ['bar' => null]];
+ yield ['foo[123][456][789] ?? "default"', [123 => []]];
}
/**
diff --git a/src/Symfony/Component/HttpClient/Response/AsyncContext.php b/src/Symfony/Component/HttpClient/Response/AsyncContext.php
index 55903463ae435..c5cb471bbe47e 100644
--- a/src/Symfony/Component/HttpClient/Response/AsyncContext.php
+++ b/src/Symfony/Component/HttpClient/Response/AsyncContext.php
@@ -95,7 +95,7 @@ public function pause(float $duration): void
if (\is_callable($pause = $this->response->getInfo('pause_handler'))) {
$pause($duration);
} elseif (0 < $duration) {
- usleep(1E6 * $duration);
+ usleep((int) (1E6 * $duration));
}
}
diff --git a/src/Symfony/Component/HttpClient/Response/TransportResponseTrait.php b/src/Symfony/Component/HttpClient/Response/TransportResponseTrait.php
index ac53b99a6282a..aa4568612bb4d 100644
--- a/src/Symfony/Component/HttpClient/Response/TransportResponseTrait.php
+++ b/src/Symfony/Component/HttpClient/Response/TransportResponseTrait.php
@@ -296,7 +296,7 @@ public static function stream(iterable $responses, float $timeout = null): \Gene
}
if (-1 === self::select($multi, min($timeoutMin, $timeoutMax - $elapsedTimeout))) {
- usleep(min(500, 1E6 * $timeoutMin));
+ usleep((int) min(500, 1E6 * $timeoutMin));
}
$elapsedTimeout = microtime(true) - $lastActivity;
diff --git a/src/Symfony/Component/HttpFoundation/RequestMatcher.php b/src/Symfony/Component/HttpFoundation/RequestMatcher.php
index 8c5f1d8134635..ac155fa30c47c 100644
--- a/src/Symfony/Component/HttpFoundation/RequestMatcher.php
+++ b/src/Symfony/Component/HttpFoundation/RequestMatcher.php
@@ -88,7 +88,7 @@ public function matchHost(?string $regexp)
}
/**
- * Adds a check for the the URL port.
+ * Adds a check for the URL port.
*
* @param int|null $port The port number to connect to
*
diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php
index 3b0f89509f65c..6643cc58eede1 100644
--- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php
+++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php
@@ -73,6 +73,7 @@ public function getArguments(Request $request, callable $controller, \Reflection
$argumentValueResolvers = [
$this->namedResolvers->get($resolverName),
+ new RequestAttributeValueResolver(),
new DefaultValueResolver(),
];
}
diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/BackedEnumValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/BackedEnumValueResolver.php
index 4f0ca76d30226..620e2de080a35 100644
--- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/BackedEnumValueResolver.php
+++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/BackedEnumValueResolver.php
@@ -86,7 +86,7 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable
try {
return [$enumType::from($value)];
- } catch (\ValueError $e) {
+ } catch (\ValueError|\TypeError $e) {
throw new NotFoundHttpException(sprintf('Could not resolve the "%s $%s" controller argument: ', $argument->getType(), $argument->getName()).$e->getMessage(), $e);
}
}
diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php
index 8083dd78ef357..38ee7758a70b6 100644
--- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php
+++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php
@@ -119,7 +119,7 @@ public function onKernelControllerArguments(ControllerArgumentsEvent $event): vo
$payload = $e->getData();
}
- if (null !== $payload) {
+ if (null !== $payload && !\count($violations)) {
$violations->addAll($this->validator->validate($payload, null, $argument->validationGroups ?? null));
}
diff --git a/src/Symfony/Component/HttpKernel/EventListener/AbstractSessionListener.php b/src/Symfony/Component/HttpKernel/EventListener/AbstractSessionListener.php
index ec27eaec122e5..3cdb3b395c969 100644
--- a/src/Symfony/Component/HttpKernel/EventListener/AbstractSessionListener.php
+++ b/src/Symfony/Component/HttpKernel/EventListener/AbstractSessionListener.php
@@ -35,13 +35,14 @@
*
* @author Johannes M. Schmitt
* @author Tobias Schultze
- *
- * @internal
*/
abstract class AbstractSessionListener implements EventSubscriberInterface, ResetInterface
{
public const NO_AUTO_CACHE_CONTROL_HEADER = 'Symfony-Session-NoAutoCacheControl';
+ /**
+ * @internal
+ */
protected $container;
private bool $debug;
@@ -50,6 +51,9 @@ abstract class AbstractSessionListener implements EventSubscriberInterface, Rese
*/
private $sessionOptions;
+ /**
+ * @internal
+ */
public function __construct(ContainerInterface $container = null, bool $debug = false, array $sessionOptions = [])
{
$this->container = $container;
@@ -57,6 +61,9 @@ public function __construct(ContainerInterface $container = null, bool $debug =
$this->sessionOptions = $sessionOptions;
}
+ /**
+ * @internal
+ */
public function onKernelRequest(RequestEvent $event): void
{
if (!$event->isMainRequest()) {
@@ -90,6 +97,9 @@ public function onKernelRequest(RequestEvent $event): void
}
}
+ /**
+ * @internal
+ */
public function onKernelResponse(ResponseEvent $event): void
{
if (!$event->isMainRequest() || (!$this->container->has('initialized_session') && !$event->getRequest()->hasSession())) {
@@ -218,6 +228,9 @@ public function onKernelResponse(ResponseEvent $event): void
}
}
+ /**
+ * @internal
+ */
public function onSessionUsage(): void
{
if (!$this->debug) {
@@ -253,6 +266,9 @@ public function onSessionUsage(): void
throw new UnexpectedSessionUsageException('Session was used while the request was declared stateless.');
}
+ /**
+ * @internal
+ */
public static function getSubscribedEvents(): array
{
return [
@@ -262,6 +278,9 @@ public static function getSubscribedEvents(): array
];
}
+ /**
+ * @internal
+ */
public function reset(): void
{
if (\PHP_SESSION_ACTIVE === session_status()) {
@@ -278,6 +297,8 @@ public function reset(): void
/**
* Gets the session object.
+ *
+ * @internal
*/
abstract protected function getSession(): ?SessionInterface;
diff --git a/src/Symfony/Component/HttpKernel/EventListener/LocaleListener.php b/src/Symfony/Component/HttpKernel/EventListener/LocaleListener.php
index 4516048be7f4c..65a3bfde46db1 100644
--- a/src/Symfony/Component/HttpKernel/EventListener/LocaleListener.php
+++ b/src/Symfony/Component/HttpKernel/EventListener/LocaleListener.php
@@ -69,7 +69,7 @@ private function setLocale(Request $request): void
if ($locale = $request->attributes->get('_locale')) {
$request->setLocale($locale);
} elseif ($this->useAcceptLanguageHeader) {
- if ($preferredLanguage = $request->getPreferredLanguage($this->enabledLocales)) {
+ if ($request->getLanguages() && $preferredLanguage = $request->getPreferredLanguage($this->enabledLocales)) {
$request->setLocale($preferredLanguage);
}
$request->attributes->set('_vary_by_language', true);
diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php
index f7b33bab29be8..c55a03eebdd9b 100644
--- a/src/Symfony/Component/HttpKernel/Kernel.php
+++ b/src/Symfony/Component/HttpKernel/Kernel.php
@@ -76,11 +76,11 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl
*/
private static array $freshCache = [];
- public const VERSION = '6.3.10';
- public const VERSION_ID = 60310;
+ public const VERSION = '6.3.11';
+ public const VERSION_ID = 60311;
public const MAJOR_VERSION = 6;
public const MINOR_VERSION = 3;
- public const RELEASE_VERSION = 10;
+ public const RELEASE_VERSION = 11;
public const EXTRA_VERSION = '';
public const END_OF_MAINTENANCE = '01/2024';
diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/BackedEnumValueResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/BackedEnumValueResolverTest.php
index 9e2986273653a..9dc6a083123f5 100644
--- a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/BackedEnumValueResolverTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/BackedEnumValueResolverTest.php
@@ -16,6 +16,7 @@
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\BackedEnumValueResolver;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+use Symfony\Component\HttpKernel\Tests\Fixtures\IntEnum;
use Symfony\Component\HttpKernel\Tests\Fixtures\Suit;
class BackedEnumValueResolverTest extends TestCase
@@ -134,6 +135,18 @@ public function testResolveThrowsOnUnexpectedType()
$resolver->resolve($request, $metadata);
}
+ public function testResolveThrowsOnTypeError()
+ {
+ $resolver = new BackedEnumValueResolver();
+ $request = self::createRequest(['suit' => 'value']);
+ $metadata = self::createArgumentMetadata('suit', IntEnum::class);
+
+ $this->expectException(NotFoundHttpException::class);
+ $this->expectExceptionMessage('Could not resolve the "Symfony\Component\HttpKernel\Tests\Fixtures\IntEnum $suit" controller argument: Symfony\Component\HttpKernel\Tests\Fixtures\IntEnum::from(): Argument #1 ($value) must be of type int, string given');
+
+ $resolver->resolve($request, $metadata);
+ }
+
private static function createRequest(array $attributes = []): Request
{
return new Request([], [], $attributes);
diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php
index 4ca326392be56..179f14a1271e8 100644
--- a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php
@@ -27,7 +27,6 @@
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Validator\Constraints as Assert;
-use Symfony\Component\Validator\ConstraintViolation;
use Symfony\Component\Validator\ConstraintViolationList;
use Symfony\Component\Validator\Exception\ValidationFailedException;
use Symfony\Component\Validator\Validator\ValidatorInterface;
@@ -226,14 +225,11 @@ public function testWithoutValidatorAndCouldNotDenormalize()
public function testValidationNotPassed()
{
$content = '{"price": 50, "title": ["not a string"]}';
- $payload = new RequestPayload(50);
$serializer = new Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]);
$validator = $this->createMock(ValidatorInterface::class);
- $validator->expects($this->once())
- ->method('validate')
- ->with($payload)
- ->willReturn(new ConstraintViolationList([new ConstraintViolation('Test', null, [], '', null, '')]));
+ $validator->expects($this->never())
+ ->method('validate');
$resolver = new RequestPayloadValueResolver($serializer, $validator);
@@ -253,7 +249,36 @@ public function testValidationNotPassed()
$validationFailedException = $e->getPrevious();
$this->assertInstanceOf(ValidationFailedException::class, $validationFailedException);
$this->assertSame('This value should be of type unknown.', $validationFailedException->getViolations()[0]->getMessage());
- $this->assertSame('Test', $validationFailedException->getViolations()[1]->getMessage());
+ }
+ }
+
+ public function testValidationNotPerformedWhenPartialDenormalizationReturnsViolation()
+ {
+ $content = '{"password": "abc"}';
+ $serializer = new Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]);
+
+ $validator = $this->createMock(ValidatorInterface::class);
+ $validator->expects($this->never())
+ ->method('validate');
+
+ $resolver = new RequestPayloadValueResolver($serializer, $validator);
+
+ $argument = new ArgumentMetadata('invalid', User::class, false, false, null, false, [
+ MapRequestPayload::class => new MapRequestPayload(),
+ ]);
+ $request = Request::create('/', 'POST', server: ['CONTENT_TYPE' => 'application/json'], content: $content);
+
+ $kernel = $this->createMock(HttpKernelInterface::class);
+ $arguments = $resolver->resolve($request, $argument);
+ $event = new ControllerArgumentsEvent($kernel, function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST);
+
+ try {
+ $resolver->onKernelControllerArguments($event);
+ $this->fail(sprintf('Expected "%s" to be thrown.', HttpException::class));
+ } catch (HttpException $e) {
+ $validationFailedException = $e->getPrevious();
+ $this->assertInstanceOf(ValidationFailedException::class, $validationFailedException);
+ $this->assertSame('This value should be of type unknown.', $validationFailedException->getViolations()[0]->getMessage());
}
}
@@ -612,3 +637,24 @@ public function __construct(public readonly float $price)
{
}
}
+
+class User
+{
+ public function __construct(
+ #[Assert\NotBlank, Assert\Email]
+ private string $email,
+ #[Assert\NotBlank]
+ private string $password,
+ ) {
+ }
+
+ public function getEmail(): string
+ {
+ return $this->email;
+ }
+
+ public function getPassword(): string
+ {
+ return $this->password;
+ }
+}
diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolverTest.php
index ef44f45bae078..d34b0b4b450a1 100644
--- a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolverTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolverTest.php
@@ -22,6 +22,8 @@
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver;
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestAttributeValueResolver;
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
+use Symfony\Component\HttpKernel\Controller\ValueResolverInterface;
+use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactory;
use Symfony\Component\HttpKernel\Exception\ResolverNotFoundException;
use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\ExtendingRequest;
@@ -298,17 +300,21 @@ public function testTargetedResolver()
public function testTargetedResolverWithDefaultValue()
{
- $resolver = self::getResolver([], [RequestAttributeValueResolver::class => new RequestAttributeValueResolver()]);
+ $resolver = self::getResolver([], [TestEntityValueResolver::class => new TestEntityValueResolver()]);
$request = Request::create('/');
$controller = $this->controllerTargetingResolverWithDefaultValue(...);
- $this->assertSame([2], $resolver->getArguments($request, $controller));
+ /** @var Post[] $arguments */
+ $arguments = $resolver->getArguments($request, $controller);
+
+ $this->assertCount(1, $arguments);
+ $this->assertSame('Default', $arguments[0]->title);
}
public function testTargetedResolverWithNullableValue()
{
- $resolver = self::getResolver([], [RequestAttributeValueResolver::class => new RequestAttributeValueResolver()]);
+ $resolver = self::getResolver([], [TestEntityValueResolver::class => new TestEntityValueResolver()]);
$request = Request::create('/');
$controller = $this->controllerTargetingResolverWithNullableValue(...);
@@ -316,6 +322,17 @@ public function testTargetedResolverWithNullableValue()
$this->assertSame([null], $resolver->getArguments($request, $controller));
}
+ public function testTargetedResolverWithRequestAttributeValue()
+ {
+ $resolver = self::getResolver([], [TestEntityValueResolver::class => new TestEntityValueResolver()]);
+
+ $request = Request::create('/');
+ $request->attributes->set('foo', $object = new Post('Random '.time()));
+ $controller = $this->controllerTargetingResolverWithTestEntity(...);
+
+ $this->assertSame([$object], $resolver->getArguments($request, $controller));
+ }
+
public function testDisabledResolver()
{
$resolver = self::getResolver(namedResolvers: []);
@@ -393,11 +410,15 @@ public function controllerTargetingResolver(#[ValueResolver(DefaultValueResolver
{
}
- public function controllerTargetingResolverWithDefaultValue(#[ValueResolver(RequestAttributeValueResolver::class)] int $foo = 2)
+ public function controllerTargetingResolverWithDefaultValue(#[ValueResolver(TestEntityValueResolver::class)] Post $foo = new Post('Default'))
+ {
+ }
+
+ public function controllerTargetingResolverWithNullableValue(#[ValueResolver(TestEntityValueResolver::class)] ?Post $foo)
{
}
- public function controllerTargetingResolverWithNullableValue(#[ValueResolver(RequestAttributeValueResolver::class)] ?int $foo)
+ public function controllerTargetingResolverWithTestEntity(#[ValueResolver(TestEntityValueResolver::class)] Post $foo)
{
}
@@ -422,3 +443,21 @@ public function controllerTargetingUnknownResolver(
function controller_function($foo, $foobar)
{
}
+
+class TestEntityValueResolver implements ValueResolverInterface
+{
+ public function resolve(Request $request, ArgumentMetadata $argument): iterable
+ {
+ return Post::class === $argument->getType() && $request->request->has('title')
+ ? [new Post($request->request->get('title'))]
+ : [];
+ }
+}
+
+class Post
+{
+ public function __construct(
+ public readonly string $title,
+ ) {
+ }
+}
diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/LocaleListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/LocaleListenerTest.php
index 58ad7f2ff6f4b..7df00988d45db 100644
--- a/src/Symfony/Component/HttpKernel/Tests/EventListener/LocaleListenerTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/LocaleListenerTest.php
@@ -130,6 +130,28 @@ public function testRequestPreferredLocaleFromAcceptLanguageHeader()
$this->assertEquals('fr', $request->getLocale());
}
+ public function testRequestDefaultLocaleIfNoAcceptLanguageHeaderIsPresent()
+ {
+ $request = new Request();
+ $listener = new LocaleListener($this->requestStack, 'de', null, true, ['lt', 'de']);
+ $event = $this->getEvent($request);
+
+ $listener->setDefaultLocale($event);
+ $listener->onKernelRequest($event);
+ $this->assertEquals('de', $request->getLocale());
+ }
+
+ public function testRequestVaryByLanguageAttributeIsSetIfUsingAcceptLanguageHeader()
+ {
+ $request = new Request();
+ $listener = new LocaleListener($this->requestStack, 'de', null, true, ['lt', 'de']);
+ $event = $this->getEvent($request);
+
+ $listener->setDefaultLocale($event);
+ $listener->onKernelRequest($event);
+ $this->assertTrue($request->attributes->get('_vary_by_language'));
+ }
+
public function testRequestSecondPreferredLocaleFromAcceptLanguageHeader()
{
$request = Request::create('/');
diff --git a/src/Symfony/Component/HttpKernel/Tests/Fixtures/IntEnum.php b/src/Symfony/Component/HttpKernel/Tests/Fixtures/IntEnum.php
new file mode 100644
index 0000000000000..8f694553b9f0c
--- /dev/null
+++ b/src/Symfony/Component/HttpKernel/Tests/Fixtures/IntEnum.php
@@ -0,0 +1,18 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpKernel\Tests\Fixtures;
+
+enum IntEnum: int
+{
+ case One = 1;
+ case Two = 2;
+}
diff --git a/src/Symfony/Component/Lock/Tests/Store/MongoDbStoreFactoryTest.php b/src/Symfony/Component/Lock/Tests/Store/MongoDbStoreFactoryTest.php
index 7782f9753632a..e5c4d0c8104d0 100644
--- a/src/Symfony/Component/Lock/Tests/Store/MongoDbStoreFactoryTest.php
+++ b/src/Symfony/Component/Lock/Tests/Store/MongoDbStoreFactoryTest.php
@@ -25,7 +25,7 @@
*/
class MongoDbStoreFactoryTest extends TestCase
{
- public static function setupBeforeClass(): void
+ public static function setUpBeforeClass(): void
{
if (!class_exists(Client::class)) {
throw new SkippedTestSuiteError('The mongodb/mongodb package is required.');
diff --git a/src/Symfony/Component/Lock/Tests/Store/MongoDbStoreTest.php b/src/Symfony/Component/Lock/Tests/Store/MongoDbStoreTest.php
index 27af141074171..3ff67f4c2261c 100644
--- a/src/Symfony/Component/Lock/Tests/Store/MongoDbStoreTest.php
+++ b/src/Symfony/Component/Lock/Tests/Store/MongoDbStoreTest.php
@@ -30,7 +30,7 @@ class MongoDbStoreTest extends AbstractStoreTestCase
{
use ExpiringStoreTestTrait;
- public static function setupBeforeClass(): void
+ public static function setUpBeforeClass(): void
{
if (!class_exists(Client::class)) {
throw new SkippedTestSuiteError('The mongodb/mongodb package is required.');
diff --git a/src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Transport/SesApiAsyncAwsTransportTest.php b/src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Transport/SesApiAsyncAwsTransportTest.php
index a88714c3c4ba2..42fbf74748421 100644
--- a/src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Transport/SesApiAsyncAwsTransportTest.php
+++ b/src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Transport/SesApiAsyncAwsTransportTest.php
@@ -38,35 +38,35 @@ public static function getTransportData()
{
return [
[
- new SesApiAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY']))),
+ new SesApiAsyncAwsTransport(new SesClient(Configuration::create(['sharedConfigFile' => false, 'accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY']))),
'ses+api://ACCESS_KEY@us-east-1',
],
[
- new SesApiAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY', 'region' => 'us-west-1']))),
+ new SesApiAsyncAwsTransport(new SesClient(Configuration::create(['sharedConfigFile' => false, 'accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY', 'region' => 'us-west-1']))),
'ses+api://ACCESS_KEY@us-west-1',
],
[
- new SesApiAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY', 'endpoint' => 'https://example.com']))),
+ new SesApiAsyncAwsTransport(new SesClient(Configuration::create(['sharedConfigFile' => false, 'accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY', 'endpoint' => 'https://example.com']))),
'ses+api://ACCESS_KEY@example.com',
],
[
- new SesApiAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY', 'endpoint' => 'https://example.com:99']))),
+ new SesApiAsyncAwsTransport(new SesClient(Configuration::create(['sharedConfigFile' => false, 'accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY', 'endpoint' => 'https://example.com:99']))),
'ses+api://ACCESS_KEY@example.com:99',
],
[
- new SesApiAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY', 'sessionToken' => 'SESSION_TOKEN']))),
+ new SesApiAsyncAwsTransport(new SesClient(Configuration::create(['sharedConfigFile' => false, 'accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY', 'sessionToken' => 'SESSION_TOKEN']))),
'ses+api://ACCESS_KEY@us-east-1',
],
[
- new SesApiAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY', 'region' => 'us-west-1', 'sessionToken' => 'SESSION_TOKEN']))),
+ new SesApiAsyncAwsTransport(new SesClient(Configuration::create(['sharedConfigFile' => false, 'accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY', 'region' => 'us-west-1', 'sessionToken' => 'SESSION_TOKEN']))),
'ses+api://ACCESS_KEY@us-west-1',
],
[
- new SesApiAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY', 'endpoint' => 'https://example.com', 'sessionToken' => 'SESSION_TOKEN']))),
+ new SesApiAsyncAwsTransport(new SesClient(Configuration::create(['sharedConfigFile' => false, 'accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY', 'endpoint' => 'https://example.com', 'sessionToken' => 'SESSION_TOKEN']))),
'ses+api://ACCESS_KEY@example.com',
],
[
- new SesApiAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY', 'endpoint' => 'https://example.com:99', 'sessionToken' => 'SESSION_TOKEN']))),
+ new SesApiAsyncAwsTransport(new SesClient(Configuration::create(['sharedConfigFile' => false, 'accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY', 'endpoint' => 'https://example.com:99', 'sessionToken' => 'SESSION_TOKEN']))),
'ses+api://ACCESS_KEY@example.com:99',
],
];
@@ -99,7 +99,7 @@ public function testSend()
]);
});
- $transport = new SesApiAsyncAwsTransport(new SesClient(Configuration::create([]), new NullProvider(), $client));
+ $transport = new SesApiAsyncAwsTransport(new SesClient(Configuration::create(['sharedConfigFile' => false]), new NullProvider(), $client));
$mail = new Email();
$mail->subject('Hello!')
diff --git a/src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Transport/SesHttpAsyncAwsTransportTest.php b/src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Transport/SesHttpAsyncAwsTransportTest.php
index 24c7959b950ec..e4cd8ca9e805a 100644
--- a/src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Transport/SesHttpAsyncAwsTransportTest.php
+++ b/src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Transport/SesHttpAsyncAwsTransportTest.php
@@ -38,35 +38,35 @@ public static function getTransportData()
{
return [
[
- new SesHttpAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY']))),
+ new SesHttpAsyncAwsTransport(new SesClient(Configuration::create(['sharedConfigFile' => false, 'accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY']))),
'ses+https://ACCESS_KEY@us-east-1',
],
[
- new SesHttpAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY', 'region' => 'us-west-1']))),
+ new SesHttpAsyncAwsTransport(new SesClient(Configuration::create(['sharedConfigFile' => false, 'accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY', 'region' => 'us-west-1']))),
'ses+https://ACCESS_KEY@us-west-1',
],
[
- new SesHttpAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY', 'endpoint' => 'https://example.com']))),
+ new SesHttpAsyncAwsTransport(new SesClient(Configuration::create(['sharedConfigFile' => false, 'accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY', 'endpoint' => 'https://example.com']))),
'ses+https://ACCESS_KEY@example.com',
],
[
- new SesHttpAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY', 'endpoint' => 'https://example.com:99']))),
+ new SesHttpAsyncAwsTransport(new SesClient(Configuration::create(['sharedConfigFile' => false, 'accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY', 'endpoint' => 'https://example.com:99']))),
'ses+https://ACCESS_KEY@example.com:99',
],
[
- new SesHttpAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY', 'sessionToken' => 'SESSION_TOKEN']))),
+ new SesHttpAsyncAwsTransport(new SesClient(Configuration::create(['sharedConfigFile' => false, 'accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY', 'sessionToken' => 'SESSION_TOKEN']))),
'ses+https://ACCESS_KEY@us-east-1',
],
[
- new SesHttpAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY', 'region' => 'us-west-1', 'sessionToken' => 'SESSION_TOKEN']))),
+ new SesHttpAsyncAwsTransport(new SesClient(Configuration::create(['sharedConfigFile' => false, 'accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY', 'region' => 'us-west-1', 'sessionToken' => 'SESSION_TOKEN']))),
'ses+https://ACCESS_KEY@us-west-1',
],
[
- new SesHttpAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY', 'endpoint' => 'https://example.com', 'sessionToken' => 'SESSION_TOKEN']))),
+ new SesHttpAsyncAwsTransport(new SesClient(Configuration::create(['sharedConfigFile' => false, 'accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY', 'endpoint' => 'https://example.com', 'sessionToken' => 'SESSION_TOKEN']))),
'ses+https://ACCESS_KEY@example.com',
],
[
- new SesHttpAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY', 'endpoint' => 'https://example.com:99', 'sessionToken' => 'SESSION_TOKEN']))),
+ new SesHttpAsyncAwsTransport(new SesClient(Configuration::create(['sharedConfigFile' => false, 'accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY', 'endpoint' => 'https://example.com:99', 'sessionToken' => 'SESSION_TOKEN']))),
'ses+https://ACCESS_KEY@example.com:99',
],
];
@@ -96,7 +96,7 @@ public function testSend()
]);
});
- $transport = new SesHttpAsyncAwsTransport(new SesClient(Configuration::create([]), new NullProvider(), $client));
+ $transport = new SesHttpAsyncAwsTransport(new SesClient(Configuration::create(['sharedConfigFile' => false]), new NullProvider(), $client));
$mail = new Email();
$mail->subject('Hello!')
diff --git a/src/Symfony/Component/Mailer/Bridge/Postmark/CHANGELOG.md b/src/Symfony/Component/Mailer/Bridge/Postmark/CHANGELOG.md
index 7d0d5cd14e77f..b0c7906410f16 100644
--- a/src/Symfony/Component/Mailer/Bridge/Postmark/CHANGELOG.md
+++ b/src/Symfony/Component/Mailer/Bridge/Postmark/CHANGELOG.md
@@ -1,6 +1,11 @@
CHANGELOG
=========
+6.3
+---
+
+ * Add support for webhooks
+
4.4.0
-----
diff --git a/src/Symfony/Component/Mailer/Exception/UnsupportedSchemeException.php b/src/Symfony/Component/Mailer/Exception/UnsupportedSchemeException.php
index b0612b23808fe..23a8ec7e70944 100644
--- a/src/Symfony/Component/Mailer/Exception/UnsupportedSchemeException.php
+++ b/src/Symfony/Component/Mailer/Exception/UnsupportedSchemeException.php
@@ -40,6 +40,10 @@ class UnsupportedSchemeException extends LogicException
'class' => Bridge\Mailjet\Transport\MailjetTransportFactory::class,
'package' => 'symfony/mailjet-mailer',
],
+ 'mailpace' => [
+ 'class' => Bridge\MailPace\Transport\MailPaceTransportFactory::class,
+ 'package' => 'symfony/mail-pace-mailer',
+ ],
'mandrill' => [
'class' => Bridge\Mailchimp\Transport\MandrillTransportFactory::class,
'package' => 'symfony/mailchimp-mailer',
diff --git a/src/Symfony/Component/Mailer/Tests/Exception/UnsupportedSchemeExceptionTest.php b/src/Symfony/Component/Mailer/Tests/Exception/UnsupportedSchemeExceptionTest.php
index 5dcf0f1bbfd7c..f95e294d5c097 100644
--- a/src/Symfony/Component/Mailer/Tests/Exception/UnsupportedSchemeExceptionTest.php
+++ b/src/Symfony/Component/Mailer/Tests/Exception/UnsupportedSchemeExceptionTest.php
@@ -20,6 +20,7 @@
use Symfony\Component\Mailer\Bridge\MailerSend\Transport\MailerSendTransportFactory;
use Symfony\Component\Mailer\Bridge\Mailgun\Transport\MailgunTransportFactory;
use Symfony\Component\Mailer\Bridge\Mailjet\Transport\MailjetTransportFactory;
+use Symfony\Component\Mailer\Bridge\MailPace\Transport\MailPaceTransportFactory;
use Symfony\Component\Mailer\Bridge\OhMySmtp\Transport\OhMySmtpTransportFactory;
use Symfony\Component\Mailer\Bridge\Postmark\Transport\PostmarkTransportFactory;
use Symfony\Component\Mailer\Bridge\Sendgrid\Transport\SendgridTransportFactory;
@@ -41,6 +42,7 @@ public static function setUpBeforeClass(): void
MailerSendTransportFactory::class => false,
MailgunTransportFactory::class => false,
MailjetTransportFactory::class => false,
+ MailPaceTransportFactory::class => false,
MandrillTransportFactory::class => false,
OhMySmtpTransportFactory::class => false,
PostmarkTransportFactory::class => false,
@@ -70,6 +72,7 @@ public static function messageWhereSchemeIsPartOfSchemeToPackageMapProvider(): \
yield ['mailersend', 'symfony/mailersend-mailer'];
yield ['mailgun', 'symfony/mailgun-mailer'];
yield ['mailjet', 'symfony/mailjet-mailer'];
+ yield ['mailpace', 'symfony/mail-pace-mailer'];
yield ['mandrill', 'symfony/mailchimp-mailer'];
yield ['ohmysmtp', 'symfony/oh-my-smtp-mailer'];
yield ['postmark', 'symfony/postmark-mailer'];
diff --git a/src/Symfony/Component/Mailer/Transport/AbstractTransport.php b/src/Symfony/Component/Mailer/Transport/AbstractTransport.php
index 1b12e4360cd29..ba1f97129f7ef 100644
--- a/src/Symfony/Component/Mailer/Transport/AbstractTransport.php
+++ b/src/Symfony/Component/Mailer/Transport/AbstractTransport.php
@@ -129,7 +129,7 @@ private function checkThrottling(): void
$sleep = (1 / $this->rate) - (microtime(true) - $this->lastSent);
if (0 < $sleep) {
$this->logger->debug(sprintf('Email transport "%s" sleeps for %.2f seconds', __CLASS__, $sleep));
- usleep($sleep * 1000000);
+ usleep((int) ($sleep * 1000000));
}
$this->lastSent = microtime(true);
}
diff --git a/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/DoctrineIntegrationTest.php b/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/DoctrineIntegrationTest.php
index c9ff7c851e845..e5f702ba5dd54 100644
--- a/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/DoctrineIntegrationTest.php
+++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/DoctrineIntegrationTest.php
@@ -72,10 +72,29 @@ public function testSendWithDelay()
$stmt = $stmt->execute();
}
- $available_at = new \DateTimeImmutable($stmt instanceof Result ? $stmt->fetchOne() : $stmt->fetchColumn());
+ $availableAt = new \DateTimeImmutable($stmt instanceof Result ? $stmt->fetchOne() : $stmt->fetchColumn(), new \DateTimeZone('UTC'));
- $now = new \DateTimeImmutable('now + 60 seconds');
- $this->assertGreaterThan($now, $available_at);
+ $now = new \DateTimeImmutable('now + 60 seconds', new \DateTimeZone('UTC'));
+ $this->assertGreaterThan($now, $availableAt);
+ }
+
+ public function testSendWithNegativeDelay()
+ {
+ $this->connection->send('{"message": "Hi, I am not actually delayed"}', ['type' => DummyMessage::class], -600000);
+
+ $qb = $this->driverConnection->createQueryBuilder()
+ ->select('m.available_at')
+ ->from('messenger_messages', 'm')
+ ->where('m.body = :body')
+ ->setParameter('body', '{"message": "Hi, I am not actually delayed"}');
+
+ // DBAL 2 compatibility
+ $result = method_exists($qb, 'executeQuery') ? $qb->executeQuery() : $qb->execute();
+
+ $availableAt = new \DateTimeImmutable($result->fetchOne(), new \DateTimeZone('UTC'));
+
+ $now = new \DateTimeImmutable('now - 60 seconds', new \DateTimeZone('UTC'));
+ $this->assertLessThan($now, $availableAt);
}
public function testItRetrieveTheFirstAvailableMessage()
@@ -156,7 +175,7 @@ public function testItCountMessages()
public function testItRetrieveTheMessageThatIsOlderThanRedeliverTimeout()
{
$this->connection->setup();
- $twoHoursAgo = new \DateTimeImmutable('now -2 hours');
+ $twoHoursAgo = new \DateTimeImmutable('now -2 hours', new \DateTimeZone('UTC'));
$this->driverConnection->insert('messenger_messages', [
'body' => '{"message": "Hi requeued"}',
'headers' => json_encode(['type' => DummyMessage::class]),
diff --git a/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/Connection.php b/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/Connection.php
index d596bd1c99284..2f29c408f91bf 100644
--- a/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/Connection.php
+++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/Connection.php
@@ -126,7 +126,7 @@ public static function buildConfiguration(#[\SensitiveParameter] string $dsn, ar
public function send(string $body, array $headers, int $delay = 0): string
{
$now = new \DateTimeImmutable('UTC');
- $availableAt = $now->modify(sprintf('+%d seconds', $delay / 1000));
+ $availableAt = $now->modify(sprintf('%+d seconds', $delay / 1000));
$queryBuilder = $this->driverConnection->createQueryBuilder()
->insert($this->configuration['table_name'])
diff --git a/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/RedisExtIntegrationTest.php b/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/RedisExtIntegrationTest.php
index 2bbc8db84ce5e..86d4616fea7d7 100644
--- a/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/RedisExtIntegrationTest.php
+++ b/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/RedisExtIntegrationTest.php
@@ -264,15 +264,19 @@ public function testLazy()
$connection = Connection::fromDsn('redis://localhost/messenger-lazy?lazy=1', [], $redis);
$connection->add('1', []);
- $this->assertNotEmpty($message = $connection->get());
- $this->assertSame([
- 'message' => json_encode([
- 'body' => '1',
- 'headers' => [],
- ]),
- ], $message['data']);
- $connection->reject($message['id']);
- $redis->del('messenger-lazy');
+
+ try {
+ $this->assertNotEmpty($message = $connection->get());
+ $this->assertSame([
+ 'message' => json_encode([
+ 'body' => '1',
+ 'headers' => [],
+ ]),
+ ], $message['data']);
+ $connection->reject($message['id']);
+ } finally {
+ $redis->del('messenger-lazy');
+ }
}
public function testDbIndex()
@@ -299,13 +303,16 @@ public function testFromDsnWithMultipleHosts()
public function testJsonError()
{
$redis = $this->createRedisClient();
- $connection = Connection::fromDsn('redis://localhost/json-error', [], $redis);
+ $connection = Connection::fromDsn('redis://localhost/messenger-json-error', [], $redis);
try {
$connection->add("\xB1\x31", []);
+
+ $this->fail('Expected exception to be thrown.');
} catch (TransportException $e) {
+ $this->assertSame('Malformed UTF-8 characters, possibly incorrectly encoded', $e->getMessage());
+ } finally {
+ $redis->del('messenger-json-error');
}
-
- $this->assertSame('Malformed UTF-8 characters, possibly incorrectly encoded', $e->getMessage());
}
public function testGetNonBlocking()
@@ -314,11 +321,14 @@ public function testGetNonBlocking()
$connection = Connection::fromDsn('redis://localhost/messenger-getnonblocking', ['sentinel_master' => null], $redis);
- $this->assertNull($connection->get()); // no message, should return null immediately
- $connection->add('1', []);
- $this->assertNotEmpty($message = $connection->get());
- $connection->reject($message['id']);
- $redis->del('messenger-getnonblocking');
+ try {
+ $this->assertNull($connection->get()); // no message, should return null immediately
+ $connection->add('1', []);
+ $this->assertNotEmpty($message = $connection->get());
+ $connection->reject($message['id']);
+ } finally {
+ $redis->del('messenger-getnonblocking');
+ }
}
public function testGetAfterReject()
@@ -326,17 +336,18 @@ public function testGetAfterReject()
$redis = $this->createRedisClient();
$connection = Connection::fromDsn('redis://localhost/messenger-rejectthenget', ['sentinel_master' => null], $redis);
- $connection->add('1', []);
- $connection->add('2', []);
-
- $failing = $connection->get();
- $connection->reject($failing['id']);
-
- $connection = Connection::fromDsn('redis://localhost/messenger-rejectthenget', ['sentinel_master' => null]);
+ try {
+ $connection->add('1', []);
+ $connection->add('2', []);
- $this->assertNotNull($connection->get());
+ $failing = $connection->get();
+ $connection->reject($failing['id']);
- $redis->del('messenger-rejectthenget');
+ $connection = Connection::fromDsn('redis://localhost/messenger-rejectthenget', ['sentinel_master' => null]);
+ $this->assertNotNull($connection->get());
+ } finally {
+ $redis->del('messenger-rejectthenget');
+ }
}
public function testItProperlyHandlesEmptyMessages()
diff --git a/src/Symfony/Component/Messenger/Bridge/Redis/Transport/Connection.php b/src/Symfony/Component/Messenger/Bridge/Redis/Transport/Connection.php
index 063f2056793d8..2ef00fcf552a2 100644
--- a/src/Symfony/Component/Messenger/Bridge/Redis/Transport/Connection.php
+++ b/src/Symfony/Component/Messenger/Bridge/Redis/Transport/Connection.php
@@ -147,6 +147,10 @@ public function __construct(array $options, \Redis|Relay|\RedisCluster $redis =
*/
private static function initializeRedis(\Redis|Relay $redis, string $host, int $port, string|array|null $auth, array $params): \Redis|Relay
{
+ if ($redis->isConnected()) {
+ return $redis;
+ }
+
$connect = isset($params['persistent_id']) ? 'pconnect' : 'connect';
$redis->{$connect}($host, $port, $params['timeout'], $params['persistent_id'], $params['retry_interval'], $params['read_timeout'], ...(\defined('Redis::SCAN_PREFIX') || \extension_loaded('relay')) ? [['stream' => $params['ssl'] ?? null]] : []);
diff --git a/src/Symfony/Component/Notifier/Bridge/Clickatell/ClickatellTransport.php b/src/Symfony/Component/Notifier/Bridge/Clickatell/ClickatellTransport.php
index 40b555c07185d..bc5df314e094d 100644
--- a/src/Symfony/Component/Notifier/Bridge/Clickatell/ClickatellTransport.php
+++ b/src/Symfony/Component/Notifier/Bridge/Clickatell/ClickatellTransport.php
@@ -79,7 +79,7 @@ protected function doSend(MessageInterface $message): SentMessage
try {
$statusCode = $response->getStatusCode();
} catch (TransportExceptionInterface $e) {
- throw new TransportException('Could not reach the remote Clicktell server.', $response, 0, $e);
+ throw new TransportException('Could not reach the remote Clickatell server.', $response, 0, $e);
}
if (202 === $statusCode) {
diff --git a/src/Symfony/Component/Notifier/Bridge/FakeChat/FakeChatTransportFactory.php b/src/Symfony/Component/Notifier/Bridge/FakeChat/FakeChatTransportFactory.php
index e5bc2c2096061..38a9109b55293 100644
--- a/src/Symfony/Component/Notifier/Bridge/FakeChat/FakeChatTransportFactory.php
+++ b/src/Symfony/Component/Notifier/Bridge/FakeChat/FakeChatTransportFactory.php
@@ -17,6 +17,8 @@
use Symfony\Component\Notifier\Exception\UnsupportedSchemeException;
use Symfony\Component\Notifier\Transport\AbstractTransportFactory;
use Symfony\Component\Notifier\Transport\Dsn;
+use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
+use Symfony\Contracts\HttpClient\HttpClientInterface;
/**
* @author Oskar Stark
@@ -27,9 +29,9 @@ final class FakeChatTransportFactory extends AbstractTransportFactory
private ?MailerInterface $mailer;
private ?LoggerInterface $logger;
- public function __construct(MailerInterface $mailer = null, LoggerInterface $logger = null)
+ public function __construct(MailerInterface $mailer = null, LoggerInterface $logger = null, EventDispatcherInterface $dispatcher = null, HttpClientInterface $client = null)
{
- parent::__construct();
+ parent::__construct($dispatcher, $client);
$this->mailer = $mailer;
$this->logger = $logger;
@@ -48,7 +50,7 @@ public function create(Dsn $dsn): FakeChatEmailTransport|FakeChatLoggerTransport
$to = $dsn->getRequiredOption('to');
$from = $dsn->getRequiredOption('from');
- return (new FakeChatEmailTransport($this->mailer, $to, $from))->setHost($mailerTransport);
+ return (new FakeChatEmailTransport($this->mailer, $to, $from, $this->client, $this->dispatcher))->setHost($mailerTransport);
}
if ('fakechat+logger' === $scheme) {
@@ -56,7 +58,7 @@ public function create(Dsn $dsn): FakeChatEmailTransport|FakeChatLoggerTransport
$this->throwMissingDependencyException($scheme, LoggerInterface::class, 'psr/log');
}
- return new FakeChatLoggerTransport($this->logger);
+ return new FakeChatLoggerTransport($this->logger, $this->client, $this->dispatcher);
}
throw new UnsupportedSchemeException($dsn, 'fakechat', $this->getSupportedSchemes());
diff --git a/src/Symfony/Component/Notifier/Bridge/FakeSms/FakeSmsTransportFactory.php b/src/Symfony/Component/Notifier/Bridge/FakeSms/FakeSmsTransportFactory.php
index b97ff34ef8f82..6afd9f3a8e8d4 100644
--- a/src/Symfony/Component/Notifier/Bridge/FakeSms/FakeSmsTransportFactory.php
+++ b/src/Symfony/Component/Notifier/Bridge/FakeSms/FakeSmsTransportFactory.php
@@ -17,6 +17,8 @@
use Symfony\Component\Notifier\Exception\UnsupportedSchemeException;
use Symfony\Component\Notifier\Transport\AbstractTransportFactory;
use Symfony\Component\Notifier\Transport\Dsn;
+use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
+use Symfony\Contracts\HttpClient\HttpClientInterface;
/**
* @author James Hemery
@@ -28,9 +30,9 @@ final class FakeSmsTransportFactory extends AbstractTransportFactory
private ?MailerInterface $mailer;
private ?LoggerInterface $logger;
- public function __construct(MailerInterface $mailer = null, LoggerInterface $logger = null)
+ public function __construct(MailerInterface $mailer = null, LoggerInterface $logger = null, EventDispatcherInterface $dispatcher = null, HttpClientInterface $client = null)
{
- parent::__construct();
+ parent::__construct($dispatcher, $client);
$this->mailer = $mailer;
$this->logger = $logger;
@@ -49,7 +51,7 @@ public function create(Dsn $dsn): FakeSmsEmailTransport|FakeSmsLoggerTransport
$to = $dsn->getRequiredOption('to');
$from = $dsn->getRequiredOption('from');
- return (new FakeSmsEmailTransport($this->mailer, $to, $from))->setHost($mailerTransport);
+ return (new FakeSmsEmailTransport($this->mailer, $to, $from, $this->client, $this->dispatcher))->setHost($mailerTransport);
}
if ('fakesms+logger' === $scheme) {
@@ -57,7 +59,7 @@ public function create(Dsn $dsn): FakeSmsEmailTransport|FakeSmsLoggerTransport
$this->throwMissingDependencyException($scheme, LoggerInterface::class, 'psr/log');
}
- return new FakeSmsLoggerTransport($this->logger);
+ return new FakeSmsLoggerTransport($this->logger, $this->client, $this->dispatcher);
}
throw new UnsupportedSchemeException($dsn, 'fakesms', $this->getSupportedSchemes());
diff --git a/src/Symfony/Component/Notifier/Bridge/GoogleChat/GoogleChatTransport.php b/src/Symfony/Component/Notifier/Bridge/GoogleChat/GoogleChatTransport.php
index b1a8507a98a43..20bc407dcc6e5 100644
--- a/src/Symfony/Component/Notifier/Bridge/GoogleChat/GoogleChatTransport.php
+++ b/src/Symfony/Component/Notifier/Bridge/GoogleChat/GoogleChatTransport.php
@@ -36,7 +36,7 @@ final class GoogleChatTransport extends AbstractTransport
private ?string $threadKey;
/**
- * @param string $space The space name the the webhook url "/v1/spaces//messages"
+ * @param string $space The space name of the webhook url "/v1/spaces//messages"
* @param string $accessKey The "key" parameter of the webhook url
* @param string $accessToken The "token" parameter of the webhook url
* @param string|null $threadKey Opaque thread identifier string that can be specified to group messages into a single thread.
diff --git a/src/Symfony/Component/Notifier/Bridge/Mercure/MercureTransportFactory.php b/src/Symfony/Component/Notifier/Bridge/Mercure/MercureTransportFactory.php
index ede0a83fd2738..f235beb651441 100644
--- a/src/Symfony/Component/Notifier/Bridge/Mercure/MercureTransportFactory.php
+++ b/src/Symfony/Component/Notifier/Bridge/Mercure/MercureTransportFactory.php
@@ -17,6 +17,8 @@
use Symfony\Component\Notifier\Exception\UnsupportedSchemeException;
use Symfony\Component\Notifier\Transport\AbstractTransportFactory;
use Symfony\Component\Notifier\Transport\Dsn;
+use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
+use Symfony\Contracts\HttpClient\HttpClientInterface;
/**
* @author Mathias Arlaud
@@ -25,9 +27,9 @@ final class MercureTransportFactory extends AbstractTransportFactory
{
private HubRegistry $registry;
- public function __construct(HubRegistry $registry)
+ public function __construct(HubRegistry $registry, EventDispatcherInterface $dispatcher = null, HttpClientInterface $client = null)
{
- parent::__construct();
+ parent::__construct($dispatcher, $client);
$this->registry = $registry;
}
@@ -47,7 +49,7 @@ public function create(Dsn $dsn): MercureTransport
throw new IncompleteDsnException(sprintf('Hub "%s" not found. Did you mean one of: "%s"?', $hubId, implode('", "', array_keys($this->registry->all()))));
}
- return new MercureTransport($hub, $hubId, $topic);
+ return new MercureTransport($hub, $hubId, $topic, $this->client, $this->dispatcher);
}
protected function getSupportedSchemes(): array
diff --git a/src/Symfony/Component/Notifier/Bridge/Smsc/SmscTransport.php b/src/Symfony/Component/Notifier/Bridge/Smsc/SmscTransport.php
index 2ae998cd4164d..c6845fe05e4b8 100644
--- a/src/Symfony/Component/Notifier/Bridge/Smsc/SmscTransport.php
+++ b/src/Symfony/Component/Notifier/Bridge/Smsc/SmscTransport.php
@@ -34,9 +34,9 @@ final class SmscTransport extends AbstractTransport
private ?string $password;
private string $from;
- public function __construct(?string $username, #[\SensitiveParameter] ?string $password, string $from, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null)
+ public function __construct(string $login, #[\SensitiveParameter] string $password, string $from, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null)
{
- $this->login = $username;
+ $this->login = $login;
$this->password = $password;
$this->from = $from;
diff --git a/src/Symfony/Component/Process/Pipes/WindowsPipes.php b/src/Symfony/Component/Process/Pipes/WindowsPipes.php
index 0d6ab12d3bb1c..4c36a86480448 100644
--- a/src/Symfony/Component/Process/Pipes/WindowsPipes.php
+++ b/src/Symfony/Component/Process/Pipes/WindowsPipes.php
@@ -140,7 +140,7 @@ public function readAndWrite(bool $blocking, bool $close = false): array
if ($w) {
@stream_select($r, $w, $e, 0, Process::TIMEOUT_PRECISION * 1E6);
} elseif ($this->fileHandles) {
- usleep(Process::TIMEOUT_PRECISION * 1E6);
+ usleep((int) (Process::TIMEOUT_PRECISION * 1E6));
}
}
foreach ($this->fileHandles as $type => $fileHandle) {
diff --git a/src/Symfony/Component/RateLimiter/Policy/FixedWindowLimiter.php b/src/Symfony/Component/RateLimiter/Policy/FixedWindowLimiter.php
index 05fab322f03be..6e71fe2f85a65 100644
--- a/src/Symfony/Component/RateLimiter/Policy/FixedWindowLimiter.php
+++ b/src/Symfony/Component/RateLimiter/Policy/FixedWindowLimiter.php
@@ -59,12 +59,15 @@ public function reserve(int $tokens = 1, float $maxTime = null): Reservation
$now = microtime(true);
$availableTokens = $window->getAvailableTokens($now);
- if ($availableTokens >= max(1, $tokens)) {
+ if (0 === $tokens) {
+ $waitDuration = $window->calculateTimeForTokens(1, $now);
+ $reservation = new Reservation($now + $waitDuration, new RateLimit($window->getAvailableTokens($now), \DateTimeImmutable::createFromFormat('U', floor($now + $waitDuration)), true, $this->limit));
+ } elseif ($availableTokens >= $tokens) {
$window->add($tokens, $now);
$reservation = new Reservation($now, new RateLimit($window->getAvailableTokens($now), \DateTimeImmutable::createFromFormat('U', floor($now)), true, $this->limit));
} else {
- $waitDuration = $window->calculateTimeForTokens(max(1, $tokens), $now);
+ $waitDuration = $window->calculateTimeForTokens($tokens, $now);
if (null !== $maxTime && $waitDuration > $maxTime) {
// process needs to wait longer than set interval
diff --git a/src/Symfony/Component/RateLimiter/Policy/SlidingWindowLimiter.php b/src/Symfony/Component/RateLimiter/Policy/SlidingWindowLimiter.php
index 07b08b2a3ae22..eeea6cff4520a 100644
--- a/src/Symfony/Component/RateLimiter/Policy/SlidingWindowLimiter.php
+++ b/src/Symfony/Component/RateLimiter/Policy/SlidingWindowLimiter.php
@@ -69,12 +69,13 @@ public function consume(int $tokens = 1): RateLimit
return new RateLimit($availableTokens, $window->getRetryAfter(), false, $this->limit);
}
- $window->add($tokens);
-
- if (0 < $tokens) {
- $this->storage->save($window);
+ if (0 === $tokens) {
+ return new RateLimit($availableTokens, $availableTokens ? \DateTimeImmutable::createFromFormat('U.u', sprintf('%.6F', microtime(true))) : $window->getRetryAfter(), true, $this->limit);
}
+ $window->add($tokens);
+ $this->storage->save($window);
+
return new RateLimit($this->getAvailableTokens($window->getHitCount()), $window->getRetryAfter(), true, $this->limit);
} finally {
$this->lock?->release();
diff --git a/src/Symfony/Component/RateLimiter/Policy/TokenBucketLimiter.php b/src/Symfony/Component/RateLimiter/Policy/TokenBucketLimiter.php
index 09244d3a0b60d..d1ebeb2e6ca9f 100644
--- a/src/Symfony/Component/RateLimiter/Policy/TokenBucketLimiter.php
+++ b/src/Symfony/Component/RateLimiter/Policy/TokenBucketLimiter.php
@@ -67,11 +67,20 @@ public function reserve(int $tokens = 1, float $maxTime = null): Reservation
$now = microtime(true);
$availableTokens = $bucket->getAvailableTokens($now);
- if ($availableTokens >= max(1, $tokens)) {
+ if ($availableTokens >= $tokens) {
// tokens are now available, update bucket
$bucket->setTokens($availableTokens - $tokens);
- $reservation = new Reservation($now, new RateLimit($bucket->getAvailableTokens($now), \DateTimeImmutable::createFromFormat('U', floor($now)), true, $this->maxBurst));
+ if (0 === $availableTokens) {
+ // This means 0 tokens where consumed (discouraged in most cases).
+ // Return the first time a new token is available
+ $waitDuration = $this->rate->calculateTimeForTokens(1);
+ $waitTime = \DateTimeImmutable::createFromFormat('U', floor($now + $waitDuration));
+ } else {
+ $waitTime = \DateTimeImmutable::createFromFormat('U', floor($now));
+ }
+
+ $reservation = new Reservation($now, new RateLimit($bucket->getAvailableTokens($now), $waitTime, true, $this->maxBurst));
} else {
$remainingTokens = $tokens - $availableTokens;
$waitDuration = $this->rate->calculateTimeForTokens($remainingTokens);
diff --git a/src/Symfony/Component/RateLimiter/Tests/Policy/FixedWindowLimiterTest.php b/src/Symfony/Component/RateLimiter/Tests/Policy/FixedWindowLimiterTest.php
index 3e422fbec55b0..603ab058b61f3 100644
--- a/src/Symfony/Component/RateLimiter/Tests/Policy/FixedWindowLimiterTest.php
+++ b/src/Symfony/Component/RateLimiter/Tests/Policy/FixedWindowLimiterTest.php
@@ -123,7 +123,21 @@ public function testPeekConsume()
$rateLimit = $limiter->consume(0);
$this->assertSame(10, $rateLimit->getLimit());
$this->assertTrue($rateLimit->isAccepted());
+ $this->assertEquals(
+ \DateTimeImmutable::createFromFormat('U', (string) floor(microtime(true))),
+ $rateLimit->getRetryAfter()
+ );
}
+
+ $limiter->consume();
+
+ $rateLimit = $limiter->consume(0);
+ $this->assertEquals(0, $rateLimit->getRemainingTokens());
+ $this->assertTrue($rateLimit->isAccepted());
+ $this->assertEquals(
+ \DateTimeImmutable::createFromFormat('U', (string) floor(microtime(true) + 60)),
+ $rateLimit->getRetryAfter()
+ );
}
public static function provideConsumeOutsideInterval(): \Generator
diff --git a/src/Symfony/Component/RateLimiter/Tests/Policy/SlidingWindowLimiterTest.php b/src/Symfony/Component/RateLimiter/Tests/Policy/SlidingWindowLimiterTest.php
index 59a4f399ee1c4..7573f54aef95f 100644
--- a/src/Symfony/Component/RateLimiter/Tests/Policy/SlidingWindowLimiterTest.php
+++ b/src/Symfony/Component/RateLimiter/Tests/Policy/SlidingWindowLimiterTest.php
@@ -31,6 +31,7 @@ protected function setUp(): void
ClockMock::register(InMemoryStorage::class);
ClockMock::register(RateLimit::class);
+ ClockMock::register(SlidingWindowLimiter::class);
}
public function testConsume()
@@ -82,11 +83,26 @@ public function testPeekConsume()
$limiter->consume(9);
+ // peek by consuming 0 tokens twice (making sure peeking doesn't claim a token)
for ($i = 0; $i < 2; ++$i) {
$rateLimit = $limiter->consume(0);
$this->assertTrue($rateLimit->isAccepted());
$this->assertSame(10, $rateLimit->getLimit());
+ $this->assertEquals(
+ \DateTimeImmutable::createFromFormat('U.u', sprintf('%.6F', microtime(true))),
+ $rateLimit->getRetryAfter()
+ );
}
+
+ $limiter->consume();
+
+ $rateLimit = $limiter->consume(0);
+ $this->assertEquals(0, $rateLimit->getRemainingTokens());
+ $this->assertTrue($rateLimit->isAccepted());
+ $this->assertEquals(
+ \DateTimeImmutable::createFromFormat('U.u', sprintf('%.6F', microtime(true) + 12)),
+ $rateLimit->getRetryAfter()
+ );
}
private function createLimiter(): SlidingWindowLimiter
diff --git a/src/Symfony/Component/RateLimiter/Tests/Policy/TokenBucketLimiterTest.php b/src/Symfony/Component/RateLimiter/Tests/Policy/TokenBucketLimiterTest.php
index e426f765f7b8c..f6252f7752539 100644
--- a/src/Symfony/Component/RateLimiter/Tests/Policy/TokenBucketLimiterTest.php
+++ b/src/Symfony/Component/RateLimiter/Tests/Policy/TokenBucketLimiterTest.php
@@ -134,11 +134,26 @@ public function testPeekConsume()
$limiter->consume(9);
+ // peek by consuming 0 tokens twice (making sure peeking doesn't claim a token)
for ($i = 0; $i < 2; ++$i) {
$rateLimit = $limiter->consume(0);
$this->assertTrue($rateLimit->isAccepted());
$this->assertSame(10, $rateLimit->getLimit());
+ $this->assertEquals(
+ \DateTimeImmutable::createFromFormat('U', (string) floor(microtime(true))),
+ $rateLimit->getRetryAfter()
+ );
}
+
+ $limiter->consume();
+
+ $rateLimit = $limiter->consume(0);
+ $this->assertEquals(0, $rateLimit->getRemainingTokens());
+ $this->assertTrue($rateLimit->isAccepted());
+ $this->assertEquals(
+ \DateTimeImmutable::createFromFormat('U', (string) floor(microtime(true) + 1)),
+ $rateLimit->getRetryAfter()
+ );
}
public function testBucketRefilledWithStrictFrequency()
diff --git a/src/Symfony/Component/Routing/Tests/Generator/Dumper/CompiledUrlGeneratorDumperTest.php b/src/Symfony/Component/Routing/Tests/Generator/Dumper/CompiledUrlGeneratorDumperTest.php
index fedd25c71d283..64e47438386d4 100644
--- a/src/Symfony/Component/Routing/Tests/Generator/Dumper/CompiledUrlGeneratorDumperTest.php
+++ b/src/Symfony/Component/Routing/Tests/Generator/Dumper/CompiledUrlGeneratorDumperTest.php
@@ -123,14 +123,16 @@ public function testDumpWithSimpleLocalizedRoutes()
public function testDumpWithRouteNotFoundLocalizedRoutes()
{
- $this->expectException(RouteNotFoundException::class);
- $this->expectExceptionMessage('Unable to generate a URL for the named route "test" as such route does not exist.');
$this->routeCollection->add('test.en', (new Route('/testing/is/fun'))->setDefault('_locale', 'en')->setDefault('_canonical_route', 'test')->setRequirement('_locale', 'en'));
$code = $this->generatorDumper->dump();
file_put_contents($this->testTmpFilepath, $code);
$projectUrlGenerator = new CompiledUrlGenerator(require $this->testTmpFilepath, new RequestContext('/app.php'), null, 'pl_PL');
+
+ $this->expectException(RouteNotFoundException::class);
+ $this->expectExceptionMessage('Unable to generate a URL for the named route "test" as such route does not exist.');
+
$projectUrlGenerator->generate('test');
}
@@ -183,22 +185,25 @@ public function testDumpWithTooManyRoutes()
public function testDumpWithoutRoutes()
{
- $this->expectException(\InvalidArgumentException::class);
file_put_contents($this->testTmpFilepath, $this->generatorDumper->dump());
$projectUrlGenerator = new CompiledUrlGenerator(require $this->testTmpFilepath, new RequestContext('/app.php'));
+ $this->expectException(\InvalidArgumentException::class);
+
$projectUrlGenerator->generate('Test', []);
}
public function testGenerateNonExistingRoute()
{
- $this->expectException(RouteNotFoundException::class);
$this->routeCollection->add('Test', new Route('/test'));
file_put_contents($this->testTmpFilepath, $this->generatorDumper->dump());
$projectUrlGenerator = new CompiledUrlGenerator(require $this->testTmpFilepath, new RequestContext());
+
+ $this->expectException(RouteNotFoundException::class);
+
$projectUrlGenerator->generate('NonExisting', []);
}
@@ -287,66 +292,72 @@ public function testAliases()
public function testTargetAliasNotExisting()
{
- $this->expectException(RouteNotFoundException::class);
-
- $this->routeCollection->addAlias('a', 'not-existing');
+ $this->routeCollection->add('not-existing', new Route('/not-existing'));
+ $this->routeCollection->addAlias('alias', 'not-existing');
file_put_contents($this->testTmpFilepath, $this->generatorDumper->dump());
- $compiledUrlGenerator = new CompiledUrlGenerator(require $this->testTmpFilepath, new RequestContext());
+ $compiledRoutes = require $this->testTmpFilepath;
+ unset($compiledRoutes['alias']);
+ $this->expectException(RouteNotFoundException::class);
+
+ $compiledUrlGenerator = new CompiledUrlGenerator($compiledRoutes, new RequestContext());
$compiledUrlGenerator->generate('a');
}
public function testTargetAliasWithNamePrefixNotExisting()
{
- $this->expectException(RouteNotFoundException::class);
-
$subCollection = new RouteCollection();
- $subCollection->addAlias('a', 'not-existing');
+ $subCollection->add('not-existing', new Route('/not-existing'));
+ $subCollection->addAlias('alias', 'not-existing');
$subCollection->addNamePrefix('sub_');
$this->routeCollection->addCollection($subCollection);
file_put_contents($this->testTmpFilepath, $this->generatorDumper->dump());
- $compiledUrlGenerator = new CompiledUrlGenerator(require $this->testTmpFilepath, new RequestContext());
+ $compiledRoutes = require $this->testTmpFilepath;
+ unset($compiledRoutes['sub_alias']);
- $compiledUrlGenerator->generate('sub_a');
+ $this->expectException(RouteNotFoundException::class);
+
+ $compiledUrlGenerator = new CompiledUrlGenerator($compiledRoutes, new RequestContext());
+ $compiledUrlGenerator->generate('sub_alias');
}
public function testCircularReferenceShouldThrowAnException()
{
- $this->expectException(RouteCircularReferenceException::class);
- $this->expectExceptionMessage('Circular reference detected for route "b", path: "b -> a -> b".');
-
$this->routeCollection->addAlias('a', 'b');
$this->routeCollection->addAlias('b', 'a');
+ $this->expectException(RouteCircularReferenceException::class);
+ $this->expectExceptionMessage('Circular reference detected for route "b", path: "b -> a -> b".');
+
$this->generatorDumper->dump();
}
public function testDeepCircularReferenceShouldThrowAnException()
{
- $this->expectException(RouteCircularReferenceException::class);
- $this->expectExceptionMessage('Circular reference detected for route "b", path: "b -> c -> b".');
-
$this->routeCollection->addAlias('a', 'b');
$this->routeCollection->addAlias('b', 'c');
$this->routeCollection->addAlias('c', 'b');
+ $this->expectException(RouteCircularReferenceException::class);
+ $this->expectExceptionMessage('Circular reference detected for route "b", path: "b -> c -> b".');
+
$this->generatorDumper->dump();
}
public function testIndirectCircularReferenceShouldThrowAnException()
{
- $this->expectException(RouteCircularReferenceException::class);
- $this->expectExceptionMessage('Circular reference detected for route "b", path: "b -> c -> a -> b".');
-
$this->routeCollection->addAlias('a', 'b');
$this->routeCollection->addAlias('b', 'c');
$this->routeCollection->addAlias('c', 'a');
+ $this->expectException(RouteCircularReferenceException::class);
+ $this->expectExceptionMessage('Circular reference detected for route "b", path: "b -> c -> a -> b".');
+
$this->generatorDumper->dump();
}
diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php
index 2e11136ba0b87..06c316eb0c460 100644
--- a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php
+++ b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php
@@ -367,32 +367,32 @@ protected function instantiateObject(array &$data, string $class, array &$contex
} elseif ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) {
$parameterData = $data[$key];
if (null === $parameterData && $constructorParameter->allowsNull()) {
- $params[] = null;
+ $params[$paramName] = null;
$unsetKeys[] = $key;
continue;
}
try {
- $params[] = $this->denormalizeParameter($reflectionClass, $constructorParameter, $paramName, $parameterData, $attributeContext, $format);
+ $params[$paramName] = $this->denormalizeParameter($reflectionClass, $constructorParameter, $paramName, $parameterData, $attributeContext, $format);
} catch (NotNormalizableValueException $exception) {
if (!isset($context['not_normalizable_value_exceptions'])) {
throw $exception;
}
$context['not_normalizable_value_exceptions'][] = $exception;
- $params[] = $parameterData;
+ $params[$paramName] = $parameterData;
}
$unsetKeys[] = $key;
} elseif (\array_key_exists($key, $context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class] ?? [])) {
- $params[] = $context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key];
+ $params[$paramName] = $context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key];
} elseif (\array_key_exists($key, $this->defaultContext[self::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class] ?? [])) {
- $params[] = $this->defaultContext[self::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key];
+ $params[$paramName] = $this->defaultContext[self::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key];
} elseif ($constructorParameter->isDefaultValueAvailable()) {
- $params[] = $constructorParameter->getDefaultValue();
+ $params[$paramName] = $constructorParameter->getDefaultValue();
} elseif (!($context[self::REQUIRE_ALL_PROPERTIES] ?? $this->defaultContext[self::REQUIRE_ALL_PROPERTIES] ?? false) && $constructorParameter->hasType() && $constructorParameter->getType()->allowsNull()) {
- $params[] = null;
+ $params[$paramName] = null;
} else {
if (!isset($context['not_normalizable_value_exceptions'])) {
$missingConstructorArguments[] = $constructorParameter->name;
@@ -445,6 +445,15 @@ protected function instantiateObject(array &$data, string $class, array &$contex
unset($context['has_constructor']);
+ if (!$reflectionClass->isInstantiable()) {
+ throw NotNormalizableValueException::createForUnexpectedDataType(
+ sprintf('Failed to create object because the class "%s" is not instantiable.', $class),
+ $data,
+ ['unknown'],
+ $context['deserialization_path'] ?? null
+ );
+ }
+
return new $class();
}
diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php
index 6567c8869be67..d43cbbbddc8b2 100644
--- a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php
+++ b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php
@@ -190,12 +190,7 @@ public function normalize(mixed $object, string $format = null, array $context =
$attributeValue = $attribute === $this->classDiscriminatorResolver?->getMappingForMappedObject($object)?->getTypeProperty()
? $this->classDiscriminatorResolver?->getTypeForMappedObject($object)
: $this->getAttributeValue($object, $attribute, $format, $attributeContext);
- } catch (UninitializedPropertyException $e) {
- if ($context[self::SKIP_UNINITIALIZED_VALUES] ?? $this->defaultContext[self::SKIP_UNINITIALIZED_VALUES] ?? true) {
- continue;
- }
- throw $e;
- } catch (\Error $e) {
+ } catch (UninitializedPropertyException|\Error $e) {
if (($context[self::SKIP_UNINITIALIZED_VALUES] ?? $this->defaultContext[self::SKIP_UNINITIALIZED_VALUES] ?? true) && $this->isUninitializedValueError($e)) {
continue;
}
@@ -373,6 +368,10 @@ public function denormalize(mixed $data, string $type, string $format = null, ar
? $discriminatorMapping
: $this->getAttributeValue($object, $attribute, $format, $attributeContext);
} catch (NoSuchPropertyException) {
+ } catch (UninitializedPropertyException|\Error $e) {
+ if (!(($context[self::SKIP_UNINITIALIZED_VALUES] ?? $this->defaultContext[self::SKIP_UNINITIALIZED_VALUES] ?? true) && $this->isUninitializedValueError($e))) {
+ throw $e;
+ }
}
}
@@ -491,7 +490,7 @@ private function validateAndDenormalize(array $types, string $currentClass, stri
}
break;
case Type::BUILTIN_TYPE_INT:
- if (ctype_digit('-' === $data[0] ? substr($data, 1) : $data)) {
+ if (ctype_digit(isset($data[0]) && '-' === $data[0] ? substr($data, 1) : $data)) {
$data = (int) $data;
} else {
throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be int ("%s" given).', $attribute, $currentClass, $data), $data, [Type::BUILTIN_TYPE_INT], $context['deserialization_path'] ?? null);
@@ -769,9 +768,10 @@ private function getCacheKey(?string $format, array $context): bool|string
* This error may occur when specific object normalizer implementation gets attribute value
* by accessing a public uninitialized property or by calling a method accessing such property.
*/
- private function isUninitializedValueError(\Error $e): bool
+ private function isUninitializedValueError(\Error|UninitializedPropertyException $e): bool
{
- return str_starts_with($e->getMessage(), 'Typed property')
+ return $e instanceof UninitializedPropertyException
+ || str_starts_with($e->getMessage(), 'Typed property')
&& str_ends_with($e->getMessage(), 'must not be accessed before initialization');
}
diff --git a/src/Symfony/Component/Serializer/Normalizer/BackedEnumNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/BackedEnumNormalizer.php
index 3934794472738..fc7a7018868fc 100644
--- a/src/Symfony/Component/Serializer/Normalizer/BackedEnumNormalizer.php
+++ b/src/Symfony/Component/Serializer/Normalizer/BackedEnumNormalizer.php
@@ -77,7 +77,7 @@ public function denormalize(mixed $data, string $type, string $format = null, ar
return $type::from($data);
} catch (\ValueError $e) {
if (isset($context['has_constructor'])) {
- throw new InvalidArgumentException('The data must belong to a backed enumeration of type '.$type);
+ throw new InvalidArgumentException('The data must belong to a backed enumeration of type '.$type, 0, $e);
}
throw NotNormalizableValueException::createForUnexpectedDataType('The data must belong to a backed enumeration of type '.$type, $data, [$type], $context['deserialization_path'] ?? null, true, 0, $e);
diff --git a/src/Symfony/Component/Serializer/Serializer.php b/src/Symfony/Component/Serializer/Serializer.php
index ff28cbaafa666..f6528149255a9 100644
--- a/src/Symfony/Component/Serializer/Serializer.php
+++ b/src/Symfony/Component/Serializer/Serializer.php
@@ -193,6 +193,7 @@ public function normalize(mixed $data, string $format = null, array $context = [
/**
* @throws NotNormalizableValueException
+ * @throws PartialDenormalizationException Occurs when one or more properties of $type fails to denormalize
*/
public function denormalize(mixed $data, string $type, string $format = null, array $context = []): mixed
{
diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/DummyNullableInt.php b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyNullableInt.php
new file mode 100644
index 0000000000000..2671f66a97aff
--- /dev/null
+++ b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyNullableInt.php
@@ -0,0 +1,20 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Serializer\Tests\Fixtures;
+
+/**
+ * @author Nicolas PHILIPPE
+ */
+class DummyNullableInt
+{
+ public int|null $value = null;
+}
diff --git a/src/Symfony/Component/Serializer/Tests/Php80Dummy.php b/src/Symfony/Component/Serializer/Tests/Fixtures/Php80Dummy.php
similarity index 84%
rename from src/Symfony/Component/Serializer/Tests/Php80Dummy.php
rename to src/Symfony/Component/Serializer/Tests/Fixtures/Php80Dummy.php
index baa75b1246659..85c354314fccb 100644
--- a/src/Symfony/Component/Serializer/Tests/Php80Dummy.php
+++ b/src/Symfony/Component/Serializer/Tests/Fixtures/Php80Dummy.php
@@ -9,7 +9,7 @@
* file that was distributed with this source code.
*/
-namespace Symfony\Component\Serializer\Tests;
+namespace Symfony\Component\Serializer\Tests\Fixtures;
final class Php80Dummy
{
diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/Php80WithOptionalConstructorParameter.php b/src/Symfony/Component/Serializer/Tests/Fixtures/Php80WithOptionalConstructorParameter.php
new file mode 100644
index 0000000000000..6593635df4125
--- /dev/null
+++ b/src/Symfony/Component/Serializer/Tests/Fixtures/Php80WithOptionalConstructorParameter.php
@@ -0,0 +1,22 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Serializer\Tests\Fixtures;
+
+final class Php80WithOptionalConstructorParameter
+{
+ public function __construct(
+ public string $one,
+ public string $two,
+ public ?string $three = null,
+ ) {
+ }
+}
diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractNormalizerTest.php
index cf51ce840d8ff..9720d323ba29a 100644
--- a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractNormalizerTest.php
+++ b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractNormalizerTest.php
@@ -16,6 +16,7 @@
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException;
+use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
use Symfony\Component\Serializer\Mapping\AttributeMetadata;
use Symfony\Component\Serializer\Mapping\ClassMetadata;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
@@ -33,6 +34,7 @@
use Symfony\Component\Serializer\Tests\Fixtures\NullableOptionalConstructorArgumentDummy;
use Symfony\Component\Serializer\Tests\Fixtures\StaticConstructorDummy;
use Symfony\Component\Serializer\Tests\Fixtures\StaticConstructorNormalizer;
+use Symfony\Component\Serializer\Tests\Fixtures\UnitEnumDummy;
use Symfony\Component\Serializer\Tests\Fixtures\VariadicConstructorTypedArgsDummy;
/**
@@ -287,4 +289,16 @@ public function testIgnore()
$this->assertSame([], $normalizer->normalize($dummy));
}
+
+ /**
+ * @requires PHP 8.1.2
+ */
+ public function testDenormalizeWhenObjectNotInstantiable()
+ {
+ $this->expectException(NotNormalizableValueException::class);
+
+ $normalizer = new ObjectNormalizer();
+
+ $normalizer->denormalize('{}', UnitEnumDummy::class);
+ }
}
diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/Features/SkipUninitializedValuesTestTrait.php b/src/Symfony/Component/Serializer/Tests/Normalizer/Features/SkipUninitializedValuesTestTrait.php
index fb055abd1ba3e..dfcb904abce7b 100644
--- a/src/Symfony/Component/Serializer/Tests/Normalizer/Features/SkipUninitializedValuesTestTrait.php
+++ b/src/Symfony/Component/Serializer/Tests/Normalizer/Features/SkipUninitializedValuesTestTrait.php
@@ -12,14 +12,14 @@
namespace Symfony\Component\Serializer\Tests\Normalizer\Features;
use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException;
-use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
+use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
/**
* Test AbstractObjectNormalizer::SKIP_UNINITIALIZED_VALUES.
*/
trait SkipUninitializedValuesTestTrait
{
- abstract protected function getNormalizerForSkipUninitializedValues(): NormalizerInterface;
+ abstract protected function getNormalizerForSkipUninitializedValues(): AbstractObjectNormalizer;
/**
* @dataProvider skipUninitializedValuesFlagProvider
@@ -31,6 +31,15 @@ public function testSkipUninitializedValues(array $context)
$normalizer = $this->getNormalizerForSkipUninitializedValues();
$result = $normalizer->normalize($object, null, $context);
$this->assertSame(['initialized' => 'value'], $result);
+
+ $normalizer->denormalize(
+ ['unInitialized' => 'value'],
+ TypedPropertiesObjectWithGetters::class,
+ null,
+ ['object_to_populate' => $objectToPopulate = new TypedPropertiesObjectWithGetters(), 'deep_object_to_populate' => true] + $context
+ );
+
+ $this->assertSame('value', $objectToPopulate->getUninitialized());
}
public function skipUninitializedValuesFlagProvider(): iterable
diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php
index ccbb7be0e56f6..2f61ceb673b52 100644
--- a/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php
+++ b/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php
@@ -493,7 +493,7 @@ protected function getNormalizerForCacheableObjectAttributesTest(): GetSetMethod
return new GetSetMethodNormalizer();
}
- protected function getNormalizerForSkipUninitializedValues(): NormalizerInterface
+ protected function getNormalizerForSkipUninitializedValues(): GetSetMethodNormalizer
{
return new GetSetMethodNormalizer(new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())));
}
diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php
index d87b7a67a6f80..153cab7590d9f 100644
--- a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php
+++ b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php
@@ -41,6 +41,7 @@
use Symfony\Component\Serializer\Tests\Fixtures\OtherSerializedNameDummy;
use Symfony\Component\Serializer\Tests\Fixtures\Php74Dummy;
use Symfony\Component\Serializer\Tests\Fixtures\Php74DummyPrivate;
+use Symfony\Component\Serializer\Tests\Fixtures\Php80Dummy;
use Symfony\Component\Serializer\Tests\Fixtures\SiblingHolder;
use Symfony\Component\Serializer\Tests\Normalizer\Features\AttributesTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\CacheableObjectAttributesTestTrait;
@@ -58,7 +59,6 @@
use Symfony\Component\Serializer\Tests\Normalizer\Features\TypedPropertiesObject;
use Symfony\Component\Serializer\Tests\Normalizer\Features\TypedPropertiesObjectWithGetters;
use Symfony\Component\Serializer\Tests\Normalizer\Features\TypeEnforcementTestTrait;
-use Symfony\Component\Serializer\Tests\Php80Dummy;
/**
* @author Kévin Dunglas
diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php
index 915622f2f233c..e7eff114f9695 100644
--- a/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php
+++ b/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php
@@ -26,7 +26,6 @@
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
-use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\Normalizer\PropertyNormalizer;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\SerializerInterface;
@@ -500,7 +499,7 @@ protected function getNormalizerForCacheableObjectAttributesTest(): AbstractObje
return new PropertyNormalizer();
}
- protected function getNormalizerForSkipUninitializedValues(): NormalizerInterface
+ protected function getNormalizerForSkipUninitializedValues(): PropertyNormalizer
{
return new PropertyNormalizer(new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())));
}
diff --git a/src/Symfony/Component/Serializer/Tests/SerializerTest.php b/src/Symfony/Component/Serializer/Tests/SerializerTest.php
index bce705af22307..daabf8e6cab0a 100644
--- a/src/Symfony/Component/Serializer/Tests/SerializerTest.php
+++ b/src/Symfony/Component/Serializer/Tests/SerializerTest.php
@@ -19,6 +19,7 @@
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
use Symfony\Component\Serializer\Encoder\CsvEncoder;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
+use Symfony\Component\Serializer\Encoder\XmlEncoder;
use Symfony\Component\Serializer\Exception\ExtraAttributesException;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Exception\LogicException;
@@ -57,6 +58,7 @@
use Symfony\Component\Serializer\Tests\Fixtures\DummyMessageNumberOne;
use Symfony\Component\Serializer\Tests\Fixtures\DummyMessageNumberThree;
use Symfony\Component\Serializer\Tests\Fixtures\DummyMessageNumberTwo;
+use Symfony\Component\Serializer\Tests\Fixtures\DummyNullableInt;
use Symfony\Component\Serializer\Tests\Fixtures\DummyObjectWithEnumConstructor;
use Symfony\Component\Serializer\Tests\Fixtures\DummyObjectWithEnumProperty;
use Symfony\Component\Serializer\Tests\Fixtures\DummyWithObjectOrNull;
@@ -66,6 +68,7 @@
use Symfony\Component\Serializer\Tests\Fixtures\NormalizableTraversableDummy;
use Symfony\Component\Serializer\Tests\Fixtures\ObjectCollectionPropertyDummy;
use Symfony\Component\Serializer\Tests\Fixtures\Php74Full;
+use Symfony\Component\Serializer\Tests\Fixtures\Php80WithOptionalConstructorParameter;
use Symfony\Component\Serializer\Tests\Fixtures\Php80WithPromotedTypedConstructor;
use Symfony\Component\Serializer\Tests\Fixtures\TraversableDummy;
use Symfony\Component\Serializer\Tests\Fixtures\TrueBuiltInDummy;
@@ -752,6 +755,16 @@ public function testDeserializeWrappedScalar()
$this->assertSame(42, $serializer->deserialize('{"wrapper": 42}', 'int', 'json', [UnwrappingDenormalizer::UNWRAP_PATH => '[wrapper]']));
}
+ public function testDeserializeNullableIntInXml()
+ {
+ $extractor = new PropertyInfoExtractor([], [new ReflectionExtractor()]);
+ $serializer = new Serializer([new ObjectNormalizer(null, null, null, $extractor)], ['xml' => new XmlEncoder()]);
+
+ $obj = $serializer->deserialize('', DummyNullableInt::class, 'xml');
+ $this->assertInstanceOf(DummyNullableInt::class, $obj);
+ $this->assertNull($obj->value);
+ }
+
public function testUnionTypeDeserializable()
{
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
@@ -1487,6 +1500,58 @@ public function testSerializerUsesSupportedTypesMethod()
$serializer->denormalize('foo', Model::class, 'json');
$serializer->denormalize('foo', Model::class, 'json');
}
+
+ public function testPartialDenormalizationWithMissingConstructorTypes()
+ {
+ $json = '{"one": "one string", "three": "three string"}';
+
+ $extractor = new PropertyInfoExtractor([], [new ReflectionExtractor()]);
+
+ $serializer = new Serializer(
+ [new ObjectNormalizer(null, null, null, $extractor)],
+ ['json' => new JsonEncoder()]
+ );
+
+ try {
+ $serializer->deserialize($json, Php80WithOptionalConstructorParameter::class, 'json', [
+ DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS => true,
+ ]);
+
+ $this->fail();
+ } catch (\Throwable $th) {
+ $this->assertInstanceOf(PartialDenormalizationException::class, $th);
+ }
+
+ $this->assertInstanceOf(Php80WithOptionalConstructorParameter::class, $object = $th->getData());
+
+ $this->assertSame('one string', $object->one);
+ $this->assertFalse(isset($object->two));
+ $this->assertSame('three string', $object->three);
+
+ $exceptionsAsArray = array_map(function (NotNormalizableValueException $e): array {
+ return [
+ 'currentType' => $e->getCurrentType(),
+ 'expectedTypes' => $e->getExpectedTypes(),
+ 'path' => $e->getPath(),
+ 'useMessageForUser' => $e->canUseMessageForUser(),
+ 'message' => $e->getMessage(),
+ ];
+ }, $th->getErrors());
+
+ $expected = [
+ [
+ 'currentType' => 'array',
+ 'expectedTypes' => [
+ 'unknown',
+ ],
+ 'path' => null,
+ 'useMessageForUser' => true,
+ 'message' => 'Failed to create object because the class misses the "two" property.',
+ ],
+ ];
+
+ $this->assertSame($expected, $exceptionsAsArray);
+ }
}
class Model
diff --git a/src/Symfony/Component/String/Tests/AbstractUnicodeTestCase.php b/src/Symfony/Component/String/Tests/AbstractUnicodeTestCase.php
index 1ed16bca1cd6a..e95dddd9ca4c4 100644
--- a/src/Symfony/Component/String/Tests/AbstractUnicodeTestCase.php
+++ b/src/Symfony/Component/String/Tests/AbstractUnicodeTestCase.php
@@ -92,14 +92,21 @@ public function testCodePointsAt(array $expected, string $string, int $offset, i
public static function provideCodePointsAt(): array
{
- return [
+ $data = [
[[], '', 0],
[[], 'a', 1],
[[0x53], 'Späßchen', 0],
[[0xE4], 'Späßchen', 2],
[[0xDF], 'Späßchen', -5],
- [[0x260E], '☢☎❄', 1],
];
+
+ // Skip this set if we encounter an issue in PCRE2
+ // @see https://github.com/PCRE2Project/pcre2/issues/361
+ if (3 === grapheme_strlen('☢☎❄')) {
+ $data[] = [[0x260E], '☢☎❄', 1];
+ }
+
+ return $data;
}
public static function provideLength(): array
diff --git a/src/Symfony/Component/Translation/Bridge/Crowdin/CrowdinProvider.php b/src/Symfony/Component/Translation/Bridge/Crowdin/CrowdinProvider.php
index 23113bd237b74..75f01b4c29df1 100644
--- a/src/Symfony/Component/Translation/Bridge/Crowdin/CrowdinProvider.php
+++ b/src/Symfony/Component/Translation/Bridge/Crowdin/CrowdinProvider.php
@@ -56,10 +56,13 @@ public function __toString(): string
public function write(TranslatorBagInterface $translatorBag): void
{
$fileList = $this->getFileList();
+ $languageMapping = $this->getLanguageMapping();
$responses = [];
foreach ($translatorBag->getCatalogues() as $catalogue) {
+ $locale = $catalogue->getLocale();
+
foreach ($catalogue->getDomains() as $domain) {
if (0 === \count($catalogue->all($domain))) {
continue;
@@ -86,7 +89,7 @@ public function write(TranslatorBagInterface $translatorBag): void
continue;
}
- $responses[] = $this->uploadTranslations($fileId, $domain, $content, $catalogue->getLocale());
+ $responses[] = $this->uploadTranslations($fileId, $domain, $content, $languageMapping[$locale] ?? $locale);
}
}
}
@@ -105,12 +108,11 @@ public function write(TranslatorBagInterface $translatorBag): void
public function read(array $domains, array $locales): TranslatorBag
{
$fileList = $this->getFileList();
+ $languageMapping = $this->getLanguageMapping();
$translatorBag = new TranslatorBag();
$responses = [];
- $localeLanguageMap = $this->mapLocalesToLanguageId($locales);
-
foreach ($domains as $domain) {
$fileId = $this->getFileIdByDomain($fileList, $domain);
@@ -120,7 +122,7 @@ public function read(array $domains, array $locales): TranslatorBag
foreach ($locales as $locale) {
if ($locale !== $this->defaultLocale) {
- $response = $this->exportProjectTranslations($localeLanguageMap[$locale], $fileId);
+ $response = $this->exportProjectTranslations($languageMapping[$locale] ?? $locale, $fileId);
} else {
$response = $this->downloadSourceFile($fileId);
}
@@ -406,37 +408,24 @@ private function getFileList(): array
return $result;
}
- private function mapLocalesToLanguageId(array $locales): array
+ private function getLanguageMapping(): array
{
/**
- * We cannot query by locales, we need to fetch all and filter out the relevant ones.
- *
- * @see https://developer.crowdin.com/api/v2/#operation/api.languages.getMany (Crowdin API)
- * @see https://developer.crowdin.com/enterprise/api/v2/#operation/api.languages.getMany (Crowdin Enterprise API)
+ * @see https://developer.crowdin.com/api/v2/#operation/api.projects.get (Crowdin API)
+ * @see https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.get (Crowdin Enterprise API)
*/
- $response = $this->client->request('GET', '../../languages?limit=500');
+ $response = $this->client->request('GET', '');
if (200 !== $response->getStatusCode()) {
- throw new ProviderException('Unable to list set languages.', $response);
+ throw new ProviderException('Unable to get project info.', $response);
}
- $localeLanguageMap = [];
- foreach ($response->toArray()['data'] as $language) {
- foreach (['locale', 'osxLocale', 'id'] as $key) {
- if (\in_array($language['data'][$key], $locales, true)) {
- $localeLanguageMap[$language['data'][$key]] = $language['data']['id'];
- }
- }
- }
-
- if (\count($localeLanguageMap) !== \count($locales)) {
- $message = implode('", "', array_diff($locales, array_keys($localeLanguageMap)));
- $message = sprintf('Unable to find all requested locales: "%s" not found.', $message);
- $this->logger->error($message);
-
- throw new ProviderException($message, $response);
+ $projectInfo = $response->toArray()['data'];
+ $mapping = [];
+ foreach ($projectInfo['languageMapping'] ?? [] as $key => $value) {
+ $mapping[$value['locale']] = $key;
}
- return $localeLanguageMap;
+ return $mapping;
}
}
diff --git a/src/Symfony/Component/Translation/Bridge/Crowdin/Tests/CrowdinProviderTest.php b/src/Symfony/Component/Translation/Bridge/Crowdin/Tests/CrowdinProviderTest.php
index b9e28fd163508..828277b614e05 100644
--- a/src/Symfony/Component/Translation/Bridge/Crowdin/Tests/CrowdinProviderTest.php
+++ b/src/Symfony/Component/Translation/Bridge/Crowdin/Tests/CrowdinProviderTest.php
@@ -110,6 +110,12 @@ public function testCompleteWriteProcessAddFiles()
return new MockResponse(json_encode(['data' => []]));
},
+ 'getProject' => function (string $method, string $url): ResponseInterface {
+ $this->assertSame('GET', $method);
+ $this->assertSame('https://api.crowdin.com/api/v2/projects/1/', $url);
+
+ return new MockResponse(json_encode(['data' => ['languageMapping' => []]]));
+ },
'addStorage' => function (string $method, string $url, array $options = []) use ($expectedMessagesFileContent): ResponseInterface {
$this->assertSame('POST', $method);
$this->assertSame('https://api.crowdin.com/api/v2/storages', $url);
@@ -188,6 +194,12 @@ public function testWriteAddFileServerError()
return new MockResponse(json_encode(['data' => []]));
},
+ 'getProject' => function (string $method, string $url): ResponseInterface {
+ $this->assertSame('GET', $method);
+ $this->assertSame('https://api.crowdin.com/api/v2/projects/1/', $url);
+
+ return new MockResponse(json_encode(['data' => ['languageMapping' => []]]));
+ },
'addStorage' => function (string $method, string $url, array $options = []) use ($expectedMessagesFileContent): ResponseInterface {
$this->assertSame('POST', $method);
$this->assertSame('https://api.crowdin.com/api/v2/storages', $url);
@@ -260,6 +272,12 @@ public function testWriteUpdateFileServerError()
],
]));
},
+ 'getProject' => function (string $method, string $url): ResponseInterface {
+ $this->assertSame('GET', $method);
+ $this->assertSame('https://api.crowdin.com/api/v2/projects/1/', $url);
+
+ return new MockResponse(json_encode(['data' => ['languageMapping' => []]]));
+ },
'addStorage' => function (string $method, string $url, array $options = []) use ($expectedMessagesFileContent): ResponseInterface {
$this->assertSame('POST', $method);
$this->assertSame('https://api.crowdin.com/api/v2/storages', $url);
@@ -349,6 +367,12 @@ public function testWriteUploadTranslationsServerError()
],
]));
},
+ 'getProject' => function (string $method, string $url): ResponseInterface {
+ $this->assertSame('GET', $method);
+ $this->assertSame('https://api.crowdin.com/api/v2/projects/1/', $url);
+
+ return new MockResponse(json_encode(['data' => ['languageMapping' => []]]));
+ },
'addStorage' => function (string $method, string $url, array $options = []) use ($expectedMessagesFileContent): ResponseInterface {
$this->assertSame('POST', $method);
$this->assertSame('https://api.crowdin.com/api/v2/storages', $url);
@@ -442,6 +466,12 @@ public function testCompleteWriteProcessUpdateFiles()
],
]));
},
+ 'getProject' => function (string $method, string $url): ResponseInterface {
+ $this->assertSame('GET', $method);
+ $this->assertSame('https://api.crowdin.com/api/v2/projects/1/', $url);
+
+ return new MockResponse(json_encode(['data' => ['languageMapping' => []]]));
+ },
'addStorage' => function (string $method, string $url, array $options = []) use ($expectedMessagesFileContent): ResponseInterface {
$this->assertSame('POST', $method);
$this->assertSame('https://api.crowdin.com/api/v2/storages', $url);
@@ -512,6 +542,20 @@ public function testCompleteWriteProcessAddFileAndUploadTranslations(TranslatorB
],
]));
},
+ 'getProject' => function (string $method, string $url): ResponseInterface {
+ $this->assertSame('GET', $method);
+ $this->assertSame('https://api.crowdin.com/api/v2/projects/1/', $url);
+
+ return new MockResponse(json_encode([
+ 'data' => [
+ 'languageMapping' => [
+ 'pt-PT' => [
+ 'locale' => 'pt',
+ ],
+ ],
+ ],
+ ]));
+ },
'addStorage' => function (string $method, string $url, array $options = []) use ($expectedMessagesFileContent): ResponseInterface {
$this->assertSame('POST', $method);
$this->assertSame('https://api.crowdin.com/api/v2/storages', $url);
@@ -542,6 +586,22 @@ public function testCompleteWriteProcessAddFileAndUploadTranslations(TranslatorB
$this->assertSame(sprintf('https://api.crowdin.com/api/v2/projects/1/translations/%s', $expectedLocale), $url);
$this->assertSame('{"storageId":19,"fileId":12}', $options['body']);
+ return new MockResponse();
+ },
+ 'addStorage3' => function (string $method, string $url, array $options = []) use ($expectedMessagesTranslationsContent): ResponseInterface {
+ $this->assertSame('POST', $method);
+ $this->assertSame('https://api.crowdin.com/api/v2/storages', $url);
+ $this->assertSame('Content-Type: application/octet-stream', $options['normalized_headers']['content-type'][0]);
+ $this->assertSame('Crowdin-API-FileName: messages.xlf', $options['normalized_headers']['crowdin-api-filename'][0]);
+ $this->assertStringMatchesFormat($expectedMessagesTranslationsContent, $options['body']);
+
+ return new MockResponse(json_encode(['data' => ['id' => 19]], ['http_code' => 201]));
+ },
+ 'uploadTranslations2' => function (string $method, string $url, array $options = []) use ($expectedLocale): ResponseInterface {
+ $this->assertSame('POST', $method);
+ $this->assertSame(sprintf('https://api.crowdin.com/api/v2/projects/1/translations/%s', $expectedLocale), $url);
+ $this->assertSame('{"storageId":19,"fileId":12}', $options['body']);
+
return new MockResponse();
},
];
@@ -582,6 +642,33 @@ public static function getResponsesForProcessAddFileAndUploadTranslations(): \Ge
+XLIFF
+ ];
+
+ $translatorBagPt = new TranslatorBag();
+ $translatorBagPt->addCatalogue($arrayLoader->load([
+ 'a' => 'trans_en_a',
+ ], 'en'));
+ $translatorBagPt->addCatalogue($arrayLoader->load([
+ 'a' => 'trans_pt_a',
+ ], 'pt'));
+
+ yield [$translatorBagPt, 'pt-PT', <<<'XLIFF'
+
+
+
+
+
+
+
+
+ a
+ trans_pt_a
+
+
+
+
+
XLIFF
];
@@ -632,25 +719,15 @@ public function testReadForOneLocaleAndOneDomain(string $locale, string $domain,
],
]));
},
- 'listLanguages' => function (string $method, string $url, array $options = []): ResponseInterface {
+ 'getProject' => function (string $method, string $url): ResponseInterface {
$this->assertSame('GET', $method);
- $this->assertSame('https://api.crowdin.com/api/v2/languages?limit=500', $url);
- $this->assertSame('Authorization: Bearer API_TOKEN', $options['normalized_headers']['authorization'][0]);
+ $this->assertSame('https://api.crowdin.com/api/v2/projects/1/', $url);
return new MockResponse(json_encode([
'data' => [
- [
- 'data' => [
- 'id' => 'en-GB',
- 'osxLocale' => 'en_GB',
- 'locale' => 'en-GB',
- ],
- ],
- [
- 'data' => [
- 'id' => 'fr',
- 'osxLocale' => 'fr_FR',
- 'locale' => 'fr-FR',
+ 'languageMapping' => [
+ 'pt-PT' => [
+ 'locale' => 'pt',
],
],
],
@@ -770,25 +847,15 @@ public function testReadForDefaultLocaleAndOneDomain(string $locale, string $dom
],
]));
},
- 'listLanguages' => function (string $method, string $url, array $options = []): ResponseInterface {
+ 'getProject' => function (string $method, string $url): ResponseInterface {
$this->assertSame('GET', $method);
- $this->assertSame('https://api.crowdin.com/api/v2/languages?limit=500', $url);
- $this->assertSame('Authorization: Bearer API_TOKEN', $options['normalized_headers']['authorization'][0]);
+ $this->assertSame('https://api.crowdin.com/api/v2/projects/1/', $url);
return new MockResponse(json_encode([
'data' => [
- [
- 'data' => [
- 'id' => 'en',
- 'osxLocale' => 'en_GB',
- 'locale' => 'en-GB',
- ],
- ],
- [
- 'data' => [
- 'id' => 'fr',
- 'osxLocale' => 'fr_FR',
- 'locale' => 'fr-FR',
+ 'languageMapping' => [
+ 'pt-PT' => [
+ 'locale' => 'pt',
],
],
],
@@ -874,25 +941,15 @@ public function testReadServerException()
],
]));
},
- 'listLanguages' => function (string $method, string $url, array $options = []): ResponseInterface {
+ 'getProject' => function (string $method, string $url): ResponseInterface {
$this->assertSame('GET', $method);
- $this->assertSame('https://api.crowdin.com/api/v2/languages?limit=500', $url);
- $this->assertSame('Authorization: Bearer API_TOKEN', $options['normalized_headers']['authorization'][0]);
+ $this->assertSame('https://api.crowdin.com/api/v2/projects/1/', $url);
return new MockResponse(json_encode([
'data' => [
- [
- 'data' => [
- 'id' => 'en',
- 'osxLocale' => 'en_GB',
- 'locale' => 'en-GB',
- ],
- ],
- [
- 'data' => [
- 'id' => 'fr',
- 'osxLocale' => 'fr_FR',
- 'locale' => 'fr-FR',
+ 'languageMapping' => [
+ 'pt-PT' => [
+ 'locale' => 'pt',
],
],
],
@@ -933,25 +990,15 @@ public function testReadDownloadServerException()
],
]));
},
- 'listLanguages' => function (string $method, string $url, array $options = []): ResponseInterface {
+ 'getProject' => function (string $method, string $url): ResponseInterface {
$this->assertSame('GET', $method);
- $this->assertSame('https://api.crowdin.com/api/v2/languages?limit=500', $url);
- $this->assertSame('Authorization: Bearer API_TOKEN', $options['normalized_headers']['authorization'][0]);
+ $this->assertSame('https://api.crowdin.com/api/v2/projects/1/', $url);
return new MockResponse(json_encode([
'data' => [
- [
- 'data' => [
- 'id' => 'en',
- 'osxLocale' => 'en_GB',
- 'locale' => 'en-GB',
- ],
- ],
- [
- 'data' => [
- 'id' => 'fr',
- 'osxLocale' => 'fr_FR',
- 'locale' => 'fr-FR',
+ 'languageMapping' => [
+ 'pt-PT' => [
+ 'locale' => 'pt',
],
],
],
diff --git a/src/Symfony/Component/Validator/Constraints/Collection.php b/src/Symfony/Component/Validator/Constraints/Collection.php
index ee50fca169840..99bb5994b6d5f 100644
--- a/src/Symfony/Component/Validator/Constraints/Collection.php
+++ b/src/Symfony/Component/Validator/Constraints/Collection.php
@@ -11,6 +11,7 @@
namespace Symfony\Component\Validator\Constraints;
+use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
/**
@@ -43,9 +44,10 @@ class Collection extends Composite
public function __construct(mixed $fields = null, array $groups = null, mixed $payload = null, bool $allowExtraFields = null, bool $allowMissingFields = null, string $extraFieldsMessage = null, string $missingFieldsMessage = null)
{
- // no known options set? $fields is the fields array
if (\is_array($fields)
- && !array_intersect(array_keys($fields), ['groups', 'fields', 'allowExtraFields', 'allowMissingFields', 'extraFieldsMessage', 'missingFieldsMessage'])) {
+ && (($firstField = reset($fields)) instanceof Constraint
+ || ($firstField[0] ?? null) instanceof Constraint
+ )) {
$fields = ['fields' => $fields];
}
diff --git a/src/Symfony/Component/Validator/Constraints/Email.php b/src/Symfony/Component/Validator/Constraints/Email.php
index 46928894f3b83..c6642543442c5 100644
--- a/src/Symfony/Component/Validator/Constraints/Email.php
+++ b/src/Symfony/Component/Validator/Constraints/Email.php
@@ -43,7 +43,7 @@ class Email extends Constraint
];
protected const ERROR_NAMES = [
- self::INVALID_FORMAT_ERROR => 'STRICT_CHECK_FAILED_ERROR',
+ self::INVALID_FORMAT_ERROR => 'INVALID_FORMAT_ERROR',
];
/**
diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.ar.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.ar.xlf
index fce6604a533cd..0487d4225cc3b 100644
--- a/src/Symfony/Component/Validator/Resources/translations/validators.ar.xlf
+++ b/src/Symfony/Component/Validator/Resources/translations/validators.ar.xlf
@@ -426,6 +426,10 @@
Using hidden overlay characters is not allowed.لا يسمح باستخدام أحرف التراكب المخفية.
+
+ The extension of the file is invalid ({{ extension }}). Allowed extensions are {{ extensions }}.
+ امتداد الملف غير صحيح ({{ extension }}). الامتدادات المسموح بها هي {{ extensions }}.
+
This value should be false.
- Vrednost bi trebalo da bude netačna.
+ Vrednost bi trebala da bude netačna.This value should be true.
- Vrednost bi trebalo da bude tačna.
+ Vrednost bi trebala da bude tačna.This value should be of type {{ type }}.
- Vrednost bi trebalo da bude tipa {{ type }}.
+ Vrednost bi trebala da bude tipa {{ type }}.This value should be blank.
- Vrednost bi trebalo da bude prazna.
+ Vrednost bi trebala da bude prazna.The value you selected is not a valid choice.
- Odabrana vrednost nije validan izbor.
+ Vrednost koju ste izabrali nije validan izbor.You must select at least {{ limit }} choice.|You must select at least {{ limit }} choices.
- Morate odabrati bar {{ limit }} mogućnost.|Morate odabrati bar {{ limit }} mogućnosti.|Morate odabrati bar {{ limit }} mogućnosti.
+ Morate izabrati najmanje {{ limit }} mogućnosti.|Morate izabrati najmanje {{ limit }} mogućnosti.You must select at most {{ limit }} choice.|You must select at most {{ limit }} choices.
- Morate odabrati najviše {{ limit }} mogućnost.|Morate odabrati najviše {{ limit }} mogućnosti.|Morate odabrati najviše {{ limit }} mogućnosti.
+ Morate izabrati najviše {{ limit }} mogućnosti.|Morate izabrati najviše {{ limit }} mogućnosti.One or more of the given values is invalid.
- Jedna ili više vrednosti nisu validne.
+ Jedna ili više od odabranih vrednosti nisu validne.This field was not expected.
@@ -44,11 +44,11 @@
This value is not a valid date.
- Vrednost nije validan datum.
+ Vrednost nije validna kao datum.This value is not a valid datetime.
- Vrednost nije validno vreme.
+ Vrednost nije validna kao datum i vreme.This value is not a valid email address.
@@ -56,47 +56,47 @@
The file could not be found.
- Datoteka ne može biti pronađena.
+ Fajl ne može biti pronađen.The file is not readable.
- Datoteka nije čitljiva.
+ Fajl nije čitljiv.The file is too large ({{ size }} {{ suffix }}). Allowed maximum size is {{ limit }} {{ suffix }}.
- Datoteka je prevelika ({{ size }} {{ suffix }}). Najveća dozvoljena veličina je {{ limit }} {{ suffix }}.
+ Fajl je preveliki ({{ size }} {{ suffix }}). Najveća dozvoljena veličina fajla je {{ limit }} {{ suffix }}.The mime type of the file is invalid ({{ type }}). Allowed mime types are {{ types }}.
- MIME tip datoteke nije validan ({{ type }}). Dozvoljeni MIME tipovi su {{ types }}.
+ MIME tip fajla nije validan ({{ type }}). Dozvoljeni MIME tipovi su {{ types }}.This value should be {{ limit }} or less.
- Vrednost bi trebalo da bude {{ limit }} ili manje.
+ Vrednost bi trebala da bude {{ limit }} ili manje.This value is too long. It should have {{ limit }} character or less.|This value is too long. It should have {{ limit }} characters or less.
- Vrednost je predugačka. Trebalo bi da ima {{ limit }} karakter ili manje.|Vrednost je predugačka. Trebalo bi da ima {{ limit }} karaktera ili manje.|Vrednost je predugačka. Trebalo bi da ima {{ limit }} karaktera ili manje.
+ Vrednost je predugačka. Trebalo bi da ima {{ limit }} karaktera ili manje.|Vrednost je predugačka. Trebalo bi da ima {{ limit }} karaktera ili manje.This value should be {{ limit }} or more.
- Vrednost bi trebalo da bude {{ limit }} ili više.
+ Vrednost bi trebala da bude {{ limit }} ili više.This value is too short. It should have {{ limit }} character or more.|This value is too short. It should have {{ limit }} characters or more.
- Vrednost je prekratka. Trebalo bi da ima {{ limit }} karakter ili više.|Vrednost je prekratka. Trebalo bi da ima {{ limit }} karaktera ili više.|Vrednost je prekratka. Trebalo bi da ima {{ limit }} karaktera ili više.
+ Vrednost je prekratka. Trebalo bi da ima {{ limit }} karaktera ili više.|Vrednost je prekratka. Trebalo bi da ima {{ limit }} karaktera ili više.This value should not be blank.
- Vrednost ne bi trebalo da bude prazna.
+ Vrednost ne bi trebala da bude prazna.This value should not be null.
- Vrednost ne bi trebalo da bude prazna.
+ Vrednost ne bi trebala da bude null.This value should be null.
- Vrednost bi trebalo da bude prazna.
+ Vrednost bi trebala da bude null.This value is not valid.
@@ -112,27 +112,27 @@
The two values should be equal.
- Obe vrednosti bi trebalo da budu jednake.
+ Obe vrednosti bi trebale da budu jednake.The file is too large. Allowed maximum size is {{ limit }} {{ suffix }}.
- Datoteka je prevelika. Najveća dozvoljena veličina je {{ limit }} {{ suffix }}.
+ Fajl je preveliki. Najveća dozvoljena veličina je {{ limit }} {{ suffix }}.The file is too large.
- Datoteka je prevelika.
+ Fajl je preveliki.The file could not be uploaded.
- Datoteka ne može biti otpremljena.
+ Fajl ne može biti otpremljen.This value should be a valid number.
- Vrednost bi trebalo da bude validan broj.
+ Vrednost bi trebala da bude validan broj.This file is not a valid image.
- Ova datoteka nije validna slika.
+ Ovaj fajl nije validan kao slika.This is not a valid IP address.
@@ -172,23 +172,23 @@
The image height is too small ({{ height }}px). Minimum height expected is {{ min_height }}px.
- Visina slike je premala ({{ height }} piksela). Najmanja dozvoljena visina je {{ min_height }} piksela.
+ Visina slike je preniska ({{ height }} piksela). Najmanja dozvoljena visina je {{ min_height }} piksela.This value should be the user's current password.
- Vrednost bi trebalo da bude trenutna korisnička lozinka.
+ Vrednost bi trebala da bude trenutna korisnička lozinka.This value should have exactly {{ limit }} character.|This value should have exactly {{ limit }} characters.
- Vrednost bi trebalo da ima tačno {{ limit }} karakter.|Vrednost bi trebalo da ima tačno {{ limit }} karaktera.|Vrednost bi trebalo da ima tačno {{ limit }} karaktera.
+ Vrednost bi trebala da ima tačno {{ limit }} karaktera.|Vrednost bi trebala da ima tačno {{ limit }} karaktera.The file was only partially uploaded.
- Datoteka je samo parcijalno otpremljena.
+ Fajl je samo parcijalno otpremljena.No file was uploaded.
- Datoteka nije otpremljena.
+ Fajl nije otpremljen.No temporary folder was configured in php.ini.
@@ -196,7 +196,7 @@
Cannot write temporary file to disk.
- Nemoguće pisanje privremene datoteke na disk.
+ Nije moguće upisati privremeni fajl na disk.A PHP extension caused the upload to fail.
@@ -204,79 +204,79 @@
This collection should contain {{ limit }} element or more.|This collection should contain {{ limit }} elements or more.
- Ova kolekcija bi trebalo da sadrži {{ limit }} ili više elemenata.|Ova kolekcija bi trebalo da sadrži {{ limit }} ili više elemenata.|Ova kolekcija bi trebalo da sadrži {{ limit }} ili više elemenata.
+ Ova kolekcija bi trebala da sadrži {{ limit }} ili više elemenata.|Ova kolekcija bi trebala da sadrži {{ limit }} ili više elemenata.This collection should contain {{ limit }} element or less.|This collection should contain {{ limit }} elements or less.
- Ova kolekcija bi trebalo da sadrži {{ limit }} ili manje elemenata.|Ova kolekcija bi trebalo da sadrži {{ limit }} ili manje elemenata.|Ova kolekcija bi trebalo da sadrži {{ limit }} ili manje elemenata.
+ Ova kolekcija bi trebala da sadrži {{ limit }} ili manje elemenata.|Ova kolekcija bi trebala da sadrži {{ limit }} ili manje elemenata.This collection should contain exactly {{ limit }} element.|This collection should contain exactly {{ limit }} elements.
- Ova kolekcija bi trebalo da sadrži tačno {{ limit }} element.|Ova kolekcija bi trebalo da sadrži tačno {{ limit }} elementa.|Ova kolekcija bi trebalo da sadrži tačno {{ limit }} elemenata.
+ Ova kolekcija bi trebala da sadrži tačno {{ limit }} element.|Ova kolekcija bi trebala da sadrži tačno {{ limit }} elementa.Invalid card number.
- Broj kartice nije validan.
+ Nevalidan broj kartice.Unsupported card type or invalid card number.
- Tip kartice nije podržan ili broj kartice nije validan.
+ Nevalidan broj kartice ili nepodržan tip kartice.This is not a valid International Bank Account Number (IBAN).
- Ovo nije validan međunarodni broj bankovnog računa (IBAN).
+ Nevalidan međunarodni broj bankovnog računa (IBAN).This value is not a valid ISBN-10.
- Ovo nije validan ISBN-10.
+ Nevalidna vrednost ISBN-10.This value is not a valid ISBN-13.
- Ovo nije validan ISBN-13.
+ Nevalidna vrednost ISBN-13.This value is neither a valid ISBN-10 nor a valid ISBN-13.
- Ovo nije validan ISBN-10 ili ISBN-13.
+ Vrednost nije ni validan ISBN-10 ni validan ISBN-13.This value is not a valid ISSN.
- Ovo nije validan ISSN.
+ Nevalidna vrednost ISSN.This value is not a valid currency.
- Ovo nije validna valuta.
+ Vrednost nije validna valuta.This value should be equal to {{ compared_value }}.
- Ova vrednost bi trebalo da bude jednaka {{ compared_value }}.
+ Ova vrednost bi trebala da bude jednaka {{ compared_value }}.This value should be greater than {{ compared_value }}.
- Ova vrednost bi trebalo da bude veća od {{ compared_value }}.
+ Ova vrednost bi trebala da bude veća od {{ compared_value }}.This value should be greater than or equal to {{ compared_value }}.
- Ova vrednost bi trebalo da bude veća ili jednaka {{ compared_value }}.
+ Ova vrednost bi trebala da je veća ili jednaka {{ compared_value }}.This value should be identical to {{ compared_value_type }} {{ compared_value }}.
- Ova vrednost bi trebalo da bude identična sa {{ compared_value_type }} {{ compared_value }}.
+ Ova vrednost bi trebala da bude identična sa {{ compared_value_type }} {{ compared_value }}.This value should be less than {{ compared_value }}.
- Ova vrednost bi trebalo da bude manja od {{ compared_value }}.
+ Ova vrednost bi trebala da bude manja od {{ compared_value }}.This value should be less than or equal to {{ compared_value }}.
- Ova vrednost bi trebalo da bude manja ili jednaka {{ compared_value }}.
+ Ova vrednost bi trebala da bude manja ili jednaka {{ compared_value }}.This value should not be equal to {{ compared_value }}.
- Ova vrednost ne bi trebalo da bude jednaka {{ compared_value }}.
+ Ova vrednost ne bi trebala da bude jednaka {{ compared_value }}.This value should not be identical to {{ compared_value_type }} {{ compared_value }}.
- Ova vrednost ne bi trebalo da bude identična sa {{ compared_value_type }} {{ compared_value }}.
+ Ova vrednost ne bi trebala da bude identična sa {{ compared_value_type }} {{ compared_value }}.The image ratio is too big ({{ ratio }}). Allowed maximum ratio is {{ max_ratio }}.
@@ -292,7 +292,7 @@
The image is landscape oriented ({{ width }}x{{ height }}px). Landscape oriented images are not allowed.
- Slika je pejzažno orijentisana ({{ width }}x{{ height }} piksela). Pejzažna orijentisane slike nisu dozvoljene.
+ Slika je pejzažno orijentisana ({{ width }}x{{ height }} piksela). Pejzažno orijentisane slike nisu dozvoljene.The image is portrait oriented ({{ width }}x{{ height }}px). Portrait oriented images are not allowed.
@@ -300,7 +300,7 @@
An empty file is not allowed.
- Prazna datoteka nije dozvoljena.
+ Prazan fajl nije dozvoljen.The host could not be resolved.
@@ -324,7 +324,7 @@
This value should be a multiple of {{ compared_value }}.
- Ova vrednost bi trebalo da bude deljiva sa {{ compared_value }}.
+ Ova vrednost bi trebala da bude višestruka u odnosu na {{ compared_value }}.This Business Identifier Code (BIC) is not associated with IBAN {{ iban }}.
@@ -332,7 +332,7 @@
This value should be valid JSON.
- Ova vrednost bi trebalo da bude validan JSON.
+ Ova vrednost bi trebala da bude validan JSON.This collection should contain only unique elements.
@@ -344,7 +344,7 @@
This value should be either positive or zero.
- Ova vrednost bi trebala biti pozitivna ili nula.
+ Ova vrednost bi trebala biti ili pozitivna ili nula.This value should be negative.
@@ -352,7 +352,7 @@
This value should be either negative or zero.
- Ova vrednost bi trebala biti negativna ili nula.
+ Ova vrednost bi trebala biti ili negativna ili nula.This value is not a valid timezone.
@@ -360,7 +360,7 @@
This password has been leaked in a data breach, it must not be used. Please use another password.
- Ova lozinka je kompromitovana prilikom prethodnih napada, nemojte je koristiti. Koristite drugu lozinku.
+ Lozinka je kompromitovana prilikom curenja podataka usled napada, nemojte je koristiti. Koristite drugu lozinku.This value should be between {{ min }} and {{ max }}.
@@ -372,11 +372,11 @@
The number of elements in this collection should be a multiple of {{ compared_value }}.
- Broj elemenata u ovoj kolekciji bi trebalo da bude deljiv sa {{ compared_value }}.
+ Broj elemenata u ovoj kolekciji bi trebala da bude višestruka u odnosu na {{ compared_value }}.This value should satisfy at least one of the following constraints:
- Ova vrednost bi trebalo da zadovoljava namjanje jedno od narednih ograničenja:
+ Ova vrednost bi trebala da zadovoljava namjanje jedno od narednih ograničenja:Each element of this collection should satisfy its own set of constraints.
@@ -384,7 +384,7 @@
This value is not a valid International Securities Identification Number (ISIN).
- Ova vrednost nije ispravna međunarodna identifikaciona oznaka hartija od vrednosti (ISIN).
+ Ova vrednost nije ispravan međunarodni sigurnosni i identifikacioni broj (ISIN).This value should be a valid expression.
@@ -392,11 +392,11 @@
This value is not a valid CSS color.
- Ova vrednost nije ispravna CSS boja.
+ Ova vrednost nije validna CSS boja.This value is not a valid CIDR notation.
- Ova vrednost nije ispravna CIDR notacija.
+ Ova vrednost nije validna CIDR notacija.The value of the netmask should be between {{ min }} and {{ max }}.
@@ -404,7 +404,7 @@
The filename is too long. It should have {{ filename_max_length }} character or less.|The filename is too long. It should have {{ filename_max_length }} characters or less.
- Naziv datoteke je suviše dugačak. Treba da ima {{ filename_max_length }} karakter ili manje.|Naziv datoteke je suviše dugačak. Treba da ima {{ filename_max_length }} karaktera ili manje.
+ Naziv fajla je suviše dugačak. Treba da ima {{ filename_max_length }} karaktera ili manje.|Naziv fajla je suviše dugačak. Treba da ima {{ filename_max_length }} karaktera ili manje.The password strength is too low. Please use a stronger password.
@@ -424,7 +424,11 @@
Using hidden overlay characters is not allowed.
- Korišćenje skrivenih preklopnih karaktera nije dozvoljeno.
+ Korišćenje skrivenih pokrivenih karaktera nije dozvoljeno.
+
+
+ The extension of the file is invalid ({{ extension }}). Allowed extensions are {{ extensions }}.
+ Ekstenzija fajla je nevalidna ({{ extension }}). Dozvoljene ekstenzije su {{ extensions }}.