Merge branch '4.4' into 5.0

* 4.4:
  [DI] Fix EnvVar not loaded when Loader requires an env var
  Fixed #34713 Move new messages to intl domain when possible
  [FrameworkBundle] Fix small typo in output comment
  chown and chgrp should also accept int as owner and group
  Revert "Fixed translations file dumper behavior"
  Fix RememberMe with null password
  [Validator] Fix plurals for sr_Latn (Serbian language written in latin script) validation messages
  Set booted flag to false when test kernel is unset
  [FrameworkBundle] remove messenger cache if not enabled
  [PhpUnitBridge][SymfonyTestsListenerTrait] Remove some unneeded code
  [HttpClient] Fix strict parsing of response status codes
  fix PHP const mapping keys using the inline notation
  [SecurityBundle] Drop duplicated code
  [FrameworkBundle] Make sure one can use fragments.hinclude_default_template
  Fix that no-cache requires positive validation with the origin, even for fresh responses
  Improve upgrading instructions for deprecated router options
  [DI] Suggest typed argument when binding fails with untyped argument
This commit is contained in:
Nicolas Grekas 2020-01-21 09:40:24 +01:00
commit 3ccb3bf96a
30 changed files with 264 additions and 92 deletions

View File

@ -393,7 +393,7 @@ Routing
-------
* The `generator_base_class`, `generator_cache_class`, `matcher_base_class`, and `matcher_cache_class` router
options have been removed.
options have been removed. If you are using multiple Router instances and need separate caches for them, set a unique `cache_dir` per Router instance instead.
* `Serializable` implementing methods for `Route` and `CompiledRoute` are final.
Instead of overwriting them, use `__serialize` and `__unserialize` as extension points which are forward compatible
with the new serialization methods in PHP 7.4.

View File

