diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b958112d1b58a..19e868793ac36 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -24,13 +24,8 @@ /src/Symfony/Component/Form/ @xabbuh @yceruto # HttpKernel /src/Symfony/Component/HttpKernel/Log/Logger.php @dunglas -# LDAP -/src/Symfony/Component/Ldap/ @csarrazi # Lock /src/Symfony/Component/Lock/ @jderusse -# Messenger -/src/Symfony/Bridge/Doctrine/Messenger/ @sroze -/src/Symfony/Component/Messenger/ @sroze # Notifer /src/Symfony/Component/Notifier/ @OskarStark # OptionsResolver diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index e6333054fe172..58caff2209f37 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,6 @@ | Q | A | ------------- | --- -| Branch? | 6.1 for features / 4.4, 5.4 or 6.0 for bug fixes +| Branch? | 6.2 for features / 4.4, 5.4, 6.0 or 6.1 for bug fixes | Bug fix? | yes/no | New feature? | yes/no | Deprecations? | yes/no diff --git a/.github/composer-config.json b/.github/composer-config.json index 65919964fa8a1..2bdec1a826251 100644 --- a/.github/composer-config.json +++ b/.github/composer-config.json @@ -10,6 +10,9 @@ "symfony/translation": "source", "symfony/validator": "source", "*": "dist" + }, + "allow-plugins": { + "symfony/flex": true } } } diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index fc578fd6283a1..c6bbe332ade24 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -91,7 +91,7 @@ jobs: zookeeper: image: wurstmeister/zookeeper:3.4.6 kafka: - image: wurstmeister/kafka:2.12-2.4.1 + image: wurstmeister/kafka:2.12-2.0.1 ports: - 9092:9092 env: @@ -108,11 +108,13 @@ jobs: - name: Install system dependencies run: | echo "::group::apt-get update" + sudo wget -O - https://packages.couchbase.com/clients/c/repos/deb/couchbase.key | sudo apt-key add - + echo "deb https://packages.couchbase.com/clients/c/repos/deb/ubuntu2004 focal focal/main" | sudo tee /etc/apt/sources.list.d/couchbase.list sudo apt-get update echo "::endgroup::" echo "::group::install tools & libraries" - sudo apt-get install librdkafka-dev redis-server + sudo apt-get install librdkafka-dev redis-server libcouchbase-dev sudo -- sh -c 'echo unixsocket /var/run/redis/redis-server.sock >> /etc/redis/redis.conf' sudo -- sh -c 'echo unixsocketperm 777 >> /etc/redis/redis.conf' sudo service redis-server restart @@ -129,7 +131,7 @@ jobs: uses: shivammathur/setup-php@v2 with: coverage: "none" - extensions: "json,couchbase,memcached,mongodb-1.10.0,redis-5.3.4,rdkafka,xsl,ldap" + extensions: "json,couchbase-3.2.2,memcached,mongodb-1.10.0,redis-5.3.4,rdkafka,xsl,ldap" ini-values: date.timezone=Europe/Paris,memory_limit=-1,default_socket_timeout=10,session.gc_probability=0,apc.enable_cli=1,zend.assertions=1 php-version: "${{ matrix.php }}" tools: pecl diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 2836521932ddf..0000000000000 --- a/.travis.yml +++ /dev/null @@ -1,123 +0,0 @@ -language: php - -dist: bionic - -git: - depth: 1 - -addons: - apt_packages: - - parallel - - zookeeperd - - libzookeeper-mt-dev - -matrix: - include: - - php: 7.3 - fast_finish: true - -cache: - directories: - - .phpunit - - ~/php-ext - -before_install: - - | - # General configuration - set -e - stty cols 120 - sudo sed -i 's/127\.0\.1\.1 localhost/127.0.0.1 localhost/' /etc/hosts - cp .github/composer-config.json "$(composer config home)/config.json" - - nanoseconds () { - local cmd="date" - local format="+%s%N" - local os=$(uname) - if hash gdate > /dev/null 2>&1; then - cmd="gdate" - elif [[ "$os" = Darwin ]]; then - format="+%s000000000" - fi - $cmd -u $format - } - export -f nanoseconds - - # tfold is a helper to create folded reports - tfold () { - local title="$PHP $1" - local fold=$(echo $title | sed -r 's/[^-_A-Za-z0-9]+/./g') - shift - local id=$(printf %08x $(( RANDOM * RANDOM ))) - local start=$(nanoseconds) - echo -e "travis_fold:start:$fold" - echo -e "travis_time:start:$id" - echo -e "\\e[1;34m$title\\e[0m" - - bash -xc "$*" 2>&1 - local ok=$? - local end=$(nanoseconds) - echo -e "\\ntravis_time:end:$id:start=$start,finish=$end,duration=$(($end-$start))" - (exit $ok) && - echo -e "\\e[32mOK\\e[0m $title\\n\\ntravis_fold:end:$fold" || - echo -e "\\e[41mKO\\e[0m $title\\n" - (exit $ok) - } - export -f tfold - - # tpecl is a helper to compile and cache php extensions - tpecl () { - local ext_name=$1 - local ext_so=$2 - local INI=$3 - local input=${4:-yes} - local ext_dir=$(php -r "echo ini_get('extension_dir');") - local ext_cache=~/php-ext/$(basename $ext_dir)/$ext_name - - if [[ -e $ext_cache/$ext_so ]]; then - echo extension = $ext_cache/$ext_so >> $INI - else - rm ~/.pearrc /tmp/pear 2>/dev/null || true - mkdir -p $ext_cache - echo $input | pecl -q install -f $ext_name && - cp $ext_dir/$ext_so $ext_cache - fi - } - export -f tpecl - - - | - # php.ini configuration - for PHP in $TRAVIS_PHP_VERSION $php_extra; do - INI=~/.phpenv/versions/$PHP/etc/conf.d/travis.ini - echo date.timezone = Europe/Paris >> $INI - echo memory_limit = -1 >> $INI - echo default_socket_timeout = 10 >> $INI - echo session.gc_probability = 0 >> $INI - echo opcache.enable_cli = 1 >> $INI - echo apc.enable_cli = 1 >> $INI - done - find ~/.phpenv -name xdebug.ini -delete - - composer self-update - composer self-update --2 - - - | - # Install extra PHP extensions - for PHP in $TRAVIS_PHP_VERSION $php_extra; do - export PHP=$PHP - phpenv global $PHP - INI=~/.phpenv/versions/$PHP/etc/conf.d/travis.ini - if [[ $PHP != 8.* ]]; then - tfold ext.zookeeper tpecl zookeeper-0.7.2 zookeeper.so $INI - fi - done - -install: - - export COMPONENTS=$(find src/Symfony -mindepth 2 -type f -name phpunit.xml.dist -not -wholename '*/Bridge/PhpUnit/*' -printf '%h\n' | sort) - - export COMPOSER_ROOT_VERSION=$(grep ' VERSION = ' src/Symfony/Component/HttpKernel/Kernel.php | grep -P -o '[0-9]+\.[0-9]+').x-dev - - composer update --no-progress --ansi - - ./phpunit install - -script: - - echo "$COMPONENTS" | parallel --gnu -j +3 "tfold {} ./phpunit --exclude-group tty,benchmark,intl-data {}" - - tfold src/Symfony/Component/Console.tty ./phpunit src/Symfony/Component/Console --group tty - - tfold src/Symfony/Bridge/Twig.tty ./phpunit src/Symfony/Bridge/Twig --group tty diff --git a/CHANGELOG-5.4.md b/CHANGELOG-5.4.md index 14694594d1031..c445966b598d9 100644 --- a/CHANGELOG-5.4.md +++ b/CHANGELOG-5.4.md @@ -7,6 +7,50 @@ in 5.4 minor versions. To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v5.4.0...v5.4.1 +* 5.4.9 (2022-05-27) + + * bug #46386 [Console]  Fix missing negative variation of negatable options in shell completion (GromNaN) + * bug #46448 [DependencyInjection] Fix "proxy" tag: resolve its parameters and pass it to child definitions (nicolas-grekas) + * bug #46442 [FrameworkBundle] Revert "bug #46125 Always add CacheCollectorPass (fancyweb)" (chalasr) + * bug #46443 [DoctrineBridge] Don't reinit managers when they are proxied as ghost objects (nicolas-grekas) + * bug #46427 [FrameworkBundle] fix wiring of annotations.cached_reader (nicolas-grekas) + * bug #46425 [DependencyInjection] Ignore unused bindings defined by attribute (nicolas-grekas) + * bug #46434 [FrameworkBundle] Fix BC break in abstract config commands (yceruto) + * bug #46424 [Form] do not accept array input when a form is not multiple (xabbuh) + * bug #46367 [Mime] Throw exception when body in Email attach method is not ok (alamirault) + * bug #46421 [VarDumper][VarExporter] Deal with DatePeriod->include_end_date on PHP 8.2 (nicolas-grekas) + * bug #46401 [Cache] Throw when "redis_sentinel" is used with a non-Predis "class" option (buffcode) + * bug #46414 Bootstrap 4 fieldset for row errors (konradkozaczenko) + * bug #46412 [FrameworkBundle] Fix dumping extension config without bundle (yceruto) + * bug #46382 [HttpClient] Honor "max_duration" when replacing requests with async decorators (nicolas-grekas) + * bug #46407 [Filesystem] Safeguard (sym)link calls (derrabus) + * bug #46098 [Form] Fix same choice loader with different choice values (HeahDude) + * bug #46380 [HttpClient] Add missing HttpOptions::setMaxDuration() (nicolas-grekas) + * bug #46249 [HttpFoundation] [Session] Regenerate invalid session id (peter17) + * bug #46328 [Config] Allow scalar configuration in PHP Configuration (jderusse, HypeMC) + * bug #46366 [Mime] Add null check for EmailHeaderSame (magikid) + * bug #46364 [Config] Fix looking for single files in phars with GlobResource (nicolas-grekas) + * bug #46365 [HttpKernel] Revert "bug #46327 Allow ErrorHandler ^5.0 to be used" (nicolas-grekas) + * bug #46114 Fixes "Incorrectly nested style tag found" error when using multi-line header content (Perturbatio) + * bug #46325 [Ldap] Fix LDAP connection options (buffcode) + * bug #46341 Fix aliases handling in command name completion (Seldaek) + * bug #46317 [Security/Http] Ignore invalid URLs found in failure/success paths (nicolas-grekas) + * bug #46309 [Security] Fix division by zero (tvlooy) + * bug #46327 [HttpKernel] Allow ErrorHandler ^5.0 to be used in HttpKernel 4.4 (mpdude) + * bug #46297 [Serializer] Fix JsonSerializableNormalizer ignores circular reference handler in $context (BreyndotEchse) + * bug #46291 [Console] Suppress unhandled error in some specific use-cases. (rw4lll) + * bug #46302 [ErrorHandler] Fix list of tentative return types (nicolas-grekas) + * bug #45981 [Serializer][PropertyInfo] Fix support for "false" built-in type on PHP 8.2 (alexandre-daubois) + * bug #46277 [HttpKernel] Fix SessionListener without session in request (edditor) + * bug #46282 [DoctrineBridge] Treat firstResult === 0 like null (derrabus) + * bug #46239 [Translation] Refresh local translations on PushCommand if the provider has domains (Florian-B) + * bug #46278 [Workflow] Fix deprecated syntax for interpolated strings (nicolas-grekas) + * bug #46264 [Console] Better required argument check in InputArgument (jnoordsij) + * bug #46262 [EventDispatcher] Fix removing listeners when using first-class callable syntax (javer) + * bug #46216 [Form] fix populating single widget time view data with different timezones (xabbuh) + * bug #46221 [DomCrawler][VarDumper] Fix html-encoding emojis (nicolas-grekas) + * bug #46167 [VarExporter] Fix exporting DateTime objects on PHP 8.2 (nicolas-grekas) + * 5.4.8 (2022-04-27) * bug #46154 [Mailer] Restore X-Transport after failure (zenas1210) diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php index e38a7268be623..3a80de9d126de 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php @@ -53,7 +53,7 @@ public function getEntities() */ public function getEntitiesByIds(string $identifier, array $values) { - if (null !== $this->queryBuilder->getMaxResults() || null !== $this->queryBuilder->getFirstResult()) { + if (null !== $this->queryBuilder->getMaxResults() || 0 < (int) $this->queryBuilder->getFirstResult()) { // an offset or a limit would apply on results including the where clause with submitted id values // that could make invalid choices valid $choices = []; diff --git a/src/Symfony/Bridge/Doctrine/Form/DataTransformer/CollectionToArrayTransformer.php b/src/Symfony/Bridge/Doctrine/Form/DataTransformer/CollectionToArrayTransformer.php index e2355b4a60fb8..d8235a681479f 100644 --- a/src/Symfony/Bridge/Doctrine/Form/DataTransformer/CollectionToArrayTransformer.php +++ b/src/Symfony/Bridge/Doctrine/Form/DataTransformer/CollectionToArrayTransformer.php @@ -24,8 +24,6 @@ class CollectionToArrayTransformer implements DataTransformerInterface /** * Transforms a collection into an array. * - * @return mixed - * * @throws TransformationFailedException */ public function transform($collection) @@ -48,9 +46,7 @@ public function transform($collection) } /** - * Transforms choice keys into entities. - * - * @param mixed $array An array of entities + * Transforms an array into a collection. * * @return Collection */ diff --git a/src/Symfony/Bridge/Doctrine/ManagerRegistry.php b/src/Symfony/Bridge/Doctrine/ManagerRegistry.php index cd88bedca1e3b..c27a743512764 100644 --- a/src/Symfony/Bridge/Doctrine/ManagerRegistry.php +++ b/src/Symfony/Bridge/Doctrine/ManagerRegistry.php @@ -12,6 +12,7 @@ namespace Symfony\Bridge\Doctrine; use Doctrine\Persistence\AbstractManagerRegistry; +use ProxyManager\Proxy\GhostObjectInterface; use ProxyManager\Proxy\LazyLoadingInterface; use Symfony\Bridge\ProxyManager\LazyProxy\Instantiator\RuntimeInstantiator; use Symfony\Component\DependencyInjection\Container; @@ -19,7 +20,7 @@ /** * References Doctrine connections and entity/document managers. * - * @author Lukas Kahwe Smith + * @author Lukas Kahwe Smith */ abstract class ManagerRegistry extends AbstractManagerRegistry { @@ -53,6 +54,9 @@ protected function resetService($name) if (!$manager instanceof LazyLoadingInterface) { throw new \LogicException('Resetting a non-lazy manager service is not supported. '.(interface_exists(LazyLoadingInterface::class) && class_exists(RuntimeInstantiator::class) ? sprintf('Declare the "%s" service as lazy.', $name) : 'Try running "composer require symfony/proxy-manager-bridge".')); } + if ($manager instanceof GhostObjectInterface) { + throw new \LogicException('Resetting a lazy-ghost-object manager service is not supported.'); + } $manager->setProxyInitializer(\Closure::bind( function (&$wrappedInstance, LazyLoadingInterface $manager) use ($name) { if (isset($this->aliases[$name])) { diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/DoctrineChoiceLoaderTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/DoctrineChoiceLoaderTest.php index 90cf50684d5d9..2d0e0d058baa1 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/DoctrineChoiceLoaderTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/DoctrineChoiceLoaderTest.php @@ -152,8 +152,7 @@ public function testLoadChoiceListUsesObjectLoaderIfAvailable() $this->assertEquals($choiceList, $loaded = $loader->loadChoiceList()); // no further loads on subsequent calls - - $this->assertSame($loaded, $loader->loadChoiceList()); + $this->assertEquals($loaded, $loader->loadChoiceList()); } public function testLoadValuesForChoices() diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php index d1c2c04b88956..66eccf108c36a 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php @@ -1779,4 +1779,32 @@ public function testSubmitNullMultipleUsesDefaultEmptyData() $this->assertEquals($collection, $form->getNormData()); $this->assertEquals($collection, $form->getData()); } + + public function testWithSameLoaderAndDifferentChoiceValueCallbacks() + { + $entity1 = new SingleIntIdEntity(1, 'Foo'); + $entity2 = new SingleIntIdEntity(2, 'Bar'); + $this->persist([$entity1, $entity2]); + + $view = $this->factory->create(FormTypeTest::TESTED_TYPE) + ->add('entity_one', self::TESTED_TYPE, [ + 'em' => 'default', + 'class' => self::SINGLE_IDENT_CLASS, + ]) + ->add('entity_two', self::TESTED_TYPE, [ + 'em' => 'default', + 'class' => self::SINGLE_IDENT_CLASS, + 'choice_value' => function ($choice) { + return $choice ? $choice->name : ''; + }, + ]) + ->createView() + ; + + $this->assertSame('1', $view['entity_one']->vars['choices'][1]->value); + $this->assertSame('2', $view['entity_one']->vars['choices'][2]->value); + + $this->assertSame('Foo', $view['entity_two']->vars['choices']['Foo']->value); + $this->assertSame('Bar', $view['entity_two']->vars['choices']['Bar']->value); + } } diff --git a/src/Symfony/Bridge/Doctrine/composer.json b/src/Symfony/Bridge/Doctrine/composer.json index ebe340ca9f40f..d1445b164c5e5 100644 --- a/src/Symfony/Bridge/Doctrine/composer.json +++ b/src/Symfony/Bridge/Doctrine/composer.json @@ -30,7 +30,7 @@ "symfony/cache": "^5.4|^6.0", "symfony/config": "^4.4|^5.0|^6.0", "symfony/dependency-injection": "^4.4|^5.0|^6.0", - "symfony/form": "^5.1.3|^6.0", + "symfony/form": "^5.4.9|^6.0.9", "symfony/http-kernel": "^5.0|^6.0", "symfony/messenger": "^4.4|^5.0|^6.0", "symfony/doctrine-messenger": "^5.1|^6.0", diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_horizontal_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_horizontal_layout.html.twig index a75e364187743..990b324cb0d17 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_horizontal_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_horizontal_layout.html.twig @@ -49,6 +49,7 @@ col-sm-2
{{- form_widget(form, widget_attr) -}} {{- form_help(form) -}} + {{- form_errors(form) -}}
{##} diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/AbstractConfigCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/AbstractConfigCommand.php index e415cf298be09..f0d5a98148640 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/AbstractConfigCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/AbstractConfigCommand.php @@ -96,20 +96,20 @@ protected function findExtension(string $name) $guess = $bundle->getName(); $minScore = $distance; } + } - $extension = $bundle->getContainerExtension(); + $container = $this->getContainerBuilder($kernel); - if ($extension) { - if ($name === $extension->getAlias()) { - return $extension; - } + if ($container->hasExtension($name)) { + return $container->getExtension($name); + } - $distance = levenshtein($name, $extension->getAlias()); + foreach ($container->getExtensions() as $extension) { + $distance = levenshtein($name, $extension->getAlias()); - if ($distance < $minScore) { - $guess = $extension->getAlias(); - $minScore = $distance; - } + if ($distance < $minScore) { + $guess = $extension->getAlias(); + $minScore = $distance; } } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddAnnotationsCachedReaderPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddAnnotationsCachedReaderPass.php index a0581ff21fb6f..d7db6ebf050a4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddAnnotationsCachedReaderPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddAnnotationsCachedReaderPass.php @@ -29,7 +29,6 @@ public function process(ContainerBuilder $container) // "annotation_reader" at build time don't get any cache foreach ($container->findTaggedServiceIds('annotations.cached_reader') as $id => $tags) { $reader = $container->getDefinition($id); - $reader->setPublic(false); $properties = $reader->getProperties(); if (isset($properties['cacheProviderBackup'])) { diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php index a556599e76d0c..386ee7d0c45df 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php @@ -30,6 +30,7 @@ class UnusedTagsPass implements CompilerPassInterface 'chatter.transport_factory', 'config_cache.resource_checker', 'console.command', + 'container.do_not_inline', 'container.env_var_loader', 'container.env_var_processor', 'container.hot_path', diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 950db3489023e..df425dc5e1dca 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -634,6 +634,8 @@ public function load(array $configs, ContainerBuilder $container) ->addTag('routing.route_loader'); $container->setParameter('container.behavior_describing_tags', [ + 'annotations.cached_reader', + 'container.do_not_inline', 'container.service_locator', 'container.service_subscriber', 'kernel.event_subscriber', @@ -1661,11 +1663,9 @@ private function registerAnnotationsConfiguration(array $config, ContainerBuilde $container ->getDefinition('annotations.cached_reader') - ->setPublic(true) // set to false in AddAnnotationsCachedReaderPass ->replaceArgument(2, $config['debug']) // reference the cache provider without using it until AddAnnotationsCachedReaderPass runs ->addArgument(new ServiceClosureArgument(new Reference($cacheService))) - ->addTag('annotations.cached_reader') ; $container->setAlias('annotation_reader', 'annotations.cached_reader'); diff --git a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php index 510c212611457..4ec54eccf555d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php @@ -160,12 +160,12 @@ public function build(ContainerBuilder $container) $container->addCompilerPass(new RegisterReverseContainerPass(false), PassConfig::TYPE_AFTER_REMOVING); $container->addCompilerPass(new RemoveUnusedSessionMarshallingHandlerPass()); $container->addCompilerPass(new SessionPass()); - $container->addCompilerPass(new CacheCollectorPass(), PassConfig::TYPE_BEFORE_REMOVING); if ($container->getParameter('kernel.debug')) { $container->addCompilerPass(new AddDebugLogProcessorPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 2); $container->addCompilerPass(new UnusedTagsPass(), PassConfig::TYPE_AFTER_REMOVING); $container->addCompilerPass(new ContainerBuilderDebugDumpPass(), PassConfig::TYPE_BEFORE_REMOVING, -255); + $container->addCompilerPass(new CacheCollectorPass(), PassConfig::TYPE_BEFORE_REMOVING); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/annotations.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/annotations.php index a8481258a83fa..64e3f4e31dff6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/annotations.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/annotations.php @@ -38,6 +38,8 @@ inline_service(ArrayAdapter::class), abstract_arg('Debug-Flag'), ]) + ->tag('annotations.cached_reader') + ->tag('container.do_not_inline') ->set('annotations.filesystem_cache_adapter', FilesystemAdapter::class) ->args([ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index 4b0fe8d36127c..234c80a3c9f7c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -1969,6 +1969,8 @@ public function testRegisterParameterCollectingBehaviorDescribingTags() $this->assertTrue($container->hasParameter('container.behavior_describing_tags')); $this->assertEquals([ + 'annotations.cached_reader', + 'container.do_not_inline', 'container.service_locator', 'container.service_subscriber', 'kernel.event_subscriber', diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/alias_with_definition_2.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/alias_with_definition_2.txt index 12e90d48ae40b..4fd20cf7e5fab 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/alias_with_definition_2.txt +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/alias_with_definition_2.txt @@ -8,9 +8,9 @@ ----------------- --------------------------------- Service ID .service_2 Class Full\Qualified\Class2 - Tags tag1 (attr1: val1, attr2: val2)  - tag1 (attr3: val3)  - tag2 + Tags tag1 (attr1: val1, attr2: val2) + tag1 (attr3: val3) + tag2 Calls setMailer Public no Synthetic yes diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_2.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_2.txt index 2d5b03794ea80..0ceb807a45c2f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_2.txt +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_2.txt @@ -3,9 +3,9 @@ ----------------- --------------------------------- Service ID - Class Full\Qualified\Class2 - Tags tag1 (attr1: val1, attr2: val2)  - tag1 (attr3: val3)  - tag2 + Tags tag1 (attr1: val1, attr2: val2) + tag1 (attr3: val3) + tag2 Calls setMailer Public no Synthetic yes diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_1.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_1.txt index af3410f83d5b9..635fadc7d8742 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_1.txt +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_1.txt @@ -13,13 +13,13 @@ Autoconfigured no Factory Class Full\Qualified\FactoryClass Factory Method get - Arguments Service(.definition_2)  - %parameter%  - Inlined Service  - Array (3 element(s))  - Iterator (2 element(s))  - - Service(definition_1)  - - Service(.definition_2)  - Abstract argument (placeholder) + Arguments Service(.definition_2) + %parameter% + Inlined Service + Array (3 element(s)) + Iterator (2 element(s)) + - Service(definition_1) + - Service(.definition_2) + Abstract argument (placeholder) ---------------- --------------------------------- diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_2.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_2.txt index 2d5b03794ea80..0ceb807a45c2f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_2.txt +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_2.txt @@ -3,9 +3,9 @@ ----------------- --------------------------------- Service ID - Class Full\Qualified\Class2 - Tags tag1 (attr1: val1, attr2: val2)  - tag1 (attr3: val3)  - tag2 + Tags tag1 (attr1: val1, attr2: val2) + tag1 (attr3: val3) + tag2 Calls setMailer Public no Synthetic yes diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_1.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_1.txt index 25074dfd18b2c..9814273b7a221 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_1.txt +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_1.txt @@ -11,7 +11,7 @@ | Requirements | name: [a-z]+ | | Class | Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\RouteStub | | Defaults | name: Joseph | -| Options | compiler_class: Symfony\Component\Routing\RouteCompiler | -| | opt1: val1 | -| | opt2: val2 | +| Options | compiler_class: Symfony\Component\Routing\RouteCompiler | +| | opt1: val1 | +| | opt2: val2 | +--------------+-------------------------------------------------------------------+ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_1_link.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_1_link.txt index 4d4a18e5a71b8..ad7a4c8c844fb 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_1_link.txt +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_1_link.txt @@ -10,9 +10,9 @@ | Method | GET|HEAD | | Requirements | name: [a-z]+ | | Class | Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\RouteStub | -| Defaults | _controller: ]8;;myeditor://open?file=[:file:]&line=58\Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\MyController::__invoke()]8;;\ | -| | name: Joseph | -| Options | compiler_class: Symfony\Component\Routing\RouteCompiler | -| | opt1: val1 | -| | opt2: val2 | +| Defaults | _controller: ]8;;myeditor://open?file=[:file:]&line=58\Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\MyController::__invoke()]8;;\ | +| | name: Joseph | +| Options | compiler_class: Symfony\Component\Routing\RouteCompiler | +| | opt1: val1 | +| | opt2: val2 | +--------------+-----------------------------------------------------------------------------------------------+ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_2.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_2.txt index 5853dd013d3a3..533409d402add 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_2.txt +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_2.txt @@ -11,8 +11,8 @@ | Requirements | NO CUSTOM | | Class | Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\RouteStub | | Defaults | NONE | -| Options | compiler_class: Symfony\Component\Routing\RouteCompiler | -| | opt1: val1 | -| | opt2: val2 | +| Options | compiler_class: Symfony\Component\Routing\RouteCompiler | +| | opt1: val1 | +| | opt2: val2 | | Condition | context.getMethod() in ['GET', 'HEAD', 'POST'] | +--------------+-------------------------------------------------------------------+ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_2_link.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_2_link.txt index a690b9798d90a..8e3fe4ca7d65f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_2_link.txt +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_2_link.txt @@ -11,8 +11,8 @@ | Requirements | NO CUSTOM | | Class | Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\RouteStub | | Defaults | _controller: ]8;;myeditor://open?file=[:file:]&line=58\Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\MyController::__invoke()]8;;\ | -| Options | compiler_class: Symfony\Component\Routing\RouteCompiler | -| | opt1: val1 | -| | opt2: val2 | +| Options | compiler_class: Symfony\Component\Routing\RouteCompiler | +| | opt1: val1 | +| | opt2: val2 | | Condition | context.getMethod() in ['GET', 'HEAD', 'POST'] | +--------------+-----------------------------------------------------------------------------------------------+ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ConfigDebugCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ConfigDebugCommandTest.php index 8135f4dcfe419..463318af39f47 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ConfigDebugCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ConfigDebugCommandTest.php @@ -70,6 +70,15 @@ public function testDefaultParameterValueIsResolvedIfConfigIsExisting() $this->assertStringContainsString(sprintf("dsn: 'file:%s/profiler'", $kernelCacheDir), $tester->getDisplay()); } + public function testDumpExtensionConfigWithoutBundle() + { + $tester = $this->createCommandTester(); + $ret = $tester->execute(['name' => 'test_dump']); + + $this->assertSame(0, $ret, 'Returns 0 in case of success'); + $this->assertStringContainsString('enabled: true', $tester->getDisplay()); + } + public function testDumpUndefinedBundleOption() { $tester = $this->createCommandTester(); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ConfigDumpReferenceCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ConfigDumpReferenceCommandTest.php index e86a7ff790ef7..4747b785f8149 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ConfigDumpReferenceCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ConfigDumpReferenceCommandTest.php @@ -50,6 +50,15 @@ public function testDumpBundleName() $this->assertStringContainsString(' custom:', $tester->getDisplay()); } + public function testDumpExtensionConfigWithoutBundle() + { + $tester = $this->createCommandTester(); + $ret = $tester->execute(['name' => 'test_dump']); + + $this->assertSame(0, $ret, 'Returns 0 in case of success'); + $this->assertStringContainsString('enabled: true', $tester->getDisplay()); + } + public function testDumpAtPath() { $tester = $this->createCommandTester(); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Extension/TestDumpExtension.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Extension/TestDumpExtension.php new file mode 100644 index 0000000000000..d8cef92850992 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Extension/TestDumpExtension.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Functional\Extension; + +use Symfony\Component\Config\Definition\Builder\TreeBuilder; +use Symfony\Component\Config\Definition\ConfigurationInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Extension\Extension; + +class TestDumpExtension extends Extension implements ConfigurationInterface +{ + public function getConfigTreeBuilder(): TreeBuilder + { + $treeBuilder = new TreeBuilder('test_dump'); + $treeBuilder->getRootNode() + ->children() + ->booleanNode('enabled')->defaultTrue()->end() + ->end() + ; + + return $treeBuilder; + } + + public function load(array $configs, ContainerBuilder $container): void + { + } + + public function getConfiguration(array $config, ContainerBuilder $container): ConfigurationInterface + { + return $this; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/AppKernel.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/AppKernel.php index c66074b132695..49fb0ca2e6f8d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/AppKernel.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/AppKernel.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional\app; use Psr\Log\NullLogger; +use Symfony\Bundle\FrameworkBundle\Tests\Functional\Extension\TestDumpExtension; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; use Symfony\Component\Config\Loader\LoaderInterface; @@ -80,6 +81,7 @@ public function registerContainerConfiguration(LoaderInterface $loader) protected function build(ContainerBuilder $container) { $container->register('logger', NullLogger::class); + $container->registerExtension(new TestDumpExtension()); } public function __sleep(): array diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index 1f4bc282ed530..c9482a7676d0c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -39,7 +39,7 @@ "doctrine/persistence": "^1.3|^2|^3", "symfony/asset": "^5.3|^6.0", "symfony/browser-kit": "^5.4|^6.0", - "symfony/console": "^5.4|^6.0", + "symfony/console": "^5.4.9|^6.0.9", "symfony/css-selector": "^4.4|^5.0|^6.0", "symfony/dom-crawler": "^4.4.30|^5.3.7|^6.0", "symfony/dotenv": "^5.1|^6.0", diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LdapFactoryTrait.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LdapFactoryTrait.php index 8af8e4424b270..deccbb3975469 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LdapFactoryTrait.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LdapFactoryTrait.php @@ -35,10 +35,6 @@ public function getKey(): string public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId): string { $key = str_replace('-', '_', $this->getKey()); - if (!class_exists(LdapAuthenticator::class)) { - throw new \LogicException(sprintf('The "%s" authenticator requires the "symfony/ldap" package version "5.1" or higher.', $key)); - } - $authenticatorId = parent::createAuthenticator($container, $firewallName, $config, $userProviderId); $container->setDefinition('security.listener.'.$key.'.'.$firewallName, new Definition(CheckLdapCredentialsListener::class)) diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LoginLinkFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LoginLinkFactory.php index 5badfb237c5da..0c620e799799f 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LoginLinkFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LoginLinkFactory.php @@ -20,7 +20,6 @@ use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface; use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface; -use Symfony\Component\Security\Http\LoginLink\LoginLinkHandler; /** * @internal @@ -88,10 +87,6 @@ public function getKey(): string public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId): string { - if (!class_exists(LoginLinkHandler::class)) { - throw new \LogicException('Login login link requires symfony/security-http:^5.2.'); - } - if (!$container->hasDefinition('security.authenticator.login_link')) { $loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__).'/../../Resources/config')); $loader->load('security_authenticator_login_link.php'); diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LoginThrottlingFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LoginThrottlingFactory.php index dc829be2edd9e..9714fab632acc 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LoginThrottlingFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LoginThrottlingFactory.php @@ -19,7 +19,6 @@ use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpFoundation\RateLimiter\RequestRateLimiterInterface; use Symfony\Component\RateLimiter\RateLimiterFactory; -use Symfony\Component\Security\Http\EventListener\LoginThrottlingListener; use Symfony\Component\Security\Http\RateLimiter\DefaultLoginRateLimiter; /** @@ -67,10 +66,6 @@ public function addConfiguration(NodeDefinition $builder) public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId): array { - if (!class_exists(LoginThrottlingListener::class)) { - throw new \LogicException('Login throttling requires symfony/security-http:^5.2.'); - } - if (!class_exists(RateLimiterFactory::class)) { throw new \LogicException('Login throttling requires the Rate Limiter component. Try running "composer require symfony/rate-limiter".'); } diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.php b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.php index 72129d1bbf865..1beb6d371740c 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.php +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.php @@ -121,6 +121,7 @@ ->args([ service('security.http_utils'), [], // Options + service('logger')->nullOnInvalid(), ]) ->set('security.authentication.custom_failure_handler', CustomAuthenticationFailureHandler::class) diff --git a/src/Symfony/Component/Cache/Tests/Adapter/RedisAdapterSentinelTest.php b/src/Symfony/Component/Cache/Tests/Adapter/RedisAdapterSentinelTest.php index 881630c062d6c..efdcf57875959 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/RedisAdapterSentinelTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/RedisAdapterSentinelTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\SkippedTestSuiteError; use Symfony\Component\Cache\Adapter\AbstractAdapter; use Symfony\Component\Cache\Adapter\RedisAdapter; +use Symfony\Component\Cache\Exception\CacheException; use Symfony\Component\Cache\Exception\InvalidArgumentException; /** diff --git a/src/Symfony/Component/Cache/Traits/RedisTrait.php b/src/Symfony/Component/Cache/Traits/RedisTrait.php index 149c9d4c98410..629f4696c7171 100644 --- a/src/Symfony/Component/Cache/Traits/RedisTrait.php +++ b/src/Symfony/Component/Cache/Traits/RedisTrait.php @@ -182,6 +182,10 @@ public static function createConnection(string $dsn, array $options = []) $class = $params['redis_cluster'] ? \RedisCluster::class : (1 < \count($hosts) ? \RedisArray::class : \Redis::class); } else { $class = $params['class'] ?? \Predis\Client::class; + + if (isset($params['redis_sentinel']) && !is_a($class, \Predis\Client::class, true) && !class_exists(\RedisSentinel::class)) { + throw new CacheException(sprintf('Cannot use Redis Sentinel: class "%s" does not extend "Predis\Client" and ext-redis >= 5.2 not found: "%s".', $class, $dsn)); + } } if (is_a($class, \Redis::class, true)) { diff --git a/src/Symfony/Component/Config/Builder/ClassBuilder.php b/src/Symfony/Component/Config/Builder/ClassBuilder.php index 82fadf691f69d..9960d650806a4 100644 --- a/src/Symfony/Component/Config/Builder/ClassBuilder.php +++ b/src/Symfony/Component/Config/Builder/ClassBuilder.php @@ -68,7 +68,7 @@ public function build(): string } $require .= sprintf('require_once __DIR__.\DIRECTORY_SEPARATOR.\'%s\';', implode('\'.\DIRECTORY_SEPARATOR.\'', $path))."\n"; } - $use = ''; + $use = $require ? "\n" : ''; foreach (array_keys($this->use) as $statement) { $use .= sprintf('use %s;', $statement)."\n"; } @@ -81,7 +81,7 @@ public function build(): string foreach ($this->methods as $method) { $lines = explode("\n", $method->getContent()); foreach ($lines as $line) { - $body .= ' '.$line."\n"; + $body .= ($line ? ' '.$line : '')."\n"; } } @@ -89,9 +89,7 @@ public function build(): string namespace NAMESPACE; -REQUIRE -USE - +REQUIREUSE /** * This class is automatically generated to help in creating a config. */ diff --git a/src/Symfony/Component/Config/Builder/ConfigBuilderGenerator.php b/src/Symfony/Component/Config/Builder/ConfigBuilderGenerator.php index 920f12104f3a6..c63917bcb0eaa 100644 --- a/src/Symfony/Component/Config/Builder/ConfigBuilderGenerator.php +++ b/src/Symfony/Component/Config/Builder/ConfigBuilderGenerator.php @@ -136,14 +136,39 @@ private function handleArrayNode(ArrayNode $node, ClassBuilder $class, string $n $class->addRequire($childClass); $this->classes[] = $childClass; - $property = $class->addProperty($node->getName(), $childClass->getFqcn()); - $body = ' + $hasNormalizationClosures = $this->hasNormalizationClosures($node); + $property = $class->addProperty( + $node->getName(), + $this->getType($childClass->getFqcn(), $hasNormalizationClosures) + ); + $body = $hasNormalizationClosures ? ' +/** + * @return CLASS|$this + */ +public function NAME($value = []) +{ + if (!\is_array($value)) { + $this->_usedProperties[\'PROPERTY\'] = true; + $this->PROPERTY = $value; + + return $this; + } + + if (!$this->PROPERTY instanceof CLASS) { + $this->_usedProperties[\'PROPERTY\'] = true; + $this->PROPERTY = new CLASS($value); + } elseif (0 < \func_num_args()) { + throw new InvalidConfigurationException(\'The node created by "NAME()" has already been initialized. You cannot pass values the second time you call NAME().\'); + } + + return $this->PROPERTY; +}' : ' public function NAME(array $value = []): CLASS { if (null === $this->PROPERTY) { $this->_usedProperties[\'PROPERTY\'] = true; $this->PROPERTY = new CLASS($value); - } elseif ([] !== $value) { + } elseif (0 < \func_num_args()) { throw new InvalidConfigurationException(\'The node created by "NAME()" has already been initialized. You cannot pass values the second time you call NAME().\'); } @@ -227,10 +252,29 @@ public function NAME(string $VAR, $VALUE): self } $class->addRequire($childClass); $this->classes[] = $childClass; - $property = $class->addProperty($node->getName(), $childClass->getFqcn().'[]'); + + $hasNormalizationClosures = $this->hasNormalizationClosures($node) || $this->hasNormalizationClosures($prototype); + $property = $class->addProperty( + $node->getName(), + $this->getType($childClass->getFqcn().'[]', $hasNormalizationClosures) + ); if (null === $key = $node->getKeyAttribute()) { - $body = ' + $body = $hasNormalizationClosures ? ' +/** + * @return CLASS|$this + */ +public function NAME($value = []) +{ + $this->_usedProperties[\'PROPERTY\'] = true; + if (!\is_array($value)) { + $this->PROPERTY[] = $value; + + return $this; + } + + return $this->PROPERTY[] = new CLASS($value); +}' : ' public function NAME(array $value = []): CLASS { $this->_usedProperties[\'PROPERTY\'] = true; @@ -239,19 +283,38 @@ public function NAME(array $value = []): CLASS }'; $class->addMethod($methodName, $body, ['PROPERTY' => $property->getName(), 'CLASS' => $childClass->getFqcn()]); } else { - $body = ' -public function NAME(string $VAR, array $VALUE = []): CLASS + $body = $hasNormalizationClosures ? ' +/** + * @return CLASS|$this + */ +public function NAME(string $VAR, $VALUE = []) { - if (!isset($this->PROPERTY[$VAR])) { + if (!\is_array($VALUE)) { $this->_usedProperties[\'PROPERTY\'] = true; + $this->PROPERTY[$VAR] = $VALUE; - return $this->PROPERTY[$VAR] = new CLASS($VALUE); + return $this; } - if ([] === $VALUE) { - return $this->PROPERTY[$VAR]; + + if (!isset($this->PROPERTY[$VAR]) || !$this->PROPERTY[$VAR] instanceof CLASS) { + $this->_usedProperties[\'PROPERTY\'] = true; + $this->PROPERTY[$VAR] = new CLASS($VALUE); + } elseif (1 < \func_num_args()) { + throw new InvalidConfigurationException(\'The node created by "NAME()" has already been initialized. You cannot pass values the second time you call NAME().\'); } - throw new InvalidConfigurationException(\'The node created by "NAME()" has already been initialized. You cannot pass values the second time you call NAME().\'); + return $this->PROPERTY[$VAR]; +}' : ' +public function NAME(string $VAR, array $VALUE = []): CLASS +{ + if (!isset($this->PROPERTY[$VAR])) { + $this->_usedProperties[\'PROPERTY\'] = true; + $this->PROPERTY[$VAR] = new CLASS($VALUE); + } elseif (1 < \func_num_args()) { + throw new InvalidConfigurationException(\'The node created by "NAME()" has already been initialized. You cannot pass values the second time you call NAME().\'); + } + + return $this->PROPERTY[$VAR]; }'; $class->addUse(InvalidConfigurationException::class); $class->addMethod($methodName, $body, ['PROPERTY' => $property->getName(), 'CLASS' => $childClass->getFqcn(), 'VAR' => '' === $key ? 'key' : $key, 'VALUE' => 'value' === $key ? 'data' : 'value']); @@ -375,16 +438,22 @@ private function buildToArray(ClassBuilder $class): void $code = '$this->PROPERTY'; if (null !== $p->getType()) { if ($p->isArray()) { - $code = 'array_map(function ($v) { return $v->toArray(); }, $this->PROPERTY)'; + $code = $p->areScalarsAllowed() + ? 'array_map(function ($v) { return $v instanceof CLASS ? $v->toArray() : $v; }, $this->PROPERTY)' + : 'array_map(function ($v) { return $v->toArray(); }, $this->PROPERTY)' + ; } else { - $code = '$this->PROPERTY->toArray()'; + $code = $p->areScalarsAllowed() + ? '$this->PROPERTY instanceof CLASS ? $this->PROPERTY->toArray() : $this->PROPERTY' + : '$this->PROPERTY->toArray()' + ; } } $body .= strtr(' if (isset($this->_usedProperties[\'PROPERTY\'])) { $output[\'ORG_NAME\'] = '.$code.'; - }', ['PROPERTY' => $p->getName(), 'ORG_NAME' => $p->getOriginalName()]); + }', ['PROPERTY' => $p->getName(), 'ORG_NAME' => $p->getOriginalName(), 'CLASS' => $p->getType()]); } $extraKeys = $class->shouldAllowExtraKeys() ? ' + $this->_extraKeys' : ''; @@ -405,9 +474,15 @@ private function buildConstructor(ClassBuilder $class): void $code = '$value[\'ORG_NAME\']'; if (null !== $p->getType()) { if ($p->isArray()) { - $code = 'array_map(function ($v) { return new '.$p->getType().'($v); }, $value[\'ORG_NAME\'])'; + $code = $p->areScalarsAllowed() + ? 'array_map(function ($v) { return \is_array($v) ? new '.$p->getType().'($v) : $v; }, $value[\'ORG_NAME\'])' + : 'array_map(function ($v) { return new '.$p->getType().'($v); }, $value[\'ORG_NAME\'])' + ; } else { - $code = 'new '.$p->getType().'($value[\'ORG_NAME\'])'; + $code = $p->areScalarsAllowed() + ? '\is_array($value[\'ORG_NAME\']) ? new '.$p->getType().'($value[\'ORG_NAME\']) : $value[\'ORG_NAME\']' + : 'new '.$p->getType().'($value[\'ORG_NAME\'])' + ; } } @@ -435,8 +510,7 @@ private function buildConstructor(ClassBuilder $class): void $class->addMethod('__construct', ' public function __construct(array $value = []) -{ -'.$body.' +{'.$body.' }'); } @@ -467,4 +541,21 @@ private function getSubNamespace(ClassBuilder $rootClass): string { return sprintf('%s\\%s', $rootClass->getNamespace(), substr($rootClass->getName(), 0, -6)); } + + private function hasNormalizationClosures(NodeInterface $node): bool + { + try { + $r = new \ReflectionProperty($node, 'normalizationClosures'); + } catch (\ReflectionException $e) { + return false; + } + $r->setAccessible(true); + + return [] !== $r->getValue($node); + } + + private function getType(string $classType, bool $hasNormalizationClosures): string + { + return $classType.($hasNormalizationClosures ? '|scalar' : ''); + } } diff --git a/src/Symfony/Component/Config/Builder/Property.php b/src/Symfony/Component/Config/Builder/Property.php index 1b24c47cc909b..5a6f7e31bfe70 100644 --- a/src/Symfony/Component/Config/Builder/Property.php +++ b/src/Symfony/Component/Config/Builder/Property.php @@ -23,6 +23,7 @@ class Property private $name; private $originalName; private $array = false; + private $scalarsAllowed = false; private $type = null; private $content; @@ -47,6 +48,11 @@ public function setType(string $type): void $this->array = false; $this->type = $type; + if ('|scalar' === substr($type, -7)) { + $this->scalarsAllowed = true; + $this->type = $type = substr($type, 0, -7); + } + if ('[]' === substr($type, -2)) { $this->array = true; $this->type = substr($type, 0, -2); @@ -72,4 +78,9 @@ public function isArray(): bool { return $this->array; } + + public function areScalarsAllowed(): bool + { + return $this->scalarsAllowed; + } } diff --git a/src/Symfony/Component/Config/Resource/GlobResource.php b/src/Symfony/Component/Config/Resource/GlobResource.php index 093f55916f008..2bfbe2e4b2475 100644 --- a/src/Symfony/Component/Config/Resource/GlobResource.php +++ b/src/Symfony/Component/Config/Resource/GlobResource.php @@ -109,7 +109,9 @@ public function getIterator(): \Traversable $prefix = str_replace('\\', '/', $this->prefix); $paths = null; - if (!str_starts_with($this->prefix, 'phar://') && !str_contains($this->pattern, '/**/')) { + if ('' === $this->pattern && is_file($prefix)) { + $paths = [$this->prefix]; + } elseif (!str_starts_with($this->prefix, 'phar://') && !str_contains($this->pattern, '/**/')) { if ($this->globBrace || !str_contains($this->pattern, '{')) { $paths = glob($this->prefix.$this->pattern, \GLOB_NOSORT | $this->globBrace); } elseif (!str_contains($this->pattern, '\\') || !preg_match('/\\\\[,{}]/', $this->pattern)) { @@ -170,14 +172,21 @@ function (\SplFileInfo $file, $path) { throw new \LogicException(sprintf('Extended glob pattern "%s" cannot be used as the Finder component is not installed.', $this->pattern)); } + if (is_file($prefix = $this->prefix)) { + $prefix = \dirname($prefix); + $pattern = basename($prefix).$this->pattern; + } else { + $pattern = $this->pattern; + } + $finder = new Finder(); - $regex = Glob::toRegex($this->pattern); + $regex = Glob::toRegex($pattern); if ($this->recursive) { $regex = substr_replace($regex, '(/|$)', -2, 1); } - $prefixLen = \strlen($this->prefix); - foreach ($finder->followLinks()->sortByName()->in($this->prefix) as $path => $info) { + $prefixLen = \strlen($prefix); + foreach ($finder->followLinks()->sortByName()->in($prefix) as $path => $info) { $normalizedPath = str_replace('\\', '/', $path); if (!preg_match($regex, substr($normalizedPath, $prefixLen)) || !$info->isFile()) { continue; diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/AddToList.config.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/AddToList.config.php index 5bb32b89be990..4dc2d3e87ab53 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/AddToList.config.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/AddToList.config.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + use Symfony\Config\AddToListConfig; return static function (AddToListConfig $config) { diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/AddToList.output.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/AddToList.output.php index 6efdb83839604..1949f59c24963 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/AddToList.output.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/AddToList.output.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + return [ 'translator' => [ 'fallbacks' => ['sv', 'fr', 'es'], diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/AddToList.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/AddToList.php index 949cbe9e4e5ac..5f324d29cb0b4 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/AddToList.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/AddToList.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\Config\Tests\Builder\Fixtures; use Symfony\Component\Config\Definition\Builder\TreeBuilder; diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/AddToList/Symfony/Config/AddToList/Messenger/ReceivingConfig.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/AddToList/Symfony/Config/AddToList/Messenger/ReceivingConfig.php index c757266195482..5d8d28af443c3 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/AddToList/Symfony/Config/AddToList/Messenger/ReceivingConfig.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/AddToList/Symfony/Config/AddToList/Messenger/ReceivingConfig.php @@ -2,11 +2,9 @@ namespace Symfony\Config\AddToList\Messenger; - use Symfony\Component\Config\Loader\ParamConfigurator; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; - /** * This class is automatically generated to help in creating a config. */ @@ -15,7 +13,7 @@ class ReceivingConfig private $priority; private $color; private $_usedProperties = []; - + /** * @default null * @param ParamConfigurator|int $value @@ -25,10 +23,10 @@ public function priority($value): self { $this->_usedProperties['priority'] = true; $this->priority = $value; - + return $this; } - + /** * @default null * @param ParamConfigurator|mixed $value @@ -38,30 +36,29 @@ public function color($value): self { $this->_usedProperties['color'] = true; $this->color = $value; - + return $this; } - + public function __construct(array $value = []) { - if (array_key_exists('priority', $value)) { $this->_usedProperties['priority'] = true; $this->priority = $value['priority']; unset($value['priority']); } - + if (array_key_exists('color', $value)) { $this->_usedProperties['color'] = true; $this->color = $value['color']; unset($value['color']); } - + if ([] !== $value) { throw new InvalidConfigurationException(sprintf('The following keys are not supported by "%s": ', __CLASS__).implode(', ', array_keys($value))); } } - + public function toArray(): array { $output = []; @@ -71,7 +68,7 @@ public function toArray(): array if (isset($this->_usedProperties['color'])) { $output['color'] = $this->color; } - + return $output; } diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/AddToList/Symfony/Config/AddToList/Messenger/RoutingConfig.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/AddToList/Symfony/Config/AddToList/Messenger/RoutingConfig.php index 275dca34da3af..563ec25dcdaf2 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/AddToList/Symfony/Config/AddToList/Messenger/RoutingConfig.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/AddToList/Symfony/Config/AddToList/Messenger/RoutingConfig.php @@ -2,11 +2,9 @@ namespace Symfony\Config\AddToList\Messenger; - use Symfony\Component\Config\Loader\ParamConfigurator; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; - /** * This class is automatically generated to help in creating a config. */ @@ -14,7 +12,7 @@ class RoutingConfig { private $senders; private $_usedProperties = []; - + /** * @param ParamConfigurator|list $value * @return $this @@ -23,31 +21,30 @@ public function senders($value): self { $this->_usedProperties['senders'] = true; $this->senders = $value; - + return $this; } - + public function __construct(array $value = []) { - if (array_key_exists('senders', $value)) { $this->_usedProperties['senders'] = true; $this->senders = $value['senders']; unset($value['senders']); } - + if ([] !== $value) { throw new InvalidConfigurationException(sprintf('The following keys are not supported by "%s": ', __CLASS__).implode(', ', array_keys($value))); } } - + public function toArray(): array { $output = []; if (isset($this->_usedProperties['senders'])) { $output['senders'] = $this->senders; } - + return $output; } diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/AddToList/Symfony/Config/AddToList/MessengerConfig.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/AddToList/Symfony/Config/AddToList/MessengerConfig.php index 85b593a1b05f1..bc8625d9fb0c1 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/AddToList/Symfony/Config/AddToList/MessengerConfig.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/AddToList/Symfony/Config/AddToList/MessengerConfig.php @@ -7,7 +7,6 @@ use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; - /** * This class is automatically generated to help in creating a config. */ @@ -16,48 +15,45 @@ class MessengerConfig private $routing; private $receiving; private $_usedProperties = []; - + public function routing(string $message_class, array $value = []): \Symfony\Config\AddToList\Messenger\RoutingConfig { if (!isset($this->routing[$message_class])) { $this->_usedProperties['routing'] = true; - - return $this->routing[$message_class] = new \Symfony\Config\AddToList\Messenger\RoutingConfig($value); - } - if ([] === $value) { - return $this->routing[$message_class]; + $this->routing[$message_class] = new \Symfony\Config\AddToList\Messenger\RoutingConfig($value); + } elseif (1 < \func_num_args()) { + throw new InvalidConfigurationException('The node created by "routing()" has already been initialized. You cannot pass values the second time you call routing().'); } - - throw new InvalidConfigurationException('The node created by "routing()" has already been initialized. You cannot pass values the second time you call routing().'); + + return $this->routing[$message_class]; } - + public function receiving(array $value = []): \Symfony\Config\AddToList\Messenger\ReceivingConfig { $this->_usedProperties['receiving'] = true; - + return $this->receiving[] = new \Symfony\Config\AddToList\Messenger\ReceivingConfig($value); } - + public function __construct(array $value = []) { - if (array_key_exists('routing', $value)) { $this->_usedProperties['routing'] = true; $this->routing = array_map(function ($v) { return new \Symfony\Config\AddToList\Messenger\RoutingConfig($v); }, $value['routing']); unset($value['routing']); } - + if (array_key_exists('receiving', $value)) { $this->_usedProperties['receiving'] = true; $this->receiving = array_map(function ($v) { return new \Symfony\Config\AddToList\Messenger\ReceivingConfig($v); }, $value['receiving']); unset($value['receiving']); } - + if ([] !== $value) { throw new InvalidConfigurationException(sprintf('The following keys are not supported by "%s": ', __CLASS__).implode(', ', array_keys($value))); } } - + public function toArray(): array { $output = []; @@ -67,7 +63,7 @@ public function toArray(): array if (isset($this->_usedProperties['receiving'])) { $output['receiving'] = array_map(function ($v) { return $v->toArray(); }, $this->receiving); } - + return $output; } diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/AddToList/Symfony/Config/AddToList/TranslatorConfig.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/AddToList/Symfony/Config/AddToList/TranslatorConfig.php index 79f041cea6da0..25813dade63a8 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/AddToList/Symfony/Config/AddToList/TranslatorConfig.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/AddToList/Symfony/Config/AddToList/TranslatorConfig.php @@ -2,11 +2,9 @@ namespace Symfony\Config\AddToList; - use Symfony\Component\Config\Loader\ParamConfigurator; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; - /** * This class is automatically generated to help in creating a config. */ @@ -15,7 +13,7 @@ class TranslatorConfig private $fallbacks; private $sources; private $_usedProperties = []; - + /** * @param ParamConfigurator|list $value * @return $this @@ -24,10 +22,10 @@ public function fallbacks($value): self { $this->_usedProperties['fallbacks'] = true; $this->fallbacks = $value; - + return $this; } - + /** * @param ParamConfigurator|mixed $value * @return $this @@ -36,30 +34,29 @@ public function source(string $source_class, $value): self { $this->_usedProperties['sources'] = true; $this->sources[$source_class] = $value; - + return $this; } - + public function __construct(array $value = []) { - if (array_key_exists('fallbacks', $value)) { $this->_usedProperties['fallbacks'] = true; $this->fallbacks = $value['fallbacks']; unset($value['fallbacks']); } - + if (array_key_exists('sources', $value)) { $this->_usedProperties['sources'] = true; $this->sources = $value['sources']; unset($value['sources']); } - + if ([] !== $value) { throw new InvalidConfigurationException(sprintf('The following keys are not supported by "%s": ', __CLASS__).implode(', ', array_keys($value))); } } - + public function toArray(): array { $output = []; @@ -69,7 +66,7 @@ public function toArray(): array if (isset($this->_usedProperties['sources'])) { $output['sources'] = $this->sources; } - + return $output; } diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/AddToList/Symfony/Config/AddToListConfig.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/AddToList/Symfony/Config/AddToListConfig.php index e6f0c262b88db..a8b0adb28b5f2 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/AddToList/Symfony/Config/AddToListConfig.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/AddToList/Symfony/Config/AddToListConfig.php @@ -7,7 +7,6 @@ use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; - /** * This class is automatically generated to help in creating a config. */ @@ -16,56 +15,55 @@ class AddToListConfig implements \Symfony\Component\Config\Builder\ConfigBuilder private $translator; private $messenger; private $_usedProperties = []; - + public function translator(array $value = []): \Symfony\Config\AddToList\TranslatorConfig { if (null === $this->translator) { $this->_usedProperties['translator'] = true; $this->translator = new \Symfony\Config\AddToList\TranslatorConfig($value); - } elseif ([] !== $value) { + } elseif (0 < \func_num_args()) { throw new InvalidConfigurationException('The node created by "translator()" has already been initialized. You cannot pass values the second time you call translator().'); } - + return $this->translator; } - + public function messenger(array $value = []): \Symfony\Config\AddToList\MessengerConfig { if (null === $this->messenger) { $this->_usedProperties['messenger'] = true; $this->messenger = new \Symfony\Config\AddToList\MessengerConfig($value); - } elseif ([] !== $value) { + } elseif (0 < \func_num_args()) { throw new InvalidConfigurationException('The node created by "messenger()" has already been initialized. You cannot pass values the second time you call messenger().'); } - + return $this->messenger; } - + public function getExtensionAlias(): string { return 'add_to_list'; } - + public function __construct(array $value = []) { - if (array_key_exists('translator', $value)) { $this->_usedProperties['translator'] = true; $this->translator = new \Symfony\Config\AddToList\TranslatorConfig($value['translator']); unset($value['translator']); } - + if (array_key_exists('messenger', $value)) { $this->_usedProperties['messenger'] = true; $this->messenger = new \Symfony\Config\AddToList\MessengerConfig($value['messenger']); unset($value['messenger']); } - + if ([] !== $value) { throw new InvalidConfigurationException(sprintf('The following keys are not supported by "%s": ', __CLASS__).implode(', ', array_keys($value))); } } - + public function toArray(): array { $output = []; @@ -75,7 +73,7 @@ public function toArray(): array if (isset($this->_usedProperties['messenger'])) { $output['messenger'] = $this->messenger->toArray(); } - + return $output; } diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/ArrayExtraKeys.config.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ArrayExtraKeys.config.php index 45069e7490c22..598255fec6bf6 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/ArrayExtraKeys.config.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ArrayExtraKeys.config.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + use Symfony\Config\ArrayExtraKeysConfig; return static function (ArrayExtraKeysConfig $config) { diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/ArrayExtraKeys.output.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ArrayExtraKeys.output.php index d2fdc1ef5c8e4..ff0bdbd686cb7 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/ArrayExtraKeys.output.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ArrayExtraKeys.output.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + return [ 'foo' => [ 'baz' => 'foo_baz', diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/ArrayExtraKeys.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ArrayExtraKeys.php index 751fe5c2934cc..96c515e55b46d 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/ArrayExtraKeys.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ArrayExtraKeys.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\Config\Tests\Builder\Fixtures; use Symfony\Component\Config\Definition\Builder\TreeBuilder; diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/ArrayExtraKeys/Symfony/Config/ArrayExtraKeys/BarConfig.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ArrayExtraKeys/Symfony/Config/ArrayExtraKeys/BarConfig.php index 256454f164bbf..bb3c6f568607b 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/ArrayExtraKeys/Symfony/Config/ArrayExtraKeys/BarConfig.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ArrayExtraKeys/Symfony/Config/ArrayExtraKeys/BarConfig.php @@ -2,10 +2,8 @@ namespace Symfony\Config\ArrayExtraKeys; - use Symfony\Component\Config\Loader\ParamConfigurator; - /** * This class is automatically generated to help in creating a config. */ @@ -15,7 +13,7 @@ class BarConfig private $grault; private $_usedProperties = []; private $_extraKeys; - + /** * @default null * @param ParamConfigurator|mixed $value @@ -25,10 +23,10 @@ public function corge($value): self { $this->_usedProperties['corge'] = true; $this->corge = $value; - + return $this; } - + /** * @default null * @param ParamConfigurator|mixed $value @@ -38,29 +36,28 @@ public function grault($value): self { $this->_usedProperties['grault'] = true; $this->grault = $value; - + return $this; } - + public function __construct(array $value = []) { - if (array_key_exists('corge', $value)) { $this->_usedProperties['corge'] = true; $this->corge = $value['corge']; unset($value['corge']); } - + if (array_key_exists('grault', $value)) { $this->_usedProperties['grault'] = true; $this->grault = $value['grault']; unset($value['grault']); } - + $this->_extraKeys = $value; - + } - + public function toArray(): array { $output = []; @@ -70,10 +67,10 @@ public function toArray(): array if (isset($this->_usedProperties['grault'])) { $output['grault'] = $this->grault; } - + return $output + $this->_extraKeys; } - + /** * @param ParamConfigurator|mixed $value * @return $this @@ -81,7 +78,7 @@ public function toArray(): array public function set(string $key, $value): self { $this->_extraKeys[$key] = $value; - + return $this; } diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/ArrayExtraKeys/Symfony/Config/ArrayExtraKeys/BazConfig.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ArrayExtraKeys/Symfony/Config/ArrayExtraKeys/BazConfig.php index d64633eab9c66..e198dac797e05 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/ArrayExtraKeys/Symfony/Config/ArrayExtraKeys/BazConfig.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ArrayExtraKeys/Symfony/Config/ArrayExtraKeys/BazConfig.php @@ -2,31 +2,28 @@ namespace Symfony\Config\ArrayExtraKeys; - use Symfony\Component\Config\Loader\ParamConfigurator; - /** * This class is automatically generated to help in creating a config. */ class BazConfig { private $_extraKeys; - + public function __construct(array $value = []) { - $this->_extraKeys = $value; - + } - + public function toArray(): array { $output = []; - + return $output + $this->_extraKeys; } - + /** * @param ParamConfigurator|mixed $value * @return $this @@ -34,7 +31,7 @@ public function toArray(): array public function set(string $key, $value): self { $this->_extraKeys[$key] = $value; - + return $this; } diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/ArrayExtraKeys/Symfony/Config/ArrayExtraKeys/FooConfig.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ArrayExtraKeys/Symfony/Config/ArrayExtraKeys/FooConfig.php index c8f713341eda3..1204c3a06f74a 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/ArrayExtraKeys/Symfony/Config/ArrayExtraKeys/FooConfig.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ArrayExtraKeys/Symfony/Config/ArrayExtraKeys/FooConfig.php @@ -2,10 +2,8 @@ namespace Symfony\Config\ArrayExtraKeys; - use Symfony\Component\Config\Loader\ParamConfigurator; - /** * This class is automatically generated to help in creating a config. */ @@ -15,7 +13,7 @@ class FooConfig private $qux; private $_usedProperties = []; private $_extraKeys; - + /** * @default null * @param ParamConfigurator|mixed $value @@ -25,10 +23,10 @@ public function baz($value): self { $this->_usedProperties['baz'] = true; $this->baz = $value; - + return $this; } - + /** * @default null * @param ParamConfigurator|mixed $value @@ -38,29 +36,28 @@ public function qux($value): self { $this->_usedProperties['qux'] = true; $this->qux = $value; - + return $this; } - + public function __construct(array $value = []) { - if (array_key_exists('baz', $value)) { $this->_usedProperties['baz'] = true; $this->baz = $value['baz']; unset($value['baz']); } - + if (array_key_exists('qux', $value)) { $this->_usedProperties['qux'] = true; $this->qux = $value['qux']; unset($value['qux']); } - + $this->_extraKeys = $value; - + } - + public function toArray(): array { $output = []; @@ -70,10 +67,10 @@ public function toArray(): array if (isset($this->_usedProperties['qux'])) { $output['qux'] = $this->qux; } - + return $output + $this->_extraKeys; } - + /** * @param ParamConfigurator|mixed $value * @return $this @@ -81,7 +78,7 @@ public function toArray(): array public function set(string $key, $value): self { $this->_extraKeys[$key] = $value; - + return $this; } diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/ArrayExtraKeys/Symfony/Config/ArrayExtraKeysConfig.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ArrayExtraKeys/Symfony/Config/ArrayExtraKeysConfig.php index 3d8adb7095b33..2d7628f038736 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/ArrayExtraKeys/Symfony/Config/ArrayExtraKeysConfig.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ArrayExtraKeys/Symfony/Config/ArrayExtraKeysConfig.php @@ -8,7 +8,6 @@ use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; - /** * This class is automatically generated to help in creating a config. */ @@ -18,69 +17,68 @@ class ArrayExtraKeysConfig implements \Symfony\Component\Config\Builder\ConfigBu private $bar; private $baz; private $_usedProperties = []; - + public function foo(array $value = []): \Symfony\Config\ArrayExtraKeys\FooConfig { if (null === $this->foo) { $this->_usedProperties['foo'] = true; $this->foo = new \Symfony\Config\ArrayExtraKeys\FooConfig($value); - } elseif ([] !== $value) { + } elseif (0 < \func_num_args()) { throw new InvalidConfigurationException('The node created by "foo()" has already been initialized. You cannot pass values the second time you call foo().'); } - + return $this->foo; } - + public function bar(array $value = []): \Symfony\Config\ArrayExtraKeys\BarConfig { $this->_usedProperties['bar'] = true; - + return $this->bar[] = new \Symfony\Config\ArrayExtraKeys\BarConfig($value); } - + public function baz(array $value = []): \Symfony\Config\ArrayExtraKeys\BazConfig { if (null === $this->baz) { $this->_usedProperties['baz'] = true; $this->baz = new \Symfony\Config\ArrayExtraKeys\BazConfig($value); - } elseif ([] !== $value) { + } elseif (0 < \func_num_args()) { throw new InvalidConfigurationException('The node created by "baz()" has already been initialized. You cannot pass values the second time you call baz().'); } - + return $this->baz; } - + public function getExtensionAlias(): string { return 'array_extra_keys'; } - + public function __construct(array $value = []) { - if (array_key_exists('foo', $value)) { $this->_usedProperties['foo'] = true; $this->foo = new \Symfony\Config\ArrayExtraKeys\FooConfig($value['foo']); unset($value['foo']); } - + if (array_key_exists('bar', $value)) { $this->_usedProperties['bar'] = true; $this->bar = array_map(function ($v) { return new \Symfony\Config\ArrayExtraKeys\BarConfig($v); }, $value['bar']); unset($value['bar']); } - + if (array_key_exists('baz', $value)) { $this->_usedProperties['baz'] = true; $this->baz = new \Symfony\Config\ArrayExtraKeys\BazConfig($value['baz']); unset($value['baz']); } - + if ([] !== $value) { throw new InvalidConfigurationException(sprintf('The following keys are not supported by "%s": ', __CLASS__).implode(', ', array_keys($value))); } } - + public function toArray(): array { $output = []; @@ -93,7 +91,7 @@ public function toArray(): array if (isset($this->_usedProperties['baz'])) { $output['baz'] = $this->baz->toArray(); } - + return $output; } diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues.config.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues.config.php index 4b86755c91a5b..430258ac382fa 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues.config.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues.config.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + use Symfony\Config\NodeInitialValuesConfig; return static function (NodeInitialValuesConfig $config) { diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues.output.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues.output.php index 7fe70f9645b9e..c4ad8affe2144 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues.output.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues.output.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + return [ 'some_clever_name' => [ 'first' => 'bar', diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues.php index c290cf9730670..153af57be9b5b 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\Config\Tests\Builder\Fixtures; use Symfony\Component\Config\Definition\Builder\TreeBuilder; diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues/Symfony/Config/NodeInitialValues/Messenger/TransportsConfig.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues/Symfony/Config/NodeInitialValues/Messenger/TransportsConfig.php index 3acc0247ac726..5fa9c2b426464 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues/Symfony/Config/NodeInitialValues/Messenger/TransportsConfig.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues/Symfony/Config/NodeInitialValues/Messenger/TransportsConfig.php @@ -2,11 +2,9 @@ namespace Symfony\Config\NodeInitialValues\Messenger; - use Symfony\Component\Config\Loader\ParamConfigurator; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; - /** * This class is automatically generated to help in creating a config. */ @@ -16,7 +14,7 @@ class TransportsConfig private $serializer; private $options; private $_usedProperties = []; - + /** * @default null * @param ParamConfigurator|mixed $value @@ -26,10 +24,10 @@ public function dsn($value): self { $this->_usedProperties['dsn'] = true; $this->dsn = $value; - + return $this; } - + /** * @default null * @param ParamConfigurator|mixed $value @@ -39,10 +37,10 @@ public function serializer($value): self { $this->_usedProperties['serializer'] = true; $this->serializer = $value; - + return $this; } - + /** * @param ParamConfigurator|list $value * @return $this @@ -51,36 +49,35 @@ public function options($value): self { $this->_usedProperties['options'] = true; $this->options = $value; - + return $this; } - + public function __construct(array $value = []) { - if (array_key_exists('dsn', $value)) { $this->_usedProperties['dsn'] = true; $this->dsn = $value['dsn']; unset($value['dsn']); } - + if (array_key_exists('serializer', $value)) { $this->_usedProperties['serializer'] = true; $this->serializer = $value['serializer']; unset($value['serializer']); } - + if (array_key_exists('options', $value)) { $this->_usedProperties['options'] = true; $this->options = $value['options']; unset($value['options']); } - + if ([] !== $value) { throw new InvalidConfigurationException(sprintf('The following keys are not supported by "%s": ', __CLASS__).implode(', ', array_keys($value))); } } - + public function toArray(): array { $output = []; @@ -93,7 +90,7 @@ public function toArray(): array if (isset($this->_usedProperties['options'])) { $output['options'] = $this->options; } - + return $output; } diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues/Symfony/Config/NodeInitialValues/MessengerConfig.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues/Symfony/Config/NodeInitialValues/MessengerConfig.php index 12ff61109cae7..d174c8932b4f8 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues/Symfony/Config/NodeInitialValues/MessengerConfig.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues/Symfony/Config/NodeInitialValues/MessengerConfig.php @@ -6,7 +6,6 @@ use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; - /** * This class is automatically generated to help in creating a config. */ @@ -14,42 +13,39 @@ class MessengerConfig { private $transports; private $_usedProperties = []; - + public function transports(string $name, array $value = []): \Symfony\Config\NodeInitialValues\Messenger\TransportsConfig { if (!isset($this->transports[$name])) { $this->_usedProperties['transports'] = true; - - return $this->transports[$name] = new \Symfony\Config\NodeInitialValues\Messenger\TransportsConfig($value); + $this->transports[$name] = new \Symfony\Config\NodeInitialValues\Messenger\TransportsConfig($value); + } elseif (1 < \func_num_args()) { + throw new InvalidConfigurationException('The node created by "transports()" has already been initialized. You cannot pass values the second time you call transports().'); } - if ([] === $value) { - return $this->transports[$name]; - } - - throw new InvalidConfigurationException('The node created by "transports()" has already been initialized. You cannot pass values the second time you call transports().'); + + return $this->transports[$name]; } - + public function __construct(array $value = []) { - if (array_key_exists('transports', $value)) { $this->_usedProperties['transports'] = true; $this->transports = array_map(function ($v) { return new \Symfony\Config\NodeInitialValues\Messenger\TransportsConfig($v); }, $value['transports']); unset($value['transports']); } - + if ([] !== $value) { throw new InvalidConfigurationException(sprintf('The following keys are not supported by "%s": ', __CLASS__).implode(', ', array_keys($value))); } } - + public function toArray(): array { $output = []; if (isset($this->_usedProperties['transports'])) { $output['transports'] = array_map(function ($v) { return $v->toArray(); }, $this->transports); } - + return $output; } diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues/Symfony/Config/NodeInitialValues/SomeCleverNameConfig.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues/Symfony/Config/NodeInitialValues/SomeCleverNameConfig.php index 3ca87c25eec12..9629e95bfa443 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues/Symfony/Config/NodeInitialValues/SomeCleverNameConfig.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues/Symfony/Config/NodeInitialValues/SomeCleverNameConfig.php @@ -2,11 +2,9 @@ namespace Symfony\Config\NodeInitialValues; - use Symfony\Component\Config\Loader\ParamConfigurator; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; - /** * This class is automatically generated to help in creating a config. */ @@ -16,7 +14,7 @@ class SomeCleverNameConfig private $second; private $third; private $_usedProperties = []; - + /** * @default null * @param ParamConfigurator|mixed $value @@ -26,10 +24,10 @@ public function first($value): self { $this->_usedProperties['first'] = true; $this->first = $value; - + return $this; } - + /** * @default null * @param ParamConfigurator|mixed $value @@ -39,10 +37,10 @@ public function second($value): self { $this->_usedProperties['second'] = true; $this->second = $value; - + return $this; } - + /** * @default null * @param ParamConfigurator|mixed $value @@ -52,36 +50,35 @@ public function third($value): self { $this->_usedProperties['third'] = true; $this->third = $value; - + return $this; } - + public function __construct(array $value = []) { - if (array_key_exists('first', $value)) { $this->_usedProperties['first'] = true; $this->first = $value['first']; unset($value['first']); } - + if (array_key_exists('second', $value)) { $this->_usedProperties['second'] = true; $this->second = $value['second']; unset($value['second']); } - + if (array_key_exists('third', $value)) { $this->_usedProperties['third'] = true; $this->third = $value['third']; unset($value['third']); } - + if ([] !== $value) { throw new InvalidConfigurationException(sprintf('The following keys are not supported by "%s": ', __CLASS__).implode(', ', array_keys($value))); } } - + public function toArray(): array { $output = []; @@ -94,7 +91,7 @@ public function toArray(): array if (isset($this->_usedProperties['third'])) { $output['third'] = $this->third; } - + return $output; } diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues/Symfony/Config/NodeInitialValuesConfig.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues/Symfony/Config/NodeInitialValuesConfig.php index 1ba307fb491eb..d019ecf81bb76 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues/Symfony/Config/NodeInitialValuesConfig.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues/Symfony/Config/NodeInitialValuesConfig.php @@ -7,7 +7,6 @@ use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; - /** * This class is automatically generated to help in creating a config. */ @@ -16,56 +15,55 @@ class NodeInitialValuesConfig implements \Symfony\Component\Config\Builder\Confi private $someCleverName; private $messenger; private $_usedProperties = []; - + public function someCleverName(array $value = []): \Symfony\Config\NodeInitialValues\SomeCleverNameConfig { if (null === $this->someCleverName) { $this->_usedProperties['someCleverName'] = true; $this->someCleverName = new \Symfony\Config\NodeInitialValues\SomeCleverNameConfig($value); - } elseif ([] !== $value) { + } elseif (0 < \func_num_args()) { throw new InvalidConfigurationException('The node created by "someCleverName()" has already been initialized. You cannot pass values the second time you call someCleverName().'); } - + return $this->someCleverName; } - + public function messenger(array $value = []): \Symfony\Config\NodeInitialValues\MessengerConfig { if (null === $this->messenger) { $this->_usedProperties['messenger'] = true; $this->messenger = new \Symfony\Config\NodeInitialValues\MessengerConfig($value); - } elseif ([] !== $value) { + } elseif (0 < \func_num_args()) { throw new InvalidConfigurationException('The node created by "messenger()" has already been initialized. You cannot pass values the second time you call messenger().'); } - + return $this->messenger; } - + public function getExtensionAlias(): string { return 'node_initial_values'; } - + public function __construct(array $value = []) { - if (array_key_exists('some_clever_name', $value)) { $this->_usedProperties['someCleverName'] = true; $this->someCleverName = new \Symfony\Config\NodeInitialValues\SomeCleverNameConfig($value['some_clever_name']); unset($value['some_clever_name']); } - + if (array_key_exists('messenger', $value)) { $this->_usedProperties['messenger'] = true; $this->messenger = new \Symfony\Config\NodeInitialValues\MessengerConfig($value['messenger']); unset($value['messenger']); } - + if ([] !== $value) { throw new InvalidConfigurationException(sprintf('The following keys are not supported by "%s": ', __CLASS__).implode(', ', array_keys($value))); } } - + public function toArray(): array { $output = []; @@ -75,7 +73,7 @@ public function toArray(): array if (isset($this->_usedProperties['messenger'])) { $output['messenger'] = $this->messenger->toArray(); } - + return $output; } diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/Placeholders.config.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/Placeholders.config.php index 089252f32f341..8e6b5ae3f90d0 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/Placeholders.config.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/Placeholders.config.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\DependencyInjection\Loader\Configurator; use Symfony\Config\PlaceholdersConfig; diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/Placeholders.output.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/Placeholders.output.php index ca27298c368e9..56a5af670807a 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/Placeholders.output.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/Placeholders.output.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + return [ 'enabled' => '%env(bool:FOO_ENABLED)%', 'favorite_float' => '%eulers_number%', diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/Placeholders.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/Placeholders.php index 78baa477355e7..5d9c680daaced 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/Placeholders.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/Placeholders.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\Config\Tests\Builder\Fixtures; use Symfony\Component\Config\Definition\Builder\TreeBuilder; @@ -15,7 +24,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->children() ->booleanNode('enabled')->defaultFalse()->end() ->floatNode('favorite_float')->end() - ->arrayNode('good_integers') + ->arrayNode('good_integers') ->integerPrototype()->end() ->end() ->end() diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/Placeholders/Symfony/Config/PlaceholdersConfig.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/Placeholders/Symfony/Config/PlaceholdersConfig.php index 15fe9b492270d..6b5872c9f0254 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/Placeholders/Symfony/Config/PlaceholdersConfig.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/Placeholders/Symfony/Config/PlaceholdersConfig.php @@ -2,11 +2,9 @@ namespace Symfony\Config; - use Symfony\Component\Config\Loader\ParamConfigurator; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; - /** * This class is automatically generated to help in creating a config. */ @@ -16,7 +14,7 @@ class PlaceholdersConfig implements \Symfony\Component\Config\Builder\ConfigBuil private $favoriteFloat; private $goodIntegers; private $_usedProperties = []; - + /** * @default false * @param ParamConfigurator|bool $value @@ -26,10 +24,10 @@ public function enabled($value): self { $this->_usedProperties['enabled'] = true; $this->enabled = $value; - + return $this; } - + /** * @default null * @param ParamConfigurator|float $value @@ -39,10 +37,10 @@ public function favoriteFloat($value): self { $this->_usedProperties['favoriteFloat'] = true; $this->favoriteFloat = $value; - + return $this; } - + /** * @param ParamConfigurator|list $value * @return $this @@ -51,41 +49,40 @@ public function goodIntegers($value): self { $this->_usedProperties['goodIntegers'] = true; $this->goodIntegers = $value; - + return $this; } - + public function getExtensionAlias(): string { return 'placeholders'; } - + public function __construct(array $value = []) { - if (array_key_exists('enabled', $value)) { $this->_usedProperties['enabled'] = true; $this->enabled = $value['enabled']; unset($value['enabled']); } - + if (array_key_exists('favorite_float', $value)) { $this->_usedProperties['favoriteFloat'] = true; $this->favoriteFloat = $value['favorite_float']; unset($value['favorite_float']); } - + if (array_key_exists('good_integers', $value)) { $this->_usedProperties['goodIntegers'] = true; $this->goodIntegers = $value['good_integers']; unset($value['good_integers']); } - + if ([] !== $value) { throw new InvalidConfigurationException(sprintf('The following keys are not supported by "%s": ', __CLASS__).implode(', ', array_keys($value))); } } - + public function toArray(): array { $output = []; @@ -98,7 +95,7 @@ public function toArray(): array if (isset($this->_usedProperties['goodIntegers'])) { $output['good_integers'] = $this->goodIntegers; } - + return $output; } diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/PrimitiveTypes.config.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/PrimitiveTypes.config.php index b4498957057c4..fd4c0ce5f2cfa 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/PrimitiveTypes.config.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/PrimitiveTypes.config.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + use Symfony\Config\PrimitiveTypesConfig; return static function (PrimitiveTypesConfig $config) { diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/PrimitiveTypes.output.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/PrimitiveTypes.output.php index 366fd5c19f4cb..867e387dbf3c2 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/PrimitiveTypes.output.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/PrimitiveTypes.output.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + return [ 'boolean_node' => true, 'enum_node' => 'foo', diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/PrimitiveTypes.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/PrimitiveTypes.php index 3d36f72bff2db..6ffcf5a4ef533 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/PrimitiveTypes.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/PrimitiveTypes.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\Config\Tests\Builder\Fixtures; use Symfony\Component\Config\Definition\Builder\TreeBuilder; diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/PrimitiveTypes/Symfony/Config/PrimitiveTypesConfig.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/PrimitiveTypes/Symfony/Config/PrimitiveTypesConfig.php index 8a1be4e46a204..9c56f26d91cf9 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/PrimitiveTypes/Symfony/Config/PrimitiveTypesConfig.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/PrimitiveTypes/Symfony/Config/PrimitiveTypesConfig.php @@ -2,11 +2,9 @@ namespace Symfony\Config; - use Symfony\Component\Config\Loader\ParamConfigurator; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; - /** * This class is automatically generated to help in creating a config. */ @@ -19,7 +17,7 @@ class PrimitiveTypesConfig implements \Symfony\Component\Config\Builder\ConfigBu private $scalarNode; private $scalarNodeWithDefault; private $_usedProperties = []; - + /** * @default null * @param ParamConfigurator|bool $value @@ -29,10 +27,10 @@ public function booleanNode($value): self { $this->_usedProperties['booleanNode'] = true; $this->booleanNode = $value; - + return $this; } - + /** * @default null * @param ParamConfigurator|'foo'|'bar'|'baz' $value @@ -42,10 +40,10 @@ public function enumNode($value): self { $this->_usedProperties['enumNode'] = true; $this->enumNode = $value; - + return $this; } - + /** * @default null * @param ParamConfigurator|float $value @@ -55,10 +53,10 @@ public function floatNode($value): self { $this->_usedProperties['floatNode'] = true; $this->floatNode = $value; - + return $this; } - + /** * @default null * @param ParamConfigurator|int $value @@ -68,10 +66,10 @@ public function integerNode($value): self { $this->_usedProperties['integerNode'] = true; $this->integerNode = $value; - + return $this; } - + /** * @default null * @param ParamConfigurator|mixed $value @@ -81,10 +79,10 @@ public function scalarNode($value): self { $this->_usedProperties['scalarNode'] = true; $this->scalarNode = $value; - + return $this; } - + /** * @default true * @param ParamConfigurator|mixed $value @@ -94,59 +92,58 @@ public function scalarNodeWithDefault($value): self { $this->_usedProperties['scalarNodeWithDefault'] = true; $this->scalarNodeWithDefault = $value; - + return $this; } - + public function getExtensionAlias(): string { return 'primitive_types'; } - + public function __construct(array $value = []) { - if (array_key_exists('boolean_node', $value)) { $this->_usedProperties['booleanNode'] = true; $this->booleanNode = $value['boolean_node']; unset($value['boolean_node']); } - + if (array_key_exists('enum_node', $value)) { $this->_usedProperties['enumNode'] = true; $this->enumNode = $value['enum_node']; unset($value['enum_node']); } - + if (array_key_exists('float_node', $value)) { $this->_usedProperties['floatNode'] = true; $this->floatNode = $value['float_node']; unset($value['float_node']); } - + if (array_key_exists('integer_node', $value)) { $this->_usedProperties['integerNode'] = true; $this->integerNode = $value['integer_node']; unset($value['integer_node']); } - + if (array_key_exists('scalar_node', $value)) { $this->_usedProperties['scalarNode'] = true; $this->scalarNode = $value['scalar_node']; unset($value['scalar_node']); } - + if (array_key_exists('scalar_node_with_default', $value)) { $this->_usedProperties['scalarNodeWithDefault'] = true; $this->scalarNodeWithDefault = $value['scalar_node_with_default']; unset($value['scalar_node_with_default']); } - + if ([] !== $value) { throw new InvalidConfigurationException(sprintf('The following keys are not supported by "%s": ', __CLASS__).implode(', ', array_keys($value))); } } - + public function toArray(): array { $output = []; @@ -168,7 +165,7 @@ public function toArray(): array if (isset($this->_usedProperties['scalarNodeWithDefault'])) { $output['scalar_node_with_default'] = $this->scalarNodeWithDefault; } - + return $output; } diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes.config.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes.config.php new file mode 100644 index 0000000000000..d0c6d320709c2 --- /dev/null +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes.config.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Config\ScalarNormalizedTypesConfig; + +return static function (ScalarNormalizedTypesConfig $config) { + $config + ->simpleArray('foo') + ->keyedArray('key', 'value') + ->object(true) + ->listObject('bar') + ->listObject('baz') + ->listObject()->name('qux'); + + $config + ->keyedListObject('Foo\\Bar', true) + ->keyedListObject('Foo\\Baz')->settings(['one', 'two']); + + $config->nested([ + 'nested_object' => true, + 'nested_list_object' => ['one', 'two'], + ]); +}; diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes.output.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes.output.php new file mode 100644 index 0000000000000..9dcf3a9f3d335 --- /dev/null +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes.output.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +return [ + 'simple_array' => 'foo', + 'keyed_array' => [ + 'key' => 'value', + ], + 'object' => true, + 'list_object' => [ + 'bar', + 'baz', + ['name' => 'qux'], + ], + 'keyed_list_object' => [ + 'Foo\\Bar' => true, + 'Foo\\Baz' => [ + 'settings' => ['one', 'two'], + ], + ], + 'nested' => [ + 'nested_object' => true, + 'nested_list_object' => ['one', 'two'], + ], +]; diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes.php new file mode 100644 index 0000000000000..2c33797427eed --- /dev/null +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes.php @@ -0,0 +1,144 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Tests\Builder\Fixtures; + +use Symfony\Component\Config\Definition\Builder\TreeBuilder; +use Symfony\Component\Config\Definition\ConfigurationInterface; + +class ScalarNormalizedTypes implements ConfigurationInterface +{ + public function getConfigTreeBuilder(): TreeBuilder + { + $tb = new TreeBuilder('scalar_normalized_types'); + $rootNode = $tb->getRootNode(); + $rootNode + ->children() + ->arrayNode('simple_array') + ->beforeNormalization()->ifString()->then(function ($v) { return [$v]; })->end() + ->prototype('scalar')->end() + ->end() + ->arrayNode('keyed_array') + ->useAttributeAsKey('name') + ->prototype('array') + ->beforeNormalization() + ->ifString()->then(function ($v) { return [$v]; }) + ->end() + ->prototype('scalar')->end() + ->end() + ->end() + ->arrayNode('object') + ->addDefaultsIfNotSet() + ->beforeNormalization() + ->ifTrue(function ($v) { return !\is_array($v); }) + ->then(function ($v) { return ['enabled' => $v]; }) + ->end() + ->children() + ->booleanNode('enabled')->defaultNull()->end() + ->scalarNode('date_format')->end() + ->booleanNode('remove_used_context_fields')->end() + ->end() + ->end() + ->arrayNode('list_object') + ->beforeNormalization() + ->always() + ->then(function ($values) { + // inspired by Workflow places + if (isset($values[0]) && \is_string($values[0])) { + return array_map(function (string $value) { + return ['name' => $value]; + }, $values); + } + + if (isset($values[0]) && \is_array($values[0])) { + return $values; + } + + foreach ($values as $name => $value) { + if (\is_array($value) && \array_key_exists('name', $value)) { + continue; + } + $value['name'] = $name; + $values[$name] = $value; + } + + return array_values($values); + }) + ->end() + ->isRequired() + ->requiresAtLeastOneElement() + ->prototype('array') + ->children() + ->scalarNode('name') + ->isRequired() + ->cannotBeEmpty() + ->end() + ->arrayNode('data') + ->normalizeKeys(false) + ->defaultValue([]) + ->prototype('variable')->end() + ->end() + ->end() + ->end() + ->end() + ->arrayNode('keyed_list_object') + ->useAttributeAsKey('class') + ->prototype('array') + ->beforeNormalization() + ->ifTrue(function ($v) { return !\is_array($v); }) + ->then(function ($v) { return ['enabled' => $v]; }) + ->end() + ->children() + ->booleanNode('enabled')->defaultTrue()->end() + ->arrayNode('settings') + ->prototype('scalar')->end() + ->end() + ->end() + ->end() + ->end() + ->arrayNode('nested') + ->children() + ->arrayNode('nested_object') + ->addDefaultsIfNotSet() + ->beforeNormalization() + ->ifTrue(function ($v) { return !\is_array($v); }) + ->then(function ($v) { return ['enabled' => $v]; }) + ->end() + ->children() + ->booleanNode('enabled')->defaultNull()->end() + ->end() + ->end() + ->arrayNode('nested_list_object') + ->beforeNormalization() + ->ifTrue(function ($v) { return isset($v[0]) && \is_string($v[0]); }) + ->then(function ($values) { + return array_map(function (string $value) { + return ['name' => $value]; + }, $values); + }) + ->end() + ->prototype('array') + ->children() + ->scalarNode('name') + ->isRequired() + ->cannotBeEmpty() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ; + + return $tb; + } +} diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes/Symfony/Config/ScalarNormalizedTypes/KeyedListObjectConfig.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes/Symfony/Config/ScalarNormalizedTypes/KeyedListObjectConfig.php new file mode 100644 index 0000000000000..b9f25b224bcc2 --- /dev/null +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes/Symfony/Config/ScalarNormalizedTypes/KeyedListObjectConfig.php @@ -0,0 +1,74 @@ +_usedProperties['enabled'] = true; + $this->enabled = $value; + + return $this; + } + + /** + * @param ParamConfigurator|list $value + * @return $this + */ + public function settings($value): self + { + $this->_usedProperties['settings'] = true; + $this->settings = $value; + + return $this; + } + + public function __construct(array $value = []) + { + if (array_key_exists('enabled', $value)) { + $this->_usedProperties['enabled'] = true; + $this->enabled = $value['enabled']; + unset($value['enabled']); + } + + if (array_key_exists('settings', $value)) { + $this->_usedProperties['settings'] = true; + $this->settings = $value['settings']; + unset($value['settings']); + } + + if ([] !== $value) { + throw new InvalidConfigurationException(sprintf('The following keys are not supported by "%s": ', __CLASS__).implode(', ', array_keys($value))); + } + } + + public function toArray(): array + { + $output = []; + if (isset($this->_usedProperties['enabled'])) { + $output['enabled'] = $this->enabled; + } + if (isset($this->_usedProperties['settings'])) { + $output['settings'] = $this->settings; + } + + return $output; + } + +} diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes/Symfony/Config/ScalarNormalizedTypes/ListObjectConfig.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes/Symfony/Config/ScalarNormalizedTypes/ListObjectConfig.php new file mode 100644 index 0000000000000..969f4685c507d --- /dev/null +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes/Symfony/Config/ScalarNormalizedTypes/ListObjectConfig.php @@ -0,0 +1,74 @@ +_usedProperties['name'] = true; + $this->name = $value; + + return $this; + } + + /** + * @param ParamConfigurator|list $value + * @return $this + */ + public function data($value): self + { + $this->_usedProperties['data'] = true; + $this->data = $value; + + return $this; + } + + public function __construct(array $value = []) + { + if (array_key_exists('name', $value)) { + $this->_usedProperties['name'] = true; + $this->name = $value['name']; + unset($value['name']); + } + + if (array_key_exists('data', $value)) { + $this->_usedProperties['data'] = true; + $this->data = $value['data']; + unset($value['data']); + } + + if ([] !== $value) { + throw new InvalidConfigurationException(sprintf('The following keys are not supported by "%s": ', __CLASS__).implode(', ', array_keys($value))); + } + } + + public function toArray(): array + { + $output = []; + if (isset($this->_usedProperties['name'])) { + $output['name'] = $this->name; + } + if (isset($this->_usedProperties['data'])) { + $output['data'] = $this->data; + } + + return $output; + } + +} diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes/Symfony/Config/ScalarNormalizedTypes/Nested/NestedListObjectConfig.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes/Symfony/Config/ScalarNormalizedTypes/Nested/NestedListObjectConfig.php new file mode 100644 index 0000000000000..0815c1faaa5e0 --- /dev/null +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes/Symfony/Config/ScalarNormalizedTypes/Nested/NestedListObjectConfig.php @@ -0,0 +1,52 @@ +_usedProperties['name'] = true; + $this->name = $value; + + return $this; + } + + public function __construct(array $value = []) + { + if (array_key_exists('name', $value)) { + $this->_usedProperties['name'] = true; + $this->name = $value['name']; + unset($value['name']); + } + + if ([] !== $value) { + throw new InvalidConfigurationException(sprintf('The following keys are not supported by "%s": ', __CLASS__).implode(', ', array_keys($value))); + } + } + + public function toArray(): array + { + $output = []; + if (isset($this->_usedProperties['name'])) { + $output['name'] = $this->name; + } + + return $output; + } + +} diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes/Symfony/Config/ScalarNormalizedTypes/Nested/NestedObjectConfig.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes/Symfony/Config/ScalarNormalizedTypes/Nested/NestedObjectConfig.php new file mode 100644 index 0000000000000..279652138872e --- /dev/null +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes/Symfony/Config/ScalarNormalizedTypes/Nested/NestedObjectConfig.php @@ -0,0 +1,52 @@ +_usedProperties['enabled'] = true; + $this->enabled = $value; + + return $this; + } + + public function __construct(array $value = []) + { + if (array_key_exists('enabled', $value)) { + $this->_usedProperties['enabled'] = true; + $this->enabled = $value['enabled']; + unset($value['enabled']); + } + + if ([] !== $value) { + throw new InvalidConfigurationException(sprintf('The following keys are not supported by "%s": ', __CLASS__).implode(', ', array_keys($value))); + } + } + + public function toArray(): array + { + $output = []; + if (isset($this->_usedProperties['enabled'])) { + $output['enabled'] = $this->enabled; + } + + return $output; + } + +} diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes/Symfony/Config/ScalarNormalizedTypes/NestedConfig.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes/Symfony/Config/ScalarNormalizedTypes/NestedConfig.php new file mode 100644 index 0000000000000..5774fdbb83821 --- /dev/null +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes/Symfony/Config/ScalarNormalizedTypes/NestedConfig.php @@ -0,0 +1,88 @@ +_usedProperties['nestedObject'] = true; + $this->nestedObject = $value; + + return $this; + } + + if (!$this->nestedObject instanceof \Symfony\Config\ScalarNormalizedTypes\Nested\NestedObjectConfig) { + $this->_usedProperties['nestedObject'] = true; + $this->nestedObject = new \Symfony\Config\ScalarNormalizedTypes\Nested\NestedObjectConfig($value); + } elseif (0 < \func_num_args()) { + throw new InvalidConfigurationException('The node created by "nestedObject()" has already been initialized. You cannot pass values the second time you call nestedObject().'); + } + + return $this->nestedObject; + } + + /** + * @return \Symfony\Config\ScalarNormalizedTypes\Nested\NestedListObjectConfig|$this + */ + public function nestedListObject($value = []) + { + $this->_usedProperties['nestedListObject'] = true; + if (!\is_array($value)) { + $this->nestedListObject[] = $value; + + return $this; + } + + return $this->nestedListObject[] = new \Symfony\Config\ScalarNormalizedTypes\Nested\NestedListObjectConfig($value); + } + + public function __construct(array $value = []) + { + if (array_key_exists('nested_object', $value)) { + $this->_usedProperties['nestedObject'] = true; + $this->nestedObject = \is_array($value['nested_object']) ? new \Symfony\Config\ScalarNormalizedTypes\Nested\NestedObjectConfig($value['nested_object']) : $value['nested_object']; + unset($value['nested_object']); + } + + if (array_key_exists('nested_list_object', $value)) { + $this->_usedProperties['nestedListObject'] = true; + $this->nestedListObject = array_map(function ($v) { return \is_array($v) ? new \Symfony\Config\ScalarNormalizedTypes\Nested\NestedListObjectConfig($v) : $v; }, $value['nested_list_object']); + unset($value['nested_list_object']); + } + + if ([] !== $value) { + throw new InvalidConfigurationException(sprintf('The following keys are not supported by "%s": ', __CLASS__).implode(', ', array_keys($value))); + } + } + + public function toArray(): array + { + $output = []; + if (isset($this->_usedProperties['nestedObject'])) { + $output['nested_object'] = $this->nestedObject instanceof \Symfony\Config\ScalarNormalizedTypes\Nested\NestedObjectConfig ? $this->nestedObject->toArray() : $this->nestedObject; + } + if (isset($this->_usedProperties['nestedListObject'])) { + $output['nested_list_object'] = array_map(function ($v) { return $v instanceof \Symfony\Config\ScalarNormalizedTypes\Nested\NestedListObjectConfig ? $v->toArray() : $v; }, $this->nestedListObject); + } + + return $output; + } + +} diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes/Symfony/Config/ScalarNormalizedTypes/ObjectConfig.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes/Symfony/Config/ScalarNormalizedTypes/ObjectConfig.php new file mode 100644 index 0000000000000..74d0b6ee9bb3d --- /dev/null +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes/Symfony/Config/ScalarNormalizedTypes/ObjectConfig.php @@ -0,0 +1,98 @@ +_usedProperties['enabled'] = true; + $this->enabled = $value; + + return $this; + } + + /** + * @default null + * @param ParamConfigurator|mixed $value + * @return $this + */ + public function dateFormat($value): self + { + $this->_usedProperties['dateFormat'] = true; + $this->dateFormat = $value; + + return $this; + } + + /** + * @default null + * @param ParamConfigurator|bool $value + * @return $this + */ + public function removeUsedContextFields($value): self + { + $this->_usedProperties['removeUsedContextFields'] = true; + $this->removeUsedContextFields = $value; + + return $this; + } + + public function __construct(array $value = []) + { + if (array_key_exists('enabled', $value)) { + $this->_usedProperties['enabled'] = true; + $this->enabled = $value['enabled']; + unset($value['enabled']); + } + + if (array_key_exists('date_format', $value)) { + $this->_usedProperties['dateFormat'] = true; + $this->dateFormat = $value['date_format']; + unset($value['date_format']); + } + + if (array_key_exists('remove_used_context_fields', $value)) { + $this->_usedProperties['removeUsedContextFields'] = true; + $this->removeUsedContextFields = $value['remove_used_context_fields']; + unset($value['remove_used_context_fields']); + } + + if ([] !== $value) { + throw new InvalidConfigurationException(sprintf('The following keys are not supported by "%s": ', __CLASS__).implode(', ', array_keys($value))); + } + } + + public function toArray(): array + { + $output = []; + if (isset($this->_usedProperties['enabled'])) { + $output['enabled'] = $this->enabled; + } + if (isset($this->_usedProperties['dateFormat'])) { + $output['date_format'] = $this->dateFormat; + } + if (isset($this->_usedProperties['removeUsedContextFields'])) { + $output['remove_used_context_fields'] = $this->removeUsedContextFields; + } + + return $output; + } + +} diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes/Symfony/Config/ScalarNormalizedTypesConfig.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes/Symfony/Config/ScalarNormalizedTypesConfig.php new file mode 100644 index 0000000000000..5ba2f1112ba10 --- /dev/null +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes/Symfony/Config/ScalarNormalizedTypesConfig.php @@ -0,0 +1,194 @@ + $value + * @return $this + */ + public function simpleArray($value): self + { + $this->_usedProperties['simpleArray'] = true; + $this->simpleArray = $value; + + return $this; + } + + /** + * @param ParamConfigurator|array $value + * @return $this + */ + public function keyedArray(string $name, $value): self + { + $this->_usedProperties['keyedArray'] = true; + $this->keyedArray[$name] = $value; + + return $this; + } + + /** + * @return \Symfony\Config\ScalarNormalizedTypes\ObjectConfig|$this + */ + public function object($value = []) + { + if (!\is_array($value)) { + $this->_usedProperties['object'] = true; + $this->object = $value; + + return $this; + } + + if (!$this->object instanceof \Symfony\Config\ScalarNormalizedTypes\ObjectConfig) { + $this->_usedProperties['object'] = true; + $this->object = new \Symfony\Config\ScalarNormalizedTypes\ObjectConfig($value); + } elseif (0 < \func_num_args()) { + throw new InvalidConfigurationException('The node created by "object()" has already been initialized. You cannot pass values the second time you call object().'); + } + + return $this->object; + } + + /** + * @return \Symfony\Config\ScalarNormalizedTypes\ListObjectConfig|$this + */ + public function listObject($value = []) + { + $this->_usedProperties['listObject'] = true; + if (!\is_array($value)) { + $this->listObject[] = $value; + + return $this; + } + + return $this->listObject[] = new \Symfony\Config\ScalarNormalizedTypes\ListObjectConfig($value); + } + + /** + * @return \Symfony\Config\ScalarNormalizedTypes\KeyedListObjectConfig|$this + */ + public function keyedListObject(string $class, $value = []) + { + if (!\is_array($value)) { + $this->_usedProperties['keyedListObject'] = true; + $this->keyedListObject[$class] = $value; + + return $this; + } + + if (!isset($this->keyedListObject[$class]) || !$this->keyedListObject[$class] instanceof \Symfony\Config\ScalarNormalizedTypes\KeyedListObjectConfig) { + $this->_usedProperties['keyedListObject'] = true; + $this->keyedListObject[$class] = new \Symfony\Config\ScalarNormalizedTypes\KeyedListObjectConfig($value); + } elseif (1 < \func_num_args()) { + throw new InvalidConfigurationException('The node created by "keyedListObject()" has already been initialized. You cannot pass values the second time you call keyedListObject().'); + } + + return $this->keyedListObject[$class]; + } + + public function nested(array $value = []): \Symfony\Config\ScalarNormalizedTypes\NestedConfig + { + if (null === $this->nested) { + $this->_usedProperties['nested'] = true; + $this->nested = new \Symfony\Config\ScalarNormalizedTypes\NestedConfig($value); + } elseif (0 < \func_num_args()) { + throw new InvalidConfigurationException('The node created by "nested()" has already been initialized. You cannot pass values the second time you call nested().'); + } + + return $this->nested; + } + + public function getExtensionAlias(): string + { + return 'scalar_normalized_types'; + } + + public function __construct(array $value = []) + { + if (array_key_exists('simple_array', $value)) { + $this->_usedProperties['simpleArray'] = true; + $this->simpleArray = $value['simple_array']; + unset($value['simple_array']); + } + + if (array_key_exists('keyed_array', $value)) { + $this->_usedProperties['keyedArray'] = true; + $this->keyedArray = $value['keyed_array']; + unset($value['keyed_array']); + } + + if (array_key_exists('object', $value)) { + $this->_usedProperties['object'] = true; + $this->object = \is_array($value['object']) ? new \Symfony\Config\ScalarNormalizedTypes\ObjectConfig($value['object']) : $value['object']; + unset($value['object']); + } + + if (array_key_exists('list_object', $value)) { + $this->_usedProperties['listObject'] = true; + $this->listObject = array_map(function ($v) { return \is_array($v) ? new \Symfony\Config\ScalarNormalizedTypes\ListObjectConfig($v) : $v; }, $value['list_object']); + unset($value['list_object']); + } + + if (array_key_exists('keyed_list_object', $value)) { + $this->_usedProperties['keyedListObject'] = true; + $this->keyedListObject = array_map(function ($v) { return \is_array($v) ? new \Symfony\Config\ScalarNormalizedTypes\KeyedListObjectConfig($v) : $v; }, $value['keyed_list_object']); + unset($value['keyed_list_object']); + } + + if (array_key_exists('nested', $value)) { + $this->_usedProperties['nested'] = true; + $this->nested = new \Symfony\Config\ScalarNormalizedTypes\NestedConfig($value['nested']); + unset($value['nested']); + } + + if ([] !== $value) { + throw new InvalidConfigurationException(sprintf('The following keys are not supported by "%s": ', __CLASS__).implode(', ', array_keys($value))); + } + } + + public function toArray(): array + { + $output = []; + if (isset($this->_usedProperties['simpleArray'])) { + $output['simple_array'] = $this->simpleArray; + } + if (isset($this->_usedProperties['keyedArray'])) { + $output['keyed_array'] = $this->keyedArray; + } + if (isset($this->_usedProperties['object'])) { + $output['object'] = $this->object instanceof \Symfony\Config\ScalarNormalizedTypes\ObjectConfig ? $this->object->toArray() : $this->object; + } + if (isset($this->_usedProperties['listObject'])) { + $output['list_object'] = array_map(function ($v) { return $v instanceof \Symfony\Config\ScalarNormalizedTypes\ListObjectConfig ? $v->toArray() : $v; }, $this->listObject); + } + if (isset($this->_usedProperties['keyedListObject'])) { + $output['keyed_list_object'] = array_map(function ($v) { return $v instanceof \Symfony\Config\ScalarNormalizedTypes\KeyedListObjectConfig ? $v->toArray() : $v; }, $this->keyedListObject); + } + if (isset($this->_usedProperties['nested'])) { + $output['nested'] = $this->nested->toArray(); + } + + return $output; + } + +} diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/VariableType.config.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/VariableType.config.php index 10b70bf6d26f9..ed65c1f2c36cb 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/VariableType.config.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/VariableType.config.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + use Symfony\Config\VariableTypeConfig; return static function (VariableTypeConfig $config) { diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/VariableType.output.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/VariableType.output.php index 87c38a584a28a..71ef3f426a98a 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/VariableType.output.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/VariableType.output.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + return [ 'any_value' => 'foobar', ]; diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/VariableType.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/VariableType.php index c3fa62cfd9e49..11a8bea6f8674 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/VariableType.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/VariableType.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\Config\Tests\Builder\Fixtures; use Symfony\Component\Config\Definition\Builder\TreeBuilder; diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/VariableType/Symfony/Config/VariableTypeConfig.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/VariableType/Symfony/Config/VariableTypeConfig.php index a36bf5f31c966..078226d25946f 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/VariableType/Symfony/Config/VariableTypeConfig.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/VariableType/Symfony/Config/VariableTypeConfig.php @@ -2,11 +2,9 @@ namespace Symfony\Config; - use Symfony\Component\Config\Loader\ParamConfigurator; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; - /** * This class is automatically generated to help in creating a config. */ @@ -14,7 +12,7 @@ class VariableTypeConfig implements \Symfony\Component\Config\Builder\ConfigBuil { private $anyValue; private $_usedProperties = []; - + /** * @default null * @param ParamConfigurator|mixed $value @@ -24,36 +22,35 @@ public function anyValue($value): self { $this->_usedProperties['anyValue'] = true; $this->anyValue = $value; - + return $this; } - + public function getExtensionAlias(): string { return 'variable_type'; } - + public function __construct(array $value = []) { - if (array_key_exists('any_value', $value)) { $this->_usedProperties['anyValue'] = true; $this->anyValue = $value['any_value']; unset($value['any_value']); } - + if ([] !== $value) { throw new InvalidConfigurationException(sprintf('The following keys are not supported by "%s": ', __CLASS__).implode(', ', array_keys($value))); } } - + public function toArray(): array { $output = []; if (isset($this->_usedProperties['anyValue'])) { $output['any_value'] = $this->anyValue; } - + return $output; } diff --git a/src/Symfony/Component/Config/Tests/Builder/GeneratedConfigTest.php b/src/Symfony/Component/Config/Tests/Builder/GeneratedConfigTest.php index 9def6476b57dd..03f5cd43db87d 100644 --- a/src/Symfony/Component/Config/Tests/Builder/GeneratedConfigTest.php +++ b/src/Symfony/Component/Config/Tests/Builder/GeneratedConfigTest.php @@ -56,6 +56,7 @@ protected function tearDown(): void public function fixtureNames() { $array = [ + 'ScalarNormalizedTypes' => 'scalar_normalized_types', 'PrimitiveTypes' => 'primitive_types', 'VariableType' => 'variable_type', 'AddToList' => 'add_to_list', diff --git a/src/Symfony/Component/Config/Tests/Fixtures/some.phar b/src/Symfony/Component/Config/Tests/Fixtures/some.phar new file mode 100644 index 0000000000000..93d4e87c0b89b Binary files /dev/null and b/src/Symfony/Component/Config/Tests/Fixtures/some.phar differ diff --git a/src/Symfony/Component/Config/Tests/Resource/GlobResourceTest.php b/src/Symfony/Component/Config/Tests/Resource/GlobResourceTest.php index fd317eb55c067..96953f3e32b8c 100644 --- a/src/Symfony/Component/Config/Tests/Resource/GlobResourceTest.php +++ b/src/Symfony/Component/Config/Tests/Resource/GlobResourceTest.php @@ -208,4 +208,29 @@ public function testSerializeUnserialize() $this->assertEquals($p->getValue($resource), $p->getValue($newResource)); } + + public function testPhar() + { + $s = \DIRECTORY_SEPARATOR; + $cwd = getcwd(); + chdir(\dirname(__DIR__).'/Fixtures'); + try { + $resource = new GlobResource('phar://some.phar', '*', true); + $files = array_keys(iterator_to_array($resource)); + $this->assertSame(["phar://some.phar{$s}ProjectWithXsdExtensionInPhar.php", "phar://some.phar{$s}schema{$s}project-1.0.xsd"], $files); + + $resource = new GlobResource("phar://some.phar{$s}ProjectWithXsdExtensionInPhar.php", '', true); + $files = array_keys(iterator_to_array($resource)); + $this->assertSame(["phar://some.phar{$s}ProjectWithXsdExtensionInPhar.php"], $files); + } finally { + chdir($cwd); + } + } + + public function testFilePrefix() + { + $resource = new GlobResource(__FILE__, '/**/', true); + $files = array_keys(iterator_to_array($resource)); + $this->assertSame([], $files); + } } diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index 3decfc04bb338..366d61204f710 100644 --- a/src/Symfony/Component/Console/Application.php +++ b/src/Symfony/Component/Console/Application.php @@ -363,9 +363,18 @@ public function complete(CompletionInput $input, CompletionSuggestions $suggesti CompletionInput::TYPE_ARGUMENT_VALUE === $input->getCompletionType() && 'command' === $input->getCompletionName() ) { - $suggestions->suggestValues(array_filter(array_map(function (Command $command) { - return $command->isHidden() ? null : $command->getName(); - }, $this->all()))); + $commandNames = []; + foreach ($this->all() as $name => $command) { + // skip hidden commands and aliased commands as they already get added below + if ($command->isHidden() || $command->getName() !== $name) { + continue; + } + $commandNames[] = $command->getName(); + foreach ($command->getAliases() as $name) { + $commandNames[] = $name; + } + } + $suggestions->suggestValues(array_filter($commandNames)); return; } diff --git a/src/Symfony/Component/Console/Command/CompleteCommand.php b/src/Symfony/Component/Console/Command/CompleteCommand.php index 97357d6737ed3..a94021ce46a27 100644 --- a/src/Symfony/Component/Console/Command/CompleteCommand.php +++ b/src/Symfony/Component/Console/Command/CompleteCommand.php @@ -105,11 +105,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int } elseif ( $completionInput->mustSuggestArgumentValuesFor('command') && $command->getName() !== $completionInput->getCompletionValue() + && !\in_array($completionInput->getCompletionValue(), $command->getAliases(), true) ) { $this->log(' No command found, completing using the Application class.'); // expand shortcut names ("cache:cl") into their full name ("cache:clear") - $suggestions->suggestValue($command->getName()); + $suggestions->suggestValues(array_filter(array_merge([$command->getName()], $command->getAliases()))); } else { $command->mergeApplicationDefinition(); $completionInput->bind($command->getDefinition()); diff --git a/src/Symfony/Component/Console/Command/DumpCompletionCommand.php b/src/Symfony/Component/Console/Command/DumpCompletionCommand.php index 64a8005301350..dc0cfaef7b589 100644 --- a/src/Symfony/Component/Console/Command/DumpCompletionCommand.php +++ b/src/Symfony/Component/Console/Command/DumpCompletionCommand.php @@ -41,7 +41,7 @@ protected function configure() { $fullCommand = $_SERVER['PHP_SELF']; $commandName = basename($fullCommand); - $fullCommand = realpath($fullCommand) ?: $fullCommand; + $fullCommand = @realpath($fullCommand) ?: $fullCommand; $this ->setHelp(<<%command.full_name% bash | sudo tee /etc/bash_completion.d/${commandName} + %command.full_name% bash | sudo tee /etc/bash_completion.d/{$commandName} Or dump the script to a local file and source it: @@ -70,7 +70,7 @@ protected function configure() Add this to the end of your shell configuration file (e.g. "~/.bashrc"): - eval "$(${fullCommand} completion bash)" + eval "$({$fullCommand} completion bash)" EOH ) ->addArgument('shell', InputArgument::OPTIONAL, 'The shell type (e.g. "bash"), the value of the "$SHELL" env var will be used if this is not given') diff --git a/src/Symfony/Component/Console/Completion/Output/BashCompletionOutput.php b/src/Symfony/Component/Console/Completion/Output/BashCompletionOutput.php index 8d5ffa6b67d4b..c6f76eb8fbd40 100644 --- a/src/Symfony/Component/Console/Completion/Output/BashCompletionOutput.php +++ b/src/Symfony/Component/Console/Completion/Output/BashCompletionOutput.php @@ -24,6 +24,9 @@ public function write(CompletionSuggestions $suggestions, OutputInterface $outpu $values = $suggestions->getValueSuggestions(); foreach ($suggestions->getOptionSuggestions() as $option) { $values[] = '--'.$option->getName(); + if ($option->isNegatable()) { + $values[] = '--no-'.$option->getName(); + } } $output->writeln(implode("\n", $values)); } diff --git a/src/Symfony/Component/Console/Helper/Table.php b/src/Symfony/Component/Console/Helper/Table.php index 067d4f1573c79..794c87b0f7b18 100644 --- a/src/Symfony/Component/Console/Helper/Table.php +++ b/src/Symfony/Component/Console/Helper/Table.php @@ -623,7 +623,7 @@ private function buildTableRows(array $rows): TableRows } $escaped = implode("\n", array_map([OutputFormatter::class, 'escapeTrailingBackslash'], explode("\n", $cell))); $cell = $cell instanceof TableCell ? new TableCell($escaped, ['colspan' => $cell->getColspan()]) : $escaped; - $lines = explode("\n", str_replace("\n", "\n", $cell)); + $lines = explode("\n", str_replace("\n", "\n", $cell)); foreach ($lines as $lineKey => $line) { if ($colspan > 1) { $line = new TableCell($line, ['colspan' => $colspan]); @@ -800,18 +800,18 @@ private function calculateColumnsWidth(iterable $groups) continue; } - foreach ($row as $i => $cell) { - if ($cell instanceof TableCell) { - $textContent = Helper::removeDecoration($this->output->getFormatter(), $cell); - $textLength = Helper::width($textContent); - if ($textLength > 0) { - $contentColumns = str_split($textContent, ceil($textLength / $cell->getColspan())); - foreach ($contentColumns as $position => $content) { - $row[$i + $position] = $content; + foreach ($row as $i => $cell) { + if ($cell instanceof TableCell) { + $textContent = Helper::removeDecoration($this->output->getFormatter(), $cell); + $textLength = Helper::width($textContent); + if ($textLength > 0) { + $contentColumns = str_split($textContent, ceil($textLength / $cell->getColspan())); + foreach ($contentColumns as $position => $content) { + $row[$i + $position] = $content; + } } } } - } $lengths[] = $this->getCellWidth($row, $column); } diff --git a/src/Symfony/Component/Console/Input/InputArgument.php b/src/Symfony/Component/Console/Input/InputArgument.php index e891b9443f747..ecfcdad58b907 100644 --- a/src/Symfony/Component/Console/Input/InputArgument.php +++ b/src/Symfony/Component/Console/Input/InputArgument.php @@ -92,7 +92,7 @@ public function isArray() */ public function setDefault($default = null) { - if (self::REQUIRED === $this->mode && null !== $default) { + if ($this->isRequired() && null !== $default) { throw new LogicException('Cannot set a default value except for InputArgument::OPTIONAL mode.'); } diff --git a/src/Symfony/Component/Console/Tests/Command/CompleteCommandTest.php b/src/Symfony/Component/Console/Tests/Command/CompleteCommandTest.php index 189928897cc7c..642d5c69a58ac 100644 --- a/src/Symfony/Component/Console/Tests/Command/CompleteCommandTest.php +++ b/src/Symfony/Component/Console/Tests/Command/CompleteCommandTest.php @@ -102,9 +102,10 @@ public function testCompleteCommandName(array $input, array $suggestions) public function provideCompleteCommandNameInputs() { - yield 'empty' => [['bin/console'], ['help', 'list', 'completion', 'hello']]; - yield 'partial' => [['bin/console', 'he'], ['help', 'list', 'completion', 'hello']]; - yield 'complete-shortcut-name' => [['bin/console', 'hell'], ['hello']]; + yield 'empty' => [['bin/console'], ['help', 'list', 'completion', 'hello', 'ahoy']]; + yield 'partial' => [['bin/console', 'he'], ['help', 'list', 'completion', 'hello', 'ahoy']]; + yield 'complete-shortcut-name' => [['bin/console', 'hell'], ['hello', 'ahoy']]; + yield 'complete-aliases' => [['bin/console', 'ah'], ['hello', 'ahoy']]; } /** @@ -118,8 +119,10 @@ public function testCompleteCommandInputDefinition(array $input, array $suggesti public function provideCompleteCommandInputDefinitionInputs() { - yield 'definition' => [['bin/console', 'hello', '-'], ['--help', '--quiet', '--verbose', '--version', '--ansi', '--no-interaction']]; + yield 'definition' => [['bin/console', 'hello', '-'], ['--help', '--quiet', '--verbose', '--version', '--ansi', '--no-ansi', '--no-interaction']]; yield 'custom' => [['bin/console', 'hello'], ['Fabien', 'Robin', 'Wouter']]; + yield 'definition-aliased' => [['bin/console', 'ahoy', '-'], ['--help', '--quiet', '--verbose', '--version', '--ansi', '--no-ansi', '--no-interaction']]; + yield 'custom-aliased' => [['bin/console', 'ahoy'], ['Fabien', 'Robin', 'Wouter']]; } private function execute(array $input) @@ -134,6 +137,7 @@ class CompleteCommandTest_HelloCommand extends Command public function configure(): void { $this->setName('hello') + ->setAliases(['ahoy']) ->addArgument('name', InputArgument::REQUIRED) ; } diff --git a/src/Symfony/Component/Console/Tests/Completion/Output/BashCompletionOutputTest.php b/src/Symfony/Component/Console/Tests/Completion/Output/BashCompletionOutputTest.php new file mode 100644 index 0000000000000..84ec56accbbfd --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Completion/Output/BashCompletionOutputTest.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Tests\Completion\Output; + +use Symfony\Component\Console\Completion\Output\BashCompletionOutput; +use Symfony\Component\Console\Completion\Output\CompletionOutputInterface; + +class BashCompletionOutputTest extends CompletionOutputTestCase +{ + public function getCompletionOutput(): CompletionOutputInterface + { + return new BashCompletionOutput(); + } + + public function getExpectedOptionsOutput(): string + { + return "--option1\n--negatable\n--no-negatable".\PHP_EOL; + } + + public function getExpectedValuesOutput(): string + { + return "Green\nRed\nYellow".\PHP_EOL; + } +} diff --git a/src/Symfony/Component/Console/Tests/Completion/Output/CompletionOutputTestCase.php b/src/Symfony/Component/Console/Tests/Completion/Output/CompletionOutputTestCase.php new file mode 100644 index 0000000000000..c4551e5b67b70 --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Completion/Output/CompletionOutputTestCase.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Tests\Completion\Output; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Completion\Output\CompletionOutputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\StreamOutput; + +abstract class CompletionOutputTestCase extends TestCase +{ + abstract public function getCompletionOutput(): CompletionOutputInterface; + + abstract public function getExpectedOptionsOutput(): string; + + abstract public function getExpectedValuesOutput(): string; + + public function testOptionsOutput() + { + $options = [ + new InputOption('option1', 'o', InputOption::VALUE_NONE), + new InputOption('negatable', null, InputOption::VALUE_NEGATABLE), + ]; + $suggestions = new CompletionSuggestions(); + $suggestions->suggestOptions($options); + $stream = fopen('php://memory', 'rw+'); + $this->getCompletionOutput()->write($suggestions, new StreamOutput($stream)); + fseek($stream, 0); + $this->assertEquals($this->getExpectedOptionsOutput(), stream_get_contents($stream)); + } + + public function testValuesOutput() + { + $suggestions = new CompletionSuggestions(); + $suggestions->suggestValues(['Green', 'Red', 'Yellow']); + $stream = fopen('php://memory', 'rw+'); + $this->getCompletionOutput()->write($suggestions, new StreamOutput($stream)); + fseek($stream, 0); + $this->assertEquals($this->getExpectedValuesOutput(), stream_get_contents($stream)); + } +} diff --git a/src/Symfony/Component/Console/Tests/Helper/TableTest.php b/src/Symfony/Component/Console/Tests/Helper/TableTest.php index 8465c51dc7417..9d944084b6029 100644 --- a/src/Symfony/Component/Console/Tests/Helper/TableTest.php +++ b/src/Symfony/Component/Console/Tests/Helper/TableTest.php @@ -616,8 +616,8 @@ public function renderProvider() 'default', <<<'TABLE' +-------+------------+ -| Dont break | -| here | +| Dont break | +| here | +-------+------------+ | foo | Dont break | | bar | here | @@ -1285,6 +1285,26 @@ public function renderSetTitle() | 80-902734-1-6 | And Then There Were None | Agatha Christie | +---------------+--------- Page 1/2 -------+------------------+ +TABLE + , + true, + ], + 'header contains multiple lines' => [ + 'Multiline'."\n".'header'."\n".'here', + 'footer', + 'default', + <<<'TABLE' ++---------------+---- Multiline +header +here -+------------------+ +| ISBN | Title | Author | ++---------------+--------------------------+------------------+ +| 99921-58-10-7 | Divine Comedy | Dante Alighieri | +| 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | +| 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien | +| 80-902734-1-6 | And Then There Were None | Agatha Christie | ++---------------+---------- footer --------+------------------+ + TABLE ], [ diff --git a/src/Symfony/Component/Console/Tests/Input/InputArgumentTest.php b/src/Symfony/Component/Console/Tests/Input/InputArgumentTest.php index 39a3de5c6200c..e3cf9246725f5 100644 --- a/src/Symfony/Component/Console/Tests/Input/InputArgumentTest.php +++ b/src/Symfony/Component/Console/Tests/Input/InputArgumentTest.php @@ -88,6 +88,14 @@ public function testSetDefaultWithRequiredArgument() $argument->setDefault('default'); } + public function testSetDefaultWithRequiredArrayArgument() + { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Cannot set a default value except for InputArgument::OPTIONAL mode.'); + $argument = new InputArgument('foo', InputArgument::REQUIRED | InputArgument::IS_ARRAY); + $argument->setDefault([]); + } + public function testSetDefaultWithArrayArgument() { $this->expectException(\LogicException::class); diff --git a/src/Symfony/Component/DependencyInjection/Compiler/DecoratorServicePass.php b/src/Symfony/Component/DependencyInjection/Compiler/DecoratorServicePass.php index 0284587280332..9170249178b89 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/DecoratorServicePass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/DecoratorServicePass.php @@ -51,6 +51,10 @@ public function process(ContainerBuilder $container) } $decoratingDefinitions = []; + $tagsToKeep = $container->hasParameter('container.behavior_describing_tags') + ? $container->getParameter('container.behavior_describing_tags') + : ['container.do_not_inline', 'container.service_locator', 'container.service_subscriber']; + foreach ($definitions as [$id, $definition]) { $decoratedService = $definition->getDecoratedService(); [$inner, $renamedId] = $decoratedService; @@ -101,8 +105,8 @@ public function process(ContainerBuilder $container) $decoratingTags = $decoratingDefinition->getTags(); $resetTags = []; - // container.service_locator and container.service_subscriber have special logic and they must not be transferred out to decorators - foreach (['container.service_locator', 'container.service_subscriber'] as $containerTag) { + // Behavior-describing tags must not be transferred out to decorators + foreach ($tagsToKeep as $containerTag) { if (isset($decoratingTags[$containerTag])) { $resetTags[$containerTag] = $decoratingTags[$containerTag]; unset($decoratingTags[$containerTag]); diff --git a/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php index 00ebdfe0bb012..2285f8ea5b784 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php @@ -163,7 +163,7 @@ protected function processValue($value, bool $isRoot = false) */ private function isInlineableDefinition(string $id, Definition $definition): bool { - if ($definition->hasErrors() || $definition->isDeprecated() || $definition->isLazy() || $definition->isSynthetic()) { + if ($definition->hasErrors() || $definition->isDeprecated() || $definition->isLazy() || $definition->isSynthetic() || $definition->hasTag('container.do_not_inline')) { return false; } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/RegisterAutoconfigureAttributesPass.php b/src/Symfony/Component/DependencyInjection/Compiler/RegisterAutoconfigureAttributesPass.php index 08befc4dda12f..cc3b117a4df5e 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/RegisterAutoconfigureAttributesPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/RegisterAutoconfigureAttributesPass.php @@ -82,7 +82,8 @@ private static function registerForAutoconfiguration(ContainerBuilder $container ], ], ], - $class->getFileName() + $class->getFileName(), + false ); }; diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveChildDefinitionsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveChildDefinitionsPass.php index 46a2d7882cf89..aefd2294a56d9 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ResolveChildDefinitionsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveChildDefinitionsPass.php @@ -193,6 +193,12 @@ private function doResolveDefinition(ChildDefinition $definition): Definition // and it's not legal on an instanceof $def->setAutoconfigured($definition->isAutoconfigured()); + if (!$def->hasTag('proxy')) { + foreach ($parentDef->getTag('proxy') as $v) { + $def->addTag('proxy', $v); + } + } + return $def; } } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveParameterPlaceHoldersPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveParameterPlaceHoldersPass.php index b1c81ee3a1414..0099a3bbc7887 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ResolveParameterPlaceHoldersPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveParameterPlaceHoldersPass.php @@ -85,6 +85,11 @@ protected function processValue($value, bool $isRoot = false) if (isset($changes['file'])) { $value->setFile($this->bag->resolveValue($value->getFile())); } + $tags = $value->getTags(); + if (isset($tags['proxy'])) { + $tags['proxy'] = $this->bag->resolveValue($tags['proxy']); + $value->setTags($tags); + } } $value = parent::processValue($value, $isRoot); diff --git a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php index 7efda3c43c17f..01e20c44edf94 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php @@ -220,7 +220,7 @@ private function parseImports(array $content, string $file) } } - private function parseDefinitions(array $content, string $file) + private function parseDefinitions(array $content, string $file, bool $trackBindings = true) { if (!isset($content['services'])) { return; @@ -246,14 +246,14 @@ private function parseDefinitions(array $content, string $file) if (\is_string($service) && str_starts_with($service, '@')) { throw new InvalidArgumentException(sprintf('Type definition "%s" cannot be an alias within "_instanceof" in "%s". Check your YAML syntax.', $id, $file)); } - $this->parseDefinition($id, $service, $file, []); + $this->parseDefinition($id, $service, $file, [], false, $trackBindings); } } $this->isLoadingInstanceof = false; $defaults = $this->parseDefaults($content, $file); foreach ($content['services'] as $id => $service) { - $this->parseDefinition($id, $service, $file, $defaults); + $this->parseDefinition($id, $service, $file, $defaults, false, $trackBindings); } } @@ -342,7 +342,7 @@ private function isUsingShortSyntax(array $service): bool * * @throws InvalidArgumentException When tags are invalid */ - private function parseDefinition(string $id, $service, string $file, array $defaults, bool $return = false) + private function parseDefinition(string $id, $service, string $file, array $defaults, bool $return = false, bool $trackBindings = true) { if (preg_match('/^_[a-zA-Z0-9_]*$/', $id)) { throw new InvalidArgumentException(sprintf('Service names that start with an underscore are reserved. Rename the "%s" service or define it in XML instead.', $id)); @@ -666,7 +666,7 @@ private function parseDefinition(string $id, $service, string $file, array $defa $bindingType = $this->isLoadingInstanceof ? BoundArgument::INSTANCEOF_BINDING : BoundArgument::SERVICE_BINDING; foreach ($bindings as $argument => $value) { if (!$value instanceof BoundArgument) { - $bindings[$argument] = new BoundArgument($value, true, $bindingType, $file); + $bindings[$argument] = new BoundArgument($value, $trackBindings, $bindingType, $file); } } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/InlineServiceDefinitionsPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/InlineServiceDefinitionsPassTest.php index 2fb01ad78d741..5064c1f75fe1e 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/InlineServiceDefinitionsPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/InlineServiceDefinitionsPassTest.php @@ -323,6 +323,34 @@ public function testProcessDoesNotSetLazyArgumentValuesAfterInlining() $this->assertSame('inline', (string) $values[0]); } + public function testDoNotInline() + { + $container = new ContainerBuilder(); + $container->register('decorated1', 'decorated1')->addTag('container.do_not_inline'); + $container->register('decorated2', 'decorated2')->addTag('container.do_not_inline'); + $container->setAlias('alias2', 'decorated2'); + + $container + ->register('s1', 's1') + ->setDecoratedService('decorated1') + ->setPublic(true) + ->setProperties(['inner' => new Reference('s1.inner')]); + + $container + ->register('s2', 's2') + ->setDecoratedService('alias2') + ->setPublic(true) + ->setProperties(['inner' => new Reference('s2.inner')]); + + $container->compile(); + + $this->assertFalse($container->hasAlias('alias2')); + $this->assertEquals(new Reference('decorated2'), $container->getDefinition('s2')->getProperties()['inner']); + $this->assertEquals(new Reference('s1.inner'), $container->getDefinition('s1')->getProperties()['inner']); + $this->assertSame('decorated2', $container->getDefinition('decorated2')->getClass()); + $this->assertSame('decorated1', $container->getDefinition('s1.inner')->getClass()); + } + protected function process(ContainerBuilder $container) { (new InlineServiceDefinitionsPass(new AnalyzeServiceReferencesPass()))->process($container); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterAutoconfigureAttributesPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterAutoconfigureAttributesPassTest.php index 111c41ac8a114..793962f3e0466 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterAutoconfigureAttributesPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterAutoconfigureAttributesPassTest.php @@ -34,7 +34,7 @@ public function testProcess() (new RegisterAutoconfigureAttributesPass())->process($container); - $argument = new BoundArgument(1, true, BoundArgument::INSTANCEOF_BINDING, realpath(__DIR__.'/../Fixtures/AutoconfigureAttributed.php')); + $argument = new BoundArgument(1, false, BoundArgument::INSTANCEOF_BINDING, realpath(__DIR__.'/../Fixtures/AutoconfigureAttributed.php')); $values = $argument->getValues(); --$values[1]; $argument->setValues($values); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveChildDefinitionsPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveChildDefinitionsPassTest.php index 887e2b1524855..ce1803c61cb4e 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveChildDefinitionsPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveChildDefinitionsPassTest.php @@ -121,6 +121,31 @@ public function testProcessDoesNotCopyTags() $this->assertEquals([], $def->getTags()); } + public function testProcessCopiesTagsProxy() + { + $container = new ContainerBuilder(); + + $container + ->register('parent') + ->addTag('proxy', ['a' => 'b']) + ; + + $container + ->setDefinition('child1', new ChildDefinition('parent')) + ; + $container + ->setDefinition('child2', (new ChildDefinition('parent'))->addTag('proxy', ['c' => 'd'])) + ; + + $this->process($container); + + $def = $container->getDefinition('child1'); + $this->assertSame(['proxy' => [['a' => 'b']]], $def->getTags()); + + $def = $container->getDefinition('child2'); + $this->assertSame(['proxy' => [['c' => 'd']]], $def->getTags()); + } + public function testProcessDoesNotCopyDecoratedService() { $container = new ContainerBuilder(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveParameterPlaceHoldersPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveParameterPlaceHoldersPassTest.php index c586e72c2acbc..96c45205459df 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveParameterPlaceHoldersPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveParameterPlaceHoldersPassTest.php @@ -97,6 +97,20 @@ public function testParameterNotFoundExceptionsIsNotThrown() $this->assertCount(1, $definition->getErrors()); } + public function testOnlyProxyTagIsResolved() + { + $containerBuilder = new ContainerBuilder(); + $containerBuilder->setParameter('a_param', 'here_you_go'); + $definition = $containerBuilder->register('def'); + $definition->addTag('foo', ['bar' => '%a_param%']); + $definition->addTag('proxy', ['interface' => '%a_param%']); + + $pass = new ResolveParameterPlaceHoldersPass(true, false); + $pass->process($containerBuilder); + + $this->assertSame(['foo' => [['bar' => '%a_param%']], 'proxy' => [['interface' => 'here_you_go']]], $definition->getTags()); + } + private function createContainerBuilder(): ContainerBuilder { $containerBuilder = new ContainerBuilder(); diff --git a/src/Symfony/Component/DomCrawler/Crawler.php b/src/Symfony/Component/DomCrawler/Crawler.php index ff7b0975b9016..a32bc1dd8cf51 100644 --- a/src/Symfony/Component/DomCrawler/Crawler.php +++ b/src/Symfony/Component/DomCrawler/Crawler.php @@ -1186,11 +1186,11 @@ private function convertToHtmlEntities(string $htmlContent, string $charset = 'U set_error_handler(function () { throw new \Exception(); }); try { - return mb_encode_numericentity($htmlContent, [0x80, 0xFFFF, 0, 0xFFFF], $charset); + return mb_encode_numericentity($htmlContent, [0x80, 0x10FFFF, 0, 0x1FFFFF], $charset); } catch (\Exception|\ValueError $e) { try { $htmlContent = iconv($charset, 'UTF-8', $htmlContent); - $htmlContent = mb_encode_numericentity($htmlContent, [0x80, 0xFFFF, 0, 0xFFFF], 'UTF-8'); + $htmlContent = mb_encode_numericentity($htmlContent, [0x80, 0x10FFFF, 0, 0x1FFFFF], 'UTF-8'); } catch (\Exception|\ValueError $e) { } diff --git a/src/Symfony/Component/DomCrawler/Tests/AbstractCrawlerTest.php b/src/Symfony/Component/DomCrawler/Tests/AbstractCrawlerTest.php index 01947db1bc889..63e5db8eb7f3c 100644 --- a/src/Symfony/Component/DomCrawler/Tests/AbstractCrawlerTest.php +++ b/src/Symfony/Component/DomCrawler/Tests/AbstractCrawlerTest.php @@ -381,6 +381,13 @@ public function testHtml() $this->assertSame('my value', $this->createTestCrawler(null)->filterXPath('//ol')->html('my value')); } + public function testEmojis() + { + $crawler = $this->createCrawler('

Hey 👋

'); + + $this->assertSame('

Hey 👋

', $crawler->html()); + } + public function testExtract() { $crawler = $this->createTestCrawler()->filterXPath('//ul[1]/li'); diff --git a/src/Symfony/Component/ErrorHandler/Internal/TentativeTypes.php b/src/Symfony/Component/ErrorHandler/Internal/TentativeTypes.php index 5707a8355bc90..2168a1c075816 100644 --- a/src/Symfony/Component/ErrorHandler/Internal/TentativeTypes.php +++ b/src/Symfony/Component/ErrorHandler/Internal/TentativeTypes.php @@ -37,7 +37,7 @@ class TentativeTypes 'DateTime' => [ '__wakeup' => 'void', '__set_state' => 'DateTime', - 'createFromImmutable' => 'DateTime', + 'createFromImmutable' => 'static', 'createFromFormat' => 'DateTime|false', 'getLastErrors' => 'array|false', 'format' => 'string', @@ -72,7 +72,7 @@ class TentativeTypes 'setDate' => 'DateTimeImmutable', 'setISODate' => 'DateTimeImmutable', 'setTimestamp' => 'DateTimeImmutable', - 'createFromMutable' => 'DateTimeImmutable', + 'createFromMutable' => 'static', ], 'DateTimeZone' => [ 'getName' => 'string', @@ -1102,6 +1102,8 @@ class TentativeTypes 'isDot' => 'bool', 'rewind' => 'void', 'valid' => 'bool', + 'key' => 'mixed', + 'current' => 'mixed', 'next' => 'void', 'seek' => 'void', ], @@ -1148,8 +1150,8 @@ class TentativeTypes 'getFlags' => 'int', 'setMaxLineLen' => 'void', 'getMaxLineLen' => 'int', - 'hasChildren' => 'bool', - 'getChildren' => '?RecursiveIterator', + 'hasChildren' => 'false', + 'getChildren' => 'null', 'seek' => 'void', 'getCurrentLine' => 'string', ], @@ -1238,7 +1240,7 @@ class TentativeTypes 'current' => 'never', 'next' => 'void', 'key' => 'never', - 'valid' => 'bool', + 'valid' => 'false', 'rewind' => 'void', ], 'CallbackFilterIterator' => [ diff --git a/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcher.php b/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcher.php index e1d2007fd1823..acfbf619c4f97 100644 --- a/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcher.php +++ b/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcher.php @@ -75,7 +75,7 @@ public function removeListener(string $eventName, $listener) { if (isset($this->wrappedListeners[$eventName])) { foreach ($this->wrappedListeners[$eventName] as $index => $wrappedListener) { - if ($wrappedListener->getWrappedListener() === $listener) { + if ($wrappedListener->getWrappedListener() === $listener || ($listener instanceof \Closure && $wrappedListener->getWrappedListener() == $listener)) { $listener = $wrappedListener; unset($this->wrappedListeners[$eventName][$index]); break; @@ -110,8 +110,8 @@ public function getListenerPriority(string $eventName, $listener) // we might have wrapped listeners for the event (if called while dispatching) // in that case get the priority by wrapper if (isset($this->wrappedListeners[$eventName])) { - foreach ($this->wrappedListeners[$eventName] as $index => $wrappedListener) { - if ($wrappedListener->getWrappedListener() === $listener) { + foreach ($this->wrappedListeners[$eventName] as $wrappedListener) { + if ($wrappedListener->getWrappedListener() === $listener || ($listener instanceof \Closure && $wrappedListener->getWrappedListener() == $listener)) { return $this->dispatcher->getListenerPriority($eventName, $wrappedListener); } } diff --git a/src/Symfony/Component/EventDispatcher/EventDispatcher.php b/src/Symfony/Component/EventDispatcher/EventDispatcher.php index c479a7544f0e6..8fe8fb5c299af 100644 --- a/src/Symfony/Component/EventDispatcher/EventDispatcher.php +++ b/src/Symfony/Component/EventDispatcher/EventDispatcher.php @@ -108,7 +108,7 @@ public function getListenerPriority(string $eventName, $listener) $v[0] = $v[0](); $v[1] = $v[1] ?? '__invoke'; } - if ($v === $listener) { + if ($v === $listener || ($listener instanceof \Closure && $v == $listener)) { return $priority; } } @@ -164,7 +164,7 @@ public function removeListener(string $eventName, $listener) $v[0] = $v[0](); $v[1] = $v[1] ?? '__invoke'; } - if ($v === $listener) { + if ($v === $listener || ($listener instanceof \Closure && $v == $listener)) { unset($listeners[$k], $this->sorted[$eventName], $this->optimized[$eventName]); } } diff --git a/src/Symfony/Component/EventDispatcher/Tests/EventDispatcherTest.php b/src/Symfony/Component/EventDispatcher/Tests/EventDispatcherTest.php index afbb1db2d10e2..5061854c1fb2f 100644 --- a/src/Symfony/Component/EventDispatcher/Tests/EventDispatcherTest.php +++ b/src/Symfony/Component/EventDispatcher/Tests/EventDispatcherTest.php @@ -405,6 +405,35 @@ public function testMutatingWhilePropagationIsStopped() $this->assertTrue($testLoaded); } + + /** + * @requires PHP 8.1 + */ + public function testNamedClosures() + { + $listener = new TestEventListener(); + + $callback1 = \Closure::fromCallable($listener); + $callback2 = \Closure::fromCallable($listener); + $callback3 = \Closure::fromCallable(new TestEventListener()); + + $this->assertNotSame($callback1, $callback2); + $this->assertNotSame($callback1, $callback3); + $this->assertNotSame($callback2, $callback3); + $this->assertTrue($callback1 == $callback2); + $this->assertFalse($callback1 == $callback3); + + $this->dispatcher->addListener('foo', $callback1, 3); + $this->dispatcher->addListener('foo', $callback2, 2); + $this->dispatcher->addListener('foo', $callback3, 1); + + $this->assertSame(3, $this->dispatcher->getListenerPriority('foo', $callback1)); + $this->assertSame(3, $this->dispatcher->getListenerPriority('foo', $callback2)); + + $this->dispatcher->removeListener('foo', $callback1); + + $this->assertSame(['foo' => [$callback3]], $this->dispatcher->getListeners()); + } } class CallableClass diff --git a/src/Symfony/Component/Filesystem/Filesystem.php b/src/Symfony/Component/Filesystem/Filesystem.php index 524d17c2ba7d9..785e716367b66 100644 --- a/src/Symfony/Component/Filesystem/Filesystem.php +++ b/src/Symfony/Component/Filesystem/Filesystem.php @@ -329,6 +329,8 @@ private function isReadable(string $filename): bool */ public function symlink(string $originDir, string $targetDir, bool $copyOnWindows = false) { + self::assertFunctionExists('symlink'); + if ('\\' === \DIRECTORY_SEPARATOR) { $originDir = strtr($originDir, '/', '\\'); $targetDir = strtr($targetDir, '/', '\\'); @@ -364,6 +366,8 @@ public function symlink(string $originDir, string $targetDir, bool $copyOnWindow */ public function hardlink(string $originFile, $targetFiles) { + self::assertFunctionExists('link'); + if (!$this->exists($originFile)) { throw new FileNotFoundException(null, 0, null, $originFile); } @@ -730,13 +734,22 @@ private function getSchemeAndHierarchy(string $filename): array return 2 === \count($components) ? [$components[0], $components[1]] : [null, $components[0]]; } + private static function assertFunctionExists(string $func): void + { + if (!\function_exists($func)) { + throw new IOException(sprintf('Unable to perform filesystem operation because the "%s()" function has been disabled.', $func)); + } + } + /** * @param mixed ...$args * * @return mixed */ - private static function box(callable $func, ...$args) + private static function box(string $func, ...$args) { + self::assertFunctionExists($func); + self::$lastError = null; set_error_handler(__CLASS__.'::handleError'); try { diff --git a/src/Symfony/Component/Form/ChoiceList/Loader/AbstractChoiceLoader.php b/src/Symfony/Component/Form/ChoiceList/Loader/AbstractChoiceLoader.php index a30af63b1ab57..a98cc239f55e5 100644 --- a/src/Symfony/Component/Form/ChoiceList/Loader/AbstractChoiceLoader.php +++ b/src/Symfony/Component/Form/ChoiceList/Loader/AbstractChoiceLoader.php @@ -20,11 +20,11 @@ abstract class AbstractChoiceLoader implements ChoiceLoaderInterface { /** - * The loaded choice list. + * The loaded choices. * - * @var ArrayChoiceList + * @var iterable|null */ - private $choiceList; + private $choices; /** * @final @@ -33,7 +33,7 @@ abstract class AbstractChoiceLoader implements ChoiceLoaderInterface */ public function loadChoiceList(callable $value = null): ChoiceListInterface { - return $this->choiceList ?? ($this->choiceList = new ArrayChoiceList($this->loadChoices(), $value)); + return new ArrayChoiceList($this->choices ?? $this->choices = $this->loadChoices(), $value); } /** @@ -45,10 +45,6 @@ public function loadChoicesForValues(array $values, callable $value = null) return []; } - if ($this->choiceList) { - return $this->choiceList->getChoicesForValues($values); - } - return $this->doLoadChoicesForValues($values, $value); } @@ -66,10 +62,6 @@ public function loadValuesForChoices(array $choices, callable $value = null) return array_map($value, $choices); } - if ($this->choiceList) { - return $this->choiceList->getValuesForChoices($choices); - } - return $this->doLoadValuesForChoices($choices); } diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/DateTimeToStringTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/DateTimeToStringTransformer.php index 955b46568cfcf..0aebb09fb219a 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/DateTimeToStringTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/DateTimeToStringTransformer.php @@ -47,12 +47,14 @@ class DateTimeToStringTransformer extends BaseDateTimeTransformer * @param string|null $inputTimezone The name of the input timezone * @param string|null $outputTimezone The name of the output timezone * @param string $format The date format + * @param string|null $parseFormat The parse format when different from $format */ - public function __construct(string $inputTimezone = null, string $outputTimezone = null, string $format = 'Y-m-d H:i:s') + public function __construct(string $inputTimezone = null, string $outputTimezone = null, string $format = 'Y-m-d H:i:s', string $parseFormat = null) { parent::__construct($inputTimezone, $outputTimezone); - $this->generateFormat = $this->parseFormat = $format; + $this->generateFormat = $format; + $this->parseFormat = $parseFormat ?? $format; // See https://php.net/datetime.createfromformat // The character "|" in the format makes sure that the parts of a date diff --git a/src/Symfony/Component/Form/Extension/Core/Type/TimeType.php b/src/Symfony/Component/Form/Extension/Core/Type/TimeType.php index c4df4a174cc27..6b56a120e734f 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/TimeType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/TimeType.php @@ -74,8 +74,10 @@ public function buildForm(FormBuilderInterface $builder, array $options) } }); + $parseFormat = null; + if (null !== $options['reference_date']) { - $format = 'Y-m-d '.$format; + $parseFormat = 'Y-m-d '.$format; $builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($options) { $data = $event->getData(); @@ -86,7 +88,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) }); } - $builder->addViewTransformer(new DateTimeToStringTransformer($options['model_timezone'], $options['view_timezone'], $format)); + $builder->addViewTransformer(new DateTimeToStringTransformer($options['model_timezone'], $options['view_timezone'], $format, $parseFormat)); } else { $hourOptions = $minuteOptions = $secondOptions = [ 'error_bubbling' => true, diff --git a/src/Symfony/Component/Form/Form.php b/src/Symfony/Component/Form/Form.php index 1f02ea869321d..7dd49163e1671 100644 --- a/src/Symfony/Component/Form/Form.php +++ b/src/Symfony/Component/Form/Form.php @@ -546,7 +546,7 @@ public function submit($submittedData, bool $clearMissing = true) $submittedData = null; $this->transformationFailure = new TransformationFailedException('Submitted data was expected to be text or number, file upload given.'); } - } elseif (\is_array($submittedData) && !$this->config->getCompound() && !$this->config->hasOption('multiple')) { + } elseif (\is_array($submittedData) && !$this->config->getCompound() && !$this->config->getOption('multiple', false)) { $submittedData = null; $this->transformationFailure = new TransformationFailedException('Submitted data was expected to be text or number, array given.'); } diff --git a/src/Symfony/Component/Form/Resources/translations/validators.it.xlf b/src/Symfony/Component/Form/Resources/translations/validators.it.xlf index 8e4665ce1daf5..1a8eee3ac8e26 100644 --- a/src/Symfony/Component/Form/Resources/translations/validators.it.xlf +++ b/src/Symfony/Component/Form/Resources/translations/validators.it.xlf @@ -44,15 +44,15 @@ Please choose a valid date interval. - Per favore, scegli a valid date interval. + Per favore, scegli un intervallo di date valido. Please enter a valid date and time. - Per favore, inserisci a valid date and time. + Per favore, inserisci una data e ora valida. Please enter a valid date. - Per favore, inserisci a valid date. + Per favore, inserisci una data valida. Please select a valid file. diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/LazyChoiceListTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/LazyChoiceListTest.php index 6f30d0896e1ba..94d41cf9e740f 100644 --- a/src/Symfony/Component/Form/Tests/ChoiceList/LazyChoiceListTest.php +++ b/src/Symfony/Component/Form/Tests/ChoiceList/LazyChoiceListTest.php @@ -32,7 +32,7 @@ public function testGetChoiceLoadersLoadsLoadedListOnFirstCall() $this->assertSame(['RESULT'], $list->getChoices()); $this->assertSame(['RESULT'], $list->getChoices()); - $this->assertSame(1, $calls); + $this->assertSame(2, $calls); } public function testGetValuesLoadsLoadedListOnFirstCall() @@ -46,7 +46,7 @@ public function testGetValuesLoadsLoadedListOnFirstCall() $this->assertSame(['RESULT'], $list->getValues()); $this->assertSame(['RESULT'], $list->getValues()); - $this->assertSame(1, $calls); + $this->assertSame(2, $calls); } public function testGetStructuredValuesLoadsLoadedListOnFirstCall() @@ -60,7 +60,7 @@ public function testGetStructuredValuesLoadsLoadedListOnFirstCall() $this->assertSame(['RESULT'], $list->getStructuredValues()); $this->assertSame(['RESULT'], $list->getStructuredValues()); - $this->assertSame(1, $calls); + $this->assertSame(2, $calls); } public function testGetOriginalKeysLoadsLoadedListOnFirstCall() @@ -79,7 +79,7 @@ public function testGetOriginalKeysLoadsLoadedListOnFirstCall() $this->assertSame(['foo' => 'a', 'bar' => 'b', 'baz' => 'c'], $list->getOriginalKeys()); $this->assertSame(['foo' => 'a', 'bar' => 'b', 'baz' => 'c'], $list->getOriginalKeys()); - $this->assertSame(3, $calls); + $this->assertSame(6, $calls); } public function testGetChoicesForValuesForwardsCallIfListNotLoaded() @@ -98,7 +98,7 @@ public function testGetChoicesForValuesForwardsCallIfListNotLoaded() $this->assertSame(['foo', 'bar'], $list->getChoicesForValues(['a', 'b'])); $this->assertSame(['foo', 'bar'], $list->getChoicesForValues(['a', 'b'])); - $this->assertSame(3, $calls); + $this->assertSame(6, $calls); } public function testGetChoicesForValuesUsesLoadedList() diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/Loader/CallbackChoiceLoaderTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/Loader/CallbackChoiceLoaderTest.php index 52dd5f8af580f..69eb787a23dfa 100644 --- a/src/Symfony/Component/Form/Tests/ChoiceList/Loader/CallbackChoiceLoaderTest.php +++ b/src/Symfony/Component/Form/Tests/ChoiceList/Loader/CallbackChoiceLoaderTest.php @@ -67,11 +67,18 @@ public function testLoadChoiceList() $this->assertInstanceOf(ChoiceListInterface::class, self::$loader->loadChoiceList(self::$value)); } - public function testLoadChoiceListOnlyOnce() + public function testLoadChoicesOnlyOnce() { - $loadedChoiceList = self::$loader->loadChoiceList(self::$value); + $calls = 0; + $loader = new CallbackChoiceLoader(function () use (&$calls) { + ++$calls; - $this->assertSame($loadedChoiceList, self::$loader->loadChoiceList(self::$value)); + return [1]; + }); + $loadedChoiceList = $loader->loadChoiceList(); + + $this->assertNotSame($loadedChoiceList, $loader->loadChoiceList()); + $this->assertSame(1, $calls); } public function testLoadChoicesForValuesLoadsChoiceListOnFirstCall() diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/Loader/IntlCallbackChoiceLoaderTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/Loader/IntlCallbackChoiceLoaderTest.php index a5fc262dcd3a1..e2827b0d913be 100644 --- a/src/Symfony/Component/Form/Tests/ChoiceList/Loader/IntlCallbackChoiceLoaderTest.php +++ b/src/Symfony/Component/Form/Tests/ChoiceList/Loader/IntlCallbackChoiceLoaderTest.php @@ -68,11 +68,19 @@ public function testLoadChoiceList() $this->assertInstanceOf(ChoiceListInterface::class, self::$loader->loadChoiceList(self::$value)); } - public function testLoadChoiceListOnlyOnce() + public function testLoadChoicesOnlyOnce() { - $loadedChoiceList = self::$loader->loadChoiceList(self::$value); + $calls = 0; + $loader = new IntlCallbackChoiceLoader(function () use (&$calls) { + ++$calls; - $this->assertSame($loadedChoiceList, self::$loader->loadChoiceList(self::$value)); + return self::$choices; + }); + + $loadedChoiceList = $loader->loadChoiceList(self::$value); + + $this->assertNotSame($loadedChoiceList, $loader->loadChoiceList(self::$value)); + $this->assertSame(1, $calls); } public function testLoadChoicesForValuesLoadsChoiceListOnFirstCall() diff --git a/src/Symfony/Component/Form/Tests/CompoundFormTest.php b/src/Symfony/Component/Form/Tests/CompoundFormTest.php index 4fdb085a446d9..fcc55a56385de 100644 --- a/src/Symfony/Component/Form/Tests/CompoundFormTest.php +++ b/src/Symfony/Component/Form/Tests/CompoundFormTest.php @@ -1057,7 +1057,8 @@ public function testArrayTransformationFailureOnSubmit() $this->assertNull($this->form->get('foo')->getData()); $this->assertSame('Submitted data was expected to be text or number, array given.', $this->form->get('foo')->getTransformationFailure()->getMessage()); - $this->assertSame(['bar'], $this->form->get('bar')->getData()); + $this->assertNull($this->form->get('bar')->getData()); + $this->assertSame('Submitted data was expected to be text or number, array given.', $this->form->get('bar')->getTransformationFailure()->getMessage()); } public function testFileUpload() diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php index 50b259f58e10e..352a7f9e3f1ae 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php @@ -16,6 +16,8 @@ use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView; use Symfony\Component\Form\ChoiceList\View\ChoiceView; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Exception\TransformationFailedException; +use Symfony\Component\Form\Extension\Validator\ValidatorExtension; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\Tests\Fixtures\ChoiceList\DeprecatedChoiceListFactory; use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; @@ -1954,7 +1956,12 @@ public function testSubmitInvalidNestedValue($multiple, $expanded, $submissionDa $form->submit($submissionData); $this->assertFalse($form->isSynchronized()); - $this->assertEquals('All choices submitted must be NULL, strings or ints.', $form->getTransformationFailure()->getMessage()); + $this->assertInstanceOf(TransformationFailedException::class, $form->getTransformationFailure()); + if (!$multiple && !$expanded) { + $this->assertEquals('Submitted data was expected to be text or number, array given.', $form->getTransformationFailure()->getMessage()); + } else { + $this->assertEquals('All choices submitted must be NULL, strings or ints.', $form->getTransformationFailure()->getMessage()); + } } public function invalidNestedValueTestMatrix() @@ -2264,4 +2271,32 @@ public function testUsingDeprecatedChoiceListFactory() new ChoiceType(new DeprecatedChoiceListFactory()); } + + public function testWithSameLoaderAndDifferentChoiceValueCallbacks() + { + $choiceLoader = new CallbackChoiceLoader(function () { + return [1, 2, 3]; + }); + + $view = $this->factory->create(FormTypeTest::TESTED_TYPE) + ->add('choice_one', self::TESTED_TYPE, [ + 'choice_loader' => $choiceLoader, + ]) + ->add('choice_two', self::TESTED_TYPE, [ + 'choice_loader' => $choiceLoader, + 'choice_value' => function ($choice) { + return $choice ? (string) $choice * 10 : ''; + }, + ]) + ->createView() + ; + + $this->assertSame('1', $view['choice_one']->vars['choices'][0]->value); + $this->assertSame('2', $view['choice_one']->vars['choices'][1]->value); + $this->assertSame('3', $view['choice_one']->vars['choices'][2]->value); + + $this->assertSame('10', $view['choice_two']->vars['choices'][0]->value); + $this->assertSame('20', $view['choice_two']->vars['choices'][1]->value); + $this->assertSame('30', $view['choice_two']->vars['choices'][2]->value); + } } diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/TimeTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/TimeTypeTest.php index 112318a08848c..08284dbbf00e7 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/TimeTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/TimeTypeTest.php @@ -280,6 +280,76 @@ public function testSubmitWithSecondsAndBrowserOmissionSeconds() $this->assertEquals('03:04:00', $form->getViewData()); } + public function testPreSetDataDifferentTimezones() + { + $form = $this->factory->create(static::TESTED_TYPE, null, [ + 'model_timezone' => 'UTC', + 'view_timezone' => 'Europe/Berlin', + 'input' => 'datetime', + 'with_seconds' => true, + 'reference_date' => new \DateTimeImmutable('2019-01-01', new \DateTimeZone('UTC')), + ]); + $form->setData(new \DateTime('2022-01-01 15:09:10', new \DateTimeZone('UTC'))); + + $this->assertSame('15:09:10', $form->getData()->format('H:i:s')); + $this->assertSame([ + 'hour' => '16', + 'minute' => '9', + 'second' => '10', + ], $form->getViewData()); + } + + public function testPreSetDataDifferentTimezonesDuringDaylightSavingTime() + { + $form = $this->factory->create(static::TESTED_TYPE, null, [ + 'model_timezone' => 'UTC', + 'view_timezone' => 'Europe/Berlin', + 'input' => 'datetime', + 'with_seconds' => true, + 'reference_date' => new \DateTimeImmutable('2019-07-12', new \DateTimeZone('UTC')), + ]); + $form->setData(new \DateTime('2022-04-29 15:09:10', new \DateTimeZone('UTC'))); + + $this->assertSame('15:09:10', $form->getData()->format('H:i:s')); + $this->assertSame([ + 'hour' => '17', + 'minute' => '9', + 'second' => '10', + ], $form->getViewData()); + } + + public function testPreSetDataDifferentTimezonesUsingSingleTextWidget() + { + $form = $this->factory->create(static::TESTED_TYPE, null, [ + 'model_timezone' => 'UTC', + 'view_timezone' => 'Europe/Berlin', + 'input' => 'datetime', + 'with_seconds' => true, + 'reference_date' => new \DateTimeImmutable('2019-01-01', new \DateTimeZone('UTC')), + 'widget' => 'single_text', + ]); + $form->setData(new \DateTime('2022-01-01 15:09:10', new \DateTimeZone('UTC'))); + + $this->assertSame('15:09:10', $form->getData()->format('H:i:s')); + $this->assertSame('16:09:10', $form->getViewData()); + } + + public function testPreSetDataDifferentTimezonesDuringDaylightSavingTimeUsingSingleTextWidget() + { + $form = $this->factory->create(static::TESTED_TYPE, null, [ + 'model_timezone' => 'UTC', + 'view_timezone' => 'Europe/Berlin', + 'input' => 'datetime', + 'with_seconds' => true, + 'reference_date' => new \DateTimeImmutable('2019-07-12', new \DateTimeZone('UTC')), + 'widget' => 'single_text', + ]); + $form->setData(new \DateTime('2022-04-29 15:09:10', new \DateTimeZone('UTC'))); + + $this->assertSame('15:09:10', $form->getData()->format('H:i:s')); + $this->assertSame('17:09:10', $form->getViewData()); + } + public function testSubmitDifferentTimezones() { $form = $this->factory->create(static::TESTED_TYPE, null, [ diff --git a/src/Symfony/Component/HttpClient/HttpOptions.php b/src/Symfony/Component/HttpClient/HttpOptions.php index 1638189f6439b..da55f9965f98c 100644 --- a/src/Symfony/Component/HttpClient/HttpOptions.php +++ b/src/Symfony/Component/HttpClient/HttpOptions.php @@ -197,6 +197,16 @@ public function setTimeout(float $timeout) return $this; } + /** + * @return $this + */ + public function setMaxDuration(float $maxDuration) + { + $this->options['max_duration'] = $maxDuration; + + return $this; + } + /** * @return $this */ diff --git a/src/Symfony/Component/HttpClient/Response/AmpResponse.php b/src/Symfony/Component/HttpClient/Response/AmpResponse.php index 9015a06d063a5..6d0ce6e33e01f 100644 --- a/src/Symfony/Component/HttpClient/Response/AmpResponse.php +++ b/src/Symfony/Component/HttpClient/Response/AmpResponse.php @@ -87,6 +87,7 @@ public function __construct(AmpClientState $multi, Request $request, array $opti $info['upload_content_length'] = -1.0; $info['download_content_length'] = -1.0; $info['user_data'] = $options['user_data']; + $info['max_duration'] = $options['max_duration']; $info['debug'] = ''; $onProgress = $options['on_progress'] ?? static function () {}; diff --git a/src/Symfony/Component/HttpClient/Response/AsyncContext.php b/src/Symfony/Component/HttpClient/Response/AsyncContext.php index 1af8dbee5ede3..2d95e11f507f1 100644 --- a/src/Symfony/Component/HttpClient/Response/AsyncContext.php +++ b/src/Symfony/Component/HttpClient/Response/AsyncContext.php @@ -13,6 +13,7 @@ use Symfony\Component\HttpClient\Chunk\DataChunk; use Symfony\Component\HttpClient\Chunk\LastChunk; +use Symfony\Component\HttpClient\Exception\TransportException; use Symfony\Contracts\HttpClient\ChunkInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\HttpClient\ResponseInterface; @@ -152,13 +153,18 @@ public function getResponse(): ResponseInterface */ public function replaceRequest(string $method, string $url, array $options = []): ResponseInterface { - $this->info['previous_info'][] = $this->response->getInfo(); + $this->info['previous_info'][] = $info = $this->response->getInfo(); if (null !== $onProgress = $options['on_progress'] ?? null) { $thisInfo = &$this->info; $options['on_progress'] = static function (int $dlNow, int $dlSize, array $info) use (&$thisInfo, $onProgress) { $onProgress($dlNow, $dlSize, $thisInfo + $info); }; } + if (0 < ($info['max_duration'] ?? 0) && 0 < ($info['total_time'] ?? 0)) { + if (0 >= $options['max_duration'] = $info['max_duration'] - $info['total_time']) { + throw new TransportException(sprintf('Max duration was reached for "%s".', $info['url'])); + } + } return $this->response = $this->client->request($method, $url, ['buffer' => false] + $options); } diff --git a/src/Symfony/Component/HttpClient/Response/AsyncResponse.php b/src/Symfony/Component/HttpClient/Response/AsyncResponse.php index c164fadb93ea9..80c9f7da370fa 100644 --- a/src/Symfony/Component/HttpClient/Response/AsyncResponse.php +++ b/src/Symfony/Component/HttpClient/Response/AsyncResponse.php @@ -85,6 +85,9 @@ public function __construct(HttpClientInterface $client, string $method, string if (\array_key_exists('user_data', $options)) { $this->info['user_data'] = $options['user_data']; } + if (\array_key_exists('max_duration', $options)) { + $this->info['max_duration'] = $options['max_duration']; + } } public function getStatusCode(): int diff --git a/src/Symfony/Component/HttpClient/Response/CurlResponse.php b/src/Symfony/Component/HttpClient/Response/CurlResponse.php index 5739392548c9d..c4ec5ce8d6130 100644 --- a/src/Symfony/Component/HttpClient/Response/CurlResponse.php +++ b/src/Symfony/Component/HttpClient/Response/CurlResponse.php @@ -65,6 +65,7 @@ public function __construct(CurlClientState $multi, $ch, array $options = null, $this->timeout = $options['timeout'] ?? null; $this->info['http_method'] = $method; $this->info['user_data'] = $options['user_data'] ?? null; + $this->info['max_duration'] = $options['max_duration'] ?? null; $this->info['start_time'] = $this->info['start_time'] ?? microtime(true); $info = &$this->info; $headers = &$this->headers; diff --git a/src/Symfony/Component/HttpClient/Response/MockResponse.php b/src/Symfony/Component/HttpClient/Response/MockResponse.php index 7177795d0acda..6420aa05de79a 100644 --- a/src/Symfony/Component/HttpClient/Response/MockResponse.php +++ b/src/Symfony/Component/HttpClient/Response/MockResponse.php @@ -140,6 +140,7 @@ public static function fromRequest(string $method, string $url, array $options, $response->info['http_method'] = $method; $response->info['http_code'] = 0; $response->info['user_data'] = $options['user_data'] ?? null; + $response->info['max_duration'] = $options['max_duration'] ?? null; $response->info['url'] = $url; if ($mock instanceof self) { @@ -285,6 +286,7 @@ private static function readResponse(self $response, array $options, ResponseInt $response->info = [ 'start_time' => $response->info['start_time'], 'user_data' => $response->info['user_data'], + 'max_duration' => $response->info['max_duration'], 'http_code' => $response->info['http_code'], ] + $info + $response->info; diff --git a/src/Symfony/Component/HttpClient/Response/NativeResponse.php b/src/Symfony/Component/HttpClient/Response/NativeResponse.php index c06237bec2915..c00e946f63508 100644 --- a/src/Symfony/Component/HttpClient/Response/NativeResponse.php +++ b/src/Symfony/Component/HttpClient/Response/NativeResponse.php @@ -59,6 +59,7 @@ public function __construct(NativeClientState $multi, $context, string $url, arr $this->buffer = fopen('php://temp', 'w+'); $info['user_data'] = $options['user_data']; + $info['max_duration'] = $options['max_duration']; ++$multi->responseCount; $this->initializer = static function (self $response) { diff --git a/src/Symfony/Component/HttpClient/Tests/AsyncDecoratorTraitTest.php b/src/Symfony/Component/HttpClient/Tests/AsyncDecoratorTraitTest.php index 0dfca6d4a2f97..199d2cf5d0b81 100644 --- a/src/Symfony/Component/HttpClient/Tests/AsyncDecoratorTraitTest.php +++ b/src/Symfony/Component/HttpClient/Tests/AsyncDecoratorTraitTest.php @@ -13,6 +13,7 @@ use Symfony\Component\HttpClient\AsyncDecoratorTrait; use Symfony\Component\HttpClient\DecoratorTrait; +use Symfony\Component\HttpClient\Exception\TransportException; use Symfony\Component\HttpClient\HttpClient; use Symfony\Component\HttpClient\Response\AsyncContext; use Symfony\Component\HttpClient\Response\AsyncResponse; @@ -339,4 +340,28 @@ public function request(string $method, string $url, array $options = []): Respo $this->expectExceptionMessage('Instance of "Symfony\Component\HttpClient\Response\NativeResponse" is already consumed and cannot be managed by "Symfony\Component\HttpClient\Response\AsyncResponse". A decorated client should not call any of the response\'s methods in its "request()" method.'); $response->getStatusCode(); } + + public function testMaxDuration() + { + $sawFirst = false; + $client = $this->getHttpClient(__FUNCTION__, function (ChunkInterface $chunk, AsyncContext $context) use (&$sawFirst) { + try { + if (!$chunk->isFirst() || !$sawFirst) { + $sawFirst = $sawFirst || $chunk->isFirst(); + yield $chunk; + } + } catch (TransportExceptionInterface $e) { + $context->getResponse()->cancel(); + $context->replaceRequest('GET', 'http://localhost:8057/timeout-body', ['timeout' => 0.4]); + } + }); + + $response = $client->request('GET', 'http://localhost:8057/timeout-body', ['max_duration' => 0.75, 'timeout' => 0.4]); + + $this->assertSame(0.75, $response->getInfo('max_duration')); + + $this->expectException(TransportException::class); + $this->expectExceptionMessage('Max duration was reached for "http://localhost:8057/timeout-body".'); + $response->getContent(); + } } diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/NativeSessionStorage.php b/src/Symfony/Component/HttpFoundation/Session/Storage/NativeSessionStorage.php index 02467da9e5b61..49434f5b935f9 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Storage/NativeSessionStorage.php +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/NativeSessionStorage.php @@ -145,6 +145,12 @@ public function start() throw new \RuntimeException(sprintf('Failed to start the session because headers have already been sent by "%s" at line %d.', $file, $line)); } + $sessionId = $_COOKIE[session_name()] ?? null; + if ($sessionId && !preg_match('/^[a-zA-Z0-9,-]{22,}$/', $sessionId)) { + // the session ID in the header is invalid, create a new one + session_id(session_create_id()); + } + // ok to try and start the session if (!session_start()) { throw new \RuntimeException('Failed to start the session.'); diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/NativeSessionStorageTest.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/NativeSessionStorageTest.php index 2e6daaeefb2d1..cff7beb59f4fb 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/NativeSessionStorageTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/NativeSessionStorageTest.php @@ -286,4 +286,13 @@ public function testGetBagsOnceSessionStartedIsIgnored() $this->assertEquals($storage->getBag('flashes'), $bag); } + + public function testRegenerateInvalidSessionId() + { + $_COOKIE[session_name()] = '&~['; + $started = (new NativeSessionStorage())->start(); + + $this->assertTrue($started); + $this->assertMatchesRegularExpression('/^[a-zA-Z0-9,-]{22,}$/', session_id()); + } } diff --git a/src/Symfony/Component/HttpKernel/EventListener/AbstractSessionListener.php b/src/Symfony/Component/HttpKernel/EventListener/AbstractSessionListener.php index 0924b766b352a..d0e8f45d038b6 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/AbstractSessionListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/AbstractSessionListener.php @@ -104,7 +104,7 @@ public function onKernelResponse(ResponseEvent $event) // Always remove the internal header if present $response->headers->remove(self::NO_AUTO_CACHE_CONTROL_HEADER); - if (!$session = $this->container && $this->container->has('initialized_session') ? $this->container->get('initialized_session') : $event->getRequest()->getSession()) { + if (!$session = $this->container && $this->container->has('initialized_session') ? $this->container->get('initialized_session') : ($event->getRequest()->hasSession() ? $event->getRequest()->getSession() : null)) { return; } diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 578d976fdbea6..ab99b29ec5a34 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -78,11 +78,11 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl */ private static $freshCache = []; - public const VERSION = '5.4.8'; - public const VERSION_ID = 50408; + public const VERSION = '5.4.9'; + public const VERSION_ID = 50409; public const MAJOR_VERSION = 5; public const MINOR_VERSION = 4; - public const RELEASE_VERSION = 8; + public const RELEASE_VERSION = 9; public const EXTRA_VERSION = ''; public const END_OF_MAINTENANCE = '11/2024'; diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/ResponseListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/ResponseListenerTest.php index 0a321aa5e0f10..f6ca1963c812c 100644 --- a/src/Symfony/Component/HttpKernel/Tests/EventListener/ResponseListenerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/ResponseListenerTest.php @@ -102,7 +102,7 @@ public function testSetContentLanguageHeaderWhenEmptyAndAtLeast2EnabledLocalesAr $request = Request::create('/'); $request->setLocale('fr'); - $event = new ResponseEvent($this->kernel, $request, HttpKernelInterface::MASTER_REQUEST, $response); + $event = new ResponseEvent($this->kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response); $this->dispatcher->dispatch($event, KernelEvents::RESPONSE); $this->assertEquals('fr', $response->headers->get('Content-Language')); @@ -118,7 +118,7 @@ public function testNotOverrideContentLanguageHeaderWhenNotEmpty() $request = Request::create('/'); $request->setLocale('de'); - $event = new ResponseEvent($this->kernel, $request, HttpKernelInterface::MASTER_REQUEST, $response); + $event = new ResponseEvent($this->kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response); $this->dispatcher->dispatch($event, KernelEvents::RESPONSE); $this->assertEquals('mi, en', $response->headers->get('Content-Language')); @@ -133,7 +133,7 @@ public function testNotSetContentLanguageHeaderWhenDisabled() $request = Request::create('/'); $request->setLocale('fr'); - $event = new ResponseEvent($this->kernel, $request, HttpKernelInterface::MASTER_REQUEST, $response); + $event = new ResponseEvent($this->kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response); $this->dispatcher->dispatch($event, KernelEvents::RESPONSE); $this->assertNull($response->headers->get('Content-Language')); diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/SessionListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/SessionListenerTest.php index 48f472a1ece88..ab999f5772986 100644 --- a/src/Symfony/Component/HttpKernel/Tests/EventListener/SessionListenerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/SessionListenerTest.php @@ -523,7 +523,7 @@ public function testUninitializedSessionWithoutInitializedSession() $container = new ServiceLocator([]); $listener = new SessionListener($container); - $listener->onKernelResponse(new ResponseEvent($kernel, new Request(), HttpKernelInterface::MASTER_REQUEST, $response)); + $listener->onKernelResponse(new ResponseEvent($kernel, new Request(), HttpKernelInterface::MAIN_REQUEST, $response)); $this->assertFalse($response->headers->has('Expires')); $this->assertTrue($response->headers->hasCacheControlDirective('public')); $this->assertFalse($response->headers->hasCacheControlDirective('private')); diff --git a/src/Symfony/Component/HttpKernel/Tests/Fragment/InlineFragmentRendererTest.php b/src/Symfony/Component/HttpKernel/Tests/Fragment/InlineFragmentRendererTest.php index c22a426d7d31e..69bd7445acfd6 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Fragment/InlineFragmentRendererTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Fragment/InlineFragmentRendererTest.php @@ -155,7 +155,7 @@ public function testExceptionInSubRequestsDoesNotMangleOutputBuffers() $this->assertEquals('Foo', ob_get_clean()); } - public function testLocaleAndFormatAreIsKeptInSubrequest() + public function testLocaleAndFormatAreKeptInSubrequest() { $expectedSubRequest = Request::create('/'); $expectedSubRequest->attributes->set('_format', 'foo'); diff --git a/src/Symfony/Component/Ldap/Adapter/ExtLdap/Connection.php b/src/Symfony/Component/Ldap/Adapter/ExtLdap/Connection.php index 3de499e9d1f03..c2532db8efaeb 100644 --- a/src/Symfony/Component/Ldap/Adapter/ExtLdap/Connection.php +++ b/src/Symfony/Component/Ldap/Adapter/ExtLdap/Connection.php @@ -29,6 +29,12 @@ class Connection extends AbstractConnection private const LDAP_INVALID_CREDENTIALS = 0x31; private const LDAP_TIMEOUT = 0x55; private const LDAP_ALREADY_EXISTS = 0x44; + private const PRECONNECT_OPTIONS = [ + ConnectionOptions::DEBUG_LEVEL, + ConnectionOptions::X_TLS_CACERTDIR, + ConnectionOptions::X_TLS_CACERTFILE, + ConnectionOptions::X_TLS_REQUIRE_CERT, + ]; /** @var bool */ private $bound = false; @@ -147,10 +153,18 @@ private function connect() return; } + foreach ($this->config['options'] as $name => $value) { + if (\in_array(ConnectionOptions::getOption($name), self::PRECONNECT_OPTIONS, true)) { + $this->setOption($name, $value); + } + } + $this->connection = ldap_connect($this->config['connection_string']); foreach ($this->config['options'] as $name => $value) { - $this->setOption($name, $value); + if (!\in_array(ConnectionOptions::getOption($name), self::PRECONNECT_OPTIONS, true)) { + $this->setOption($name, $value); + } } if (false === $this->connection) { diff --git a/src/Symfony/Component/Ldap/Adapter/ExtLdap/ConnectionOptions.php b/src/Symfony/Component/Ldap/Adapter/ExtLdap/ConnectionOptions.php index 50061bd80959e..58094fad5b8ea 100644 --- a/src/Symfony/Component/Ldap/Adapter/ExtLdap/ConnectionOptions.php +++ b/src/Symfony/Component/Ldap/Adapter/ExtLdap/ConnectionOptions.php @@ -40,6 +40,7 @@ final class ConnectionOptions public const DEBUG_LEVEL = 0x5001; public const TIMEOUT = 0x5002; public const NETWORK_TIMEOUT = 0x5005; + public const X_TLS_CACERTFILE = 0x6002; public const X_TLS_CACERTDIR = 0x6003; public const X_TLS_CERTFILE = 0x6004; public const X_TLS_CRL_ALL = 0x02; diff --git a/src/Symfony/Component/Mime/Email.php b/src/Symfony/Component/Mime/Email.php index 44efe7b3100d8..bba99ee10eee2 100644 --- a/src/Symfony/Component/Mime/Email.php +++ b/src/Symfony/Component/Mime/Email.php @@ -272,12 +272,16 @@ public function getPriority(): int } /** - * @param resource|string $body + * @param resource|string|null $body * * @return $this */ public function text($body, string $charset = 'utf-8') { + if (null !== $body && !\is_string($body) && !\is_resource($body)) { + throw new \TypeError(sprintf('The body must be a string, a resource or null (got "%s").', get_debug_type($body))); + } + $this->text = $body; $this->textCharset = $charset; @@ -304,6 +308,10 @@ public function getTextCharset(): ?string */ public function html($body, string $charset = 'utf-8') { + if (null !== $body && !\is_string($body) && !\is_resource($body)) { + throw new \TypeError(sprintf('The body must be a string, a resource or null (got "%s").', get_debug_type($body))); + } + $this->html = $body; $this->htmlCharset = $charset; @@ -330,6 +338,10 @@ public function getHtmlCharset(): ?string */ public function attach($body, string $name = null, string $contentType = null) { + if (!\is_string($body) && !\is_resource($body)) { + throw new \TypeError(sprintf('The body must be a string or a resource (got "%s").', get_debug_type($body))); + } + $this->attachments[] = ['body' => $body, 'name' => $name, 'content-type' => $contentType, 'inline' => false]; return $this; @@ -352,6 +364,10 @@ public function attachFromPath(string $path, string $name = null, string $conten */ public function embed($body, string $name = null, string $contentType = null) { + if (!\is_string($body) && !\is_resource($body)) { + throw new \TypeError(sprintf('The body must be a string or a resource (got "%s").', get_debug_type($body))); + } + $this->attachments[] = ['body' => $body, 'name' => $name, 'content-type' => $contentType, 'inline' => true]; return $this; @@ -463,7 +479,7 @@ private function prepareParts(): ?array $names = []; $htmlPart = null; $html = $this->html; - if (null !== $this->html) { + if (null !== $html) { $htmlPart = new TextPart($html, $this->htmlCharset, 'html'); $html = $htmlPart->getBody(); preg_match_all('(]*src\s*=\s*(?:([\'"])cid:([^"]+)\\1|cid:([^>\s]+)))i', $html, $names); diff --git a/src/Symfony/Component/Mime/Test/Constraint/EmailHeaderSame.php b/src/Symfony/Component/Mime/Test/Constraint/EmailHeaderSame.php index 74bdc63c79f71..74b412183a779 100644 --- a/src/Symfony/Component/Mime/Test/Constraint/EmailHeaderSame.php +++ b/src/Symfony/Component/Mime/Test/Constraint/EmailHeaderSame.php @@ -55,12 +55,14 @@ protected function matches($message): bool */ protected function failureDescription($message): string { - return sprintf('the Email %s (value is %s)', $this->toString(), $this->getHeaderValue($message)); + return sprintf('the Email %s (value is %s)', $this->toString(), $this->getHeaderValue($message) ?? 'null'); } - private function getHeaderValue($message): string + private function getHeaderValue($message): ?string { - $header = $message->getHeaders()->get($this->headerName); + if (null === $header = $message->getHeaders()->get($this->headerName)) { + return null; + } return $header instanceof UnstructuredHeader ? $header->getValue() : $header->getBodyAsString(); } diff --git a/src/Symfony/Component/Mime/Tests/EmailTest.php b/src/Symfony/Component/Mime/Tests/EmailTest.php index e5b48fe6cabdc..c3771b2cbb9de 100644 --- a/src/Symfony/Component/Mime/Tests/EmailTest.php +++ b/src/Symfony/Component/Mime/Tests/EmailTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Mime\Tests; +use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\TestCase; use Symfony\Component\Mime\Address; use Symfony\Component\Mime\Email; @@ -19,6 +20,7 @@ use Symfony\Component\Mime\Part\Multipart\MixedPart; use Symfony\Component\Mime\Part\Multipart\RelatedPart; use Symfony\Component\Mime\Part\TextPart; +use Symfony\Component\Mime\Test\Constraint\EmailHeaderSame; use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; @@ -458,4 +460,76 @@ public function testSymfonySerialize() $this->assertEquals($expected->getHeaders(), $n->getHeaders()); $this->assertEquals($expected->getBody(), $n->getBody()); } + + public function testMissingHeaderDoesNotThrowError() + { + $this->expectException(ExpectationFailedException::class); + $this->expectExceptionMessage('Failed asserting that the Email has header "foo" with value "bar" (value is null).'); + + $e = new Email(); + $emailHeaderSame = new EmailHeaderSame('foo', 'bar'); + $emailHeaderSame->evaluate($e); + } + + public function testAttachBodyExpectStringOrResource() + { + $this->expectException(\TypeError::class); + $this->expectExceptionMessage('The body must be a string or a resource (got "bool").'); + + (new Email())->attach(false); + } + + public function testEmbedBodyExpectStringOrResource() + { + $this->expectException(\TypeError::class); + $this->expectExceptionMessage('The body must be a string or a resource (got "bool").'); + + (new Email())->embed(false); + } + + public function testHtmlBodyExpectStringOrResourceOrNull() + { + $this->expectException(\TypeError::class); + $this->expectExceptionMessage('The body must be a string, a resource or null (got "bool").'); + + (new Email())->html(false); + } + + public function testHtmlBodyAcceptedTypes() + { + $email = new Email(); + + $email->html('foo'); + $this->assertSame('foo', $email->getHtmlBody()); + + $email->html(null); + $this->assertNull($email->getHtmlBody()); + + $contents = file_get_contents(__DIR__.'/Fixtures/mimetypes/test', 'r'); + $email->html($contents); + $this->assertSame($contents, $email->getHtmlBody()); + } + + public function testTextBodyExpectStringOrResourceOrNull() + { + $this->expectException(\TypeError::class); + $this->expectExceptionMessage('The body must be a string, a resource or null (got "bool").'); + + (new Email())->text(false); + } + + public function testTextBodyAcceptedTypes() + { + $email = new Email(); + + $email->text('foo'); + $this->assertSame('foo', $email->getTextBody()); + + $email->text(null); + $this->assertNull($email->getTextBody()); + + $contents = file_get_contents(__DIR__.'/Fixtures/mimetypes/test', 'r'); + $email->text($contents); + $this->assertSame($contents, $email->getTextBody()); + } } diff --git a/src/Symfony/Component/Notifier/Bridge/Mercure/composer.json b/src/Symfony/Component/Notifier/Bridge/Mercure/composer.json index 54137dd9d3cc3..ed13323a28166 100644 --- a/src/Symfony/Component/Notifier/Bridge/Mercure/composer.json +++ b/src/Symfony/Component/Notifier/Bridge/Mercure/composer.json @@ -18,7 +18,7 @@ "require": { "php": ">=7.2.5", "ext-json": "*", - "symfony/mercure": "^0.5.2", + "symfony/mercure": "^0.5.2|^0.6", "symfony/notifier": "^5.3|^6.0", "symfony/service-contracts": "^1.10|^2|^3" }, diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php index ec1e7eef35512..2dd4c2c4b0d16 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php @@ -305,6 +305,21 @@ public function php81TypesProvider() ]; } + /** + * @dataProvider php82TypesProvider + * @requires PHP 8.2 + */ + public function testExtractPhp82Type($property, array $type = null) + { + $this->assertEquals($type, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\Php82Dummy', $property, [])); + } + + public function php82TypesProvider() + { + yield ['nil', null]; + yield ['false', [new Type(Type::BUILTIN_TYPE_FALSE)]]; + } + /** * @dataProvider defaultValueProvider */ diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/ConstructorDummy.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/ConstructorDummy.php index 23ef5cceaef75..1c50c67e73ed6 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/ConstructorDummy.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/ConstructorDummy.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\PropertyInfo\Tests\Fixtures; /** diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DockBlockFallback.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DockBlockFallback.php index 89136280da56b..ac4d4b6583d62 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DockBlockFallback.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DockBlockFallback.php @@ -1,7 +1,5 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace A { class Property { diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/NoProperties.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/NoProperties.php index 177bbe4df0f03..e0cd0c7e21b35 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/NoProperties.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/NoProperties.php @@ -1,7 +1,5 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\PropertyInfo\Tests\Fixtures; class Php80Dummy diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php81Dummy.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php81Dummy.php index 1300c3e695f1f..842f59fbfd47b 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php81Dummy.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php81Dummy.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\PropertyInfo\Tests\Fixtures; class Php81Dummy diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php82Dummy.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php82Dummy.php new file mode 100644 index 0000000000000..b830fabf8842a --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php82Dummy.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo\Tests\Fixtures; + +class Php82Dummy +{ + public null $nil = null; + + public false $false = false; +} diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/PseudoTypeDummy.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/PseudoTypeDummy.php index 71756044fafc9..d2efecef9dcf4 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/PseudoTypeDummy.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/PseudoTypeDummy.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\PropertyInfo\Tests\Fixtures; class PseudoTypeDummy diff --git a/src/Symfony/Component/PropertyInfo/Type.php b/src/Symfony/Component/PropertyInfo/Type.php index 6aecc01c3ed96..5c1b5c1248920 100644 --- a/src/Symfony/Component/PropertyInfo/Type.php +++ b/src/Symfony/Component/PropertyInfo/Type.php @@ -24,12 +24,12 @@ class Type public const BUILTIN_TYPE_FLOAT = 'float'; public const BUILTIN_TYPE_STRING = 'string'; public const BUILTIN_TYPE_BOOL = 'bool'; - public const BUILTIN_TYPE_TRUE = 'true'; - public const BUILTIN_TYPE_FALSE = 'false'; public const BUILTIN_TYPE_RESOURCE = 'resource'; public const BUILTIN_TYPE_OBJECT = 'object'; public const BUILTIN_TYPE_ARRAY = 'array'; public const BUILTIN_TYPE_NULL = 'null'; + public const BUILTIN_TYPE_FALSE = 'false'; + public const BUILTIN_TYPE_TRUE = 'true'; public const BUILTIN_TYPE_CALLABLE = 'callable'; public const BUILTIN_TYPE_ITERABLE = 'iterable'; @@ -43,12 +43,12 @@ class Type self::BUILTIN_TYPE_FLOAT, self::BUILTIN_TYPE_STRING, self::BUILTIN_TYPE_BOOL, - self::BUILTIN_TYPE_TRUE, - self::BUILTIN_TYPE_FALSE, self::BUILTIN_TYPE_RESOURCE, self::BUILTIN_TYPE_OBJECT, self::BUILTIN_TYPE_ARRAY, self::BUILTIN_TYPE_CALLABLE, + self::BUILTIN_TYPE_FALSE, + self::BUILTIN_TYPE_TRUE, self::BUILTIN_TYPE_NULL, self::BUILTIN_TYPE_ITERABLE, ]; diff --git a/src/Symfony/Component/RateLimiter/Storage/InMemoryStorage.php b/src/Symfony/Component/RateLimiter/Storage/InMemoryStorage.php index 7dade5a4e98fd..d0bfccdb4b63c 100644 --- a/src/Symfony/Component/RateLimiter/Storage/InMemoryStorage.php +++ b/src/Symfony/Component/RateLimiter/Storage/InMemoryStorage.php @@ -22,15 +22,7 @@ class InMemoryStorage implements StorageInterface public function save(LimiterStateInterface $limiterState): void { - if (isset($this->buckets[$limiterState->getId()])) { - [$expireAt, ] = $this->buckets[$limiterState->getId()]; - } - - if (null !== ($expireSeconds = $limiterState->getExpirationTime())) { - $expireAt = microtime(true) + $expireSeconds; - } - - $this->buckets[$limiterState->getId()] = [$expireAt, serialize($limiterState)]; + $this->buckets[$limiterState->getId()] = [$this->getExpireAt($limiterState), serialize($limiterState)]; } public function fetch(string $limiterStateId): ?LimiterStateInterface @@ -57,4 +49,13 @@ public function delete(string $limiterStateId): void unset($this->buckets[$limiterStateId]); } + + private function getExpireAt(LimiterStateInterface $limiterState): ?float + { + if (null !== $expireSeconds = $limiterState->getExpirationTime()) { + return microtime(true) + $expireSeconds; + } + + return $this->buckets[$limiterState->getId()][0] ?? null; + } } diff --git a/src/Symfony/Component/RateLimiter/Tests/Policy/RateTest.php b/src/Symfony/Component/RateLimiter/Tests/Policy/RateTest.php index 39a859f587555..a780d34fdb82f 100644 --- a/src/Symfony/Component/RateLimiter/Tests/Policy/RateTest.php +++ b/src/Symfony/Component/RateLimiter/Tests/Policy/RateTest.php @@ -26,7 +26,7 @@ public function testFromString(Rate $rate) public function provideRate(): iterable { - yield [new Rate(\DateInterval::createFromDateString('15 seconds'), 10)]; + yield [new Rate(new \DateInterval('PT15S'), 10)]; yield [Rate::perSecond(10)]; yield [Rate::perMinute(10)]; yield [Rate::perHour(10)]; diff --git a/src/Symfony/Component/Security/Csrf/CsrfTokenManager.php b/src/Symfony/Component/Security/Csrf/CsrfTokenManager.php index 3e7454e793d28..14c05592d3241 100644 --- a/src/Symfony/Component/Security/Csrf/CsrfTokenManager.php +++ b/src/Symfony/Component/Security/Csrf/CsrfTokenManager.php @@ -134,6 +134,9 @@ private function derandomize(string $value): string return $value; } $key = base64_decode(strtr($parts[1], '-_', '+/')); + if ('' === $key || false === $key) { + return $value; + } $value = base64_decode(strtr($parts[2], '-_', '+/')); return $this->xor($value, $key); diff --git a/src/Symfony/Component/Security/Csrf/Tests/CsrfTokenManagerTest.php b/src/Symfony/Component/Security/Csrf/Tests/CsrfTokenManagerTest.php index d654bbf195fa4..bd911987f1f2d 100644 --- a/src/Symfony/Component/Security/Csrf/Tests/CsrfTokenManagerTest.php +++ b/src/Symfony/Component/Security/Csrf/Tests/CsrfTokenManagerTest.php @@ -193,6 +193,26 @@ public function testNonExistingTokenIsNotValid($namespace, $manager, $storage) $this->assertFalse($manager->isTokenValid(new CsrfToken('token_id', 'FOOBAR'))); } + public function testTokenShouldNotTriggerDivisionByZero() + { + [$generator, $storage] = $this->getGeneratorAndStorage(); + $manager = new CsrfTokenManager($generator, $storage); + + // Scenario: the token that was returned is abc.def.ghi, and gets modified in the browser to abc..ghi + + $storage->expects($this->once()) + ->method('hasToken') + ->with('https-token_id') + ->willReturn(true); + + $storage->expects($this->once()) + ->method('getToken') + ->with('https-token_id') + ->willReturn('def'); + + $this->assertFalse($manager->isTokenValid(new CsrfToken('token_id', 'abc..ghi'))); + } + /** * @dataProvider getManagerGeneratorAndStorage */ diff --git a/src/Symfony/Component/Security/Guard/Tests/Firewall/GuardAuthenticationListenerTest.php b/src/Symfony/Component/Security/Guard/Tests/Firewall/GuardAuthenticationListenerTest.php index a4ccf9291ecc3..19bedb20dac66 100644 --- a/src/Symfony/Component/Security/Guard/Tests/Firewall/GuardAuthenticationListenerTest.php +++ b/src/Symfony/Component/Security/Guard/Tests/Firewall/GuardAuthenticationListenerTest.php @@ -324,7 +324,7 @@ protected function setUp(): void $this->guardAuthenticatorHandler = $this->createMock(GuardAuthenticatorHandler::class); $this->request = new Request([], [], [], [], [], []); - $this->event = new RequestEvent($this->createMock(HttpKernelInterface::class), $this->request, HttpKernelInterface::MASTER_REQUEST); + $this->event = new RequestEvent($this->createMock(HttpKernelInterface::class), $this->request, HttpKernelInterface::MAIN_REQUEST); $this->logger = $this->createMock(LoggerInterface::class); $this->rememberMeServices = $this->createMock(RememberMeServicesInterface::class); diff --git a/src/Symfony/Component/Security/Http/Authentication/DefaultAuthenticationFailureHandler.php b/src/Symfony/Component/Security/Http/Authentication/DefaultAuthenticationFailureHandler.php index 31b5f64d5b3a3..781309d588f0d 100644 --- a/src/Symfony/Component/Security/Http/Authentication/DefaultAuthenticationFailureHandler.php +++ b/src/Symfony/Component/Security/Http/Authentication/DefaultAuthenticationFailureHandler.php @@ -70,31 +70,34 @@ public function setOptions(array $options) */ public function onAuthenticationFailure(Request $request, AuthenticationException $exception) { - if ($failureUrl = ParameterBagUtils::getRequestParameterValue($request, $this->options['failure_path_parameter'])) { - $this->options['failure_path'] = $failureUrl; - } + $options = $this->options; + $failureUrl = ParameterBagUtils::getRequestParameterValue($request, $options['failure_path_parameter']); - if (null === $this->options['failure_path']) { - $this->options['failure_path'] = $this->options['login_path']; + if (\is_string($failureUrl) && str_starts_with($failureUrl, '/')) { + $options['failure_path'] = $failureUrl; + } elseif ($this->logger && $failureUrl) { + $this->logger->debug(sprintf('Ignoring query parameter "%s": not a valid URL.', $options['failure_path_parameter'])); } - if ($this->options['failure_forward']) { + $options['failure_path'] ?? $options['failure_path'] = $options['login_path']; + + if ($options['failure_forward']) { if (null !== $this->logger) { - $this->logger->debug('Authentication failure, forward triggered.', ['failure_path' => $this->options['failure_path']]); + $this->logger->debug('Authentication failure, forward triggered.', ['failure_path' => $options['failure_path']]); } - $subRequest = $this->httpUtils->createRequest($request, $this->options['failure_path']); + $subRequest = $this->httpUtils->createRequest($request, $options['failure_path']); $subRequest->attributes->set(Security::AUTHENTICATION_ERROR, $exception); return $this->httpKernel->handle($subRequest, HttpKernelInterface::SUB_REQUEST); } if (null !== $this->logger) { - $this->logger->debug('Authentication failure, redirect triggered.', ['failure_path' => $this->options['failure_path']]); + $this->logger->debug('Authentication failure, redirect triggered.', ['failure_path' => $options['failure_path']]); } $request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception); - return $this->httpUtils->createRedirectResponse($request, $this->options['failure_path']); + return $this->httpUtils->createRedirectResponse($request, $options['failure_path']); } } diff --git a/src/Symfony/Component/Security/Http/Authentication/DefaultAuthenticationSuccessHandler.php b/src/Symfony/Component/Security/Http/Authentication/DefaultAuthenticationSuccessHandler.php index 0972af94be53a..2870e7768eb57 100644 --- a/src/Symfony/Component/Security/Http/Authentication/DefaultAuthenticationSuccessHandler.php +++ b/src/Symfony/Component/Security/Http/Authentication/DefaultAuthenticationSuccessHandler.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Security\Http\Authentication; +use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Http\HttpUtils; @@ -29,6 +30,7 @@ class DefaultAuthenticationSuccessHandler implements AuthenticationSuccessHandle use TargetPathTrait; protected $httpUtils; + protected $logger; protected $options; /** @deprecated since Symfony 5.2, use $firewallName instead */ protected $providerKey; @@ -44,9 +46,10 @@ class DefaultAuthenticationSuccessHandler implements AuthenticationSuccessHandle /** * @param array $options Options for processing a successful authentication attempt */ - public function __construct(HttpUtils $httpUtils, array $options = []) + public function __construct(HttpUtils $httpUtils, array $options = [], LoggerInterface $logger = null) { $this->httpUtils = $httpUtils; + $this->logger = $logger; $this->setOptions($options); } @@ -127,10 +130,16 @@ protected function determineTargetUrl(Request $request) return $this->options['default_target_path']; } - if ($targetUrl = ParameterBagUtils::getRequestParameterValue($request, $this->options['target_path_parameter'])) { + $targetUrl = ParameterBagUtils::getRequestParameterValue($request, $this->options['target_path_parameter']); + + if (\is_string($targetUrl) && str_starts_with($targetUrl, '/')) { return $targetUrl; } + if ($this->logger && $targetUrl) { + $this->logger->debug(sprintf('Ignoring query parameter "%s": not a valid URL.', $this->options['target_path_parameter'])); + } + $firewallName = $this->getFirewallName(); if (null !== $firewallName && $targetUrl = $this->getTargetPath($request->getSession(), $firewallName)) { $this->removeTargetPath($request->getSession(), $firewallName); diff --git a/src/Symfony/Component/Security/Http/Tests/Authentication/DefaultAuthenticationFailureHandlerTest.php b/src/Symfony/Component/Security/Http/Tests/Authentication/DefaultAuthenticationFailureHandlerTest.php index d95027560b706..22a2b9277bbbd 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authentication/DefaultAuthenticationFailureHandlerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authentication/DefaultAuthenticationFailureHandlerTest.php @@ -187,6 +187,26 @@ public function testFailurePathParameterCanBeOverwritten() $handler->onAuthenticationFailure($this->request, $this->exception); } + public function testFailurePathFromRequestWithInvalidUrl() + { + $options = ['failure_path_parameter' => '_my_failure_path']; + + $this->request->expects($this->once()) + ->method('get')->with('_my_failure_path') + ->willReturn('some_route_name'); + + $this->logger->expects($this->exactly(2)) + ->method('debug') + ->withConsecutive( + ['Ignoring query parameter "_my_failure_path": not a valid URL.'], + ['Authentication failure, redirect triggered.', ['failure_path' => '/login']] + ); + + $handler = new DefaultAuthenticationFailureHandler($this->httpKernel, $this->httpUtils, $options, $this->logger); + + $handler->onAuthenticationFailure($this->request, $this->exception); + } + private function getRequest() { $request = $this->createMock(Request::class); diff --git a/src/Symfony/Component/Security/Http/Tests/Authentication/DefaultAuthenticationSuccessHandlerTest.php b/src/Symfony/Component/Security/Http/Tests/Authentication/DefaultAuthenticationSuccessHandlerTest.php index d10769e77c1b6..0ca19e60a9119 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authentication/DefaultAuthenticationSuccessHandlerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authentication/DefaultAuthenticationSuccessHandlerTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Security\Http\Tests\Authentication; use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; @@ -113,4 +114,25 @@ public function getRequestRedirections() ], ]; } + + public function testTargetPathFromRequestWithInvalidUrl() + { + $httpUtils = $this->createMock(HttpUtils::class); + $options = ['target_path_parameter' => '_my_target_path']; + $token = $this->createMock(TokenInterface::class); + + $request = $this->createMock(Request::class); + $request->expects($this->once()) + ->method('get')->with('_my_target_path') + ->willReturn('some_route_name'); + + $logger = $this->createMock(LoggerInterface::class); + $logger->expects($this->once()) + ->method('debug') + ->with('Ignoring query parameter "_my_target_path": not a valid URL.'); + + $handler = new DefaultAuthenticationSuccessHandler($httpUtils, $options, $logger); + + $handler->onAuthenticationSuccess($request, $token); + } } diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/ContextListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/ContextListenerTest.php index 70737932aadaa..6c02847bc4b26 100644 --- a/src/Symfony/Component/Security/Http/Tests/Firewall/ContextListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/ContextListenerTest.php @@ -381,6 +381,30 @@ public function testSessionIsNotReported() $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST)); } + public function testOnKernelResponseRemoveListener() + { + $tokenStorage = new TokenStorage(); + $tokenStorage->setToken(new UsernamePasswordToken(new InMemoryUser('test1', 'pass1'), 'phpunit', ['ROLE_USER'])); + + $request = new Request(); + $request->attributes->set('_security_firewall_run', '_security_session'); + + $session = new Session(new MockArraySessionStorage()); + $request->setSession($session); + + $dispatcher = new EventDispatcher(); + $httpKernel = $this->createMock(HttpKernelInterface::class); + + $listener = new ContextListener($tokenStorage, [], 'session', null, $dispatcher, null, \Closure::fromCallable([$tokenStorage, 'getToken'])); + $this->assertEmpty($dispatcher->getListeners()); + + $listener(new RequestEvent($httpKernel, $request, HttpKernelInterface::MAIN_REQUEST)); + $this->assertNotEmpty($dispatcher->getListeners()); + + $listener->onKernelResponse(new ResponseEvent($httpKernel, $request, HttpKernelInterface::MAIN_REQUEST, new Response())); + $this->assertEmpty($dispatcher->getListeners()); + } + protected function runSessionOnKernelResponse($newToken, $original = null) { $session = new Session(new MockArraySessionStorage()); diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/ExceptionListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/ExceptionListenerTest.php index 35fa53bed4a21..487b0f7caf93f 100644 --- a/src/Symfony/Component/Security/Http/Tests/Firewall/ExceptionListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/ExceptionListenerTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Security\Http\Tests\Firewall; use PHPUnit\Framework\TestCase; +use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\ExceptionEvent; @@ -170,6 +171,18 @@ public function testLogoutException() $this->assertEquals(403, $event->getThrowable()->getStatusCode()); } + public function testUnregister() + { + $listener = $this->createExceptionListener(); + $dispatcher = new EventDispatcher(); + + $listener->register($dispatcher); + $this->assertNotEmpty($dispatcher->getListeners()); + + $listener->unregister($dispatcher); + $this->assertEmpty($dispatcher->getListeners()); + } + public function getAccessDeniedExceptionProvider() { return [ diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/RememberMeListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/RememberMeListenerTest.php index 7902d46568638..d2e1ceec3b567 100644 --- a/src/Symfony/Component/Security/Http/Tests/Firewall/RememberMeListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/RememberMeListenerTest.php @@ -48,7 +48,7 @@ public function testOnCoreSecurityDoesNotTryToPopulateNonEmptyTokenStorage() ->method('setToken') ; - $this->assertNull($listener(new RequestEvent($this->createMock(HttpKernelInterface::class), new Request(), HttpKernelInterface::MASTER_REQUEST))); + $this->assertNull($listener(new RequestEvent($this->createMock(HttpKernelInterface::class), new Request(), HttpKernelInterface::MAIN_REQUEST))); } public function testOnCoreSecurityDoesNothingWhenNoCookieIsSet() @@ -67,7 +67,7 @@ public function testOnCoreSecurityDoesNothingWhenNoCookieIsSet() ->willReturn(null) ; - $this->assertNull($listener(new RequestEvent($this->createMock(HttpKernelInterface::class), new Request(), HttpKernelInterface::MASTER_REQUEST))); + $this->assertNull($listener(new RequestEvent($this->createMock(HttpKernelInterface::class), new Request(), HttpKernelInterface::MAIN_REQUEST))); } public function testOnCoreSecurityIgnoresAuthenticationExceptionThrownByAuthenticationManagerImplementation() @@ -100,7 +100,7 @@ public function testOnCoreSecurityIgnoresAuthenticationExceptionThrownByAuthenti ->willThrowException($exception) ; - $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MASTER_REQUEST)); + $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST)); } public function testOnCoreSecurityIgnoresAuthenticationOptionallyRethrowsExceptionThrownAuthenticationManagerImplementation() @@ -133,7 +133,7 @@ public function testOnCoreSecurityIgnoresAuthenticationOptionallyRethrowsExcepti ->willThrowException($exception) ; - $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), new Request(), HttpKernelInterface::MASTER_REQUEST)); + $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), new Request(), HttpKernelInterface::MAIN_REQUEST)); } public function testOnCoreSecurityAuthenticationExceptionDuringAutoLoginTriggersLoginFail() @@ -163,7 +163,7 @@ public function testOnCoreSecurityAuthenticationExceptionDuringAutoLoginTriggers ->method('authenticate') ; - $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), new Request(), HttpKernelInterface::MASTER_REQUEST)); + $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), new Request(), HttpKernelInterface::MAIN_REQUEST)); } public function testOnCoreSecurity() @@ -195,7 +195,7 @@ public function testOnCoreSecurity() ->willReturn($token) ; - $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), new Request(), HttpKernelInterface::MASTER_REQUEST)); + $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), new Request(), HttpKernelInterface::MAIN_REQUEST)); } public function testSessionStrategy() @@ -243,7 +243,7 @@ public function testSessionStrategy() ->willReturn(null) ; - $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MASTER_REQUEST)); + $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST)); } public function testSessionIsMigratedByDefault() @@ -289,7 +289,7 @@ public function testSessionIsMigratedByDefault() $request = new Request(); $request->setSession($session); - $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MASTER_REQUEST)); + $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST)); } public function testOnCoreSecurityInteractiveLoginEventIsDispatchedIfDispatcherIsPresent() diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/UsernamePasswordFormAuthenticationListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/UsernamePasswordFormAuthenticationListenerTest.php index c1345583d69fb..73d03e1a27d98 100644 --- a/src/Symfony/Component/Security/Http/Tests/Firewall/UsernamePasswordFormAuthenticationListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/UsernamePasswordFormAuthenticationListenerTest.php @@ -264,7 +264,7 @@ public function testInvalidCsrfToken($invalidToken) $csrfTokenManager ); - $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MASTER_REQUEST)); + $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST)); } public function postOnlyDataProvider(): array diff --git a/src/Symfony/Component/Serializer/Normalizer/JsonSerializableNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/JsonSerializableNormalizer.php index f38956c3e3cab..5032dce47b284 100644 --- a/src/Symfony/Component/Serializer/Normalizer/JsonSerializableNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/JsonSerializableNormalizer.php @@ -27,7 +27,7 @@ class JsonSerializableNormalizer extends AbstractNormalizer public function normalize($object, string $format = null, array $context = []) { if ($this->isCircularReference($object, $context)) { - return $this->handleCircularReference($object); + return $this->handleCircularReference($object, $format, $context); } if (!$object instanceof \JsonSerializable) { diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/FalseBuiltInDummy.php b/src/Symfony/Component/Serializer/Tests/Fixtures/FalseBuiltInDummy.php new file mode 100644 index 0000000000000..0c9d03a0e7298 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/FalseBuiltInDummy.php @@ -0,0 +1,17 @@ + + * + * 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; + +class FalseBuiltInDummy +{ + public false $false = false; +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/JsonSerializableCircularReferenceDummy.php b/src/Symfony/Component/Serializer/Tests/Fixtures/JsonSerializableCircularReferenceDummy.php new file mode 100644 index 0000000000000..6dbed8f98d943 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/JsonSerializableCircularReferenceDummy.php @@ -0,0 +1,25 @@ + + * + * 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 Marvin Feldmann + */ +class JsonSerializableCircularReferenceDummy implements \JsonSerializable +{ + public function jsonSerialize(): array + { + return [ + 'me' => $this, + ]; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/JsonSerializableNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/JsonSerializableNormalizerTest.php index e734ff99e776e..177b8c6e70446 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/JsonSerializableNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/JsonSerializableNormalizerTest.php @@ -17,14 +17,19 @@ use Symfony\Component\Serializer\Exception\InvalidArgumentException; use Symfony\Component\Serializer\Normalizer\JsonSerializableNormalizer; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Symfony\Component\Serializer\Serializer; use Symfony\Component\Serializer\SerializerInterface; +use Symfony\Component\Serializer\Tests\Fixtures\JsonSerializableCircularReferenceDummy; use Symfony\Component\Serializer\Tests\Fixtures\JsonSerializableDummy; +use Symfony\Component\Serializer\Tests\Normalizer\Features\CircularReferenceTestTrait; /** * @author Fred Cox */ class JsonSerializableNormalizerTest extends TestCase { + use CircularReferenceTestTrait; + /** * @var JsonSerializableNormalizer */ @@ -86,6 +91,19 @@ public function testCircularNormalize() $this->assertEquals('string_object', $this->normalizer->normalize(new JsonSerializableDummy())); } + protected function getNormalizerForCircularReference(array $defaultContext): JsonSerializableNormalizer + { + $normalizer = new JsonSerializableNormalizer(null, null, $defaultContext); + new Serializer([$normalizer]); + + return $normalizer; + } + + protected function getSelfReferencingModel() + { + return new JsonSerializableCircularReferenceDummy(); + } + public function testInvalidDataThrowException() { $this->expectException(InvalidArgumentException::class); diff --git a/src/Symfony/Component/Serializer/Tests/SerializerTest.php b/src/Symfony/Component/Serializer/Tests/SerializerTest.php index 28ab8db8c918f..a22049b4c68da 100644 --- a/src/Symfony/Component/Serializer/Tests/SerializerTest.php +++ b/src/Symfony/Component/Serializer/Tests/SerializerTest.php @@ -56,6 +56,7 @@ use Symfony\Component\Serializer\Tests\Fixtures\DummyMessageInterface; use Symfony\Component\Serializer\Tests\Fixtures\DummyMessageNumberOne; use Symfony\Component\Serializer\Tests\Fixtures\DummyMessageNumberTwo; +use Symfony\Component\Serializer\Tests\Fixtures\FalseBuiltInDummy; use Symfony\Component\Serializer\Tests\Fixtures\NormalizableTraversableDummy; use Symfony\Component\Serializer\Tests\Fixtures\Php74Full; use Symfony\Component\Serializer\Tests\Fixtures\Php80WithPromotedTypedConstructor; @@ -750,6 +751,19 @@ public function testUnionTypeDeserializable() $this->assertEquals(new DummyUnionType(), $actual, 'Union type denormalization third case failed.'); } + /** + * @requires PHP 8.2 + */ + public function testFalseBuiltInTypes() + { + $extractor = new PropertyInfoExtractor([], [new ReflectionExtractor()]); + $serializer = new Serializer([new ObjectNormalizer(null, null, null, $extractor)], ['json' => new JsonEncoder()]); + + $actual = $serializer->deserialize('{"false":false}', FalseBuiltInDummy::class, 'json'); + + $this->assertEquals(new FalseBuiltInDummy(), $actual); + } + private function serializerWithClassDiscriminator() { $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); diff --git a/src/Symfony/Component/Translation/Command/TranslationPushCommand.php b/src/Symfony/Component/Translation/Command/TranslationPushCommand.php index 973ed5fe12b4d..bf6e8c948b8ed 100644 --- a/src/Symfony/Component/Translation/Command/TranslationPushCommand.php +++ b/src/Symfony/Component/Translation/Command/TranslationPushCommand.php @@ -131,16 +131,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int $force = $input->getOption('force'); $deleteMissing = $input->getOption('delete-missing'); + if (!$domains && $provider instanceof FilteringProvider) { + $domains = $provider->getDomains(); + } + + // Reading local translations must be done after retrieving the domains from the provider + // in order to manage only translations from configured domains $localTranslations = $this->readLocalTranslations($locales, $domains, $this->transPaths); if (!$domains) { - if ($provider instanceof FilteringProvider) { - $domains = $provider->getDomains(); - } - - if (!$domains) { - $domains = $this->getDomainsFromTranslatorBag($localTranslations); - } + $domains = $this->getDomainsFromTranslatorBag($localTranslations); } if (!$deleteMissing && $force) { diff --git a/src/Symfony/Component/VarDumper/Caster/DateCaster.php b/src/Symfony/Component/VarDumper/Caster/DateCaster.php index 99f53849b8695..18641fbc1d348 100644 --- a/src/Symfony/Component/VarDumper/Caster/DateCaster.php +++ b/src/Symfony/Component/VarDumper/Caster/DateCaster.php @@ -103,11 +103,11 @@ public static function castPeriod(\DatePeriod $p, array $a, Stub $stub, bool $is } $period = sprintf( - 'every %s, from %s (%s) %s', + 'every %s, from %s%s %s', self::formatInterval($p->getDateInterval()), + $p->include_start_date ? '[' : ']', self::formatDateTime($p->getStartDate()), - $p->include_start_date ? 'included' : 'excluded', - ($end = $p->getEndDate()) ? 'to '.self::formatDateTime($end) : 'recurring '.$p->recurrences.' time/s' + ($end = $p->getEndDate()) ? 'to '.self::formatDateTime($end).(\PHP_VERSION_ID >= 80200 && $p->include_end_date ? ']' : '[') : 'recurring '.$p->recurrences.' time/s' ); $p = [Caster::PREFIX_VIRTUAL.'period' => new ConstStub($period, implode("\n", $dates))]; diff --git a/src/Symfony/Component/VarDumper/Dumper/HtmlDumper.php b/src/Symfony/Component/VarDumper/Dumper/HtmlDumper.php index 6be9d30a899b6..4dbab6d00e40f 100644 --- a/src/Symfony/Component/VarDumper/Dumper/HtmlDumper.php +++ b/src/Symfony/Component/VarDumper/Dumper/HtmlDumper.php @@ -960,7 +960,7 @@ protected function dumpLine(int $depth, bool $endOfValue = false) } $this->lastDepth = $depth; - $this->line = mb_encode_numericentity($this->line, [0x80, 0xFFFF, 0, 0xFFFF], 'UTF-8'); + $this->line = mb_encode_numericentity($this->line, [0x80, 0x10FFFF, 0, 0x1FFFFF], 'UTF-8'); if (-1 === $depth) { AbstractDumper::dumpLine(0); diff --git a/src/Symfony/Component/VarDumper/Tests/Caster/DateCasterTest.php b/src/Symfony/Component/VarDumper/Tests/Caster/DateCasterTest.php index c1cc61504c902..335aa7dda4b1c 100644 --- a/src/Symfony/Component/VarDumper/Tests/Caster/DateCasterTest.php +++ b/src/Symfony/Component/VarDumper/Tests/Caster/DateCasterTest.php @@ -373,26 +373,26 @@ public function testCastPeriod($start, $interval, $end, $options, $xPeriod, $xDa public function providePeriods() { $periods = [ - ['2017-01-01', 'P1D', '2017-01-03', 0, 'every + 1d, from 2017-01-01 00:00:00.0 (included) to 2017-01-03 00:00:00.0', '1) 2017-01-01%a2) 2017-01-02'], - ['2017-01-01', 'P1D', 1, 0, 'every + 1d, from 2017-01-01 00:00:00.0 (included) recurring 2 time/s', '1) 2017-01-01%a2) 2017-01-02'], + ['2017-01-01', 'P1D', '2017-01-03', 0, 'every + 1d, from [2017-01-01 00:00:00.0 to 2017-01-03 00:00:00.0[', '1) 2017-01-01%a2) 2017-01-02'], + ['2017-01-01', 'P1D', 1, 0, 'every + 1d, from [2017-01-01 00:00:00.0 recurring 2 time/s', '1) 2017-01-01%a2) 2017-01-02'], - ['2017-01-01', 'P1D', '2017-01-04', 0, 'every + 1d, from 2017-01-01 00:00:00.0 (included) to 2017-01-04 00:00:00.0', '1) 2017-01-01%a2) 2017-01-02%a3) 2017-01-03'], - ['2017-01-01', 'P1D', 2, 0, 'every + 1d, from 2017-01-01 00:00:00.0 (included) recurring 3 time/s', '1) 2017-01-01%a2) 2017-01-02%a3) 2017-01-03'], + ['2017-01-01', 'P1D', '2017-01-04', 0, 'every + 1d, from [2017-01-01 00:00:00.0 to 2017-01-04 00:00:00.0[', '1) 2017-01-01%a2) 2017-01-02%a3) 2017-01-03'], + ['2017-01-01', 'P1D', 2, 0, 'every + 1d, from [2017-01-01 00:00:00.0 recurring 3 time/s', '1) 2017-01-01%a2) 2017-01-02%a3) 2017-01-03'], - ['2017-01-01', 'P1D', '2017-01-05', 0, 'every + 1d, from 2017-01-01 00:00:00.0 (included) to 2017-01-05 00:00:00.0', '1) 2017-01-01%a2) 2017-01-02%a1 more'], - ['2017-01-01', 'P1D', 3, 0, 'every + 1d, from 2017-01-01 00:00:00.0 (included) recurring 4 time/s', '1) 2017-01-01%a2) 2017-01-02%a3) 2017-01-03%a1 more'], + ['2017-01-01', 'P1D', '2017-01-05', 0, 'every + 1d, from [2017-01-01 00:00:00.0 to 2017-01-05 00:00:00.0[', '1) 2017-01-01%a2) 2017-01-02%a1 more'], + ['2017-01-01', 'P1D', 3, 0, 'every + 1d, from [2017-01-01 00:00:00.0 recurring 4 time/s', '1) 2017-01-01%a2) 2017-01-02%a3) 2017-01-03%a1 more'], - ['2017-01-01', 'P1D', '2017-01-21', 0, 'every + 1d, from 2017-01-01 00:00:00.0 (included) to 2017-01-21 00:00:00.0', '1) 2017-01-01%a17 more'], - ['2017-01-01', 'P1D', 19, 0, 'every + 1d, from 2017-01-01 00:00:00.0 (included) recurring 20 time/s', '1) 2017-01-01%a17 more'], + ['2017-01-01', 'P1D', '2017-01-21', 0, 'every + 1d, from [2017-01-01 00:00:00.0 to 2017-01-21 00:00:00.0[', '1) 2017-01-01%a17 more'], + ['2017-01-01', 'P1D', 19, 0, 'every + 1d, from [2017-01-01 00:00:00.0 recurring 20 time/s', '1) 2017-01-01%a17 more'], - ['2017-01-01 01:00:00', 'P1D', '2017-01-03 01:00:00', 0, 'every + 1d, from 2017-01-01 01:00:00.0 (included) to 2017-01-03 01:00:00.0', '1) 2017-01-01 01:00:00.0%a2) 2017-01-02 01:00:00.0'], - ['2017-01-01 01:00:00', 'P1D', 1, 0, 'every + 1d, from 2017-01-01 01:00:00.0 (included) recurring 2 time/s', '1) 2017-01-01 01:00:00.0%a2) 2017-01-02 01:00:00.0'], + ['2017-01-01 01:00:00', 'P1D', '2017-01-03 01:00:00', 0, 'every + 1d, from [2017-01-01 01:00:00.0 to 2017-01-03 01:00:00.0[', '1) 2017-01-01 01:00:00.0%a2) 2017-01-02 01:00:00.0'], + ['2017-01-01 01:00:00', 'P1D', 1, 0, 'every + 1d, from [2017-01-01 01:00:00.0 recurring 2 time/s', '1) 2017-01-01 01:00:00.0%a2) 2017-01-02 01:00:00.0'], - ['2017-01-01', 'P1DT1H', '2017-01-03', 0, 'every + 1d 01:00:00.0, from 2017-01-01 00:00:00.0 (included) to 2017-01-03 00:00:00.0', '1) 2017-01-01 00:00:00.0%a2) 2017-01-02 01:00:00.0'], - ['2017-01-01', 'P1DT1H', 1, 0, 'every + 1d 01:00:00.0, from 2017-01-01 00:00:00.0 (included) recurring 2 time/s', '1) 2017-01-01 00:00:00.0%a2) 2017-01-02 01:00:00.0'], + ['2017-01-01', 'P1DT1H', '2017-01-03', 0, 'every + 1d 01:00:00.0, from [2017-01-01 00:00:00.0 to 2017-01-03 00:00:00.0[', '1) 2017-01-01 00:00:00.0%a2) 2017-01-02 01:00:00.0'], + ['2017-01-01', 'P1DT1H', 1, 0, 'every + 1d 01:00:00.0, from [2017-01-01 00:00:00.0 recurring 2 time/s', '1) 2017-01-01 00:00:00.0%a2) 2017-01-02 01:00:00.0'], - ['2017-01-01', 'P1D', '2017-01-04', \DatePeriod::EXCLUDE_START_DATE, 'every + 1d, from 2017-01-01 00:00:00.0 (excluded) to 2017-01-04 00:00:00.0', '1) 2017-01-02%a2) 2017-01-03'], - ['2017-01-01', 'P1D', 2, \DatePeriod::EXCLUDE_START_DATE, 'every + 1d, from 2017-01-01 00:00:00.0 (excluded) recurring 2 time/s', '1) 2017-01-02%a2) 2017-01-03'], + ['2017-01-01', 'P1D', '2017-01-04', \DatePeriod::EXCLUDE_START_DATE, 'every + 1d, from ]2017-01-01 00:00:00.0 to 2017-01-04 00:00:00.0[', '1) 2017-01-02%a2) 2017-01-03'], + ['2017-01-01', 'P1D', 2, \DatePeriod::EXCLUDE_START_DATE, 'every + 1d, from ]2017-01-01 00:00:00.0 recurring 2 time/s', '1) 2017-01-02%a2) 2017-01-03'], ]; return $periods; diff --git a/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php b/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php index c261a0da8c168..ebdfa3b21d40b 100644 --- a/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php +++ b/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php @@ -39,7 +39,7 @@ public function testReflectionCaster() +name: "ReflectionClass" %Aimplements: array:%d [ %A] - constants: array:3 [ + constants: array:%d [ 0 => ReflectionClassConstant { +name: "IS_IMPLICIT_ABSTRACT" +class: "ReflectionClass" @@ -58,7 +58,7 @@ public function testReflectionCaster() modifiers: "public" value: %d } - ] +%A] properties: array:%d [ "name" => ReflectionProperty { %A +name: "name" diff --git a/src/Symfony/Component/VarExporter/Internal/Exporter.php b/src/Symfony/Component/VarExporter/Internal/Exporter.php index cf5283870c274..2395430bf5b05 100644 --- a/src/Symfony/Component/VarExporter/Internal/Exporter.php +++ b/src/Symfony/Component/VarExporter/Internal/Exporter.php @@ -108,12 +108,7 @@ public static function prepare($values, $objectsPool, &$refsPool, &$objectsCount $arrayValue = (array) $value; } elseif ($value instanceof \Serializable || $value instanceof \__PHP_Incomplete_Class - || $value instanceof \DatePeriod - || (\PHP_VERSION_ID >= 80200 && ( - $value instanceof \DateTimeInterface - || $value instanceof \DateTimeZone - || $value instanceof \DateInterval - )) + || PHP_VERSION_ID < 80200 && $value instanceof \DatePeriod ) { ++$objectsCount; $objectsPool[$value] = [$id = \count($objectsPool), serialize($value), [], 0]; diff --git a/src/Symfony/Component/VarExporter/Tests/Fixtures/datetime.php b/src/Symfony/Component/VarExporter/Tests/Fixtures/datetime.php index e9f41f9ade34c..7078ef2525674 100644 --- a/src/Symfony/Component/VarExporter/Tests/Fixtures/datetime.php +++ b/src/Symfony/Component/VarExporter/Tests/Fixtures/datetime.php @@ -1,13 +1,15 @@ '1970-01-01 00:00:00.000000', + 'timezone_type' => 1, + 'timezone' => '+00:00', + ], + -1 => [ + 'date' => '1970-01-01 00:00:00.000000', + 'timezone_type' => 1, + 'timezone' => '+00:00', + ], + -2 => [ + 'timezone_type' => 3, + 'timezone' => 'Europe/Paris', + ], + -3 => [ + 'y' => 0, + 'm' => 0, + 'd' => 7, + 'h' => 0, + 'i' => 0, + 's' => 0, + 'f' => 0.0, + 'invert' => 0, + 'days' => 7, + 'from_string' => false, + ], + -5 => [ + 'date' => '2009-10-11 00:00:00.000000', + 'timezone_type' => 3, + 'timezone' => 'Europe/Paris', + ], + -6 => [ + 'y' => 0, + 'm' => 0, + 'd' => 7, + 'h' => 0, + 'i' => 0, + 's' => 0, + 'f' => 0.0, + 'invert' => 0, + 'days' => 7, + 'from_string' => false, + ], + -4 => [ + 'start' => $o[5], + 'current' => null, + 'end' => null, + 'interval' => $o[6], + 'recurrences' => 5, + 'include_start_date' => true, + 'include_end_date' => false, + ], + ] ); diff --git a/src/Symfony/Component/Workflow/Dumper/PlantUmlDumper.php b/src/Symfony/Component/Workflow/Dumper/PlantUmlDumper.php index 76273c01d1ff7..6e75c3c820048 100644 --- a/src/Symfony/Component/Workflow/Dumper/PlantUmlDumper.php +++ b/src/Symfony/Component/Workflow/Dumper/PlantUmlDumper.php @@ -103,8 +103,8 @@ public function dump(Definition $definition, Marking $marking = null, array $opt } $lines = [ - "$fromEscaped -${transitionColor}-> ${transitionEscaped}${transitionLabel}", - "$transitionEscaped -${transitionColor}-> ${toEscaped}${transitionLabel}", + "{$fromEscaped} -{$transitionColor}-> {$transitionEscaped}{$transitionLabel}", + "{$transitionEscaped} -{$transitionColor}-> {$toEscaped}{$transitionLabel}", ]; foreach ($lines as $line) { if (!\in_array($line, $code)) { @@ -112,7 +112,7 @@ public function dump(Definition $definition, Marking $marking = null, array $opt } } } else { - $code[] = "$fromEscaped -${transitionColor}-> $toEscaped: $transitionEscapedWithStyle"; + $code[] = "{$fromEscaped} -{$transitionColor}-> {$toEscaped}: {$transitionEscapedWithStyle}"; } } }