diff --git a/src/Maker/MakeRegistrationForm.php b/src/Maker/MakeRegistrationForm.php index d623f461b..bd70e0be7 100644 --- a/src/Maker/MakeRegistrationForm.php +++ b/src/Maker/MakeRegistrationForm.php @@ -14,6 +14,7 @@ use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; use Doctrine\Common\Annotations\Annotation; use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Mapping\Column; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Bridge\Twig\Mime\TemplatedEmail; use Symfony\Bundle\MakerBundle\ConsoleStyle; @@ -370,24 +371,40 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen ); $userManipulator->setIo($io); - $userManipulator->addAnnotationToClass( - UniqueEntity::class, - [ - 'fields' => [$usernameField], - 'message' => sprintf('There is already an account with this %s', $usernameField), - ] - ); + if ($this->doctrineHelper->isDoctrineSupportingAttributes()) { + $userManipulator->addAttributeToClass( + UniqueEntity::class, + ['fields' => [$usernameField], 'message' => sprintf('There is already an account with this %s', $usernameField)] + ); + } else { + $userManipulator->addAnnotationToClass( + UniqueEntity::class, + [ + 'fields' => [$usernameField], + 'message' => sprintf('There is already an account with this %s', $usernameField), + ] + ); + } $this->fileManager->dumpFile($classDetails->getPath(), $userManipulator->getSourceCode()); } if ($this->willVerifyEmail) { $classDetails = new ClassDetails($this->userClass); $userManipulator = new ClassSourceManipulator( - file_get_contents($classDetails->getPath()) + file_get_contents($classDetails->getPath()), + false, + $this->doctrineHelper->isClassAnnotated($this->userClass), + true, + $this->doctrineHelper->doesClassUsesAttributes($this->userClass) ); $userManipulator->setIo($io); - $userManipulator->addProperty('isVerified', ['@ORM\Column(type="boolean")'], false); + $userManipulator->addProperty( + 'isVerified', + ['@ORM\Column(type="boolean")'], + false, + [$userManipulator->buildAttributeNode(Column::class, ['type' => 'boolean'], 'ORM')] + ); $userManipulator->addAccessorMethod('isVerified', 'isVerified', 'bool', false); $userManipulator->addSetter('isVerified', 'bool', false); diff --git a/src/Maker/MakeResetPassword.php b/src/Maker/MakeResetPassword.php index d73ace9f3..b84a75977 100644 --- a/src/Maker/MakeResetPassword.php +++ b/src/Maker/MakeResetPassword.php @@ -44,8 +44,10 @@ use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; +use Symfony\Component\Translation\Translator; use Symfony\Component\Validator\Validation; use Symfony\Component\Yaml\Yaml; +use Symfony\Contracts\Translation\TranslatorInterface; use SymfonyCasts\Bundle\ResetPassword\Controller\ResetPasswordControllerTrait; use SymfonyCasts\Bundle\ResetPassword\Exception\ResetPasswordExceptionInterface; use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestInterface; @@ -236,6 +238,18 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen EntityManagerInterface::class, ]; + // Namespace for ResetPasswordExceptionInterface was imported above + $problemValidateMessageOrConstant = \defined('SymfonyCasts\Bundle\ResetPassword\Exception\ResetPasswordExceptionInterface::MESSAGE_PROBLEM_VALIDATE') + ? 'ResetPasswordExceptionInterface::MESSAGE_PROBLEM_VALIDATE' + : "'There was a problem validating your password reset request'"; + $problemHandleMessageOrConstant = \defined('SymfonyCasts\Bundle\ResetPassword\Exception\ResetPasswordExceptionInterface::MESSAGE_PROBLEM_HANDLE') + ? 'ResetPasswordExceptionInterface::MESSAGE_PROBLEM_HANDLE' + : "'There was a problem handling your password reset request'"; + + if ($isTranslatorAvailable = class_exists(Translator::class)) { + $useStatements[] = TranslatorInterface::class; + } + $generator->generateController( $controllerClassNameDetails->getFullName(), 'resetPassword/ResetPasswordController.tpl.php', @@ -253,6 +267,9 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen 'password_hasher_class_details' => ($passwordClassDetails = $generator->createClassNameDetails($passwordHasher, '\\')), 'password_hasher_variable_name' => str_replace('Interface', '', sprintf('$%s', lcfirst($passwordClassDetails->getShortName()))), // @legacy see passwordHasher conditional above 'use_password_hasher' => UserPasswordHasherInterface::class === $passwordHasher, // @legacy see passwordHasher conditional above + 'problem_validate_message_or_constant' => $problemValidateMessageOrConstant, + 'problem_handle_message_or_constant' => $problemHandleMessageOrConstant, + 'translator_available' => $isTranslatorAvailable, ] ); diff --git a/src/Resources/skeleton/authenticator/Security52EmptyAuthenticator.tpl.php b/src/Resources/skeleton/authenticator/Security52EmptyAuthenticator.tpl.php index 5890d0eaa..204aa9913 100644 --- a/src/Resources/skeleton/authenticator/Security52EmptyAuthenticator.tpl.php +++ b/src/Resources/skeleton/authenticator/Security52EmptyAuthenticator.tpl.php @@ -7,7 +7,7 @@ use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator; - +use ; class extends AbstractAuthenticator { diff --git a/src/Resources/skeleton/resetPassword/ResetPasswordController.tpl.php b/src/Resources/skeleton/resetPassword/ResetPasswordController.tpl.php index 2ce3c2b13..bcf690e6e 100644 --- a/src/Resources/skeleton/resetPassword/ResetPasswordController.tpl.php +++ b/src/Resources/skeleton/resetPassword/ResetPasswordController.tpl.php @@ -34,7 +34,7 @@ public function __construct(ResetPasswordHelperInterface $resetPasswordHelper, E * @Route("", name="app_forgot_password_request") */ - public function request(Request $request, MailerInterface $mailer): Response + public function request(Request $request, MailerInterface $mailer, TranslatorInterface $translator): Response { $form = $this->createForm(::class); $form->handleRequest($request); @@ -42,7 +42,8 @@ public function request(Request $request, MailerInterface $mailer): Response if ($form->isSubmitted() && $form->isValid()) { return $this->processSendingPasswordResetEmail( $form->get('')->getData(), - $mailer + $mailer, + $translator ); } @@ -84,7 +85,7 @@ public function checkEmail(): Response * @Route("/reset/{token}", name="app_reset_password") */ - public function reset(Request $request, getShortName() ?> , string $token = null): Response + public function reset(Request $request, getShortName() ?> , TranslatorInterface $translator, string $token = null): Response { if ($token) { // We store the token in session and remove it from the URL, to avoid the URL being @@ -103,8 +104,9 @@ public function reset(Request $request, getS $user = $this->resetPasswordHelper->validateTokenAndFetchUser($token); } catch (ResetPasswordExceptionInterface $e) { $this->addFlash('reset_password_error', sprintf( - 'There was a problem validating your reset request - %s', - $e->getReason() + '%s - %s', + $translator->trans(, [], 'ResetPasswordBundle'), + $translator->trans($e->getReason(), [], 'ResetPasswordBundle')$e->getReason() )); return $this->redirectToRoute('app_forgot_password_request'); @@ -138,7 +140,7 @@ public function reset(Request $request, getS ]); } - private function processSendingPasswordResetEmail(string $emailFormData, MailerInterface $mailer): RedirectResponse + private function processSendingPasswordResetEmail(string $emailFormData, MailerInterface $mailer, TranslatorInterface $translator): RedirectResponse { $user = $this->entityManager->getRepository(::class)->findOneBy([ '' => $emailFormData, @@ -157,8 +159,9 @@ private function processSendingPasswordResetEmail(string $emailFormData, MailerI // Caution: This may reveal if a user is registered or not. // // $this->addFlash('reset_password_error', sprintf( - // 'There was a problem handling your password reset request - %s', - // $e->getReason() + // '%s - %s', + // $translator->trans(, [], 'ResetPasswordBundle'), + // $translator->trans($e->getReason(), [], 'ResetPasswordBundle')$e->getReason() // )); return $this->redirectToRoute('app_check_email'); diff --git a/src/Test/MakerTestRunner.php b/src/Test/MakerTestRunner.php index fb0665220..bde446d3e 100644 --- a/src/Test/MakerTestRunner.php +++ b/src/Test/MakerTestRunner.php @@ -170,18 +170,38 @@ public function removeFromFile(string $filename, string $find, bool $allowNotFou public function configureDatabase(bool $createSchema = true): void { - $this->replaceInFile( - '.env', - 'postgresql://symfony:ChangeMe@127.0.0.1:5432/app?serverVersion=13&charset=utf8', - getenv('TEST_DATABASE_DSN') - ); + // @legacy Drop conditional when Symfony 4.4 is no longer supported. + if (50000 > $this->environment->getSymfonyVersionInApp()) { + $this->replaceInFile( + '.env', + 'postgresql://db_user:db_password@127.0.0.1:5432/db_name?serverVersion=13&charset=utf8', + getenv('TEST_DATABASE_DSN') + ); + } else { + $this->replaceInFile( + '.env', + 'postgresql://symfony:ChangeMe@127.0.0.1:5432/app?serverVersion=13&charset=utf8', + getenv('TEST_DATABASE_DSN') + ); + } // Flex includes a recipe to suffix the dbname w/ "_test" - lets keep // things simple for these tests and not do that. - $this->removeFromFile( - 'config/packages/test/doctrine.yaml', - "dbname_suffix: '_test%env(default::TEST_TOKEN)%'" - ); + $this->modifyYamlFile('config/packages/doctrine.yaml', function (array $config) { + if (isset($config['when@test']['doctrine']['dbal']['dbname_suffix'])) { + unset($config['when@test']['doctrine']['dbal']['dbname_suffix']); + } + + return $config; + }); + + // @legacy DoctrineBundle 2.4 recipe uses when@test instead of a test/doctrine.yaml config + if ($this->filesystem->exists('config/packages/test/doctrine.yaml')) { + $this->removeFromFile( + 'config/packages/test/doctrine.yaml', + "dbname_suffix: '_test%env(default::TEST_TOKEN)%'" + ); + } // this looks silly, but it's the only way to drop the database *for sure*, // as doctrine:database:drop will error if there is no database diff --git a/src/Util/ClassSourceManipulator.php b/src/Util/ClassSourceManipulator.php index c74dc0641..1c5ea8732 100644 --- a/src/Util/ClassSourceManipulator.php +++ b/src/Util/ClassSourceManipulator.php @@ -13,6 +13,13 @@ use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; +use Doctrine\ORM\Mapping\Column; +use Doctrine\ORM\Mapping\Embedded; +use Doctrine\ORM\Mapping\JoinColumn; +use Doctrine\ORM\Mapping\ManyToMany; +use Doctrine\ORM\Mapping\ManyToOne; +use Doctrine\ORM\Mapping\OneToMany; +use Doctrine\ORM\Mapping\OneToOne; use PhpParser\Builder; use PhpParser\BuilderHelpers; use PhpParser\Comment\Doc; @@ -93,7 +100,7 @@ public function addEntityField(string $propertyName, array $columnOptions, array $attributes = []; if ($this->useAttributesForDoctrineMapping) { - $attributes[] = $this->buildAttributeNode('ORM\Column', $columnOptions); + $attributes[] = $this->buildAttributeNode(Column::class, $columnOptions, 'ORM'); } else { $comments[] = $this->buildAnnotationLine('@ORM\Column', $columnOptions); } @@ -138,10 +145,9 @@ public function addEmbeddedEntity(string $propertyName, string $className): void } else { $attributes = [ $this->buildAttributeNode( - 'ORM\\Embedded', - [ - 'class' => new ClassNameValue($className, $typeHint), - ] + Embedded::class, + ['class' => new ClassNameValue($className, $typeHint)], + 'ORM' ), ]; } @@ -333,6 +339,9 @@ public function createMethodLevelBlankLine() return $this->createBlankLineNode(self::CONTEXT_CLASS_METHOD); } + /** + * @param array $attributes + */ public function addProperty(string $name, array $annotationLines = [], $defaultValue = null, array $attributes = []): void { if ($this->propertyExists($name)) { @@ -342,14 +351,14 @@ public function addProperty(string $name, array $annotationLines = [], $defaultV $newPropertyBuilder = (new Builder\Property($name))->makePrivate(); - if ($annotationLines && $this->useAnnotations) { + if ($this->useAttributesForDoctrineMapping) { + foreach ($attributes as $attribute) { + $newPropertyBuilder->addAttribute($attribute); + } + } elseif ($annotationLines && $this->useAnnotations) { $newPropertyBuilder->setDocComment($this->createDocBlock($annotationLines)); } - foreach ($attributes as $attribute) { - $newPropertyBuilder->addAttribute($attribute); - } - if (null !== $defaultValue) { $newPropertyBuilder->setDefault($defaultValue); } @@ -358,6 +367,17 @@ public function addProperty(string $name, array $annotationLines = [], $defaultV $this->addNodeAfterProperties($newPropertyNode); } + public function addAttributeToClass(string $attributeClass, array $options): void + { + $this->addUseStatementIfNecessary($attributeClass); + + $classNode = $this->getClassNode(); + + $classNode->attrGroups[] = new Node\AttributeGroup([$this->buildAttributeNode($attributeClass, $options)]); + + $this->updateSourceCodeFromNewStmts(); + } + public function addAnnotationToClass(string $annotationClass, array $options): void { $annotationClassAlias = $this->addUseStatementIfNecessary($annotationClass); @@ -532,8 +552,9 @@ private function addSingularRelation(BaseRelation $relation): void } else { $attributes = [ $this->buildAttributeNode( - $relation instanceof RelationManyToOne ? 'ORM\\ManyToOne' : 'ORM\\OneToOne', - $annotationOptions + $relation instanceof RelationManyToOne ? ManyToOne::class : OneToOne::class, + $annotationOptions, + 'ORM' ), ]; } @@ -544,9 +565,7 @@ private function addSingularRelation(BaseRelation $relation): void 'nullable' => false, ]); } else { - $attributes[] = $this->buildAttributeNode('ORM\\JoinColumn', [ - 'nullable' => false, - ]); + $attributes[] = $this->buildAttributeNode(JoinColumn::class, ['nullable' => false], 'ORM'); } } @@ -628,8 +647,9 @@ private function addCollectionRelation(BaseCollectionRelation $relation): void } else { $attributes = [ $this->buildAttributeNode( - $relation instanceof RelationManyToMany ? 'ORM\\ManyToMany' : 'ORM\\OneToMany', - $annotationOptions + $relation instanceof RelationManyToMany ? ManyToMany::class : OneToMany::class, + $annotationOptions, + 'ORM' ), ]; } @@ -900,6 +920,30 @@ public function addUseStatementIfNecessary(string $class): string return $shortClassName; } + /** + * Builds a PHPParser attribute node. + * + * @param string $attributeClass The attribute class which should be used for the attribute E.g. #[Column()] + * @param array $options The named arguments for the attribute ($key = argument name, $value = argument value) + * @param ?string $attributePrefix If a prefix is provided, the node is built using the prefix. E.g. #[ORM\Column()] + */ + public function buildAttributeNode(string $attributeClass, array $options, ?string $attributePrefix = null): Node\Attribute + { + $options = $this->sortOptionsByClassConstructorParameters($options, $attributeClass); + + $context = $this; + $nodeArguments = array_map(static function ($option, $value) use ($context) { + return new Node\Arg($context->buildNodeExprByValue($value), false, false, [], new Node\Identifier($option)); + }, array_keys($options), array_values($options)); + + $class = $attributePrefix ? sprintf('%s\\%s', $attributePrefix, Str::getShortClassName($attributeClass)) : Str::getShortClassName($attributeClass); + + return new Node\Attribute( + new Node\Name($class), + $nodeArguments + ); + } + private function updateSourceCodeFromNewStmts(): void { $newCode = $this->printer->printFormatPreserving( @@ -1421,27 +1465,6 @@ private function buildNodeExprByValue($value): Node\Expr return $nodeValue; } - /** - * builds an PHPParser attribute node. - * - * @param string $attributeClass the attribute class which should be used for the attribute - * @param array $options the named arguments for the attribute ($key = argument name, $value = argument value) - */ - private function buildAttributeNode(string $attributeClass, array $options): Node\Attribute - { - $options = $this->sortOptionsByClassConstructorParameters($options, $attributeClass); - - $context = $this; - $nodeArguments = array_map(static function ($option, $value) use ($context) { - return new Node\Arg($context->buildNodeExprByValue($value), false, false, [], new Node\Identifier($option)); - }, array_keys($options), array_values($options)); - - return new Node\Attribute( - new Node\Name($attributeClass), - $nodeArguments - ); - } - /** * sort the given options based on the constructor parameters for the given $classString * this prevents code inspections warnings for IDEs like intellij/phpstorm. diff --git a/tests/Maker/MakeEntityTest.php b/tests/Maker/MakeEntityTest.php index 7f97326df..6f78c37ba 100644 --- a/tests/Maker/MakeEntityTest.php +++ b/tests/Maker/MakeEntityTest.php @@ -32,13 +32,15 @@ private function createMakeEntityTest(bool $withDatabase = true): MakerTestDetai ->preRun(function (MakerTestRunner $runner) use ($withDatabase) { $config = $runner->readYaml('config/packages/doctrine.yaml'); - if (isset($config['doctrine']['orm']['mappings']['App']['type']) && $this->useAttributes($runner)) { - // use attributes - $runner->replaceInFile( - 'config/packages/doctrine.yaml', - 'type: annotation', - 'type: attribute' - ); + /* @legacy Refactor when annotations are no longer supported. */ + if (isset($config['doctrine']['orm']['mappings']['App']) && !$this->useAttributes($runner)) { + // Attributes are only supported w/ PHP 8, FrameworkBundle >=5.2, + // ORM >=2.9, & DoctrineBundle >=2.4 + $runner->modifyYamlFile('config/packages/doctrine.yaml', function (array $data) { + $data['doctrine']['orm']['mappings']['App']['type'] = 'annotation'; + + return $data; + }); } if ($withDatabase) { diff --git a/tests/Maker/MakeFunctionalTestTest.php b/tests/Maker/MakeFunctionalTestTest.php index 16dcdf2e2..31d562c74 100644 --- a/tests/Maker/MakeFunctionalTestTest.php +++ b/tests/Maker/MakeFunctionalTestTest.php @@ -28,7 +28,8 @@ protected function getMakerClass(): string public function getTestDetails() { yield 'it_generates_test_with_panther' => [$this->createMakerTest() - ->addExtraDependencies('panther') + /* @legacy Allows Panther >= 1.x to be installed. (PHP <8.0 support) */ + ->addExtraDependencies('panther:*') ->run(function (MakerTestRunner $runner) { $runner->copy( 'make-functional/MainController.php', diff --git a/tests/Maker/MakeResetPasswordTest.php b/tests/Maker/MakeResetPasswordTest.php index 8f85793bc..8424a3157 100644 --- a/tests/Maker/MakeResetPasswordTest.php +++ b/tests/Maker/MakeResetPasswordTest.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\MakerBundle\Tests\Maker; +use Doctrine\ORM\Mapping\Driver\AttributeReader; use Symfony\Bundle\MakerBundle\Maker\MakeResetPassword; use Symfony\Bundle\MakerBundle\Test\MakerTestCase; use Symfony\Bundle\MakerBundle\Test\MakerTestDetails; @@ -75,6 +76,22 @@ public function getTestDetails() }), ]; + yield 'it_generates_with_translator_installed' => [$this->createResetPasswordTest() + ->addExtraDependencies('symfony/translation') + ->run(function (MakerTestRunner $runner) { + $this->makeUser($runner); + + $output = $runner->runMaker([ + 'App\Entity\User', + 'app_home', + 'victor@symfonycasts.com', + 'SymfonyCasts', + ]); + + $this->assertStringContainsString('Success', $output); + }), + ]; + yield 'it_generates_with_custom_config' => [$this->createResetPasswordTest() ->run(function (MakerTestRunner $runner) { $runner->deleteFile('config/packages/reset_password.yaml'); @@ -159,9 +176,17 @@ public function getTestDetails() $this->assertStringContainsString('\'emailAddress\' => $emailFormData,', $contentResetPasswordController); $this->assertStringContainsString('$user->setMyPassword($encodedPassword);', $contentResetPasswordController); $this->assertStringContainsString('->to($user->getEmailAddress())', $contentResetPasswordController); + // check ResetPasswordRequest $contentResetPasswordRequest = file_get_contents($runner->getPath('src/Entity/ResetPasswordRequest.php')); - $this->assertStringContainsString('ORM\ManyToOne(targetEntity=UserCustom::class)', $contentResetPasswordRequest); + + /* @legacy Drop annotation test when annotations are no longer supported. */ + if ($this->useAttributes($runner)) { + $this->assertStringContainsString('ORM\ManyToOne(targetEntity: UserCustom::class)', $contentResetPasswordRequest); + } else { + $this->assertStringContainsString('ORM\ManyToOne(targetEntity=UserCustom::class)', $contentResetPasswordRequest); + } + // check ResetPasswordRequestFormType $contentResetPasswordRequestFormType = file_get_contents($runner->getPath('/src/Form/ResetPasswordRequestFormType.php')); $this->assertStringContainsString('->add(\'emailAddress\', EmailType::class, [', $contentResetPasswordRequestFormType); @@ -198,4 +223,11 @@ private function makeUser(MakerTestRunner $runner, string $identifier = 'email', $checkPassword ? 'y' : 'n', // password ]); } + + private function useAttributes(MakerTestRunner $runner): bool + { + return \PHP_VERSION_ID >= 80000 + && $runner->doesClassExist(AttributeReader::class) + && $runner->getSymfonyVersion() >= 50200; + } } diff --git a/tests/Maker/MakeSerializerEncoderTest.php b/tests/Maker/MakeSerializerEncoderTest.php index 9f4953e1b..bd530c2ab 100644 --- a/tests/Maker/MakeSerializerEncoderTest.php +++ b/tests/Maker/MakeSerializerEncoderTest.php @@ -25,6 +25,11 @@ protected function getMakerClass(): string public function getTestDetails() { yield 'it_makes_serializer_encoder' => [$this->createMakerTest() + // serializer-pack 1.1 requires symfony/property-info >= 5.4 + // adding symfony/serializer-pack:* as an extra depends allows + // us to use serializer-pack < 1.1 which does not conflict with + // property-info < 5.4. E.g. Symfony 5.3 tests. See PR 1063 + ->addExtraDependencies('symfony/serializer-pack:*') ->run(function (MakerTestRunner $runner) { $runner->runMaker( [ diff --git a/tests/Maker/MakeSerializerNormalizerTest.php b/tests/Maker/MakeSerializerNormalizerTest.php index 2f38e7f0c..a27196d90 100644 --- a/tests/Maker/MakeSerializerNormalizerTest.php +++ b/tests/Maker/MakeSerializerNormalizerTest.php @@ -25,6 +25,11 @@ protected function getMakerClass(): string public function getTestDetails() { yield 'it_makes_serializer_normalizer' => [$this->createMakerTest() + // serializer-pack 1.1 requires symfony/property-info >= 5.4 + // adding symfony/serializer-pack:* as an extra depends allows + // us to use serializer-pack < 1.1 which does not conflict with + // property-info < 5.4. E.g. Symfony 5.3 tests. See PR 1063 + ->addExtraDependencies('symfony/serializer-pack:*') ->run(function (MakerTestRunner $runner) { $runner->runMaker( [ diff --git a/tests/Maker/MakeTestTest.php b/tests/Maker/MakeTestTest.php index 72e85f358..a2ec38948 100644 --- a/tests/Maker/MakeTestTest.php +++ b/tests/Maker/MakeTestTest.php @@ -80,7 +80,8 @@ public function getTestDetails() ]; yield 'it_makes_PantherTestCase_type' => [$this->createMakerTest() - ->addExtraDependencies('panther') + /* @legacy Allows Panther >= 1.x to be installed. (PHP <8.0 support) */ + ->addExtraDependencies('panther:*') ->run(function (MakerTestRunner $runner) { $runner->copy( 'make-test/basic_setup', diff --git a/tests/Util/ClassSourceManipulatorTest.php b/tests/Util/ClassSourceManipulatorTest.php index 21ec436e7..462cedc82 100644 --- a/tests/Util/ClassSourceManipulatorTest.php +++ b/tests/Util/ClassSourceManipulatorTest.php @@ -11,6 +11,8 @@ namespace Symfony\Bundle\MakerBundle\Tests\Util; +use Doctrine\ORM\Mapping\Column; +use Doctrine\ORM\Mapping\Entity; use PhpParser\Builder\Param; use PHPUnit\Framework\TestCase; use Symfony\Bundle\MakerBundle\Doctrine\RelationManyToMany; @@ -167,6 +169,41 @@ public function getAddSetterTests() ]; } + /** + * @dataProvider getAttributeClassTests + */ + public function testAddAttributeToClass(string $sourceFilename, string $expectedSourceFilename, string $attributeClass, array $attributeOptions, string $attributePrefix = null): void + { + // @legacy Remove conditional when PHP < 8.0 support is dropped. + if ((\PHP_VERSION_ID < 80000)) { + $this->markTestSkipped('Requires PHP >= PHP 8.0'); + } + + $source = file_get_contents(__DIR__.'/fixtures/source/'.$sourceFilename); + $expectedSource = file_get_contents(__DIR__.'/fixtures/add_class_attribute/'.$expectedSourceFilename); + $manipulator = new ClassSourceManipulator($source); + $manipulator->addAttributeToClass($attributeClass, $attributeOptions, $attributePrefix); + + self::assertSame($expectedSource, $manipulator->getSourceCode()); + } + + public function getAttributeClassTests(): \Generator + { + yield 'Empty class' => [ + 'User_empty.php', + 'User_empty.php', + Entity::class, + [], + ]; + + yield 'Class already has attributes' => [ + 'User_simple.php', + 'User_simple.php', + Column::class, + ['message' => 'We use this attribute for class level tests so we dont have to add additional test dependencies.'], + ]; + } + /** * @dataProvider getAnnotationTests */ diff --git a/tests/Util/fixtures/add_class_attribute/User_empty.php b/tests/Util/fixtures/add_class_attribute/User_empty.php new file mode 100644 index 000000000..353f72680 --- /dev/null +++ b/tests/Util/fixtures/add_class_attribute/User_empty.php @@ -0,0 +1,10 @@ +id; + } +}