@ -40,7 +40,6 @@ class SymfonyTestsListenerTrait
private $expectedDeprecations = array();
private $gatheredDeprecations = array();
private $previousErrorHandler;
private $reportUselessTests;
private $error;
private $runsInSeparateProcess = false;
@ -194,10 +193,6 @@ class SymfonyTestsListenerTrait
public function startTest($test)
{
if (-2 < $this->state && ($test instanceof \PHPUnit\Framework\TestCase || $test instanceof TestCase)) {
if (null !== $test->getTestResultObject()) {
$this->reportUselessTests = $test->getTestResultObject()->isStrictAboutTestsThatDoNotTestAnything();
}
// This event is triggered before the test is re-run in isolation
if ($this->willBeIsolated($test)) {
$this->runsInSeparateProcess = tempnam(sys_get_temp_dir(), 'deprec');
@ -243,11 +238,6 @@ class SymfonyTestsListenerTrait
$className = \get_class($test);
$groups = Test::getGroups($className, $test->getName(false));
if (null !== $this->reportUselessTests) {
$test->getTestResultObject()->beStrictAboutTestsThatDoNotTestAnything($this->reportUselessTests);
$this->reportUselessTests = null;
}
if ($errored = null !== $this->error) {
$test->getTestResultObject()->addError($test, $this->error, 0);
$this->error = null;

View File

@ -101,7 +101,7 @@ EOF
(new SymfonyStyle($input, $output))
->table(['Secret', 'Value'] + (null !== $localSecrets ? [2 => 'Local Value'] : []), $rows);
$io->comment("Local values override secret values.\nUse <info>secrets:set --local</info> to defined them.");
$io->comment("Local values override secret values.\nUse <info>secrets:set --local</info> to define them.");
return 0;
}

View File

@ -23,6 +23,7 @@ use Symfony\Component\Translation\Catalogue\MergeOperation;
use Symfony\Component\Translation\Catalogue\TargetOperation;
use Symfony\Component\Translation\Extractor\ExtractorInterface;
use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Translation\MessageCatalogueInterface;
use Symfony\Component\Translation\Reader\TranslationReaderInterface;
use Symfony\Component\Translation\Writer\TranslationWriterInterface;
@ -216,6 +217,24 @@ EOF
$resultMessage = 'Translation files were successfully updated';
// move new messages to intl domain when possible
if (class_exists(\MessageFormatter::class)) {
foreach ($operation->getDomains() as $domain) {
$intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX;
$newMessages = $operation->getNewMessages($domain);
if ([] === $newMessages || ([] === $currentCatalogue->all($intlDomain) && [] !== $currentCatalogue->all($domain))) {
continue;
}
$result = $operation->getResult();
$allIntlMessages = $result->all($intlDomain);
$currentMessages = array_diff_key($newMessages, $result->all($domain));
$result->replace($currentMessages, $domain);
$result->replace($allIntlMessages + $newMessages, $intlDomain);
}
}
// show compiled list of messages
if (true === $input->getOption('dump-messages')) {
$extractedMessagesCount = 0;

View File

@ -311,6 +311,7 @@ class FrameworkExtension extends Extension
$container->removeDefinition('console.command.messenger_failed_messages_retry');
$container->removeDefinition('console.command.messenger_failed_messages_show');
$container->removeDefinition('console.command.messenger_failed_messages_remove');
$container->removeDefinition('cache.messenger.restart_workers_signal');
}
if ($this->httpClientConfigEnabled = $this->isConfigEnabled($container, $config['http_client'])) {

View File

@ -412,6 +412,7 @@
<xsd:element name="bus" type="messenger_bus" minOccurs="0" maxOccurs="unbounded" />
</xsd:sequence>
<xsd:attribute name="default-bus" type="xsd:string" />
<xsd:attribute name="enabled" type="xsd:boolean" />
</xsd:complexType>
<xsd:complexType name="messenger_serializer">

View File

@ -43,6 +43,7 @@ abstract class KernelTestCase extends TestCase
{
static::ensureKernelShutdown();
static::$kernel = null;
static::$booted = false;
}
/**

View File

@ -0,0 +1,8 @@
<?php
$container->loadFromExtension('framework', [
'fragments' => [
'enabled' => true,
'hinclude_default_template' => 'global_hinclude_template',
],
]);

View File

@ -0,0 +1,5 @@
<?php
$container->loadFromExtension('framework', [
'messenger' => false,
]);

View File

@ -0,0 +1,11 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:framework="http://symfony.com/schema/dic/symfony"
xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
<framework:config>
<framework:fragments enabled="true" hinclude-default-template="global_hinclude_template"/>
</framework:config>
</container>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:framework="http://symfony.com/schema/dic/symfony"
xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
<framework:config>
<framework:messenger enabled="false" />
</framework:config>
</container>

View File

@ -0,0 +1,4 @@
framework:
fragments:
enabled: true
hinclude_default_template: global_hinclude_template

View File

@ -0,0 +1,2 @@
framework:
messenger: false

View File

@ -159,6 +159,13 @@ abstract class FrameworkExtensionTest extends TestCase
$this->assertFalse($container->hasDefinition('esi'));
}
public function testFragmentsAndHinclude()
{
$container = $this->createContainerFromFile('fragments_and_hinclude');
$this->assertTrue($container->hasParameter('fragment.renderer.hinclude.global_template'));
$this->assertEquals('global_hinclude_template', $container->getParameter('fragment.renderer.hinclude.global_template'));
}
public function testSsi()
{
$container = $this->createContainerFromFile('full');
@ -565,9 +572,23 @@ abstract class FrameworkExtensionTest extends TestCase
$this->assertTrue($container->hasDefinition('web_link.add_link_header_listener'));
}
public function testMessengerServicesRemovedWhenDisabled()
{
$container = $this->createContainerFromFile('messenger_disabled');
$this->assertFalse($container->hasDefinition('console.command.messenger_consume_messages'));
$this->assertFalse($container->hasDefinition('console.command.messenger_debug'));
$this->assertFalse($container->hasDefinition('console.command.messenger_stop_workers'));
$this->assertFalse($container->hasDefinition('console.command.messenger_setup_transports'));
$this->assertFalse($container->hasDefinition('console.command.messenger_failed_messages_retry'));
$this->assertFalse($container->hasDefinition('console.command.messenger_failed_messages_show'));
$this->assertFalse($container->hasDefinition('console.command.messenger_failed_messages_remove'));
$this->assertFalse($container->hasDefinition('cache.messenger.restart_workers_signal'));
}
public function testMessenger()
{
$container = $this->createContainerFromFile('messenger');
$this->assertTrue($container->hasDefinition('console.command.messenger_consume_messages'));
$this->assertTrue($container->hasAlias('messenger.default_bus'));
$this->assertTrue($container->getAlias('messenger.default_bus')->isPublic());
$this->assertTrue($container->hasDefinition('messenger.transport.amqp.factory'));

View File

@ -11,6 +11,8 @@
namespace Symfony\Bundle\SecurityBundle\Debug;
use Symfony\Component\VarDumper\Caster\ClassStub;
/**
* @author Robin Chalas <robin.chalas@gmail.com>
*
@ -35,4 +37,13 @@ trait TraceableListenerTrait
{
return $this->listener;
}
public function getInfo(): array
{
return [
'response' => $this->response,
'time' => $this->time,
'stub' => $this->stub ?? $this->stub = ClassStub::wrapCallable($this->listener),
];
}
}

View File

@ -59,13 +59,4 @@ final class WrappedLazyListener extends AbstractListener
return $ret;
}
public function getInfo(): array
{
return [
'response' => $this->response,
'time' => $this->time,
'stub' => $this->stub ?? $this->stub = ClassStub::wrapCallable($this->listener),
];
}
}

View File

@ -37,13 +37,4 @@ final class WrappedListener
$this->time = microtime(true) - $startTime;
$this->response = $event->getResponse();
}
public function getInfo(): array
{
return [
'response' => $this->response,
'time' => $this->time,
'stub' => $this->stub ?? $this->stub = ClassStub::wrapCallable($this->listener),
];
}
}

View File

@ -114,6 +114,8 @@ class ResolveBindingsPass extends AbstractRecursivePass
return parent::processValue($value, $isRoot);
}
$bindingNames = [];
foreach ($bindings as $key => $binding) {
list($bindingValue, $bindingId, $used, $bindingType, $file) = $binding->getValues();
if ($used) {
@ -123,7 +125,11 @@ class ResolveBindingsPass extends AbstractRecursivePass
$this->unusedBindings[$bindingId] = [$key, $this->currentId, $bindingType, $file];
}
if (preg_match('/^(?:(?:array|bool|float|int|string) )?\$/', $key)) {
if (preg_match('/^(?:(?:array|bool|float|int|string|([^ $]++)) )\$/', $key, $m)) {
$bindingNames[substr($key, \strlen($m[0]))] = $binding;
}
if (!isset($m[1])) {
continue;
}
@ -184,11 +190,17 @@ class ResolveBindingsPass extends AbstractRecursivePass
continue;
}
if (!$typeHint || '\\' !== $typeHint[0] || !isset($bindings[$typeHint = substr($typeHint, 1)])) {
if ($typeHint && '\\' === $typeHint[0] && isset($bindings[$typeHint = substr($typeHint, 1)])) {
$arguments[$key] = $this->getBindingValue($bindings[$typeHint]);
continue;
}
$arguments[$key] = $this->getBindingValue($bindings[$typeHint]);
if (isset($bindingNames[$parameter->name])) {
$bindingKey = array_search($binding, $bindings, true);
$argumentType = substr($bindingKey, 0, strpos($bindingKey, ' '));
$this->errorMessages[] = sprintf('Did you forget to add the type "%s" to argument "$%s" of method "%s::%s()"?', $argumentType, $parameter->name, $reflectionMethod->class, $reflectionMethod->name);
}
}
if ($arguments !== $call[1]) {

View File

@ -30,8 +30,7 @@ class EnvVarProcessor implements EnvVarProcessorInterface
public function __construct(ContainerInterface $container, \Traversable $loaders = null)
{
$this->container = $container;
$this->loaders = new \IteratorIterator($loaders ?? new \ArrayIterator());
$this->loaders = $this->loaders->getInnerIterator();
$this->loaders = $loaders ?? new \ArrayIterator();
}
/**
@ -141,20 +140,32 @@ class EnvVarProcessor implements EnvVarProcessorInterface
}
}
$loaders = $this->loaders;
$this->loaders = new \ArrayIterator();
if (false === $env || null === $env) {
$loaders = $this->loaders;
$this->loaders = new \ArrayIterator();
try {
while ((false === $env || null === $env) && $loaders->valid()) {
$loader = $loaders->current();
$loaders->next();
$this->loadedVars[] = $vars = $loader->loadEnvVars();
$env = $vars[$name] ?? false;
try {
$i = 0;
$ended = true;
$count = $loaders instanceof \Countable ? $loaders->count() : 0;
foreach ($loaders as $loader) {
if (\count($this->loadedVars) > $i++) {
continue;
}
$this->loadedVars[] = $vars = $loader->loadEnvVars();
if (false !== $env = $vars[$name] ?? false) {
$ended = false;
break;
}
}
if ($ended || $count === $i) {
$loaders = $this->loaders;
}
} catch (ParameterCircularReferenceException $e) {
// skip loaders that need an env var that is not defined
} finally {
$this->loaders = $loaders;
}
} catch (ParameterCircularReferenceException $e) {
// skip loaders that need an env var that is not defined
} finally {
$this->loaders = $loaders;
}
if (false === $env || null === $env) {

View File

@ -20,6 +20,7 @@ use Symfony\Component\DependencyInjection\Compiler\DefinitionErrorExceptionPass;
use Symfony\Component\DependencyInjection\Compiler\ResolveBindingsPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass;
use Symfony\Component\DependencyInjection\Tests\Fixtures\NamedArgumentsDummy;
@ -169,4 +170,19 @@ class ResolveBindingsPassTest extends TestCase
$this->assertSame([1 => 'bar'], $container->getDefinition(NamedArgumentsDummy::class)->getArguments());
}
public function testEmptyBindingTypehint()
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Did you forget to add the type "string" to argument "$apiKey" of method "Symfony\Component\DependencyInjection\Tests\Fixtures\NamedArgumentsDummy::__construct()"?');
$container = new ContainerBuilder();
$bindings = [
'string $apiKey' => new BoundArgument('foo'),
];
$definition = $container->register(NamedArgumentsDummy::class, NamedArgumentsDummy::class);
$definition->setBindings($bindings);
$pass = new ResolveBindingsPass();
$pass->process($container);
}
}

View File

@ -3,10 +3,12 @@
namespace Symfony\Component\DependencyInjection\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\EnvVarLoaderInterface;
use Symfony\Component\DependencyInjection\EnvVarProcessor;
use Symfony\Component\DependencyInjection\Exception\ParameterCircularReferenceException;
class EnvVarProcessorTest extends TestCase
{
@ -553,4 +555,44 @@ CSV;
$result = $processor->getEnv('string', 'FOO_ENV_LOADER', function () {});
$this->assertSame('123', $result); // check twice
}
public function testCircularEnvLoader()
{
$container = new ContainerBuilder();
$container->setParameter('env(FOO_CONTAINER)', 'foo');
$container->compile();
$index = 0;
$loaders = function () use (&$index) {
if (0 === $index++) {
throw new ParameterCircularReferenceException(['FOO_CONTAINER']);
}
yield new class() implements EnvVarLoaderInterface {
public function loadEnvVars(): array
{
return [
'FOO_ENV_LOADER' => '123',
];
}
};
};
$processor = new EnvVarProcessor($container, new RewindableGenerator($loaders, 1));
$result = $processor->getEnv('string', 'FOO_CONTAINER', function () {});
$this->assertSame('foo', $result);
$result = $processor->getEnv('string', 'FOO_ENV_LOADER', function () {});
$this->assertSame('123', $result);
$result = $processor->getEnv('default', ':BAR_CONTAINER', function ($name) use ($processor) {
$this->assertSame('BAR_CONTAINER', $name);
return $processor->getEnv('string', $name, function () {});
});
$this->assertNull($result);
$this->assertSame(2, $index);
}
}

View File

@ -206,8 +206,9 @@ class Filesystem
/**
* Change the owner of an array of files or directories.
*
* @param string|iterable $files A filename, an array of files, or a \Traversable instance to change owner
* @param string|int $user A user name or number
* @param string|iterable $files A filename, an array of files, or a \Traversable instance to change owner
* @param string|int $user A user name or number
* @param bool $recursive Whether change the owner recursively or not
*
* @throws IOException When the change fails
*/
@ -232,8 +233,9 @@ class Filesystem
/**
* Change the group of an array of files or directories.
*
* @param string|iterable $files A filename, an array of files, or a \Traversable instance to change group
* @param string|int $group A group name or number
* @param string|iterable $files A filename, an array of files, or a \Traversable instance to change group
* @param string|int $group A group name or number
* @param bool $recursive Whether change the group recursively or not
*
* @throws IOException When the change fails
*/

View File

@ -253,7 +253,7 @@ trait ResponseTrait
private static function addResponseHeaders(array $responseHeaders, array &$info, array &$headers, string &$debug = ''): void
{
foreach ($responseHeaders as $h) {
if (11 <= \strlen($h) && '/' === $h[4] && preg_match('#^HTTP/\d+(?:\.\d+)? ([12345]\d\d) .*#', $h, $m)) {
if (11 <= \strlen($h) && '/' === $h[4] && preg_match('#^HTTP/\d+(?:\.\d+)? ([12345]\d\d)(?: |$)#', $h, $m)) {
if ($headers) {
$debug .= "< \r\n";
$headers = [];

View File

@ -350,6 +350,10 @@ class HttpCache implements HttpKernelInterface, TerminableInterface
return $this->validate($request, $entry, $catch);
}
if ($entry->headers->hasCacheControlDirective('no-cache')) {
return $this->validate($request, $entry, $catch);
}
$this->record($request, 'fresh');
$entry->headers->set('Age', $entry->getAge());

View File

@ -443,6 +443,22 @@ class HttpCacheTest extends HttpCacheTestCase
$this->assertTrue($this->response->headers->has('Age'));
}
public function testRevalidatesResponsesWithNoCacheDirectiveEvenIfFresh()
{
$this->setNextResponse(200, ['Cache-Control' => 'public, no-cache, max-age=10', 'ETag' => 'some-etag'], 'OK');
$this->request('GET', '/'); // warm the cache
sleep(5);
$this->setNextResponse(304, ['Cache-Control' => 'public, no-cache, max-age=10', 'ETag' => 'some-etag']);
$this->request('GET', '/');
$this->assertHttpKernelIsCalled(); // no-cache -> MUST have revalidated at origin
$this->assertTraceContains('valid');
$this->assertEquals('OK', $this->response->getContent());
$this->assertEquals(0, $this->response->getAge());
}
public function testCachesResponsesWithAnExpirationHeader()
{
$time = \DateTime::createFromFormat('U', time() + 5);

View File

@ -51,37 +51,36 @@ abstract class FileDumper implements DumperInterface
throw new InvalidArgumentException('The file dumper needs a path option.');
}
$hasMessageFormatter = class_exists(\MessageFormatter::class);
// save a file for each domain
foreach ($messages->getDomains() as $domain) {
if ($hasMessageFormatter) {
$defaultDomain = $domain.MessageCatalogue::INTL_DOMAIN_SUFFIX;
$altDomain = $domain;
} else {
$defaultDomain = $domain;
$altDomain = $domain.MessageCatalogue::INTL_DOMAIN_SUFFIX;
}
$defaultPath = $options['path'].'/'.$this->getRelativePath($defaultDomain, $messages->getLocale());
$altPath = $options['path'].'/'.$this->getRelativePath($altDomain, $messages->getLocale());
if (!file_exists($defaultPath) && file_exists($altPath)) {
[$defaultPath, $altPath] = [$altPath, $defaultPath];
}
if (!file_exists($defaultPath)) {
$directory = \dirname($defaultPath);
$fullpath = $options['path'].'/'.$this->getRelativePath($domain, $messages->getLocale());
if (!file_exists($fullpath)) {
$directory = \dirname($fullpath);
if (!file_exists($directory) && !@mkdir($directory, 0777, true)) {
throw new RuntimeException(sprintf('Unable to create directory "%s".', $directory));
}
}
if (file_exists($altPath)) {
// clear alternative translation file
file_put_contents($altPath, $this->formatCatalogue(new MessageCatalogue($messages->getLocale()), $altDomain, $options));
$intlDomain = $domain.MessageCatalogue::INTL_DOMAIN_SUFFIX;
$intlMessages = $messages->all($intlDomain);
if ($intlMessages) {
$intlPath = $options['path'].'/'.$this->getRelativePath($intlDomain, $messages->getLocale());
file_put_contents($intlPath, $this->formatCatalogue($messages, $intlDomain, $options));
$messages->replace([], $intlDomain);
try {
if ($messages->all($domain)) {
file_put_contents($fullpath, $this->formatCatalogue($messages, $domain, $options));
}
continue;
} finally {
$messages->replace($intlMessages, $intlDomain);
}
}
file_put_contents($defaultPath, $this->formatCatalogue($messages, $domain, $options));
file_put_contents($fullpath, $this->formatCatalogue($messages, $domain, $options));
}
}

View File

@ -27,15 +27,11 @@ class FileDumperTest extends TestCase
$dumper = new ConcreteFileDumper();
$dumper->dump($catalogue, ['path' => $tempDir]);
$suffix = class_exists(\MessageFormatter::class) ? '+intl-icu' : '';
$this->assertFileExists($tempDir."/messages$suffix.en.concrete");
$this->assertFileExists($tempDir.'/messages.en.concrete');
@unlink($tempDir."/messages$suffix.en.concrete");
@unlink($tempDir.'/messages.en.concrete');
}
/**
* @requires extension intl
*/
public function testDumpIntl()
{
$tempDir = sys_get_temp_dir();
@ -46,11 +42,13 @@ class FileDumperTest extends TestCase
$catalogue->add(['bar' => 'foo'], 'd2+intl-icu');
$dumper = new ConcreteFileDumper();
@unlink($tempDir.'/d2.en.concrete');
$dumper->dump($catalogue, ['path' => $tempDir]);
$this->assertFileNotExists($tempDir.'/d1.en.concrete');
$this->assertStringEqualsFile($tempDir.'/d1.en.concrete', 'foo=bar');
@unlink($tempDir.'/d1.en.concrete');
$this->assertStringEqualsFile($tempDir.'/d1+intl-icu.en.concrete', 'bar=foo&foo=bar');
$this->assertStringEqualsFile($tempDir.'/d1+intl-icu.en.concrete', 'bar=foo');
@unlink($tempDir.'/d1+intl-icu.en.concrete');
$this->assertFileNotExists($tempDir.'/d2.en.concrete');
@ -62,8 +60,7 @@ class FileDumperTest extends TestCase
{
$tempDir = sys_get_temp_dir();
$translationsDir = $tempDir.'/test/translations';
$suffix = class_exists(\MessageFormatter::class) ? '+intl-icu' : '';
$file = $translationsDir."/messages$suffix.en.concrete";
$file = $translationsDir.'/messages.en.concrete';
$catalogue = new MessageCatalogue('en');
$catalogue->add(['foo' => 'bar']);

View File

@ -24,11 +24,11 @@
</trans-unit>
<trans-unit id="6">
<source>You must select at least {{ limit }} choice.|You must select at least {{ limit }} choices.</source>
<target>Morate odabrati bar {{ limit }} mogućnost.|Morate odabrati bar {{ limit }} mogućnosti.</target>
<target>Morate odabrati bar {{ limit }} mogućnost.|Morate odabrati bar {{ limit }} mogućnosti.|Morate odabrati bar {{ limit }} mogućnosti.</target>
</trans-unit>
<trans-unit id="7">
<source>You must select at most {{ limit }} choice.|You must select at most {{ limit }} choices.</source>
<target>Morate odabrati najviše {{ limit }} mogućnost.|Morate odabrati najviše {{ limit }} mogućnosti.</target>
<target>Morate odabrati najviše {{ limit }} mogućnost.|Morate odabrati najviše {{ limit }} mogućnosti.|Morate odabrati najviše {{ limit }} mogućnosti.</target>
</trans-unit>
<trans-unit id="8">
<source>One or more of the given values is invalid.</source>
@ -76,7 +76,7 @@
</trans-unit>
<trans-unit id="19">
<source>This value is too long. It should have {{ limit }} character or less.|This value is too long. It should have {{ limit }} characters or less.</source>
<target>Vrednost je predugačka. Trebalo bi da ima {{ limit }} karakter ili manje.|Vrednost je predugačka. Trebalo bi da ima {{ limit }} karaktera ili manje.</target>
<target>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.</target>
</trans-unit>
<trans-unit id="20">
<source>This value should be {{ limit }} or more.</source>
@ -84,7 +84,7 @@
</trans-unit>
<trans-unit id="21">
<source>This value is too short. It should have {{ limit }} character or more.|This value is too short. It should have {{ limit }} characters or more.</source>
<target>Vrednost je prekratka. Trebalo bi da ima {{ limit }} karakter ili više.|Vrednost je prekratka. Trebalo bi da ima {{ limit }} karaktera ili više.</target>
<target>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.</target>
</trans-unit>
<trans-unit id="22">
<source>This value should not be blank.</source>
@ -180,7 +180,7 @@
</trans-unit>
<trans-unit id="48">
<source>This value should have exactly {{ limit }} character.|This value should have exactly {{ limit }} characters.</source>
<target>Vrednost bi trebalo da ima tačno {{ limit }} karakter.|Vrednost bi trebalo da ima tačno {{ limit }} karaktera.</target>
<target>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.</target>
</trans-unit>
<trans-unit id="49">
<source>The file was only partially uploaded.</source>
@ -204,15 +204,15 @@
</trans-unit>
<trans-unit id="54">
<source>This collection should contain {{ limit }} element or more.|This collection should contain {{ limit }} elements or more.</source>
<target>Ova kolekcija bi trebalo da sadrži {{ limit }} ili više elemenata.|Ova kolekcija bi trebalo da sadrži {{ limit }} ili više elemenata.</target>
<target>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.</target>
</trans-unit>
<trans-unit id="55">
<source>This collection should contain {{ limit }} element or less.|This collection should contain {{ limit }} elements or less.</source>
<target>Ova kolekcija bi trebalo da sadrži {{ limit }} ili manje elemenata.|Ova kolekcija bi trebalo da sadrži {{ limit }} ili manje elemenata.</target>
<target>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.</target>
</trans-unit>
<trans-unit id="56">
<source>This collection should contain exactly {{ limit }} element.|This collection should contain exactly {{ limit }} elements.</source>
<target>Ova kolekcija bi trebalo da sadrži tačno {{ limit }} element.|Ova kolekcija bi trebalo da sadrži tačno {{ limit }} elemenata.</target>
<target>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.</target>
</trans-unit>
<trans-unit id="57">
<source>Invalid card number.</source>

View File

@ -439,6 +439,11 @@ class Inline
throw new ParseException('Missing mapping key.', self::$parsedLineNumber + 1, $mapping);
}
if ('!php/const' === $key) {
$key .= ' '.self::parseScalar($mapping, $flags, [':'], $i, false, []);
$key = self::evaluateScalar($key, $flags);
}
if (false === $i = strpos($mapping, ':', $i)) {
break;
}

View File

@ -59,6 +59,7 @@ class InlineTest extends TestCase
['!php/const PHP_INT_MAX', PHP_INT_MAX],
['[!php/const PHP_INT_MAX]', [PHP_INT_MAX]],
['{ foo: !php/const PHP_INT_MAX }', ['foo' => PHP_INT_MAX]],
['{ !php/const PHP_INT_MAX: foo }', [PHP_INT_MAX => 'foo']],
['!php/const NULL', null],
];
}