diff --git a/CHANGELOG.md b/CHANGELOG.md index d8c25ae4c..e9512b372 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [v1.52.0](https://github.com/symfony/maker-bundle/releases/tag/v1.52.0) + +### Feature + +- [#1372](https://github.com/symfony/maker-bundle/issue/1372) - Support Entity relations in form generation - *@maelanleborgne* + ## [v1.50.0](https://github.com/symfony/maker-bundle/releases/tag/v1.50.0) ### Feature diff --git a/src/Doctrine/EntityDetails.php b/src/Doctrine/EntityDetails.php index d0b39e30f..d9e429ba4 100644 --- a/src/Doctrine/EntityDetails.php +++ b/src/Doctrine/EntityDetails.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\MakerBundle\Doctrine; use Doctrine\Persistence\Mapping\ClassMetadata; +use Symfony\Bridge\Doctrine\Form\Type\EntityType; /** * @author Sadicov Vladimir @@ -55,17 +56,25 @@ public function getFormFields(): array } } - foreach ($this->metadata->associationMappings as $fieldName => $relation) { - if (\Doctrine\ORM\Mapping\ClassMetadata::ONE_TO_MANY !== $relation['type']) { - $fields[] = $fieldName; - } - } - $fieldsWithTypes = []; foreach ($fields as $field) { $fieldsWithTypes[$field] = null; } + foreach ($this->metadata->associationMappings as $fieldName => $relation) { + if (\Doctrine\ORM\Mapping\ClassMetadata::ONE_TO_MANY === $relation['type']) { + continue; + } + $fieldsWithTypes[$fieldName] = [ + 'type' => EntityType::class, + 'options_code' => sprintf('\'class\' => %s::class,', $relation['targetEntity']).PHP_EOL.'\'choice_label\' => \'id\',', + 'extra_use_classes' => [$relation['targetEntity']], + ]; + if (\Doctrine\ORM\Mapping\ClassMetadata::MANY_TO_MANY === $relation['type']) { + $fieldsWithTypes[$fieldName]['options_code'] .= "\n'multiple' => true,"; + } + } + return $fieldsWithTypes; } } diff --git a/src/Renderer/FormTypeRenderer.php b/src/Renderer/FormTypeRenderer.php index e6e8feb8b..567e3516e 100644 --- a/src/Renderer/FormTypeRenderer.php +++ b/src/Renderer/FormTypeRenderer.php @@ -39,6 +39,14 @@ public function render(ClassNameDetails $formClassDetails, array $formFields, Cl if (isset($fieldTypeOptions['type'])) { $fieldTypeUseStatements[] = $fieldTypeOptions['type']; $fieldTypeOptions['type'] = Str::getShortClassName($fieldTypeOptions['type']); + if (\array_key_exists('extra_use_classes', $fieldTypeOptions) && \count($fieldTypeOptions['extra_use_classes']) > 0) { + $extraUseClasses = array_merge($extraUseClasses, $fieldTypeOptions['extra_use_classes'] ?? []); + $fieldTypeOptions['options_code'] = str_replace( + $fieldTypeOptions['extra_use_classes'], + array_map(fn ($class) => Str::getShortClassName($class), $fieldTypeOptions['extra_use_classes']), + $fieldTypeOptions['options_code'] + ); + } } $fields[$name] = $fieldTypeOptions; diff --git a/src/Resources/skeleton/form/Type.tpl.php b/src/Resources/skeleton/form/Type.tpl.php index 04484755f..28a1963a1 100644 --- a/src/Resources/skeleton/form/Type.tpl.php +++ b/src/Resources/skeleton/form/Type.tpl.php @@ -16,7 +16,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ->add('', ::class) ->add('', , [ - + ]) diff --git a/tests/Maker/MakeFormTest.php b/tests/Maker/MakeFormTest.php index dc69add33..17511eed8 100644 --- a/tests/Maker/MakeFormTest.php +++ b/tests/Maker/MakeFormTest.php @@ -97,6 +97,90 @@ public function getTestDetails() }), ]; + yield 'it_generates_form_with_many_to_one_relation' => [$this->createMakerTest() + ->addExtraDependencies('orm') + ->run(function (MakerTestRunner $runner) { + $runner->copy( + 'make-form/relation_one_to_many/Book.php', + 'src/Entity/Book.php' + ); + $runner->copy( + 'make-form/relation_one_to_many/Author.php', + 'src/Entity/Author.php' + ); + + $runner->runMaker([ + // Entity name + 'BookType', + 'Book', + ]); + + $this->runFormTest($runner, 'it_generates_form_with_many_to_one_relation.php'); + }), + ]; + yield 'it_generates_form_with_one_to_many_relation' => [$this->createMakerTest() + ->addExtraDependencies('orm') + ->run(function (MakerTestRunner $runner) { + $runner->copy( + 'make-form/relation_one_to_many/Book.php', + 'src/Entity/Book.php' + ); + $runner->copy( + 'make-form/relation_one_to_many/Author.php', + 'src/Entity/Author.php' + ); + + $runner->runMaker([ + // Entity name + 'AuthorType', + 'Author', + ]); + + $this->runFormTest($runner, 'it_generates_form_with_one_to_many_relation.php'); + }), + ]; + yield 'it_generates_form_with_many_to_many_relation' => [$this->createMakerTest() + ->addExtraDependencies('orm') + ->run(function (MakerTestRunner $runner) { + $runner->copy( + 'make-form/relation_many_to_many/Book.php', + 'src/Entity/Book.php' + ); + $runner->copy( + 'make-form/relation_many_to_many/Library.php', + 'src/Entity/Library.php' + ); + + $runner->runMaker([ + // Entity name + 'BookType', + 'Book', + ]); + + $this->runFormTest($runner, 'it_generates_form_with_many_to_many_relation.php'); + }), + ]; + yield 'it_generates_form_with_one_to_one_relation' => [$this->createMakerTest() + ->addExtraDependencies('orm') + ->run(function (MakerTestRunner $runner) { + $runner->copy( + 'make-form/relation_one_to_one/Librarian.php', + 'src/Entity/Librarian.php' + ); + $runner->copy( + 'make-form/relation_one_to_one/Library.php', + 'src/Entity/Library.php' + ); + + $runner->runMaker([ + // Entity name + 'LibraryType', + 'Library', + ]); + + $this->runFormTest($runner, 'it_generates_form_with_one_to_one_relation.php'); + }), + ]; yield 'it_generates_form_with_embeddable_entity' => [$this->createMakerTest() ->addExtraDependencies('orm') ->run(function (MakerTestRunner $runner) { diff --git a/tests/fixtures/make-form/Property.php b/tests/fixtures/make-form/Property.php index 1ffaca9b3..f29aee37d 100644 --- a/tests/fixtures/make-form/Property.php +++ b/tests/fixtures/make-form/Property.php @@ -18,4 +18,16 @@ class Property #[ORM\ManyToOne(inversedBy: 'properties')] #[ORM\JoinColumn(name: 'sour_food_id', referencedColumnName: 'id')] private ?SourFood $sourFood = null; + + public function setSourFood(?SourFood $sourFood): static + { + $this->sourFood = $sourFood; + + return $this; + } + + public function getSourFood(): ?SourFood + { + return $this->sourFood; + } } diff --git a/tests/fixtures/make-form/SourFood.php b/tests/fixtures/make-form/SourFood.php index fbc2ebb4b..eccaf553d 100644 --- a/tests/fixtures/make-form/SourFood.php +++ b/tests/fixtures/make-form/SourFood.php @@ -44,8 +44,10 @@ public function getTitle() /** * @param mixed $title */ - public function setTitle($title) + public function setTitle($title): static { $this->title = $title; + + return $this; } } diff --git a/tests/fixtures/make-form/relation_many_to_many/Book.php b/tests/fixtures/make-form/relation_many_to_many/Book.php new file mode 100644 index 000000000..740f1e635 --- /dev/null +++ b/tests/fixtures/make-form/relation_many_to_many/Book.php @@ -0,0 +1,54 @@ +libraries = new ArrayCollection(); + } + + public function getId() + { + return $this->id; + } + + public function getTitle(): string + { + return $this->title; + } + + public function setTitle(string $title): static + { + $this->title = $title; + + return $this; + } + + public function getLibraries(): Collection + { + return $this->libraries; + } + public function setLibraries(Collection $libraries): static + { + $this->libraries = $libraries; + return $this; + } +} diff --git a/tests/fixtures/make-form/relation_many_to_many/Library.php b/tests/fixtures/make-form/relation_many_to_many/Library.php new file mode 100644 index 000000000..d4ebf3695 --- /dev/null +++ b/tests/fixtures/make-form/relation_many_to_many/Library.php @@ -0,0 +1,53 @@ +books = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getBooks(): Collection + { + return $this->books; + } + + public function addBook(Book $book): static + { + $this->books->add($book); + return $this; + } + + public function getName(): ?string + { + return $this->name; + } + public function setName(?string $name): static + { + $this->name = $name; + return $this; + } +} diff --git a/tests/fixtures/make-form/relation_one_to_many/Author.php b/tests/fixtures/make-form/relation_one_to_many/Author.php new file mode 100644 index 000000000..294793b2f --- /dev/null +++ b/tests/fixtures/make-form/relation_one_to_many/Author.php @@ -0,0 +1,47 @@ +books = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getBooks(): Collection + { + return $this->books; + } + + public function getName(): ?string + { + return $this->name; + } + public function setName(?string $name): static + { + $this->name = $name; + return $this; + } +} diff --git a/tests/fixtures/make-form/relation_one_to_many/Book.php b/tests/fixtures/make-form/relation_one_to_many/Book.php new file mode 100644 index 000000000..2077e2231 --- /dev/null +++ b/tests/fixtures/make-form/relation_one_to_many/Book.php @@ -0,0 +1,47 @@ +id; + } + + public function getTitle(): string + { + return $this->title; + } + + public function setTitle(string $title): static + { + $this->title = $title; + + return $this; + } + + public function getAuthor(): ?Author + { + return $this->author; + } + public function setAuthor(?Author $author): static + { + $this->author = $author; + return $this; + } +} diff --git a/tests/fixtures/make-form/relation_one_to_one/Librarian.php b/tests/fixtures/make-form/relation_one_to_one/Librarian.php new file mode 100644 index 000000000..e60a8d4b6 --- /dev/null +++ b/tests/fixtures/make-form/relation_one_to_one/Librarian.php @@ -0,0 +1,47 @@ +id; + } + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): static + { + $this->name = $name; + + return $this; + } + + public function getLibrary(): Library + { + return $this->library; + } + public function setLibrary(Library $library): static + { + $this->library = $library; + return $this; + } +} diff --git a/tests/fixtures/make-form/relation_one_to_one/Library.php b/tests/fixtures/make-form/relation_one_to_one/Library.php new file mode 100644 index 000000000..77b16a8d9 --- /dev/null +++ b/tests/fixtures/make-form/relation_one_to_one/Library.php @@ -0,0 +1,46 @@ +id; + } + + public function getLibrarian(): Librarian + { + return $this->librarian; + } + + public function setLibrarian(Librarian $librarian): static + { + $this->librarian = $librarian; + return $this; + } + + public function getName(): ?string + { + return $this->name; + } + public function setName(?string $name): static + { + $this->name = $name; + return $this; + } +} diff --git a/tests/fixtures/make-form/tests/it_generates_form_with_many_to_many_relation.php b/tests/fixtures/make-form/tests/it_generates_form_with_many_to_many_relation.php new file mode 100644 index 000000000..6652306b8 --- /dev/null +++ b/tests/fixtures/make-form/tests/it_generates_form_with_many_to_many_relation.php @@ -0,0 +1,109 @@ +factory->create(BookType::class); + $form->submit($formData); + + $object = new Book(); + $object->setTitle('foobar'); + $object->setLibraries($collection); + $this->assertTrue($form->isSynchronized()); + $this->assertEquals($object, $form->getData()); + + $view = $form->createView(); + $children = $view->children; + + foreach (array_keys($formData) as $key) { + $this->assertArrayHasKey($key, $children); + } + } + + public function provideFormData(): iterable + { + yield 'test_submit_with_single_choice_selected' => + [ + [ + 'title' => 'foobar', + 'libraries' => [1], + ], + new ArrayCollection([ + (new Library())->setName('bar'), + ]), + ]; + yield ['test_submit_with_multiple_choices_selected' => + [ + 'title' => 'foobar', + 'libraries' => [0, 1], + ], + new ArrayCollection([ + (new Library())->setName('foo'), + (new Library())->setName('bar'), + ]), + ]; + yield ['test_submit_with_no_choice_selected' => + [ + 'title' => 'foobar', + 'libraries' => [], + ], + new ArrayCollection([]), + ]; + } + + protected function getExtensions(): array + { + $mockEntityManager = $this->createMock(EntityManager::class); + $mockEntityManager->method('getClassMetadata') + ->willReturnMap([ + [Book::class, new ClassMetadata(Book::class)], + [Library::class, new ClassMetadata(Library::class)], + ]); + + $execute = $this->createMock(AbstractQuery::class); + $execute->method('execute') + ->willReturn([ + (new Library())->setName('foo'), + (new Library())->setName('bar'), + ]); + + $query = $this->createMock(QueryBuilder::class); + $query->method('getQuery') + ->willReturn($execute); + + + $entityRepository = $this->createMock(EntityRepository::class); + $entityRepository->method('createQueryBuilder') + ->willReturn($query); + + $mockEntityManager->method('getRepository')->willReturn($entityRepository); + + $mockRegistry = $this->createMock(ManagerRegistry::class); + $mockRegistry->method('getManagerForClass') + ->willReturn($mockEntityManager); + $mockRegistry->method('getManagers') + ->willReturn([$mockEntityManager]); + + return array_merge(parent::getExtensions(), [new DoctrineOrmExtension($mockRegistry)]); + } +} diff --git a/tests/fixtures/make-form/tests/it_generates_form_with_many_to_one_relation.php b/tests/fixtures/make-form/tests/it_generates_form_with_many_to_one_relation.php new file mode 100644 index 000000000..49e5626c0 --- /dev/null +++ b/tests/fixtures/make-form/tests/it_generates_form_with_many_to_one_relation.php @@ -0,0 +1,84 @@ +setName('foo'); + $formData = [ + 'title' => 'bar', + 'author' => 0, + ]; + + $form = $this->factory->create(BookType::class); + $form->submit($formData); + + $object = new Book(); + $object->setTitle('bar'); + $object->setAuthor($author); + $this->assertTrue($form->isSynchronized()); + $this->assertEquals($object, $form->getData()); + + $view = $form->createView(); + $children = $view->children; + + foreach (array_keys($formData) as $key) { + $this->assertArrayHasKey($key, $children); + } + } + + protected function getExtensions(): array + { + $mockEntityManager = $this->createMock(EntityManager::class); + $mockEntityManager->method('getClassMetadata') + ->willReturnMap([ + [Book::class, new ClassMetadata(Book::class)], + [Author::class, new ClassMetadata(Author::class)], + ]) + ; + + $execute = $this->createMock(AbstractQuery::class); + $execute->method('execute') + ->willReturn([ + (new Author())->setName('foo') + ]); + + $query = $this->createMock(QueryBuilder::class); + $query->method('getQuery') + ->willReturn($execute); + + + $entityRepository = $this->createMock(EntityRepository::class); + $entityRepository->method('createQueryBuilder') + ->willReturn($query) + ; + + $mockEntityManager->method('getRepository')->willReturn($entityRepository); + + $mockRegistry = $this->createMock(ManagerRegistry::class); + $mockRegistry->method('getManagerForClass') + ->willReturn($mockEntityManager) + ; + $mockRegistry->method('getManagers') + ->willReturn([$mockEntityManager]) + ; + + return array_merge(parent::getExtensions(), [new DoctrineOrmExtension($mockRegistry)]); + } +} diff --git a/tests/fixtures/make-form/tests/it_generates_form_with_one_to_many_relation.php b/tests/fixtures/make-form/tests/it_generates_form_with_one_to_many_relation.php new file mode 100644 index 000000000..296f2e8d7 --- /dev/null +++ b/tests/fixtures/make-form/tests/it_generates_form_with_one_to_many_relation.php @@ -0,0 +1,35 @@ + 'foo', + ]; + + $form = $this->factory->create(AuthorType::class); + $form->submit($formData); + + $object = new Author(); + $object->setName('foo'); + $this->assertTrue($form->isSynchronized()); + $this->assertEquals($object, $form->getData()); + + $view = $form->createView(); + $children = $view->children; + + foreach (array_keys($formData) as $key) { + $this->assertArrayHasKey($key, $children); + } + $this->assertArrayNotHasKey('books', $children); + } +} diff --git a/tests/fixtures/make-form/tests/it_generates_form_with_one_to_one_relation.php b/tests/fixtures/make-form/tests/it_generates_form_with_one_to_one_relation.php new file mode 100644 index 000000000..ef5b6de47 --- /dev/null +++ b/tests/fixtures/make-form/tests/it_generates_form_with_one_to_one_relation.php @@ -0,0 +1,84 @@ +setName('foo'); + $formData = [ + 'name' => 'bar', + 'librarian' => 0, + ]; + + $form = $this->factory->create(LibraryType::class); + $form->submit($formData); + + $object = new Library(); + $object->setName('bar'); + $object->setLibrarian($librarian); + $this->assertTrue($form->isSynchronized()); + $this->assertEquals($object, $form->getData()); + + $view = $form->createView(); + $children = $view->children; + + foreach (array_keys($formData) as $key) { + $this->assertArrayHasKey($key, $children); + } + } + + protected function getExtensions(): array + { + $mockEntityManager = $this->createMock(EntityManager::class); + $mockEntityManager->method('getClassMetadata') + ->willReturnMap([ + [Library::class, new ClassMetadata(Library::class)], + [Librarian::class, new ClassMetadata(Librarian::class)], + ]) + ; + + $execute = $this->createMock(AbstractQuery::class); + $execute->method('execute') + ->willReturn([ + (new Librarian())->setName('foo') + ]); + + $query = $this->createMock(QueryBuilder::class); + $query->method('getQuery') + ->willReturn($execute); + + + $entityRepository = $this->createMock(EntityRepository::class); + $entityRepository->method('createQueryBuilder') + ->willReturn($query) + ; + + $mockEntityManager->method('getRepository')->willReturn($entityRepository); + + $mockRegistry = $this->createMock(ManagerRegistry::class); + $mockRegistry->method('getManagerForClass') + ->willReturn($mockEntityManager) + ; + $mockRegistry->method('getManagers') + ->willReturn([$mockEntityManager]) + ; + + return array_merge(parent::getExtensions(), [new DoctrineOrmExtension($mockRegistry)]); + } +}