diff --git a/.github/ISSUE_TEMPLATE/1_Bug_report.md b/.github/ISSUE_TEMPLATE/1_Bug_report.md deleted file mode 100644 index aef16611e0f7..000000000000 --- a/.github/ISSUE_TEMPLATE/1_Bug_report.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -name: 🐛 Bug Report -about: ⚠️ See below for security reports -labels: Bug - ---- - -**Symfony version(s) affected**: x.y.z - -**Description** - - -**How to reproduce** - - -**Possible Solution** - - -**Additional context** - diff --git a/.github/ISSUE_TEMPLATE/1_Bug_report.yaml b/.github/ISSUE_TEMPLATE/1_Bug_report.yaml index 5518d4e4ad79..ef0f72c79427 100644 --- a/.github/ISSUE_TEMPLATE/1_Bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/1_Bug_report.yaml @@ -1,5 +1,5 @@ name: 🐛 Bug Report -description: ⚠️ See below for security reports +description: ⚠️ NEVER report security issues, read https://symfony.com/security instead labels: Bug body: @@ -14,7 +14,7 @@ body: id: description attributes: label: Description - description: A clear and consise description of the problem + description: A clear and concise description of the problem validations: required: true - type: textarea @@ -22,15 +22,21 @@ body: attributes: label: How to reproduce description: | - Code and/or config needed to reproduce the problem. - If it's a complex bug, create a "bug reproducer" as explained in https://symfony.com/doc/current/contributing/code/reproducer.html + ⚠️ This is the most important part of the report ⚠️ + Without a way to easily reproduce your issue, there is little chance we will be able to help you and work on a fix. + Please, take the time to show us some code and/or config that is needed for others to reproduce the problem easily. + Most of the time, creating a "bug reproducer" as explained in the URL below is the best way to help us + and increases the chances someone will have a look at it: + https://symfony.com/doc/current/contributing/code/reproducer.html validations: required: true - type: textarea id: possible-solution attributes: label: Possible Solution - description: "Optional: only if you have suggestions on a fix/reason for the bug" + description: | + Optional: only if you have suggestions on a fix/reason for the bug + Don't hesitate to create a pull request with your solution, it helps get faster feedback. - type: textarea id: additional-context attributes: diff --git a/.github/ISSUE_TEMPLATE/2_Feature_request.md b/.github/ISSUE_TEMPLATE/2_Feature_request.md deleted file mode 100644 index 908c5ee52664..000000000000 --- a/.github/ISSUE_TEMPLATE/2_Feature_request.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -name: 🚀 Feature Request -about: RFC and ideas for new features and improvements - ---- - -**Description** - - -**Example** - diff --git a/.github/ISSUE_TEMPLATE/3_Support_question.md b/.github/ISSUE_TEMPLATE/3_Support_question.md deleted file mode 100644 index 9480710c1565..000000000000 --- a/.github/ISSUE_TEMPLATE/3_Support_question.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: ⛔ Support Question -about: See https://symfony.com/support for questions about using Symfony and its components - ---- - -We use GitHub issues only to discuss about Symfony bugs and new features. For -this kind of questions about using Symfony or third-party bundles, please use -any of the support alternatives shown in https://symfony.com/support - -Thanks! diff --git a/.github/ISSUE_TEMPLATE/4_Documentation_issue.md b/.github/ISSUE_TEMPLATE/4_Documentation_issue.md deleted file mode 100644 index 0855c3c5f1e1..000000000000 --- a/.github/ISSUE_TEMPLATE/4_Documentation_issue.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -name: ⛔ Documentation Issue -about: See https://github.com/symfony/symfony-docs/issues for documentation issues - ---- - -Symfony Documentation has its own dedicated repository. Please open your -documentation-related issue at https://github.com/symfony/symfony-docs/issues - -Thanks! diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000000..34227566ed84 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Support Question + url: https://symfony.com/support + about: We use GitHub issues only to discuss about Symfony bugs and new features. For this kind of questions about using Symfony or third-party bundles, please use any of the support alternatives shown in https://symfony.com/support + - name: Documentation Issue + url: https://github.com/symfony/symfony-docs/issues + about: Symfony Documentation has its own dedicated repository. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 62662f876fd3..5e7092d38591 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,6 @@ | Q | A | ------------- | --- -| Branch? | 5.4 for features / 4.4 or 5.3 for bug fixes +| Branch? | 6.1 for features / 4.4, 5.3, 5.4 or 6.0 for bug fixes | Bug fix? | yes/no | New feature? | yes/no | Deprecations? | yes/no @@ -8,14 +8,14 @@ | License | MIT | Doc PR | symfony/symfony-docs#... diff --git a/.github/get-modified-packages.php b/.github/get-modified-packages.php index 9135a1da666e..a3682af0b9d1 100644 --- a/.github/get-modified-packages.php +++ b/.github/get-modified-packages.php @@ -17,9 +17,33 @@ return strlen($b) <=> strlen($a) ?: $a <=> $b; }); -function isComponentBridge(string $packageDir): bool +function getPackageType(string $packageDir): string { - return 0 < preg_match('@Symfony/Component/.*/Bridge/@', $packageDir); + if (preg_match('@Symfony/Bridge/@', $packageDir)) { + return 'bridge'; + } + + if (preg_match('@Symfony/Bundle/@', $packageDir)) { + return 'bundle'; + } + + if (preg_match('@Symfony/Component/[^/]+/Bridge/@', $packageDir)) { + return 'component_bridge'; + } + + if (preg_match('@Symfony/Component/@', $packageDir)) { + return 'component'; + } + + if (preg_match('@Symfony/Contracts/@', $packageDir)) { + return 'contract'; + } + + if (preg_match('@Symfony/Contracts$@', $packageDir)) { + return 'contracts'; + } + + throw new \LogicException(); } $newPackage = []; @@ -43,7 +67,7 @@ function isComponentBridge(string $packageDir): bool $output = []; foreach ($modifiedPackages as $directory => $bool) { $name = json_decode(file_get_contents($directory.'/composer.json'), true)['name'] ?? 'unknown'; - $output[] = ['name' => $name, 'directory' => $directory, 'new' => $newPackage[$directory] ?? false, 'component_bridge' => isComponentBridge($directory)]; + $output[] = ['name' => $name, 'directory' => $directory, 'new' => $newPackage[$directory] ?? false, 'type' => getPackageType($directory)]; } echo json_encode($output); diff --git a/.github/workflows/package-tests.yml b/.github/workflows/package-tests.yml index cb66e2d8d3b0..3c0a7c36be89 100644 --- a/.github/workflows/package-tests.yml +++ b/.github/workflows/package-tests.yml @@ -63,13 +63,18 @@ jobs: DIR=$(_jq '.directory') NAME=$(_jq '.name') echo "::group::$NAME" + TYPE=$(_jq '.type') localExit=0 - _file_exist $DIR/.gitattributes || localExit=1 + if [ $TYPE != 'contract' ] && [ $TYPE != 'contracts' ]; then + _file_exist $DIR/.gitattributes || localExit=1 + fi _file_exist $DIR/.gitignore || localExit=1 _file_exist $DIR/CHANGELOG.md || localExit=1 _file_exist $DIR/LICENSE || localExit=1 - _file_exist $DIR/phpunit.xml.dist || localExit=1 + if [ $TYPE != 'contract' ]; then + _file_exist $DIR/phpunit.xml.dist || localExit=1 + fi _file_exist $DIR/README.md || localExit=1 _file_not_exist $DIR/phpunit.xml || localExit=1 @@ -77,7 +82,7 @@ jobs: echo "Verifying new package" _correct_license_file $DIR/LICENSE || localExit=1 - if [ $(_jq '.component_bridge') == false ]; then + if [ $TYPE == 'component_bridge' ]; then if [ ! $(cat composer.json | jq -e ".replace.\"$NAME\"|test(\"self.version\")") ]; then echo "Composer.json's replace section needs to contain $NAME" localExit=1 diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index c60d92ba61cc..c10c70de1abb 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -21,10 +21,10 @@ jobs: matrix: include: - php: '7.2' - - php: '8.1' - php: '7.4' - mode: high-deps - php: '8.0' + mode: high-deps + - php: '8.1' mode: low-deps - php: '8.2' mode: experimental @@ -174,19 +174,21 @@ jobs: exit 0 fi - (cd src/Symfony/Component/HttpFoundation; cp composer.json composer.bak; composer require --dev --no-update mongodb/mongodb) - if [[ "${{ matrix.mode }}" = low-deps ]]; then echo "$COMPONENTS" | xargs -n1 | parallel -j +3 "_run_tests {} 'cd {} && $COMPOSER_UP --prefer-lowest --prefer-stable && $PHPUNIT'" exit 0 fi + (cd src/Symfony/Component/HttpFoundation; cp composer.json composer.bak; composer require --dev --no-update mongodb/mongodb) + (cd src/Symfony/Component/Lock; cp composer.json composer.bak; composer require --dev --no-update mongodb/mongodb) + # matrix.mode = high-deps echo "$COMPONENTS" | xargs -n1 | parallel -j +3 "_run_tests {} 'cd {} && $COMPOSER_UP && $PHPUNIT$LEGACY'" || X=1 # get a list of the patched components (relies on .github/build-packages.php being called in the previous step) (cd src/Symfony/Component/HttpFoundation; mv composer.bak composer.json) + (cd src/Symfony/Component/Lock; mv composer.bak composer.json) PATCHED_COMPONENTS=$(git diff --name-only src/ | grep composer.json || true) # for 5.4 LTS, checkout and test previous major with the patched components (only for patched components) @@ -200,6 +202,7 @@ jobs: git checkout -m FETCH_HEAD PATCHED_COMPONENTS=$(echo "$PATCHED_COMPONENTS" | xargs dirname | xargs -n1 -I{} bash -c "[ -e '{}/phpunit.xml.dist' ] && echo '{}'" | sort || true) (cd src/Symfony/Component/HttpFoundation; composer require --dev --no-update mongodb/mongodb) + (cd src/Symfony/Component/Lock; composer require --dev --no-update mongodb/mongodb) if [[ $PATCHED_COMPONENTS ]]; then echo "::group::install phpunit" ./phpunit install diff --git a/CHANGELOG-5.4.md b/CHANGELOG-5.4.md index 5ba6dab3b612..5cc4204a52f3 100644 --- a/CHANGELOG-5.4.md +++ b/CHANGELOG-5.4.md @@ -7,6 +7,30 @@ 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.1 (2021-12-09) + + * bug #44490 [DependencyInjection][Messenger] Add auto-registration for BatchHandlerInterface (GaryPEGEOT) + * bug #44523 [Console] Fix polyfill-php73 requirement (Seldaek) + * bug #44502 [HttpFoundation] do not call preg_match() on null (xabbuh) + * bug #44475 [Console] Handle alias in completion script (GromNaN) + * bug #44481 [FrameworkBundle] Fix loginUser() causing deprecation (wouterj) + * bug #44416 [Translation] Make http requests synchronous when reading the Loco API (Kocal) + * bug #44437 [HttpKernel] Fix wrong usage of SessionUtils::popSessionCookie (simonchrz) + * bug #44350 [Translation] Fix TranslationTrait (Tomasz Kusy) + * bug #44460 [SecurityBundle] Fix ambiguous deprecation message on missing provider (chalasr) + * bug #44467 [Console] Fix parameter types for `ProcessHelper::mustRun()` (derrabus) + * bug #44427 [FrameworkBundle] Fix compatibility with symfony/security-core 6.x (deps=high tests) (wouterj) + * bug #44424 [SecurityBundle] Don't rely on deprecated strategy constants (derrabus) + * bug #44399 Prevent infinite nesting of lazy `ObjectManager` instances when `ObjectManager` is reset (Ocramius) + * bug #44395 [HttpKernel] fix sending Vary: Accept-Language when appropriate (nicolas-grekas) + * bug #44385 [DependencyInjection] Skip parameter attribute configurators in AttributeAutoconfigurationPass if we can't get the constructor reflector (fancyweb) + * bug #44359 Avoid duplicated session listener registration in tests (alexander-schranz) + * bug #44375 [DoctrineBridge] fix calling get_class on non-object (kbond) + * bug #44378 [HttpFoundation] fix SessionHandlerFactory using connections (dmaicher) + * bug #44365 [SecurityBundle]  Fix invalid reference with `always_authenticate_before_granting` (chalasr) + * bug #44361 [HttpClient] Fix handling error info in MockResponse (fancyweb) + * bug #44370 [Lock] create lock table if it does not exist (martinssipenko) + * 5.4.0 (2021-11-29) * bug #44309 [Messenger] Leverage DBAL's getNativeConnection() method (derrabus) diff --git a/UPGRADE-5.4.md b/UPGRADE-5.4.md index a2ce35807705..d7492d23f3cf 100644 --- a/UPGRADE-5.4.md +++ b/UPGRADE-5.4.md @@ -38,6 +38,7 @@ FrameworkBundle * Deprecate `get()`, `has()`, `getDoctrine()`, and `dispatchMessage()` in `AbstractController`, use method/constructor injection instead * Deprecate the `cache.adapter.doctrine` service: The Doctrine Cache library is deprecated. Either switch to Symfony Cache or use the PSR-6 adapters provided by Doctrine Cache. * In `framework.cache` configuration, using `cache.adapter.pdo` adapter with a Doctrine DBAL connection is deprecated, use `cache.adapter.doctrine_dbal` instead. + * Deprecate not setting the `framework.messenger.reset_on_message` config option, its default value will change to `true` in 6.0 HttpKernel ---------- @@ -62,13 +63,12 @@ Messenger * Deprecate not setting the `delete_after_ack` config option (or DSN parameter) using the Redis transport, its default value will change to `true` in 6.0 - * Deprecate not setting the `reset_on_message` config option, its default value will change to `true` in 6.0 Monolog ------- * Deprecate `ResetLoggersWorkerSubscriber` to reset buffered logs in messenger - workers, use "reset_on_message" option in messenger configuration instead. + workers, use `framework.messenger.reset_on_message` option in FrameworkBundle messenger configuration instead. SecurityBundle -------------- @@ -170,3 +170,19 @@ Security * Deprecate passing the strategy as string to `AccessDecisionManager`, pass an instance of `AccessDecisionStrategyInterface` instead * Flag `AccessDecisionManager` as `@final` + * Deprecate passing `$credentials` to `PreAuthenticatedToken`, + `SwitchUserToken` and `UsernamePasswordToken`: + + Before: + ```php + $token = new UsernamePasswordToken($user, $credentials, $firewallName, $roles); + $token = new PreAuthenticatedToken($user, $credentials, $firewallName, $roles); + $token = new SwitchUserToken($user, $credentials, $firewallName, $roles, $originalToken); + ``` + + After: + ```php + $token = new UsernamePasswordToken($user, $firewallName, $roles); + $token = new PreAuthenticatedToken($user, $firewallName, $roles); + $token = new SwitchUserToken($user, $firewallName, $roles, $originalToken); + ``` diff --git a/UPGRADE-6.0.md b/UPGRADE-6.0.md index 10c9dcc9199e..347263738c7b 100644 --- a/UPGRADE-6.0.md +++ b/UPGRADE-6.0.md @@ -124,7 +124,6 @@ HttpFoundation * Removed the `Request::HEADER_X_FORWARDED_ALL` constant, use either `Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO` or `Request::HEADER_X_FORWARDED_AWS_ELB` or `Request::HEADER_X_FORWARDED_TRAEFIK`constants instead * Rename `RequestStack::getMasterRequest()` to `getMainRequest()` * Not passing `FILTER_REQUIRE_ARRAY` or `FILTER_FORCE_ARRAY` flags to `InputBag::filter()` when filtering an array will throw `BadRequestException` - * Removed the `Request::HEADER_X_FORWARDED_ALL` constant * Retrieving non-scalar values using `InputBag::get()` will throw `BadRequestException` (use `InputBad::all()` instead to retrieve an array) * Passing non-scalar default value as the second argument `InputBag::get()` will throw `\InvalidArgumentException` * Passing non-scalar, non-array value as the second argument `InputBag::set()` will throw `\InvalidArgumentException` @@ -173,7 +172,6 @@ Messenger * Removed the `prefetch_count` parameter in the AMQP bridge. * Removed the use of TLS option for Redis Bridge, use `rediss://127.0.0.1` instead of `redis://127.0.0.1?tls=1` * The `delete_after_ack` config option of the Redis transport now defaults to `true` - * The `reset_on_message` config option now defaults to `true` Mime ---- @@ -408,6 +406,22 @@ Security ``` * `AccessDecisionManager` does not accept strings as strategy anymore, pass an instance of `AccessDecisionStrategyInterface` instead + * Removed the `$credentials` argument of `PreAuthenticatedToken`, + `SwitchUserToken` and `UsernamePasswordToken`: + + Before: + ```php + $token = new UsernamePasswordToken($user, $credentials, $firewallName, $roles); + $token = new PreAuthenticatedToken($user, $credentials, $firewallName, $roles); + $token = new SwitchUserToken($user, $credentials, $firewallName, $roles, $originalToken); + ``` + + After: + ```php + $token = new UsernamePasswordToken($user, $firewallName, $roles); + $token = new PreAuthenticatedToken($user, $firewallName, $roles); + $token = new SwitchUserToken($user, $firewallName, $roles, $originalToken); + ``` SecurityBundle -------------- diff --git a/src/Symfony/Bridge/Doctrine/ManagerRegistry.php b/src/Symfony/Bridge/Doctrine/ManagerRegistry.php index 7a2ad9a8d9cd..cd88bedca1e3 100644 --- a/src/Symfony/Bridge/Doctrine/ManagerRegistry.php +++ b/src/Symfony/Bridge/Doctrine/ManagerRegistry.php @@ -59,7 +59,7 @@ function (&$wrappedInstance, LazyLoadingInterface $manager) use ($name) { $name = $this->aliases[$name]; } if (isset($this->fileMap[$name])) { - $wrappedInstance = $this->load($this->fileMap[$name]); + $wrappedInstance = $this->load($this->fileMap[$name], false); } else { $wrappedInstance = $this->{$this->methodMap[$name]}(false); } diff --git a/src/Symfony/Bridge/Doctrine/Tests/ManagerRegistryTest.php b/src/Symfony/Bridge/Doctrine/Tests/ManagerRegistryTest.php index a004935a6afd..dd7dabcc87db 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/ManagerRegistryTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/ManagerRegistryTest.php @@ -12,8 +12,15 @@ namespace Symfony\Bridge\Doctrine\Tests; use PHPUnit\Framework\TestCase; +use ProxyManager\Proxy\LazyLoadingInterface; +use ProxyManager\Proxy\ValueHolderInterface; use Symfony\Bridge\Doctrine\ManagerRegistry; +use Symfony\Bridge\ProxyManager\LazyProxy\PhpDumper\ProxyDumper; use Symfony\Bridge\ProxyManager\Tests\LazyProxy\Dumper\PhpDumperTest; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\Dumper\PhpDumper; +use Symfony\Component\Filesystem\Filesystem; class ManagerRegistryTest extends TestCase { @@ -39,6 +46,91 @@ public function testResetService() $this->assertSame($foo, $container->get('foo')); $this->assertObjectNotHasAttribute('bar', $foo); } + + /** + * When performing an entity manager lazy service reset, the reset operations may re-use the container + * to create a "fresh" service: when doing so, it can happen that the "fresh" service is itself a proxy. + * + * Because of that, the proxy will be populated with a wrapped value that is itself a proxy: repeating + * the reset operation keeps increasing this nesting until the application eventually runs into stack + * overflow or memory overflow operations, which can happen for long-running processes that rely on + * services that are reset very often. + */ + public function testResetServiceWillNotNestFurtherLazyServicesWithinEachOther() + { + // This test scenario only applies to containers composed as a set of generated sources + $this->dumpLazyServiceProjectAsFilesServiceContainer(); + + /** @var ContainerInterface $container */ + $container = new \LazyServiceProjectAsFilesServiceContainer(); + + $registry = new TestManagerRegistry( + 'irrelevant', + [], + ['defaultManager' => 'foo'], + 'irrelevant', + 'defaultManager', + 'irrelevant' + ); + $registry->setTestContainer($container); + + $service = $container->get('foo'); + + self::assertInstanceOf(\stdClass::class, $service); + self::assertInstanceOf(LazyLoadingInterface::class, $service); + self::assertInstanceOf(ValueHolderInterface::class, $service); + self::assertFalse($service->isProxyInitialized()); + + $service->initializeProxy(); + + self::assertTrue($container->initialized('foo')); + self::assertTrue($service->isProxyInitialized()); + + $registry->resetManager(); + $service->initializeProxy(); + + $wrappedValue = $service->getWrappedValueHolderValue(); + self::assertInstanceOf(\stdClass::class, $wrappedValue); + self::assertNotInstanceOf(LazyLoadingInterface::class, $wrappedValue); + self::assertNotInstanceOf(ValueHolderInterface::class, $wrappedValue); + } + + private function dumpLazyServiceProjectAsFilesServiceContainer() + { + if (class_exists(\LazyServiceProjectAsFilesServiceContainer::class, false)) { + return; + } + + $container = new ContainerBuilder(); + + $container->register('foo', \stdClass::class) + ->setPublic(true) + ->setLazy(true); + $container->compile(); + + $fileSystem = new Filesystem(); + + $temporaryPath = $fileSystem->tempnam(sys_get_temp_dir(), 'symfonyManagerRegistryTest'); + $fileSystem->remove($temporaryPath); + $fileSystem->mkdir($temporaryPath); + + $dumper = new PhpDumper($container); + + $dumper->setProxyDumper(new ProxyDumper()); + $containerFiles = $dumper->dump([ + 'class' => 'LazyServiceProjectAsFilesServiceContainer', + 'as_files' => true, + ]); + + array_walk( + $containerFiles, + static function (string $containerSources, string $fileName) use ($temporaryPath): void { + (new Filesystem())->dumpFile($temporaryPath.'/'.$fileName, $containerSources); + } + ); + + require $temporaryPath.'/LazyServiceProjectAsFilesServiceContainer.php'; + } } class TestManagerRegistry extends ManagerRegistry diff --git a/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php b/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php index ab35078f85fc..c1c698aea0eb 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php @@ -37,6 +37,7 @@ use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntityValidator; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; +use Symfony\Component\Validator\Exception\UnexpectedValueException; use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; /** @@ -859,6 +860,32 @@ public function testValidateUniquenessWithEmptyIterator($entity, $result) $this->assertNoViolation(); } + public function testValueMustBeObject() + { + $constraint = new UniqueEntity([ + 'message' => 'myMessage', + 'fields' => ['name'], + 'em' => self::EM_NAME, + ]); + + $this->expectException(UnexpectedValueException::class); + + $this->validator->validate('foo', $constraint); + } + + public function testValueCanBeNull() + { + $constraint = new UniqueEntity([ + 'message' => 'myMessage', + 'fields' => ['name'], + 'em' => self::EM_NAME, + ]); + + $this->validator->validate(null, $constraint); + + $this->assertNoViolation(); + } + public function resultWithEmptyIterator(): array { $entity = new SingleIntIdEntity(1, 'foo'); diff --git a/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php b/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php index c1caadd9df3d..a151c8703dd3 100644 --- a/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php +++ b/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php @@ -18,6 +18,7 @@ use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\UnexpectedTypeException; +use Symfony\Component\Validator\Exception\UnexpectedValueException; /** * Unique Entity Validator checks if one or a set of fields contain unique values. @@ -63,6 +64,10 @@ public function validate($entity, Constraint $constraint) return; } + if (!\is_object($entity)) { + throw new UnexpectedValueException($entity, 'object'); + } + if ($constraint->em) { $em = $this->registry->getManager($constraint->em); diff --git a/src/Symfony/Bridge/Doctrine/composer.json b/src/Symfony/Bridge/Doctrine/composer.json index f9772266722e..f2e0d9520c4b 100644 --- a/src/Symfony/Bridge/Doctrine/composer.json +++ b/src/Symfony/Bridge/Doctrine/composer.json @@ -53,6 +53,7 @@ }, "conflict": { "doctrine/dbal": "<2.13.1", + "doctrine/lexer": "<1.1", "doctrine/orm": "<2.7.3", "phpunit/phpunit": "<5.4.3", "symfony/cache": "<5.4", @@ -61,6 +62,7 @@ "symfony/http-kernel": "<5", "symfony/messenger": "<4.4", "symfony/property-info": "<5", + "symfony/proxy-manager-bridge": "<4.4.19", "symfony/security-bundle": "<5", "symfony/security-core": "<5.3", "symfony/validator": "<5.2" diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php index 137311bd6358..2df5b72559c6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php @@ -74,7 +74,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 1; } - $io->success('The container was lint successfully: all services are injected with values that are compatible with their type declarations.'); + $io->success('The container was linted successfully: all services are injected with values that are compatible with their type declarations.'); return 0; } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 9b761dbdd393..3c46bc460998 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -107,6 +107,7 @@ use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpTransportFactory; use Symfony\Component\Messenger\Bridge\Beanstalkd\Transport\BeanstalkdTransportFactory; use Symfony\Component\Messenger\Bridge\Redis\Transport\RedisTransportFactory; +use Symfony\Component\Messenger\Handler\BatchHandlerInterface; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; use Symfony\Component\Messenger\MessageBus; use Symfony\Component\Messenger\MessageBusInterface; @@ -350,6 +351,11 @@ public function load(array $configs, ContainerBuilder $container) $this->sessionConfigEnabled = true; $this->registerSessionConfiguration($config['session'], $container, $loader); + if (!empty($config['test'])) { + // test listener will replace the existing session listener + // as we are aliasing to avoid duplicated registered events + $container->setAlias('session_listener', 'test.session.listener'); + } } elseif (!empty($config['test'])) { $container->removeDefinition('test.session.listener'); } @@ -577,6 +583,8 @@ public function load(array $configs, ContainerBuilder $container) ->addTag('validator.initializer'); $container->registerForAutoconfiguration(MessageHandlerInterface::class) ->addTag('messenger.message_handler'); + $container->registerForAutoconfiguration(BatchHandlerInterface::class) + ->addTag('messenger.message_handler'); $container->registerForAutoconfiguration(TransportFactoryInterface::class) ->addTag('messenger.transport_factory'); $container->registerForAutoconfiguration(MimeTypeGuesserInterface::class) diff --git a/src/Symfony/Bundle/FrameworkBundle/KernelBrowser.php b/src/Symfony/Bundle/FrameworkBundle/KernelBrowser.php index 7ae8f336b740..8c32625b1002 100644 --- a/src/Symfony/Bundle/FrameworkBundle/KernelBrowser.php +++ b/src/Symfony/Bundle/FrameworkBundle/KernelBrowser.php @@ -126,7 +126,7 @@ public function loginUser(object $user, string $firewallContext = 'main'): self $token = new TestBrowserToken($user->getRoles(), $user, $firewallContext); // @deprecated since Symfony 5.4 - if (method_exists($token, 'isAuthenticated')) { + if (method_exists($token, 'setAuthenticated')) { $token->setAuthenticated(true, false); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php index 00b8d8aafbd5..53613d3b5020 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php @@ -70,7 +70,6 @@ ->args([ param('kernel.charset'), abstract_arg('The "set_content_language_from_locale" config value'), - param('kernel.enabled_locales'), ]) ->tag('kernel.event_subscriber') diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/EventDispatcherDebugCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/EventDispatcherDebugCommandTest.php index ce653c6bfaaa..a506ac2d2915 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/EventDispatcherDebugCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/EventDispatcherDebugCommandTest.php @@ -41,12 +41,6 @@ public function provideCompletionSuggestions() private function createCommandCompletionTester(): CommandCompletionTester { - $dispatcher = new EventDispatcher(); - $otherDispatcher = new EventDispatcher(); - - $dispatcher->addListener('event', ['Listener']); - $otherDispatcher->addListener('other_event', ['OtherListener']); - $dispatchers = new ServiceLocator([ 'event_dispatcher' => function () { $dispatcher = new EventDispatcher(); diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 6fe970f366f3..d64b2c38ac7e 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -122,7 +122,7 @@ public function load(array $configs, ContainerBuilder $container) if ($config['always_authenticate_before_granting']) { $authorizationChecker = $container->getDefinition('security.authorization_checker'); $authorizationCheckerArgs = $authorizationChecker->getArguments(); - array_splice($authorizationCheckerArgs, 1, 0, [new Reference('security.authentication_manager')]); + array_splice($authorizationCheckerArgs, 1, 0, [new Reference('security.authentication.manager')]); $authorizationChecker->setArguments($authorizationCheckerArgs); } @@ -704,7 +704,7 @@ private function getUserProvider(ContainerBuilder $container, string $id, array if ('remember_me' === $factoryKey || 'anonymous' === $factoryKey || 'custom_authenticators' === $factoryKey) { if ('custom_authenticators' === $factoryKey) { - trigger_deprecation('symfony/security-bundle', '5.4', 'Not configuring explicitly the provider for the "%s" listener on "%s" firewall is deprecated because it\'s ambiguous as there is more than one registered provider.', $factoryKey, $id); + trigger_deprecation('symfony/security-bundle', '5.4', 'Not configuring explicitly the provider for the "%s" firewall is deprecated because it\'s ambiguous as there is more than one registered provider. Set the "provider" key to one of the configured providers, even if your custom authenticators don\'t use it.', $id); } return 'security.user_providers'; diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig b/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig index 0771f15a803b..91e75ce0c534 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig +++ b/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig @@ -407,7 +407,7 @@ {% for voter_detail in decision.voter_details %} {{ profiler_dump(voter_detail['class']) }} - {% if collector.voterStrategy == constant('Symfony\\Component\\Security\\Core\\Authorization\\AccessDecisionManager::STRATEGY_UNANIMOUS') %} + {% if collector.voterStrategy == 'unanimous' %} attribute {{ voter_detail['attributes'][0] }} {% endif %} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php index 8747273b9e9a..4f8c8ce127a1 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php @@ -397,7 +397,7 @@ public function testFirewallWithNoUserProviderTriggerDeprecation() ], ]); - $this->expectDeprecation('Since symfony/security-bundle 5.4: Not configuring explicitly the provider for the "custom_authenticators" listener on "some_firewall" firewall is deprecated because it\'s ambiguous as there is more than one registered provider.'); + $this->expectDeprecation('Since symfony/security-bundle 5.4: Not configuring explicitly the provider for the "some_firewall" firewall is deprecated because it\'s ambiguous as there is more than one registered provider. Set the "provider" key to one of the configured providers, even if your custom authenticators don\'t use it.'); $container->compile(); } @@ -834,7 +834,7 @@ public function testLegacyAuthorizationManagerSignature() $args = $container->getDefinition('security.authorization_checker')->getArguments(); $this->assertEquals('security.token_storage', (string) $args[0]); - $this->assertEquals('security.authentication_manager', (string) $args[1]); + $this->assertEquals('security.authentication.manager', (string) $args[1]); $this->assertEquals('security.access.decision_manager', (string) $args[2]); $this->assertEquals('%security.access.always_authenticate_before_granting%', (string) $args[3]); } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/legacy_config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/legacy_config.yml index e393772ae4b2..265e1d1c83d3 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/legacy_config.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/legacy_config.yml @@ -2,6 +2,7 @@ imports: - { resource: ./legacy_base_config.yml } security: + always_authenticate_before_granting: true firewalls: default: anonymous: ~ diff --git a/src/Symfony/Component/Console/Helper/ProcessHelper.php b/src/Symfony/Component/Console/Helper/ProcessHelper.php index 3f257dea1fa5..87d3e65394ed 100644 --- a/src/Symfony/Component/Console/Helper/ProcessHelper.php +++ b/src/Symfony/Component/Console/Helper/ProcessHelper.php @@ -92,9 +92,9 @@ public function run(OutputInterface $output, $cmd, string $error = null, callabl * This is identical to run() except that an exception is thrown if the process * exits with a non-zero exit code. * - * @param string|Process $cmd An instance of Process or a command to run - * @param callable|null $callback A PHP callback to run whenever there is some - * output available on STDOUT or STDERR + * @param array|Process $cmd An instance of Process or a command to run + * @param callable|null $callback A PHP callback to run whenever there is some + * output available on STDOUT or STDERR * * @return Process * diff --git a/src/Symfony/Component/Console/Resources/completion.bash b/src/Symfony/Component/Console/Resources/completion.bash index 971af088ba11..c5e89c3c282a 100644 --- a/src/Symfony/Component/Console/Resources/completion.bash +++ b/src/Symfony/Component/Console/Resources/completion.bash @@ -9,6 +9,12 @@ _sf_{{ COMMAND_NAME }}() { # Use newline as only separator to allow space in completion values IFS=$'\n' local sf_cmd="${COMP_WORDS[0]}" + + # for an alias, get the real script behind it + if [[ $(type -t $sf_cmd) == "alias" ]]; then + sf_cmd=$(alias $sf_cmd | sed -E "s/alias $sf_cmd='(.*)'/\1/") + fi + if [ ! -f "$sf_cmd" ]; then return 1 fi diff --git a/src/Symfony/Component/Console/composer.json b/src/Symfony/Component/Console/composer.json index 2a7b429ac1f8..9a565068cded 100644 --- a/src/Symfony/Component/Console/composer.json +++ b/src/Symfony/Component/Console/composer.json @@ -19,7 +19,7 @@ "php": ">=7.2.5", "symfony/deprecation-contracts": "^2.1|^3", "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php73": "^1.8", + "symfony/polyfill-php73": "^1.9", "symfony/polyfill-php80": "^1.16", "symfony/service-contracts": "^1.1|^2|^3", "symfony/string": "^5.1|^6.0" diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AttributeAutoconfigurationPass.php b/src/Symfony/Component/DependencyInjection/Compiler/AttributeAutoconfigurationPass.php index f5094996d949..4db7185cf534 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/AttributeAutoconfigurationPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/AttributeAutoconfigurationPass.php @@ -15,6 +15,7 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Exception\LogicException; +use Symfony\Component\DependencyInjection\Exception\RuntimeException; /** * @author Alexander M. Turek @@ -99,11 +100,19 @@ protected function processValue($value, bool $isRoot = false) } } - if ($this->parameterAttributeConfigurators && $constructorReflector = $this->getConstructor($value, false)) { - foreach ($constructorReflector->getParameters() as $parameterReflector) { - foreach ($parameterReflector->getAttributes() as $attribute) { - if ($configurator = $this->parameterAttributeConfigurators[$attribute->getName()] ?? null) { - $configurator($conditionals, $attribute->newInstance(), $parameterReflector); + if ($this->parameterAttributeConfigurators) { + try { + $constructorReflector = $this->getConstructor($value, false); + } catch (RuntimeException $e) { + $constructorReflector = null; + } + + if ($constructorReflector) { + foreach ($constructorReflector->getParameters() as $parameterReflector) { + foreach ($parameterReflector->getAttributes() as $attribute) { + if ($configurator = $this->parameterAttributeConfigurators[$attribute->getName()] ?? null) { + $configurator($conditionals, $attribute->newInstance(), $parameterReflector); + } } } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/IntegrationTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/IntegrationTest.php index 1e5df4879673..6624f7490132 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/IntegrationTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/IntegrationTest.php @@ -976,6 +976,11 @@ static function (ChildDefinition $definition, CustomParameterAttribute $attribut ->setPublic(true) ->setAutoconfigured(true); + $container->register('failing_factory', \stdClass::class); + $container->register('ccc', TaggedService4::class) + ->setFactory([new Reference('failing_factory'), 'create']) + ->setAutoconfigured(true); + $collector = new TagCollector(); $container->addCompilerPass($collector); @@ -996,6 +1001,17 @@ static function (ChildDefinition $definition, CustomParameterAttribute $attribut ['property' => 'name'], ['someAttribute' => 'on name', 'priority' => 0, 'property' => 'name'], ], + 'ccc' => [ + ['class' => TaggedService4::class], + ['method' => 'fooAction'], + ['someAttribute' => 'on fooAction', 'priority' => 0, 'method' => 'fooAction'], + ['parameter' => 'param1'], + ['someAttribute' => 'on param1 in fooAction', 'priority' => 0, 'parameter' => 'param1'], + ['method' => 'barAction'], + ['someAttribute' => 'on barAction', 'priority' => 0, 'method' => 'barAction'], + ['property' => 'name'], + ['someAttribute' => 'on name', 'priority' => 0, 'property' => 'name'], + ], ], $collector->collectedTags); } diff --git a/src/Symfony/Component/ErrorHandler/ErrorHandler.php b/src/Symfony/Component/ErrorHandler/ErrorHandler.php index 87e508405b73..ffb823eed23c 100644 --- a/src/Symfony/Component/ErrorHandler/ErrorHandler.php +++ b/src/Symfony/Component/ErrorHandler/ErrorHandler.php @@ -615,7 +615,9 @@ public function handleException(\Throwable $exception) } $loggedErrors = $this->loggedErrors; - $this->loggedErrors = $exception === $handlerException ? 0 : $this->loggedErrors; + if ($exception === $handlerException) { + $this->loggedErrors &= ~$type; + } try { $this->handleException($handlerException); diff --git a/src/Symfony/Component/ErrorHandler/Tests/phpt/exception_rethrown.phpt b/src/Symfony/Component/ErrorHandler/Tests/phpt/exception_rethrown.phpt index 06540f453012..be5ce6a5cdff 100644 --- a/src/Symfony/Component/ErrorHandler/Tests/phpt/exception_rethrown.phpt +++ b/src/Symfony/Component/ErrorHandler/Tests/phpt/exception_rethrown.phpt @@ -16,7 +16,9 @@ if (true) { { public function log($level, $message, array $context = []): void { - echo 'LOG: ', $message, "\n"; + if (0 !== strpos($message, 'Deprecated: ')) { + echo 'LOG: ', $message, "\n"; + } } } } @@ -34,5 +36,5 @@ Exception {%S #message: "foo" #code: 0 #file: "%s" - #line: 25 + #line: 27 } diff --git a/src/Symfony/Component/HttpClient/Response/MockResponse.php b/src/Symfony/Component/HttpClient/Response/MockResponse.php index 71fe8fbd1759..e8f1226f8fe7 100644 --- a/src/Symfony/Component/HttpClient/Response/MockResponse.php +++ b/src/Symfony/Component/HttpClient/Response/MockResponse.php @@ -285,6 +285,10 @@ private static function readResponse(self $response, array $options, ResponseInt 'http_code' => $response->info['http_code'], ] + $info + $response->info; + if (null !== $response->info['error']) { + throw new TransportException($response->info['error']); + } + if (!isset($response->info['total_time'])) { $response->info['total_time'] = microtime(true) - $response->info['start_time']; } diff --git a/src/Symfony/Component/HttpClient/Tests/MockHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/MockHttpClientTest.php index 12bc2d88a6f7..9f47d10d0284 100644 --- a/src/Symfony/Component/HttpClient/Tests/MockHttpClientTest.php +++ b/src/Symfony/Component/HttpClient/Tests/MockHttpClientTest.php @@ -18,7 +18,6 @@ use Symfony\Component\HttpClient\Response\ResponseStream; use Symfony\Contracts\HttpClient\ChunkInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; -use Symfony\Contracts\HttpClient\ResponseInterface; class MockHttpClientTest extends HttpClientTestCase { @@ -272,16 +271,8 @@ protected function getHttpClient(string $testCase): HttpClientInterface break; case 'testDnsError': - $mock = $this->createMock(ResponseInterface::class); - $mock->expects($this->any()) - ->method('getStatusCode') - ->willThrowException(new TransportException('DSN error')); - $mock->expects($this->any()) - ->method('getInfo') - ->willReturn([]); - - $responses[] = $mock; - $responses[] = $mock; + $responses[] = $mockResponse = new MockResponse('', ['error' => 'DNS error']); + $responses[] = $mockResponse; break; case 'testToStream': @@ -296,12 +287,7 @@ protected function getHttpClient(string $testCase): HttpClientInterface break; case 'testTimeoutOnAccess': - $mock = $this->createMock(ResponseInterface::class); - $mock->expects($this->any()) - ->method('getHeaders') - ->willThrowException(new TransportException('Timeout')); - - $responses[] = $mock; + $responses[] = new MockResponse('', ['error' => 'Timeout']); break; case 'testAcceptHeader': @@ -363,16 +349,7 @@ protected function getHttpClient(string $testCase): HttpClientInterface break; case 'testMaxDuration': - $mock = $this->createMock(ResponseInterface::class); - $mock->expects($this->any()) - ->method('getContent') - ->willReturnCallback(static function (): void { - usleep(100000); - - throw new TransportException('Max duration was reached.'); - }); - - $responses[] = $mock; + $responses[] = new MockResponse('', ['error' => 'Max duration was reached.']); break; } diff --git a/src/Symfony/Component/HttpClient/Tests/Response/MockResponseTest.php b/src/Symfony/Component/HttpClient/Tests/Response/MockResponseTest.php index def2c6f0ca9a..f8c8d4cea573 100644 --- a/src/Symfony/Component/HttpClient/Tests/Response/MockResponseTest.php +++ b/src/Symfony/Component/HttpClient/Tests/Response/MockResponseTest.php @@ -4,6 +4,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\HttpClient\Exception\JsonException; +use Symfony\Component\HttpClient\Exception\TransportException; use Symfony\Component\HttpClient\Response\MockResponse; /** @@ -96,4 +97,14 @@ public function toArrayErrors() 'message' => 'JSON content was expected to decode to an array, "int" returned for "https://example.com/file.json".', ]; } + + public function testErrorIsTakenIntoAccountInInitialization() + { + $this->expectException(TransportException::class); + $this->expectExceptionMessage('ccc error'); + + MockResponse::fromRequest('GET', 'https://symfony.com', [], new MockResponse('', [ + 'error' => 'ccc error', + ]))->getStatusCode(); + } } diff --git a/src/Symfony/Component/HttpFoundation/Request.php b/src/Symfony/Component/HttpFoundation/Request.php index 10e91ca1be30..d112b1f1835e 100644 --- a/src/Symfony/Component/HttpFoundation/Request.php +++ b/src/Symfony/Component/HttpFoundation/Request.php @@ -1515,7 +1515,7 @@ public function isMethodCacheable() public function getProtocolVersion() { if ($this->isFromTrustedProxy()) { - preg_match('~^(HTTP/)?([1-9]\.[0-9]) ~', $this->headers->get('Via'), $matches); + preg_match('~^(HTTP/)?([1-9]\.[0-9]) ~', $this->headers->get('Via') ?? '', $matches); if ($matches) { return 'HTTP/'.$matches[2]; diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/SessionHandlerFactory.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/SessionHandlerFactory.php index 0f1ce5c2b8ec..f3f7b201d9de 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/SessionHandlerFactory.php +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/SessionHandlerFactory.php @@ -30,7 +30,7 @@ public static function createHandler($connection): AbstractSessionHandler throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be a string or a connection object, "%s" given.', __METHOD__, get_debug_type($connection))); } - if ($options = parse_url($connection)) { + if ($options = \is_string($connection) ? parse_url($connection) : false) { parse_str($options['query'] ?? '', $options); } diff --git a/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php b/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php index 227c1b51d6fe..155863cf77b2 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php @@ -2256,7 +2256,10 @@ public function testProtocolVersion($serverProtocol, $trustedProxy, $via, $expec $request = new Request(); $request->server->set('SERVER_PROTOCOL', $serverProtocol); $request->server->set('REMOTE_ADDR', '1.1.1.1'); - $request->headers->set('Via', $via); + + if (null !== $via) { + $request->headers->set('Via', $via); + } $this->assertSame($expected, $request->getProtocolVersion()); } @@ -2264,9 +2267,11 @@ public function testProtocolVersion($serverProtocol, $trustedProxy, $via, $expec public function protocolVersionProvider() { return [ - 'untrusted without via' => ['HTTP/2.0', false, '', 'HTTP/2.0'], + 'untrusted with empty via' => ['HTTP/2.0', false, '', 'HTTP/2.0'], + 'untrusted without via' => ['HTTP/2.0', false, null, 'HTTP/2.0'], 'untrusted with via' => ['HTTP/2.0', false, '1.0 fred, 1.1 nowhere.com (Apache/1.1)', 'HTTP/2.0'], - 'trusted without via' => ['HTTP/2.0', true, '', 'HTTP/2.0'], + 'trusted with empty via' => ['HTTP/2.0', true, '', 'HTTP/2.0'], + 'trusted without via' => ['HTTP/2.0', true, null, 'HTTP/2.0'], 'trusted with via' => ['HTTP/2.0', true, '1.0 fred, 1.1 nowhere.com (Apache/1.1)', 'HTTP/1.0'], 'trusted with via and protocol name' => ['HTTP/2.0', true, 'HTTP/1.0 fred, HTTP/1.1 nowhere.com (Apache/1.1)', 'HTTP/1.0'], 'trusted with broken via' => ['HTTP/2.0', true, 'HTTP/1^0 foo', 'HTTP/2.0'], diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/SessionHandlerFactoryTest.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/SessionHandlerFactoryTest.php index 46d6cd40151d..9f06a7c8675d 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/SessionHandlerFactoryTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/SessionHandlerFactoryTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\HttpFoundation\Tests\Session\Storage\Handler; use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler; use Symfony\Component\HttpFoundation\Session\Storage\Handler\SessionHandlerFactory; use Symfony\Component\HttpFoundation\Session\Storage\Handler\StrictSessionHandler; @@ -28,7 +29,7 @@ class SessionHandlerFactoryTest extends TestCase /** * @dataProvider provideConnectionDSN */ - public function testCreateHandler(string $connectionDSN, string $expectedPath, string $expectedHandlerType) + public function testCreateFileHandler(string $connectionDSN, string $expectedPath, string $expectedHandlerType) { $handler = SessionHandlerFactory::createHandler($connectionDSN); @@ -45,4 +46,32 @@ public function provideConnectionDSN(): array 'native file handler using provided save_path' => ['connectionDSN' => 'file://'.$base.'/session/storage', 'expectedPath' => $base.'/session/storage', 'expectedHandlerType' => StrictSessionHandler::class], ]; } + + /** + * @requires extension redis + */ + public function testCreateRedisHandlerFromConnectionObject() + { + $handler = SessionHandlerFactory::createHandler($this->createMock(\Redis::class)); + $this->assertInstanceOf(RedisSessionHandler::class, $handler); + } + + /** + * @requires extension redis + */ + public function testCreateRedisHandlerFromDsn() + { + $handler = SessionHandlerFactory::createHandler('redis://localhost?prefix=foo&ttl=3600&ignored=bar'); + $this->assertInstanceOf(RedisSessionHandler::class, $handler); + + $reflection = new \ReflectionObject($handler); + + $prefixProperty = $reflection->getProperty('prefix'); + $prefixProperty->setAccessible(true); + $this->assertSame('foo', $prefixProperty->getValue($handler)); + + $ttlProperty = $reflection->getProperty('ttl'); + $ttlProperty->setAccessible(true); + $this->assertSame('3600', $ttlProperty->getValue($handler)); + } } diff --git a/src/Symfony/Component/HttpKernel/EventListener/AbstractSessionListener.php b/src/Symfony/Component/HttpKernel/EventListener/AbstractSessionListener.php index 0867cad073de..08b6faac0e7e 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/AbstractSessionListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/AbstractSessionListener.php @@ -146,7 +146,7 @@ public function onKernelResponse(ResponseEvent $event) $sessionCookieHttpOnly = $this->sessionOptions['cookie_httponly'] ?? true; $sessionCookieSameSite = $this->sessionOptions['cookie_samesite'] ?? Cookie::SAMESITE_LAX; - SessionUtils::popSessionCookie($sessionName, $sessionCookiePath); + SessionUtils::popSessionCookie($sessionName, $sessionId); $request = $event->getRequest(); $requestSessionCookieId = $request->cookies->get($sessionName); diff --git a/src/Symfony/Component/HttpKernel/EventListener/LocaleListener.php b/src/Symfony/Component/HttpKernel/EventListener/LocaleListener.php index 5d77377c6046..f19e13649e98 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/LocaleListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/LocaleListener.php @@ -70,6 +70,7 @@ private function setLocale(Request $request) $request->setLocale($locale); } elseif ($this->useAcceptLanguageHeader && $this->enabledLocales && ($preferredLanguage = $request->getPreferredLanguage($this->enabledLocales))) { $request->setLocale($preferredLanguage); + $request->attributes->set('_vary_by_language', true); } } diff --git a/src/Symfony/Component/HttpKernel/EventListener/ResponseListener.php b/src/Symfony/Component/HttpKernel/EventListener/ResponseListener.php index bb51c6dc0dbd..a4090159bb75 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/ResponseListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/ResponseListener.php @@ -50,6 +50,9 @@ public function onKernelResponse(ResponseEvent $event) if ($this->addContentLanguageHeader && !$response->isInformational() && !$response->isEmpty() && !$response->headers->has('Content-Language')) { $response->headers->set('Content-Language', $event->getRequest()->getLocale()); + } + + if ($event->getRequest()->attributes->get('_vary_by_language')) { $response->setVary('Accept-Language', false); } diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 656324a4df7e..35353377029c 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.0'; - public const VERSION_ID = 50400; + public const VERSION = '5.4.1'; + public const VERSION_ID = 50401; public const MAJOR_VERSION = 5; public const MINOR_VERSION = 4; - public const RELEASE_VERSION = 0; + public const RELEASE_VERSION = 1; public const EXTRA_VERSION = ''; public const END_OF_MAINTENANCE = '11/2024'; diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/SessionListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/SessionListenerTest.php index d82aba64513e..9924c27d11af 100644 --- a/src/Symfony/Component/HttpKernel/Tests/EventListener/SessionListenerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/SessionListenerTest.php @@ -312,6 +312,7 @@ public function testSessionUsageLogIfStatelessAndSessionUsed() public function testSessionIsSavedWhenUnexpectedSessionExceptionThrown() { $session = $this->createMock(Session::class); + $session->expects($this->exactly(1))->method('getId')->willReturn('123456'); $session->expects($this->exactly(1))->method('getName')->willReturn('PHPSESSID'); $session->method('isStarted')->willReturn(true); $session->expects($this->exactly(2))->method('getUsageIndex')->will($this->onConsecutiveCalls(0, 1)); diff --git a/src/Symfony/Component/Lock/Store/DoctrineDbalStore.php b/src/Symfony/Component/Lock/Store/DoctrineDbalStore.php index ead96843fbb7..1a94f3fc6471 100644 --- a/src/Symfony/Component/Lock/Store/DoctrineDbalStore.php +++ b/src/Symfony/Component/Lock/Store/DoctrineDbalStore.php @@ -14,6 +14,7 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\DriverManager; use Doctrine\DBAL\Exception as DBALException; +use Doctrine\DBAL\Exception\TableNotFoundException; use Doctrine\DBAL\ParameterType; use Doctrine\DBAL\Schema\Schema; use Symfony\Component\Lock\Exception\InvalidArgumentException; @@ -90,6 +91,9 @@ public function save(Key $key) ParameterType::STRING, ParameterType::STRING, ]); + } catch (TableNotFoundException $e) { + $this->createTable(); + $this->save($key); } catch (DBALException $e) { // the lock is already acquired. It could be us. Let's try to put off. $this->putOffExpiration($key, $this->initialTtl); diff --git a/src/Symfony/Component/Lock/Tests/Store/DoctrineDbalStoreTest.php b/src/Symfony/Component/Lock/Tests/Store/DoctrineDbalStoreTest.php index c20e2d308811..6a89e49399b0 100644 --- a/src/Symfony/Component/Lock/Tests/Store/DoctrineDbalStoreTest.php +++ b/src/Symfony/Component/Lock/Tests/Store/DoctrineDbalStoreTest.php @@ -70,7 +70,6 @@ public function testDsn(string $dsn, string $file = null) try { $store = new DoctrineDbalStore($dsn); - $store->createTable(); $store->save($key); $this->assertTrue($store->exists($key)); diff --git a/src/Symfony/Component/Lock/Tests/Store/PdoStoreTest.php b/src/Symfony/Component/Lock/Tests/Store/PdoStoreTest.php index 69968a2f11b8..dd15f0f1614b 100644 --- a/src/Symfony/Component/Lock/Tests/Store/PdoStoreTest.php +++ b/src/Symfony/Component/Lock/Tests/Store/PdoStoreTest.php @@ -84,7 +84,6 @@ public function testDsn(string $dsn, string $file = null) try { $store = new PdoStore($dsn); - $store->createTable(); $store->save($key); $this->assertTrue($store->exists($key)); diff --git a/src/Symfony/Component/Lock/composer.json b/src/Symfony/Component/Lock/composer.json index dbb01a0056ee..c2b8e3078e75 100644 --- a/src/Symfony/Component/Lock/composer.json +++ b/src/Symfony/Component/Lock/composer.json @@ -23,7 +23,6 @@ }, "require-dev": { "doctrine/dbal": "^2.13|^3.0", - "mongodb/mongodb": "~1.1", "predis/predis": "~1.0" }, "conflict": { diff --git a/src/Symfony/Component/Runtime/composer.json b/src/Symfony/Component/Runtime/composer.json index f775ec3a0e94..f8112d9a43d0 100644 --- a/src/Symfony/Component/Runtime/composer.json +++ b/src/Symfony/Component/Runtime/composer.json @@ -21,10 +21,10 @@ }, "require-dev": { "composer/composer": "^1.0.2|^2.0", - "symfony/console": "^5.4|^6.0", + "symfony/console": "^4.4.30|^5.3.7|^6.0", "symfony/dotenv": "^5.1|^6.0", - "symfony/http-foundation": "^4.4|^5.0|^6.0", - "symfony/http-kernel": "^4.4|^5.0|^6.0" + "symfony/http-foundation": "^4.4.30|^5.3.7|^6.0", + "symfony/http-kernel": "^4.4.30|^5.3.7|^6.0" }, "conflict": { "symfony/dotenv": "<5.1" diff --git a/src/Symfony/Component/Translation/Bridge/Loco/LocoProvider.php b/src/Symfony/Component/Translation/Bridge/Loco/LocoProvider.php index f40e2119f644..77978f93963e 100644 --- a/src/Symfony/Component/Translation/Bridge/Loco/LocoProvider.php +++ b/src/Symfony/Component/Translation/Bridge/Loco/LocoProvider.php @@ -90,47 +90,37 @@ public function read(array $domains, array $locales): TranslatorBag { $domains = $domains ?: ['*']; $translatorBag = new TranslatorBag(); - $responses = []; foreach ($locales as $locale) { foreach ($domains as $domain) { - $responses[] = [ - 'response' => $this->client->request('GET', sprintf('export/locale/%s.xlf', rawurlencode($locale)), [ - 'query' => [ - 'filter' => $domain, - 'status' => 'translated', - ], - ]), - 'locale' => $locale, - 'domain' => $domain, - ]; - } - } - - foreach ($responses as $response) { - $locale = $response['locale']; - $domain = $response['domain']; - $response = $response['response']; + // Loco forbids concurrent requests, so the requests must be synchronous in order to prevent "429 Too Many Requests" errors. + $response = $this->client->request('GET', sprintf('export/locale/%s.xlf', rawurlencode($locale)), [ + 'query' => [ + 'filter' => $domain, + 'status' => 'translated', + ], + ]); + + if (404 === $response->getStatusCode()) { + $this->logger->warning(sprintf('Locale "%s" for domain "%s" does not exist in Loco.', $locale, $domain)); + continue; + } - if (404 === $response->getStatusCode()) { - $this->logger->warning(sprintf('Locale "%s" for domain "%s" does not exist in Loco.', $locale, $domain)); - continue; - } + $responseContent = $response->getContent(false); - $responseContent = $response->getContent(false); + if (200 !== $response->getStatusCode()) { + throw new ProviderException('Unable to read the Loco response: '.$responseContent, $response); + } - if (200 !== $response->getStatusCode()) { - throw new ProviderException('Unable to read the Loco response: '.$responseContent, $response); - } + $locoCatalogue = $this->loader->load($responseContent, $locale, $domain); + $catalogue = new MessageCatalogue($locale); - $locoCatalogue = $this->loader->load($responseContent, $locale, $domain); - $catalogue = new MessageCatalogue($locale); + foreach ($locoCatalogue->all($domain) as $key => $message) { + $catalogue->set($this->retrieveKeyFromId($key, $domain), $message, $domain); + } - foreach ($locoCatalogue->all($domain) as $key => $message) { - $catalogue->set($this->retrieveKeyFromId($key, $domain), $message, $domain); + $translatorBag->addCatalogue($catalogue); } - - $translatorBag->addCatalogue($catalogue); } return $translatorBag; diff --git a/src/Symfony/Component/Translation/Tests/Command/TranslationPullCommandTest.php b/src/Symfony/Component/Translation/Tests/Command/TranslationPullCommandTest.php index 8177c2c0c873..e5726f266c77 100644 --- a/src/Symfony/Component/Translation/Tests/Command/TranslationPullCommandTest.php +++ b/src/Symfony/Component/Translation/Tests/Command/TranslationPullCommandTest.php @@ -478,6 +478,83 @@ public function testPullMessagesWithDefaultLocale() , file_get_contents($filenameFr)); } + public function testPullMessagesMultipleDomains() + { + $arrayLoader = new ArrayLoader(); + $filenameMessages = $this->createFile(['note' => 'NOTE']); + $filenameDomain = $this->createFile(['note' => 'NOTE'], 'en', 'domain.%locale%.xlf'); + $locales = ['en']; + $domains = ['messages', 'domain']; + + $providerReadTranslatorBag = new TranslatorBag(); + $providerReadTranslatorBag->addCatalogue($arrayLoader->load([ + 'new.foo' => 'newFoo', + ], 'en')); + + $providerReadTranslatorBag->addCatalogue($arrayLoader->load([ + 'new.foo' => 'newFoo', + ], 'en', + 'domain' + )); + + $provider = $this->createMock(ProviderInterface::class); + $provider->expects($this->once()) + ->method('read') + ->with($domains, $locales) + ->willReturn($providerReadTranslatorBag); + + $provider->expects($this->once()) + ->method('__toString') + ->willReturn('null://default'); + + $tester = $this->createCommandTester($provider, $locales, $domains, 'en'); + $tester->execute(['--locales' => ['en'], '--domains' => ['messages', 'domain']]); + + $this->assertStringContainsString('[OK] New translations from "null" has been written locally (for "en" locale(s), and "messages, domain" domain(s)).', trim($tester->getDisplay())); + $this->assertXmlStringEqualsXmlString(<< + + +
+ +
+ + + new.foo + newFoo + + + note + NOTE + + +
+
+XLIFF + , file_get_contents($filenameMessages)); + $this->assertXmlStringEqualsXmlString(<< + + +
+ +
+ + + new.foo + newFoo + + + note + NOTE + + +
+
+XLIFF + , file_get_contents($filenameDomain)); + } + /** * @dataProvider provideCompletionSuggestions */ diff --git a/src/Symfony/Component/Validator/Tests/ConstraintValidatorTest.php b/src/Symfony/Component/Validator/Tests/ConstraintValidatorTest.php index 6ca3eab41fd6..aeaef472fb03 100644 --- a/src/Symfony/Component/Validator/Tests/ConstraintValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/ConstraintValidatorTest.php @@ -15,13 +15,15 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; -final class ConstraintValidatorTest extends TestCase +class ConstraintValidatorTest extends TestCase { /** * @dataProvider formatValueProvider */ public function testFormatValue($expected, $value, $format = 0) { + \Locale::setDefault('en'); + $this->assertSame($expected, (new TestFormatValueConstraintValidator())->formatValueProxy($value, $format)); } diff --git a/src/Symfony/Component/Validator/composer.json b/src/Symfony/Component/Validator/composer.json index 1639a1603a1a..40d942a1dca6 100644 --- a/src/Symfony/Component/Validator/composer.json +++ b/src/Symfony/Component/Validator/composer.json @@ -47,7 +47,7 @@ "conflict": { "doctrine/annotations": "<1.13", "doctrine/cache": "<1.11", - "doctrine/lexer": "<1.0.2", + "doctrine/lexer": "<1.1", "phpunit/phpunit": "<5.4.3", "symfony/dependency-injection": "<4.4", "symfony/expression-language": "<5.1", diff --git a/src/Symfony/Component/VarDumper/Caster/ReflectionCaster.php b/src/Symfony/Component/VarDumper/Caster/ReflectionCaster.php index a57e8b9b6995..274ee0d98f7d 100644 --- a/src/Symfony/Component/VarDumper/Caster/ReflectionCaster.php +++ b/src/Symfony/Component/VarDumper/Caster/ReflectionCaster.php @@ -144,7 +144,7 @@ public static function castReflectionGenerator(\ReflectionGenerator $c, array $a array_unshift($trace, [ 'function' => 'yield', 'file' => $function->getExecutingFile(), - 'line' => $function->getExecutingLine() - 1, + 'line' => $function->getExecutingLine() - (int) (\PHP_VERSION_ID < 80100), ]); $trace[] = $frame; $a[$prefix.'trace'] = new TraceStub($trace, false, 0, -1, -1); @@ -289,15 +289,17 @@ public static function castParameter(\ReflectionParameter $c, array $a, Stub $st unset($a[$prefix.'allowsNull']); } - try { - $a[$prefix.'default'] = $v = $c->getDefaultValue(); - if ($c->isDefaultValueConstant()) { - $a[$prefix.'default'] = new ConstStub($c->getDefaultValueConstantName(), $v); - } - if (null === $v) { - unset($a[$prefix.'allowsNull']); + if ($c->isOptional()) { + try { + $a[$prefix.'default'] = $v = $c->getDefaultValue(); + if ($c->isDefaultValueConstant()) { + $a[$prefix.'default'] = new ConstStub($c->getDefaultValueConstantName(), $v); + } + if (null === $v) { + unset($a[$prefix.'allowsNull']); + } + } catch (\ReflectionException $e) { } - } catch (\ReflectionException $e) { } return $a; diff --git a/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php b/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php index 901df856377e..c261a0da8c16 100644 --- a/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php +++ b/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php @@ -138,21 +138,8 @@ public function testReflectionParameter() { $var = new \ReflectionParameter(reflectionParameterFixture::class, 0); - if (\PHP_VERSION_ID < 80100) { - $this->assertDumpMatchesFormat( - <<<'EOTXT' -ReflectionParameter { - +name: "arg1" - position: 0 - typeHint: "Symfony\Component\VarDumper\Tests\Fixtures\NotLoadableClass" - default: null -} -EOTXT - , $var - ); - } else { - $this->assertDumpMatchesFormat( - <<<'EOTXT' + $this->assertDumpMatchesFormat( + <<<'EOTXT' ReflectionParameter { +name: "arg1" position: 0 @@ -161,8 +148,7 @@ public function testReflectionParameter() } EOTXT , $var - ); - } + ); } public function testReflectionParameterScalar() @@ -484,38 +470,20 @@ public function testGenerator() $generator = new GeneratorDemo(); $generator = $generator->baz(); - if (\PHP_VERSION_ID < 80100) { - $expectedDump = <<<'EODUMP' + $expectedDump = <<<'EODUMP' Generator { this: Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo { …} - executing: { + %s: { %sGeneratorDemo.php:14 { Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo->baz() › { › yield from bar(); › } } - } +%A} closed: false } EODUMP; - } else { - $expectedDump = <<<'EODUMP' -Generator { - this: Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo { …} - trace: { - ./src/Symfony/Component/VarDumper/Tests/Fixtures/GeneratorDemo.php:13 { - Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo->baz() - › public function baz() - › { - › yield from bar(); - } - Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo->baz() {} - } - closed: false -} -EODUMP; - } $this->assertDumpMatchesFormat($expectedDump, $generator); @@ -523,69 +491,33 @@ public function testGenerator() break; } - if (\PHP_VERSION_ID < 80100) { - $expectedDump = <<<'EODUMP' + $expectedDump = <<<'EODUMP' array:2 [ 0 => ReflectionGenerator { this: Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo { …} - trace: { - %s%eTests%eFixtures%eGeneratorDemo.php:9 { + %s: { + %s%eTests%eFixtures%eGeneratorDemo.php:%d { Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo::foo() - › { - › yield 1; - › } - } +%A › yield 1; +%A } %s%eTests%eFixtures%eGeneratorDemo.php:20 { …} %s%eTests%eFixtures%eGeneratorDemo.php:14 { …} - } +%A } closed: false } 1 => Generator { - executing: { - %sGeneratorDemo.php:10 { + %s: { + %s%eTests%eFixtures%eGeneratorDemo.php:%d { Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo::foo() › yield 1; › } › } - } - closed: false - } -] -EODUMP; - } else { - $expectedDump = <<<'EODUMP' -array:2 [ - 0 => ReflectionGenerator { - this: Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo { …} - trace: { - %s%eTests%eFixtures%eGeneratorDemo.php:9 { - Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo::foo() - › { - › yield 1; - › } - } - %s%eTests%eFixtures%eGeneratorDemo.php:20 { …} - %s%eTests%eFixtures%eGeneratorDemo.php:14 { …} - Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo->baz() {} - } - closed: false - } - 1 => Generator { - trace: { - ./src/Symfony/Component/VarDumper/Tests/Fixtures/GeneratorDemo.php:9 { - Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo::foo() - › { - › yield 1; - › } - } - Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo::foo() {} - } +%A } closed: false } ] EODUMP; - } $r = new \ReflectionGenerator($generator); $this->assertDumpMatchesFormat($expectedDump, [$r, $r->getExecutingGenerator()]);