Skip to content

Commit

Permalink
Merge branch '4.2'
Browse files Browse the repository at this point in the history
* 4.2:
  [Phpunit] fixed support for PHP 5.3
  Response prepare method update
  [Workflow] Added missing license header
  Fix case when multiple loaders are providing paths for the same namespace
  Check if Client exists when test.client does not exist, to provide clearer exception message
  throw TypeErrors to prepare for type hints in 5.0
  [Form] Preventing validation of children if parent with Valid constraint has no validation groups
  [Form] Added ResetInterface to CachingFactoryDecorator
  Remove deprecated usage
  [Tests] fixed compatbility of assertEquals(): void
  Fixed usage of TranslatorInterface in form extension (fixes #30591)
  [Intl][4.2] Fix test
  [Intl] Fix test
  [Validator] Add the missing translations for the Arabic (ar) locale
  [Intl] Add compile binary
  Fix DebugCommand when chain loader is involved
  [Form] Fixed some phpdocs
  • Loading branch information
fabpot committed Mar 22, 2019
2 parents 0b2a9d5 + 7e5dfcf commit 522594a
Show file tree
Hide file tree
Showing 38 changed files with 275 additions and 130 deletions.
Expand Up @@ -42,7 +42,7 @@ public function __construct($entityManager)
@trigger_error(sprintf('Injecting an instance of "%s" in "%s" is deprecated since Symfony 4.2, inject an instance of "%s" instead.', ClassMetadataFactory::class, __CLASS__, EntityManagerInterface::class), E_USER_DEPRECATED);
$this->classMetadataFactory = $entityManager;
} else {
throw new \InvalidArgumentException(sprintf('$entityManager must be an instance of "%s", "%s" given.', EntityManagerInterface::class, \is_object($entityManager) ? \get_class($entityManager) : \gettype($entityManager)));
throw new \TypeError(sprintf('$entityManager must be an instance of "%s", "%s" given.', EntityManagerInterface::class, \is_object($entityManager) ? \get_class($entityManager) : \gettype($entityManager)));
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/Symfony/Bridge/PhpUnit/bin/simple-phpunit
Expand Up @@ -76,11 +76,11 @@ if ('phpdbg' === PHP_SAPI) {
$PHP .= ' -qrr';
}

$defaultEnvs = [
$defaultEnvs = array(
'COMPOSER' => 'composer.json',
'COMPOSER_VENDOR_DIR' => 'vendor',
'COMPOSER_BIN_DIR' => 'bin',
];
);

foreach ($defaultEnvs as $envName => $envValue) {
if ($envValue !== getenv($envName)) {
Expand Down
92 changes: 60 additions & 32 deletions src/Symfony/Bridge/Twig/Command/DebugCommand.php
Expand Up @@ -20,6 +20,7 @@
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Finder\Finder;
use Twig\Environment;
use Twig\Loader\ChainLoader;
use Twig\Loader\FilesystemLoader;

/**
Expand All @@ -36,6 +37,7 @@ class DebugCommand extends Command
private $bundlesMetadata;
private $twigDefaultPath;
private $rootDir;
private $filesystemLoaders;

public function __construct(Environment $twig, string $projectDir = null, array $bundlesMetadata = [], string $twigDefaultPath = null, string $rootDir = null)
{
Expand Down Expand Up @@ -87,7 +89,7 @@ protected function execute(InputInterface $input, OutputInterface $output)
$name = $input->getArgument('name');
$filter = $input->getOption('filter');

if (null !== $name && !$this->twig->getLoader() instanceof FilesystemLoader) {
if (null !== $name && [] === $this->getFilesystemLoaders()) {
throw new InvalidArgumentException(sprintf('Argument "name" not supported, it requires the Twig loader "%s"', FilesystemLoader::class));
}

Expand Down Expand Up @@ -150,9 +152,11 @@ private function displayPathsText(SymfonyStyle $io, string $name)
$message = 'No template paths configured for your application';
} else {
$message = sprintf('No template paths configured for "@%s" namespace', $namespace);
$namespaces = $this->twig->getLoader()->getNamespaces();
foreach ($this->findAlternatives($namespace, $namespaces) as $namespace) {
$alternatives[] = '@'.$namespace;
foreach ($this->getFilesystemLoaders() as $loader) {
$namespaces = $loader->getNamespaces();
foreach ($this->findAlternatives($namespace, $namespaces) as $namespace) {
$alternatives[] = '@'.$namespace;
}
}
}

Expand Down Expand Up @@ -243,25 +247,25 @@ private function displayGeneralJson(SymfonyStyle $io, $filter)

private function getLoaderPaths(string $name = null): array
{
/** @var FilesystemLoader $loader */
$loader = $this->twig->getLoader();
$loaderPaths = [];
$namespaces = $loader->getNamespaces();
if (null !== $name) {
$namespace = $this->parseTemplateName($name)[0];
$namespaces = array_intersect([$namespace], $namespaces);
}
foreach ($this->getFilesystemLoaders() as $loader) {
$namespaces = $loader->getNamespaces();
if (null !== $name) {
$namespace = $this->parseTemplateName($name)[0];
$namespaces = array_intersect([$namespace], $namespaces);
}

foreach ($namespaces as $namespace) {
$paths = array_map([$this, 'getRelativePath'], $loader->getPaths($namespace));
foreach ($namespaces as $namespace) {
$paths = array_map([$this, 'getRelativePath'], $loader->getPaths($namespace));

if (FilesystemLoader::MAIN_NAMESPACE === $namespace) {
$namespace = '(None)';
} else {
$namespace = '@'.$namespace;
}
if (FilesystemLoader::MAIN_NAMESPACE === $namespace) {
$namespace = '(None)';
} else {
$namespace = '@'.$namespace;
}

$loaderPaths[$namespace] = $paths;
$loaderPaths[$namespace] = array_merge($loaderPaths[$namespace] ?? [], $paths);
}
}

return $loaderPaths;
Expand Down Expand Up @@ -437,22 +441,22 @@ private function error(SymfonyStyle $io, string $message, array $alternatives =

private function findTemplateFiles(string $name): array
{
/** @var FilesystemLoader $loader */
$loader = $this->twig->getLoader();
$files = [];
list($namespace, $shortname) = $this->parseTemplateName($name);

foreach ($loader->getPaths($namespace) as $path) {
if (!$this->isAbsolutePath($path)) {
$path = $this->projectDir.'/'.$path;
}
$filename = $path.'/'.$shortname;
$files = [];
foreach ($this->getFilesystemLoaders() as $loader) {
foreach ($loader->getPaths($namespace) as $path) {
if (!$this->isAbsolutePath($path)) {
$path = $this->projectDir.'/'.$path;
}
$filename = $path.'/'.$shortname;

if (is_file($filename)) {
if (false !== $realpath = realpath($filename)) {
$files[] = $this->getRelativePath($realpath);
} else {
$files[] = $this->getRelativePath($filename);
if (is_file($filename)) {
if (false !== $realpath = realpath($filename)) {
$files[] = $this->getRelativePath($realpath);
} else {
$files[] = $this->getRelativePath($filename);
}
}
}
}
Expand Down Expand Up @@ -535,4 +539,28 @@ private function isAbsolutePath(string $file): bool
{
return strspn($file, '/\\', 0, 1) || (\strlen($file) > 3 && ctype_alpha($file[0]) && ':' === $file[1] && strspn($file, '/\\', 2, 1)) || null !== parse_url($file, PHP_URL_SCHEME);
}

/**
* @return FilesystemLoader[]
*/
private function getFilesystemLoaders(): array
{
if (null !== $this->filesystemLoaders) {
return $this->filesystemLoaders;
}
$this->filesystemLoaders = [];

$loader = $this->twig->getLoader();
if ($loader instanceof FilesystemLoader) {
$this->filesystemLoaders[] = $loader;
} elseif ($loader instanceof ChainLoader) {
foreach ($loader->getLoaders() as $l) {
if ($l instanceof FilesystemLoader) {
$this->filesystemLoaders[] = $l;
}
}
}

return $this->filesystemLoaders;
}
}
16 changes: 15 additions & 1 deletion src/Symfony/Bridge/Twig/Tests/Command/DebugCommandTest.php
Expand Up @@ -16,6 +16,7 @@
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Twig\Environment;
use Twig\Loader\ChainLoader;
use Twig\Loader\FilesystemLoader;

class DebugCommandTest extends TestCase
Expand Down Expand Up @@ -279,7 +280,16 @@ public function getDebugTemplateNameTestData()
];
}

private function createCommandTester(array $paths = [], array $bundleMetadata = [], string $defaultPath = null, string $rootDir = null): CommandTester
public function testDebugTemplateNameWithChainLoader()
{
$tester = $this->createCommandTester(['templates/' => null], [], null, null, true);
$ret = $tester->execute(['name' => 'base.html.twig'], ['decorated' => false]);

$this->assertEquals(0, $ret, 'Returns 0 in case of success');
$this->assertContains('[OK]', $tester->getDisplay());
}

private function createCommandTester(array $paths = [], array $bundleMetadata = [], string $defaultPath = null, string $rootDir = null, bool $useChainLoader = false): CommandTester
{
$projectDir = \dirname(__DIR__).\DIRECTORY_SEPARATOR.'Fixtures';
$loader = new FilesystemLoader([], $projectDir);
Expand All @@ -291,6 +301,10 @@ private function createCommandTester(array $paths = [], array $bundleMetadata =
}
}

if ($useChainLoader) {
$loader = new ChainLoader([$loader]);
}

$application = new Application();
$application->add(new DebugCommand(new Environment($loader), $projectDir, $bundleMetadata, $defaultPath, $rootDir));
$command = $application->find('debug:twig');
Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/Bridge/Twig/composer.json
Expand Up @@ -18,7 +18,7 @@
"require": {
"php": "^7.1.3",
"symfony/contracts": "^1.0.2",
"twig/twig": "^1.37.1|^2.6.2"
"twig/twig": "^1.38.1|^2.7.1"
},
"require-dev": {
"egulias/email-validator": "^2.0",
Expand Down
Expand Up @@ -55,6 +55,7 @@
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator;
use Symfony\Component\Form\FormTypeExtensionInterface;
use Symfony\Component\Form\FormTypeGuesserInterface;
use Symfony\Component\Form\FormTypeInterface;
Expand Down Expand Up @@ -440,6 +441,11 @@ private function registerFormConfiguration(array $config, ContainerBuilder $cont
if (!class_exists(Translator::class)) {
$container->removeDefinition('form.type_extension.upload.validator');
}
if (!method_exists(CachingFactoryDecorator::class, 'reset')) {
$container->getDefinition('form.choice_list_factory.cached')
->clearTag('kernel.reset')
;
}
}

private function registerEsiConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader)
Expand Down
Expand Up @@ -57,6 +57,7 @@

<service id="form.choice_list_factory.cached" class="Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator">
<argument type="service" id="form.choice_list_factory.property_access"/>
<tag name="kernel.reset" method="reset" />
</service>

<service id="form.choice_list_factory" alias="form.choice_list_factory.cached" />
Expand Down
5 changes: 4 additions & 1 deletion src/Symfony/Bundle/FrameworkBundle/Test/WebTestCase.php
Expand Up @@ -36,7 +36,10 @@ protected static function createClient(array $options = [], array $server = [])
try {
$client = $kernel->getContainer()->get('test.client');
} catch (ServiceNotFoundException $e) {
throw new \LogicException('You cannot create the client used in functional tests if the BrowserKit component is not available. Try running "composer require symfony/browser-kit".');
if (class_exists(Client::class)) {
throw new \LogicException('You cannot create the client used in functional tests if the "framework.test" config is not set to true.');
}
throw new \LogicException('You cannot create the client used in functional tests if the BrowserKit component is not available. Try running "composer require symfony/browser-kit"');
}

$client->setServerParameters($server);
Expand Down
Expand Up @@ -41,7 +41,7 @@ public function __construct(iterable $listeners, ExceptionListener $exceptionLis
$this->logoutListener = $logoutListener;
$this->config = $config;
} else {
throw new \InvalidArgumentException(sprintf('Argument 3 passed to %s() must be instance of %s or null, %s given.', __METHOD__, LogoutListener::class, \is_object($logoutListener) ? \get_class($logoutListener) : \gettype($logoutListener)));
throw new \TypeError(sprintf('Argument 3 passed to %s() must be instance of %s or null, %s given.', __METHOD__, LogoutListener::class, \is_object($logoutListener) ? \get_class($logoutListener) : \gettype($logoutListener)));
}
}

Expand Down
16 changes: 7 additions & 9 deletions src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php
Expand Up @@ -15,9 +15,10 @@
* A list of choices with arbitrary data types.
*
* The user of this class is responsible for assigning string values to the
* choices. Both the choices and their values are passed to the constructor.
* Each choice must have a corresponding value (with the same array key) in
* the value array.
* choices annd for their uniqueness.
* Both the choices and their values are passed to the constructor.
* Each choice must have a corresponding value (with the same key) in
* the values array.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
Expand All @@ -43,12 +44,6 @@ class ArrayChoiceList implements ChoiceListInterface
* @var int[]|string[]
*/
protected $originalKeys;

/**
* The callback for creating the value for a choice.
*
* @var callable
*/
protected $valueCallback;

/**
Expand Down Expand Up @@ -212,6 +207,8 @@ protected function flatten(array $choices, $value, &$choicesByValues, &$keysByVa
/**
* Checks whether the given choices can be cast to strings without
* generating duplicates.
* This method is responsible for preventing conflict between scalar values
* and the empty value.
*
* @param array $choices The choices
* @param array|null $cache The cache for previously checked entries. Internal
Expand All @@ -232,6 +229,7 @@ private function castableToString(array $choices, array &$cache = [])
return false;
}

// prevent having false casted to the empty string by isset()
$choice = false === $choice ? '0' : (string) $choice;

if (isset($cache[$choice])) {
Expand Down
26 changes: 25 additions & 1 deletion src/Symfony/Component/Form/ChoiceList/ChoiceListInterface.php
Expand Up @@ -35,7 +35,25 @@ public function getChoices();
/**
* Returns the values for the choices.
*
* The values are strings that do not contain duplicates.
* The values are strings that do not contain duplicates:
*
* $form->add('field', 'choice', [
* 'choices' => [
* 'Decided' => ['Yes' => true, 'No' => false],
* 'Undecided' => ['Maybe' => null],
* ],
* ]);
*
* In this example, the result of this method is:
*
* [
* 'Yes' => '0',
* 'No' => '1',
* 'Maybe' => '2',
* ]
*
* Null and false MUST NOT conflict when being casted to string.
* For this some default incremented values SHOULD be computed.
*
* @return string[] The choice values
*/
Expand All @@ -62,6 +80,12 @@ public function getValues();
* 'Undecided' => ['Maybe' => '2'],
* ]
*
* Nested arrays do not make sense in a view format unless
* they are used as a convenient way of grouping.
* If the implementation does not intend to support grouped choices,
* this method SHOULD be equivalent to {@link getValues()}.
* The $groupBy callback parameter SHOULD be used instead.
*
* @return string[] The choice values
*/
public function getStructuredValues();
Expand Down
Expand Up @@ -14,13 +14,14 @@
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
use Symfony\Component\Form\ChoiceList\View\ChoiceListView;
use Symfony\Contracts\Service\ResetInterface;

/**
* Caches the choice lists created by the decorated factory.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class CachingFactoryDecorator implements ChoiceListFactoryInterface
class CachingFactoryDecorator implements ChoiceListFactoryInterface, ResetInterface
{
private $decoratedFactory;

Expand Down Expand Up @@ -134,4 +135,10 @@ public function createView(ChoiceListInterface $list, $preferredChoices = null,

return $this->views[$hash];
}

public function reset()
{
$this->lists = [];
$this->views = [];
}
}
Expand Up @@ -28,8 +28,8 @@ interface ChoiceListFactoryInterface
* The choices should be passed in the values of the choices array.
*
* Optionally, a callable can be passed for generating the choice values.
* The callable receives the choice as first and the array key as the second
* argument.
* The callable receives the choice as only argument.
* Null may be passed when the choice list contains the empty value.
*
* @param iterable $choices The choices
* @param callable|null $value The callable generating the choice
Expand All @@ -43,8 +43,8 @@ public function createListFromChoices($choices, $value = null);
* Creates a choice list that is loaded with the given loader.
*
* Optionally, a callable can be passed for generating the choice values.
* The callable receives the choice as first and the array key as the second
* argument.
* The callable receives the choice as only argument.
* Null may be passed when the choice list contains the empty value.
*
* @param ChoiceLoaderInterface $loader The choice loader
* @param callable|null $value The callable generating the choice
Expand Down

0 comments on commit 522594a

Please sign in to comment.