diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 482ea42869d98..258c37b37d388 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -99,9 +99,9 @@ jobs: ports: - 4566:4566 zookeeper: - image: wurstmeister/zookeeper:3.4.6 + image: zookeeper kafka: - image: wurstmeister/kafka:2.12-2.0.1 + image: bitnami/kafka:3.7 ports: - 9092:9092 env: @@ -192,6 +192,18 @@ jobs: ./phpunit install echo "::endgroup::" + - name: Check for changes in translation files + id: changed-translation-files + run: | + echo 'changed='$((git diff --quiet HEAD~1 HEAD -- 'src/**/Resources/translations/*.xlf' || (echo 'true' && exit 1)) && echo 'false') >> $GITHUB_OUTPUT + + - name: Check Translation Status + if: steps.changed-translation-files.outputs.changed == 'true' + run: | + php src/Symfony/Component/Translation/Resources/bin/translation-status.php -v + php .github/sync-translations.php + git diff --exit-code src/ || (echo '::error::Run "php .github/sync-translations.php" to fix XLIFF files.' && exit 1) + - name: Run tests run: ./phpunit --group integration -v env: @@ -216,15 +228,3 @@ jobs: # docker run --rm -e COMPOSER_ROOT_VERSION -v $(pwd):/app -v $(which composer):/usr/local/bin/composer -v $(which vulcain):/usr/local/bin/vulcain -w /app php:8.1-alpine ./phpunit src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php --filter testHttp2Push # sudo rm -rf .phpunit # [ -d .phpunit.bak ] && mv .phpunit.bak .phpunit - - - name: Check for changes in translation files - id: changed-translation-files - run: | - echo 'changed='$((git diff --quiet HEAD~1 HEAD -- 'src/**/Resources/translations/*.xlf' || (echo 'true' && exit 1)) && echo 'false') >> $GITHUB_OUTPUT - - - name: Check Translation Status - if: steps.changed-translation-files.outputs.changed == 'true' - run: | - php src/Symfony/Component/Translation/Resources/bin/translation-status.php -v - php .github/sync-translations.php - git diff --exit-code src/ || (echo '::error::Run "php .github/sync-translations.php" to fix XLIFF files.' && exit 1) diff --git a/CHANGELOG-7.1.md b/CHANGELOG-7.1.md index a855cd6c406ea..78fc0fb2f693d 100644 --- a/CHANGELOG-7.1.md +++ b/CHANGELOG-7.1.md @@ -7,6 +7,36 @@ in 7.1 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/v7.1.0...v7.1.1 +* 7.1.0-RC1 (2024-05-17) + + * bug #54970 [DependencyInjection] Process PHP configs using the ContainerConfigurator (MatTheCat) + * bug #49186 [Serializer] Improve exception message in UnwrappingDenormalizer (andersonamuller) + * bug #54694 [PropertyInfo] Update DoctrineExtractor for new DBAL 4 BIGINT type (llupa) + * bug #54851 [Serializer] Fixed "Warning: Attempt to read property "value" on string" (michaljusiega, xabbuh) + * bug #54913 [Serializer] Fix CurrentType for missing property (ElisDN) + * feature #54941 Les-Tilleuls.coop is sponsoring Symfony 7.1, thanks to them! \o/ (nicolas-grekas) + * bug #54797 [PhpUnitBridge] Fix `DeprecationErrorHandler` with PhpUnit 10 (HypeMC) + * bug #54878 [Filesystem] Fix dumpFile `stat failed` error hitting custom handler (acoulton) + * bug #54924 [Validator] IBAN Check digits should always between 2 and 98 (karstennilsen) + * bug #54919 [ErrorHandler] Do not call xdebug_get_function_stack() with xdebug >= 3.0 when not in develop mode (fmata) + * bug #54910 [HttpFoundation]  filter out empty HTTP header parts (xabbuh) + * bug #54888 [String] Fix folded in compat mode (smnandre) + * bug #54925 [VarDumper]  adapt namespace changes for new DOM extension classes (xabbuh) + * bug #54835 [DoctrineBridge]  fix setting validated fields not using the options array (xabbuh) + * bug #54863 [Process] Return `false` when `open_basedir` prevents access to `/dev/tty` (mjauvin) + * bug #54908 [DependencyInjection] Fix "Cannot replace arguments" errors caused by ResolveAutowireInlineAttributesPass (nicolas-grekas) + * bug #54860 [HttpClient] Revert fixing curl default options (alexandre-daubois) + * feature #54859 [Messenger] Don't mark `EnvelopeAwareExceptionInterface` internal (valtzu) + * bug #54850 [VarExporter] fix `ProxyHelper::generateLazyProxy()` when a method returns null (nikophil) + * bug #54842 [Messenger] Don't drop stamps when message validation fails (valtzu) + * bug #54838 [WebProfilerBundle] Fix assignment to constant variable (HypeMC) + * bug #54837 [Mailer] [Sendgrid] Use `DataPart::getContentId()` when `DataPart::setContentId()` is used (SherinBloemendaal) + * bug #54839 Fix exception thrown during `LDAP_MODIFY_BATCH_REMOVE_ALL` batch operations (phasdev) + * bug #54834 [Validator] Check `Locale` class existence before using it (alexandre-daubois) + * bug #54830 [HttpClient] Fix cURL default options for PHP 8.4 (alexandre-daubois) + * bug #54828 [Serializer] Fix `GetSetMethodNormalizer` not working with setters with optional args (HypeMC) + * bug #54816 [Cache] Fix support for predis/predis:^2.0 (mfettig) + * 7.1.0-BETA1 (2024-05-02) * feature #54818 [Translation] Crowdin is backing its translation bridge, thanks to them! \o/ (nicolas-grekas) diff --git a/README.md b/README.md index 3eb56ada73cfd..ecac2d733dd13 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Sponsor Symfony 7.1 is [backed][27] by - [Rector][29] - [JoliCode][30] +- [Les-Tilleuls.coop][31] **Rector** helps successful and growing companies to get the most of the code they already have. Including upgrading to the latest Symfony LTS. They deliver @@ -31,6 +32,11 @@ so you can focus on the features. strong expertise in PHP & Symfony technologies. They can help you build your projects using state-of-the-art practices. +**Les-Tilleuls.coop** is a team of 70+ Symfony experts who can help you design, develop and +fix your projects. They provide a wide range of professional services including development, +consulting, coaching, training and audits. They also are highly skilled in JS, Go and DevOps. +They are a worker cooperative! + Help Symfony by [sponsoring][28] its development! Documentation @@ -97,3 +103,4 @@ and supported by [Symfony contributors][19]. [28]: https://symfony.com/sponsor [29]: https://getrector.com [30]: https://jolicode.com +[31]: https://les-tilleuls.coop diff --git a/composer.json b/composer.json index f43a4e0f55fdb..5e3fe202240b8 100644 --- a/composer.json +++ b/composer.json @@ -39,7 +39,7 @@ "ext-xml": "*", "doctrine/event-manager": "^2", "doctrine/persistence": "^3.1", - "twig/twig": "^3.0.4", + "twig/twig": "^3.10", "psr/cache": "^2.0|^3.0", "psr/clock": "^1.0", "psr/container": "^1.1|^2.0", diff --git a/src/Symfony/Bridge/Doctrine/PropertyInfo/DoctrineExtractor.php b/src/Symfony/Bridge/Doctrine/PropertyInfo/DoctrineExtractor.php index 7af2d5059abf1..02b29ae9942d9 100644 --- a/src/Symfony/Bridge/Doctrine/PropertyInfo/DoctrineExtractor.php +++ b/src/Symfony/Bridge/Doctrine/PropertyInfo/DoctrineExtractor.php @@ -12,6 +12,7 @@ namespace Symfony\Bridge\Doctrine\PropertyInfo; use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\BigIntType; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\AssociationMapping; @@ -129,6 +130,12 @@ public function getType(string $class, string $property, array $context = []): ? } $nullable = $metadata instanceof ClassMetadata && $metadata->isNullable($property); + + // DBAL 4 has a special fallback strategy for BINGINT (int -> string) + if (Types::BIGINT === $typeOfField && !method_exists(BigIntType::class, 'getName')) { + return Type::collection(Type::int(), Type::string()); + } + $enumType = null; if (null !== $enumClass = self::getMappingValue($metadata->getFieldMapping($property), 'enumType') ?? null) { @@ -236,6 +243,15 @@ public function getTypes(string $class, string $property, array $context = []): } $nullable = $metadata instanceof ClassMetadata && $metadata->isNullable($property); + + // DBAL 4 has a special fallback strategy for BINGINT (int -> string) + if (Types::BIGINT === $typeOfField && !method_exists(BigIntType::class, 'getName')) { + return [ + new LegacyType(LegacyType::BUILTIN_TYPE_INT, $nullable), + new LegacyType(LegacyType::BUILTIN_TYPE_STRING, $nullable), + ]; + } + $enumType = null; if (null !== $enumClass = self::getMappingValue($metadata->getFieldMapping($property), 'enumType') ?? null) { $enumType = new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, $nullable, $enumClass); diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/MockableRepository.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/MockableRepository.php new file mode 100644 index 0000000000000..4ca59949345c3 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/MockableRepository.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\Fixtures; + +use Doctrine\ORM\EntityRepository; + +class MockableRepository extends EntityRepository +{ + public function findByCustom() + { + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php index 35919529be459..c0ab3e1c63783 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php @@ -15,6 +15,7 @@ use Doctrine\Common\EventManager; use Doctrine\DBAL\DriverManager; use Doctrine\DBAL\Schema\DefaultSchemaManagerFactory; +use Doctrine\DBAL\Types\BigIntType; use Doctrine\DBAL\Types\Type as DBALType; use Doctrine\ORM\EntityManager; use Doctrine\ORM\Mapping\Driver\AttributeDriver; @@ -153,10 +154,17 @@ public function testExtractEnumLegacy() */ public static function legacyTypesProvider(): array { + // DBAL 4 has a special fallback strategy for BINGINT (int -> string) + if (!method_exists(BigIntType::class, 'getName')) { + $expectedBingIntType = [new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_STRING)]; + } else { + $expectedBingIntType = [new LegacyType(LegacyType::BUILTIN_TYPE_STRING)]; + } + return [ ['id', [new LegacyType(LegacyType::BUILTIN_TYPE_INT)]], ['guid', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING)]], - ['bigint', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING)]], + ['bigint', $expectedBingIntType], ['time', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'DateTime')]], ['timeImmutable', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable')]], ['dateInterval', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'DateInterval')]], @@ -290,9 +298,16 @@ public function testExtract(string $property, ?Type $type) */ public static function typeProvider(): iterable { + // DBAL 4 has a special fallback strategy for BINGINT (int -> string) + if (!method_exists(BigIntType::class, 'getName')) { + $expectedBigIntType = Type::collection(Type::int(), Type::string()); + } else { + $expectedBigIntType = Type::string(); + } + yield ['id', Type::int()]; yield ['guid', Type::string()]; - yield ['bigint', Type::string()]; + yield ['bigint', $expectedBigIntType]; yield ['time', Type::object(\DateTime::class)]; yield ['timeImmutable', Type::object(\DateTimeImmutable::class)]; yield ['dateInterval', Type::object(\DateInterval::class)]; diff --git a/src/Symfony/Bridge/Doctrine/Tests/Security/User/EntityUserProviderTest.php b/src/Symfony/Bridge/Doctrine/Tests/Security/User/EntityUserProviderTest.php index aadc837aa4e72..a89ac84a7a9c1 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Security/User/EntityUserProviderTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Security/User/EntityUserProviderTest.php @@ -236,14 +236,11 @@ private function getManager($em, $name = null) private function getObjectManager($repository) { - $em = $this->getMockBuilder(ObjectManager::class) - ->onlyMethods(['getClassMetadata', 'getRepository']) - ->getMockForAbstractClass(); - $em->expects($this->any()) - ->method('getRepository') + $objectManager = $this->createMock(ObjectManager::class); + $objectManager->method('getRepository') ->willReturn($repository); - return $em; + return $objectManager; } private function createSchema($em) diff --git a/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php b/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php index abe31d474dfab..4cde094d0e2df 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php @@ -31,6 +31,7 @@ use Symfony\Bridge\Doctrine\Tests\Fixtures\Dto; use Symfony\Bridge\Doctrine\Tests\Fixtures\Employee; use Symfony\Bridge\Doctrine\Tests\Fixtures\HireAnEmployee; +use Symfony\Bridge\Doctrine\Tests\Fixtures\MockableRepository; use Symfony\Bridge\Doctrine\Tests\Fixtures\Person; use Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdEntity; use Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdNoToStringEntity; @@ -91,14 +92,10 @@ protected function createRegistryMock($em = null) protected function createRepositoryMock() { - $repository = $this->getMockBuilder(EntityRepository::class) + return $this->getMockBuilder(MockableRepository::class) ->disableOriginalConstructor() - ->onlyMethods(['find', 'findAll', 'findOneBy', 'findBy', 'getClassName']) - ->addMethods(['findByCustom']) - ->getMock() - ; - - return $repository; + ->onlyMethods(['find', 'findAll', 'findOneBy', 'findBy', 'getClassName', 'findByCustom']) + ->getMock(); } protected function createEntityManagerMock($repositoryMock) @@ -951,6 +948,40 @@ public function rewind(): void } public function testValidateDTOUniqueness() + { + $constraint = new UniqueEntity( + message: 'myMessage', + fields: ['name'], + em: self::EM_NAME, + entityClass: Person::class, + ); + + $entity = new Person(1, 'Foo'); + $dto = new HireAnEmployee('Foo'); + + $this->validator->validate($entity, $constraint); + + $this->assertNoViolation(); + + $this->em->persist($entity); + $this->em->flush(); + + $this->validator->validate($entity, $constraint); + + $this->assertNoViolation(); + + $this->validator->validate($dto, $constraint); + + $this->buildViolation('myMessage') + ->atPath('property.path.name') + ->setInvalidValue('Foo') + ->setCode(UniqueEntity::NOT_UNIQUE_ERROR) + ->setCause([$entity]) + ->setParameters(['{{ value }}' => '"Foo"']) + ->assertRaised(); + } + + public function testValidateDTOUniquenessDoctrineStyle() { $constraint = new UniqueEntity([ 'message' => 'myMessage', @@ -985,6 +1016,32 @@ public function testValidateDTOUniqueness() } public function testValidateMappingOfFieldNames() + { + $constraint = new UniqueEntity( + message: 'myMessage', + fields: ['primaryName' => 'name', 'secondaryName' => 'name2'], + em: self::EM_NAME, + entityClass: DoubleNameEntity::class, + ); + + $entity = new DoubleNameEntity(1, 'Foo', 'Bar'); + $dto = new CreateDoubleNameEntity('Foo', 'Bar'); + + $this->em->persist($entity); + $this->em->flush(); + + $this->validator->validate($dto, $constraint); + + $this->buildViolation('myMessage') + ->atPath('property.path.name') + ->setParameter('{{ value }}', '"Foo"') + ->setInvalidValue('Foo') + ->setCause([$entity]) + ->setCode(UniqueEntity::NOT_UNIQUE_ERROR) + ->assertRaised(); + } + + public function testValidateMappingOfFieldNamesDoctrineStyle() { $constraint = new UniqueEntity([ 'message' => 'myMessage', @@ -1011,6 +1068,21 @@ public function testValidateMappingOfFieldNames() } public function testInvalidateDTOFieldName() + { + $this->expectException(ConstraintDefinitionException::class); + $this->expectExceptionMessage('The field "primaryName" is not a property of class "Symfony\Bridge\Doctrine\Tests\Fixtures\HireAnEmployee".'); + $constraint = new UniqueEntity( + message: 'myMessage', + fields: ['primaryName' => 'name'], + em: self::EM_NAME, + entityClass: SingleStringIdEntity::class, + ); + + $dto = new HireAnEmployee('Foo'); + $this->validator->validate($dto, $constraint); + } + + public function testInvalidateDTOFieldNameDoctrineStyle() { $this->expectException(ConstraintDefinitionException::class); $this->expectExceptionMessage('The field "primaryName" is not a property of class "Symfony\Bridge\Doctrine\Tests\Fixtures\HireAnEmployee".'); @@ -1026,6 +1098,21 @@ public function testInvalidateDTOFieldName() } public function testInvalidateEntityFieldName() + { + $this->expectException(ConstraintDefinitionException::class); + $this->expectExceptionMessage('The field "name2" is not mapped by Doctrine, so it cannot be validated for uniqueness.'); + $constraint = new UniqueEntity( + message: 'myMessage', + fields: ['name2'], + em: self::EM_NAME, + entityClass: SingleStringIdEntity::class, + ); + + $dto = new HireAnEmployee('Foo'); + $this->validator->validate($dto, $constraint); + } + + public function testInvalidateEntityFieldNameDoctrineStyle() { $this->expectException(ConstraintDefinitionException::class); $this->expectExceptionMessage('The field "name2" is not mapped by Doctrine, so it cannot be validated for uniqueness.'); @@ -1041,6 +1128,36 @@ public function testInvalidateEntityFieldName() } public function testValidateDTOUniquenessWhenUpdatingEntity() + { + $constraint = new UniqueEntity( + message: 'myMessage', + fields: ['name'], + em: self::EM_NAME, + entityClass: Person::class, + identifierFieldNames: ['id'], + ); + + $entity1 = new Person(1, 'Foo'); + $entity2 = new Person(2, 'Bar'); + + $this->em->persist($entity1); + $this->em->persist($entity2); + $this->em->flush(); + + $dto = new UpdateEmployeeProfile(2, 'Foo'); + + $this->validator->validate($dto, $constraint); + + $this->buildViolation('myMessage') + ->atPath('property.path.name') + ->setInvalidValue('Foo') + ->setCode(UniqueEntity::NOT_UNIQUE_ERROR) + ->setCause([$entity1]) + ->setParameters(['{{ value }}' => '"Foo"']) + ->assertRaised(); + } + + public function testValidateDTOUniquenessWhenUpdatingEntityDoctrineStyle() { $constraint = new UniqueEntity([ 'message' => 'myMessage', @@ -1071,6 +1188,28 @@ public function testValidateDTOUniquenessWhenUpdatingEntity() } public function testValidateDTOUniquenessWhenUpdatingEntityWithTheSameValue() + { + $constraint = new UniqueEntity( + message: 'myMessage', + fields: ['name'], + em: self::EM_NAME, + entityClass: CompositeIntIdEntity::class, + identifierFieldNames: ['id1', 'id2'], + ); + + $entity = new CompositeIntIdEntity(1, 2, 'Foo'); + + $this->em->persist($entity); + $this->em->flush(); + + $dto = new UpdateCompositeIntIdEntity(1, 2, 'Foo'); + + $this->validator->validate($dto, $constraint); + + $this->assertNoViolation(); + } + + public function testValidateDTOUniquenessWhenUpdatingEntityWithTheSameValueDoctrineStyle() { $constraint = new UniqueEntity([ 'message' => 'myMessage', @@ -1093,6 +1232,35 @@ public function testValidateDTOUniquenessWhenUpdatingEntityWithTheSameValue() } public function testValidateIdentifierMappingOfFieldNames() + { + $constraint = new UniqueEntity( + message: 'myMessage', + fields: ['object1' => 'objectOne', 'object2' => 'objectTwo'], + em: self::EM_NAME, + entityClass: CompositeObjectNoToStringIdEntity::class, + identifierFieldNames: ['object1' => 'objectOne', 'object2' => 'objectTwo'], + ); + + $objectOne = new SingleIntIdNoToStringEntity(1, 'foo'); + $objectTwo = new SingleIntIdNoToStringEntity(2, 'bar'); + + $this->em->persist($objectOne); + $this->em->persist($objectTwo); + $this->em->flush(); + + $entity = new CompositeObjectNoToStringIdEntity($objectOne, $objectTwo); + + $this->em->persist($entity); + $this->em->flush(); + + $dto = new UpdateCompositeObjectNoToStringIdEntity($objectOne, $objectTwo, 'Foo'); + + $this->validator->validate($dto, $constraint); + + $this->assertNoViolation(); + } + + public function testValidateIdentifierMappingOfFieldNamesDoctrineStyle() { $constraint = new UniqueEntity([ 'message' => 'myMessage', @@ -1122,6 +1290,34 @@ public function testValidateIdentifierMappingOfFieldNames() } public function testInvalidateMissingIdentifierFieldName() + { + $this->expectException(ConstraintDefinitionException::class); + $this->expectExceptionMessage('The "Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeObjectNoToStringIdEntity" entity identifier field names should be "objectOne, objectTwo", not "objectTwo".'); + $constraint = new UniqueEntity( + message: 'myMessage', + fields: ['object1' => 'objectOne', 'object2' => 'objectTwo'], + em: self::EM_NAME, + entityClass: CompositeObjectNoToStringIdEntity::class, + identifierFieldNames: ['object2' => 'objectTwo'], + ); + + $objectOne = new SingleIntIdNoToStringEntity(1, 'foo'); + $objectTwo = new SingleIntIdNoToStringEntity(2, 'bar'); + + $this->em->persist($objectOne); + $this->em->persist($objectTwo); + $this->em->flush(); + + $entity = new CompositeObjectNoToStringIdEntity($objectOne, $objectTwo); + + $this->em->persist($entity); + $this->em->flush(); + + $dto = new UpdateCompositeObjectNoToStringIdEntity($objectOne, $objectTwo, 'Foo'); + $this->validator->validate($dto, $constraint); + } + + public function testInvalidateMissingIdentifierFieldNameDoctrineStyle() { $this->expectException(ConstraintDefinitionException::class); $this->expectExceptionMessage('The "Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeObjectNoToStringIdEntity" entity identifier field names should be "objectOne, objectTwo", not "objectTwo".'); @@ -1150,6 +1346,25 @@ public function testInvalidateMissingIdentifierFieldName() } public function testUninitializedValueThrowException() + { + $this->expectExceptionMessage('Typed property Symfony\Bridge\Doctrine\Tests\Fixtures\Dto::$foo must not be accessed before initialization'); + $constraint = new UniqueEntity( + message: 'myMessage', + fields: ['foo' => 'name'], + em: self::EM_NAME, + entityClass: DoubleNameEntity::class, + ); + + $entity = new DoubleNameEntity(1, 'Foo', 'Bar'); + $dto = new Dto(); + + $this->em->persist($entity); + $this->em->flush(); + + $this->validator->validate($dto, $constraint); + } + + public function testUninitializedValueThrowExceptionDoctrineStyle() { $this->expectExceptionMessage('Typed property Symfony\Bridge\Doctrine\Tests\Fixtures\Dto::$foo must not be accessed before initialization'); $constraint = new UniqueEntity([ @@ -1169,6 +1384,27 @@ public function testUninitializedValueThrowException() } public function testEntityManagerNullObjectWhenDTO() + { + $this->expectException(ConstraintDefinitionException::class); + $this->expectExceptionMessage('Unable to find the object manager associated with an entity of class "Symfony\Bridge\Doctrine\Tests\Fixtures\Person"'); + $constraint = new UniqueEntity( + message: 'myMessage', + fields: ['name'], + entityClass: Person::class, + // no "em" option set + ); + + $this->em = null; + $this->registry = $this->createRegistryMock($this->em); + $this->validator = $this->createValidator(); + $this->validator->initialize($this->context); + + $dto = new HireAnEmployee('Foo'); + + $this->validator->validate($dto, $constraint); + } + + public function testEntityManagerNullObjectWhenDTODoctrineStyle() { $this->expectException(ConstraintDefinitionException::class); $this->expectExceptionMessage('Unable to find the object manager associated with an entity of class "Symfony\Bridge\Doctrine\Tests\Fixtures\Person"'); diff --git a/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntity.php b/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntity.php index 5786d4c224fce..edc9011b27ea1 100644 --- a/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntity.php +++ b/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntity.php @@ -60,9 +60,9 @@ public function __construct( $payload = null, array $options = [], ) { - if (\is_array($fields) && \is_string(key($fields))) { + if (\is_array($fields) && \is_string(key($fields)) && [] === array_diff(array_keys($fields), array_keys(get_class_vars(static::class)))) { $options = array_merge($fields, $options); - } elseif (null !== $fields) { + } else { $options['fields'] = $fields; } diff --git a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php index 2916f5ec35b4d..7cf9111587835 100644 --- a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php +++ b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php @@ -367,6 +367,12 @@ private static function getPhpUnitErrorHandler(): callable if ('PHPUnit\Util\ErrorHandler::handleError' === $eh) { return $eh; + } elseif (ErrorHandler::class === $eh) { + return function (int $errorNumber, string $errorString, string $errorFile, int $errorLine) { + ErrorHandler::instance()($errorNumber, $errorString, $errorFile, $errorLine); + + return true; + }; } foreach (debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT | \DEBUG_BACKTRACE_IGNORE_ARGS) as $frame) { diff --git a/src/Symfony/Bridge/Twig/Node/TransNode.php b/src/Symfony/Bridge/Twig/Node/TransNode.php index 0224d46ae0e50..f8dc12023d8df 100644 --- a/src/Symfony/Bridge/Twig/Node/TransNode.php +++ b/src/Symfony/Bridge/Twig/Node/TransNode.php @@ -55,10 +55,8 @@ public function compile(Compiler $compiler): void $vars = null; } [$msg, $defaults] = $this->compileString($this->getNode('body'), $defaults, (bool) $vars); - $display = class_exists(YieldReady::class) ? 'yield' : 'echo'; - $compiler - ->write($display.' $this->env->getExtension(\'Symfony\Bridge\Twig\Extension\TranslationExtension\')->trans(') + ->write('yield $this->env->getExtension(\'Symfony\Bridge\Twig\Extension\TranslationExtension\')->trans(') ->subcompile($msg) ; diff --git a/src/Symfony/Bridge/Twig/Test/Traits/RuntimeLoaderProvider.php b/src/Symfony/Bridge/Twig/Test/Traits/RuntimeLoaderProvider.php index 1025288bc312e..52f84a7d8f23b 100644 --- a/src/Symfony/Bridge/Twig/Test/Traits/RuntimeLoaderProvider.php +++ b/src/Symfony/Bridge/Twig/Test/Traits/RuntimeLoaderProvider.php @@ -20,9 +20,9 @@ trait RuntimeLoaderProvider protected function registerTwigRuntimeLoader(Environment $environment, FormRenderer $renderer) { $loader = $this->createMock(RuntimeLoaderInterface::class); - $loader->expects($this->any())->method('load')->will($this->returnValueMap([ + $loader->expects($this->any())->method('load')->willReturnMap([ ['Symfony\Component\Form\FormRenderer', $renderer], - ])); + ]); $environment->addRuntimeLoader($loader); } } diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php index c214bcd8b9b07..fdf3c4f44efa6 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php @@ -30,7 +30,7 @@ class HttpKernelExtensionTest extends TestCase { public function testFragmentWithError() { - $renderer = $this->getFragmentHandler($this->throwException(new \Exception('foo'))); + $renderer = $this->getFragmentHandler(new \Exception('foo')); $this->expectException(\Twig\Error\RuntimeError::class); @@ -39,7 +39,7 @@ public function testFragmentWithError() public function testRenderFragment() { - $renderer = $this->getFragmentHandler($this->returnValue(new Response('html'))); + $renderer = $this->getFragmentHandler(new Response('html')); $response = $this->renderTemplate($renderer); @@ -83,11 +83,17 @@ public function testGenerateFragmentUri() $this->assertSame('/_fragment?_hash=PP8%2FeEbn1pr27I9wmag%2FM6jYGVwUZ0l2h0vhh2OJ6CI%3D&_path=template%3Dfoo.html.twig%26_format%3Dhtml%26_locale%3Den%26_controller%3DSymfonyBundleFrameworkBundleControllerTemplateController%253A%253AtemplateAction', $twig->render('index')); } - protected function getFragmentHandler($return) + protected function getFragmentHandler($returnOrException): FragmentHandler { $strategy = $this->createMock(FragmentRendererInterface::class); $strategy->expects($this->once())->method('getName')->willReturn('inline'); - $strategy->expects($this->once())->method('render')->will($return); + + $mocker = $strategy->expects($this->once())->method('render'); + if ($returnOrException instanceof \Exception) { + $mocker->willThrowException($returnOrException); + } else { + $mocker->willReturn($returnOrException); + } $context = new RequestStack(); diff --git a/src/Symfony/Bridge/Twig/Tests/Node/TransNodeTest.php b/src/Symfony/Bridge/Twig/Tests/Node/TransNodeTest.php index ee22531bf5071..d1f1114f9421f 100644 --- a/src/Symfony/Bridge/Twig/Tests/Node/TransNodeTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Node/TransNodeTest.php @@ -13,7 +13,6 @@ use PHPUnit\Framework\TestCase; use Symfony\Bridge\Twig\Node\TransNode; -use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Environment; use Twig\Loader\LoaderInterface; @@ -36,8 +35,7 @@ public function testCompileStrict() $this->assertEquals( sprintf( - '%s $this->env->getExtension(\'Symfony\Bridge\Twig\Extension\TranslationExtension\')->trans("trans %%var%%", array_merge(["%%var%%" => %s], %s), "messages");', - class_exists(YieldReady::class) ? 'yield' : 'echo', + 'yield $this->env->getExtension(\'Symfony\Bridge\Twig\Extension\TranslationExtension\')->trans("trans %%var%%", array_merge(["%%var%%" => %s], %s), "messages");', $this->getVariableGetterWithoutStrictCheck('var'), $this->getVariableGetterWithStrictCheck('foo') ), diff --git a/src/Symfony/Bridge/Twig/composer.json b/src/Symfony/Bridge/Twig/composer.json index 90eb69ed0c645..f7f8d32d620ea 100644 --- a/src/Symfony/Bridge/Twig/composer.json +++ b/src/Symfony/Bridge/Twig/composer.json @@ -18,7 +18,7 @@ "require": { "php": ">=8.2", "symfony/translation-contracts": "^2.5|^3", - "twig/twig": "^3.0.4" + "twig/twig": "^3.9" }, "require-dev": { "egulias/email-validator": "^2.1.10|^3|^4", diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig index 8841dd7a82d51..597a8095a0af6 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig @@ -76,7 +76,7 @@ } tab.addEventListener('click', function(e) { - const activeTab = e.target || e.srcElement; + let activeTab = e.target || e.srcElement; /* needed because when the tab contains HTML contents, user can click */ /* on any of those elements instead of their parent '