diff --git a/src/Doctrine/DoctrineEntityDetails.php b/src/Doctrine/DoctrineEntityDetails.php new file mode 100644 index 000000000..03cb28276 --- /dev/null +++ b/src/Doctrine/DoctrineEntityDetails.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MakerBundle\Doctrine; + +/** + * @author Sadicov Vladimir + * + * @internal + */ +final class DoctrineEntityDetails +{ + private $repositoryClass; + + private $identifier; + private $displayFields; + private $formFields; + + public function __construct($repositoryClass, $identifier, $displayFields, $formFields) + { + $this->repositoryClass = $repositoryClass; + $this->identifier = $identifier; + $this->displayFields = $displayFields; + $this->formFields = $formFields; + } + + public function getRepositoryClass() + { + return $this->repositoryClass; + } + + public function getIdentifier() + { + return $this->identifier; + } + + public function getDisplayFields() + { + return $this->displayFields; + } + + public function getFormFields() + { + return $this->formFields; + } +} diff --git a/src/Doctrine/DoctrineEntityHelper.php b/src/Doctrine/DoctrineEntityHelper.php new file mode 100644 index 000000000..d15efb266 --- /dev/null +++ b/src/Doctrine/DoctrineEntityHelper.php @@ -0,0 +1,100 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MakerBundle\Doctrine; + +use Doctrine\Common\Persistence\ManagerRegistry; +use Doctrine\ORM\Mapping\ClassMetadataInfo; +use Symfony\Bundle\MakerBundle\Util\ClassNameDetails; + +/** + * @author Sadicov Vladimir + * + * @internal + */ +final class DoctrineEntityHelper +{ + private $metadataFactory; + + public function __construct(ManagerRegistry $registry = null) + { + $this->metadataFactory = null !== $registry ? new DoctrineMetadataFactory($registry) : null; + } + + private function isDoctrineInstalled(): bool + { + return null !== $this->metadataFactory; + } + + public function getEntitiesForAutocomplete(): array + { + $entities = []; + + if ($this->isDoctrineInstalled()) { + $allMetadata = $this->metadataFactory->getAllMetadata(); + /** @var ClassMetadataInfo $metadata */ + foreach ($allMetadata as $metadata) { + $entityClassDetails = new ClassNameDetails($metadata->name, 'App\\Entity'); + $entities[] = $entityClassDetails->getRelativeName(); + } + } + + return $entities; + } + + /** + * @param string $entityClassName + * + * @return null|DoctrineEntityDetails + * + * @throws \Exception + */ + public function createDoctrineDetails(string $entityClassName) + { + $metadata = $this->getEntityMetadata($entityClassName); + + if (null !== $metadata) { + return new DoctrineEntityDetails( + $metadata->customRepositoryClassName, + $metadata->identifier[0], + $metadata->fieldMappings, + $this->getFormFieldsFromEntity($metadata) + ); + } + + return null; + } + + public function getFormFieldsFromEntity(ClassMetadataInfo $metadata): array + { + $fields = (array) $metadata->fieldNames; + // Remove the primary key field if it's not managed manually + if (!$metadata->isIdentifierNatural()) { + $fields = array_diff($fields, $metadata->identifier); + } + foreach ($metadata->associationMappings as $fieldName => $relation) { + if (ClassMetadataInfo::ONE_TO_MANY !== $relation['type']) { + $fields[] = $fieldName; + } + } + + return $fields; + } + + public function getEntityMetadata($entityClassName) + { + if (null === $this->metadataFactory) { + throw new \Exception('Somehow the doctrine service is missing. Is DoctrineBundle installed?'); + } + + return $this->metadataFactory->getMetadataForClass($entityClassName); + } +} diff --git a/src/Doctrine/DoctrineMetadataFactory.php b/src/Doctrine/DoctrineMetadataFactory.php new file mode 100644 index 000000000..24fce23b6 --- /dev/null +++ b/src/Doctrine/DoctrineMetadataFactory.php @@ -0,0 +1,93 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MakerBundle\Doctrine; + +use Doctrine\Common\Persistence\ManagerRegistry; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Tools\DisconnectedClassMetadataFactory; + +/** + * Simpler version of DoctrineBundle's DisconnectedMetadataFactory, to + * avoid PSR-4 issues. + * + * @internal + * + * @author Fabien Potencier + * @author Ryan Weaver + */ +final class DoctrineMetadataFactory +{ + private $registry; + + /** + * Constructor. + * + * @param ManagerRegistry $registry A ManagerRegistry instance + */ + public function __construct(ManagerRegistry $registry) + { + $this->registry = $registry; + } + + /** + * @param string $namespace + * + * @return array|ClassMetadata[] + */ + public function getMetadataForNamespace($namespace) + { + $metadata = []; + foreach ($this->getAllMetadata() as $m) { + if (0 === strpos($m->name, $namespace)) { + $metadata[] = $m; + } + } + + return $metadata; + } + + /** + * @param string $entity + * + * @return ClassMetadata|null + */ + public function getMetadataForClass(string $entity) + { + foreach ($this->registry->getManagers() as $em) { + $cmf = new DisconnectedClassMetadataFactory(); + $cmf->setEntityManager($em); + + if (!$cmf->isTransient($entity)) { + return $cmf->getMetadataFor($entity); + } + } + + return null; + } + + /** + * @return array + */ + public function getAllMetadata() + { + $metadata = []; + foreach ($this->registry->getManagers() as $em) { + $cmf = new DisconnectedClassMetadataFactory(); + $cmf->setEntityManager($em); + foreach ($cmf->getAllMetadata() as $m) { + $metadata[] = $m; + } + } + + return $metadata; + } +} diff --git a/src/Generator.php b/src/Generator.php index 12a892d55..e48139797 100644 --- a/src/Generator.php +++ b/src/Generator.php @@ -21,12 +21,14 @@ class Generator { private $fileManager; + private $twigHelper; private $pendingOperations = []; private $namespacePrefix; public function __construct(FileManager $fileManager, $namespacePrefix) { $this->fileManager = $fileManager; + $this->twigHelper = new GeneratorTwigHelper($fileManager); $this->namespacePrefix = rtrim($namespacePrefix, '\\'); } @@ -60,6 +62,10 @@ public function generateClass(string $className, string $templateName, array $va */ public function generateFile(string $targetPath, string $templateName, array $variables) { + $variables = array_merge($variables, [ + 'helper' => $this->twigHelper, + ]); + $this->addOperation($targetPath, $templateName, $variables); } diff --git a/src/GeneratorTwigHelper.php b/src/GeneratorTwigHelper.php new file mode 100644 index 000000000..d22da8a42 --- /dev/null +++ b/src/GeneratorTwigHelper.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MakerBundle; + +/** + * @author Sadicov Vladimir + */ +final class GeneratorTwigHelper +{ + private $fileManager; + + public function __construct(FileManager $fileManager) + { + $this->fileManager = $fileManager; + } + + public function getEntityFieldPrintCode($entity, $field): string + { + $printCode = $entity.'.'.$field['fieldName']; + + switch ($field['type']) { + case 'datetime': + $printCode .= ' ? '.$printCode.'|date(\'Y-m-d H:i:s\') : \'\''; + break; + case 'date': + $printCode .= ' ? '.$printCode.'|date(\'Y-m-d\') : \'\''; + break; + case 'time': + $printCode .= ' ? '.$printCode.'|date(\'H:i:s\') : \'\''; + break; + case 'array': + $printCode .= ' ? '.$printCode.'|join(\', \') : \'\''; + break; + case 'boolean': + $printCode .= ' ? \'Yes\' : \'No\''; + break; + } + + return $printCode; + } + + public function getHeadPrintCode($title): string + { + if ($this->fileManager->fileExists('templates/base.html.twig')) { + return << + +$title + +HTML; + } +} diff --git a/src/Maker/MakeCrud.php b/src/Maker/MakeCrud.php new file mode 100644 index 000000000..2da7fa81b --- /dev/null +++ b/src/Maker/MakeCrud.php @@ -0,0 +1,245 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MakerBundle\Maker; + +use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; +use Doctrine\Common\Inflector\Inflector; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; +use Symfony\Bundle\MakerBundle\ConsoleStyle; +use Symfony\Bundle\MakerBundle\DependencyBuilder; +use Symfony\Bundle\MakerBundle\Doctrine\DoctrineEntityHelper; +use Symfony\Bundle\MakerBundle\Generator; +use Symfony\Bundle\MakerBundle\InputConfiguration; +use Symfony\Bundle\MakerBundle\Str; +use Symfony\Bundle\MakerBundle\Validator; +use Symfony\Bundle\TwigBundle\TwigBundle; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Question\Question; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Security\Csrf\CsrfTokenManager; +use Symfony\Component\Validator\Validation; + +/** + * @author Sadicov Vladimir + */ +final class MakeCrud extends AbstractMaker +{ + private $entityHelper; + + public function __construct(DoctrineEntityHelper $entityHelper) + { + $this->entityHelper = $entityHelper; + } + + public static function getCommandName(): string + { + return 'make:crud'; + } + + /** + * {@inheritdoc} + */ + public function configureCommand(Command $command, InputConfiguration $inputConfig) + { + $command + ->setDescription('Creates CRUD for Doctrine entity class') + ->addArgument('entity-class', InputArgument::OPTIONAL, sprintf('The class name of the entity to create CRUD (e.g. %s)', Str::asClassName(Str::getRandomTerm()))) + ->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeCrud.txt')) + ; + + $inputConfig->setArgumentAsNonInteractive('entity-class'); + } + + public function interact(InputInterface $input, ConsoleStyle $io, Command $command) + { + if (null === $input->getArgument('entity-class')) { + $argument = $command->getDefinition()->getArgument('entity-class'); + + $entities = $this->entityHelper->getEntitiesForAutocomplete(); + + $question = new Question($argument->getDescription()); + $question->setAutocompleterValues($entities); + + $value = $io->askQuestion($question); + + $input->setArgument('entity-class', $value); + } + } + + public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator) + { + $entityClassDetails = $generator->createClassNameDetails( + Validator::entityExists($input->getArgument('entity-class'), $this->entityHelper->getEntitiesForAutocomplete()), + 'Entity\\' + ); + + $entityDoctrineDetails = $this->entityHelper->createDoctrineDetails($entityClassDetails->getFullName()); + + $repositoryVars = []; + + if (null !== $entityDoctrineDetails->getRepositoryClass()) { + $repositoryClassDetails = $generator->createClassNameDetails( + '\\'.$entityDoctrineDetails->getRepositoryClass(), + 'Repository\\', + 'Repository' + ); + + $repositoryVars = [ + 'repository_full_class_name' => $repositoryClassDetails->getFullName(), + 'repository_class_name' => $repositoryClassDetails->getShortName(), + 'repository_var' => lcfirst(Inflector::singularize($repositoryClassDetails->getShortName())), + ]; + } + + $controllerClassDetails = $generator->createClassNameDetails( + $entityClassDetails->getRelativeNameWithoutSuffix(), + 'Controller\\', + 'Controller' + ); + + $iter = 0; + do { + $formClassDetails = $generator->createClassNameDetails( + $entityClassDetails->getRelativeNameWithoutSuffix().($iter ?: ''), + 'Form\\', + 'Type' + ); + ++$iter; + } while (class_exists($formClassDetails->getFullName())); + + $entityVarPlural = lcfirst(Inflector::pluralize($entityClassDetails->getShortName())); + $entityVarSingular = lcfirst(Inflector::singularize($entityClassDetails->getShortName())); + + $entityTwigVarPlural = Str::asTwigVariable($entityVarPlural); + $entityTwigVarSingular = Str::asTwigVariable($entityVarSingular); + + $routeName = Str::asRouteName($controllerClassDetails->getRelativeNameWithoutSuffix()); + + $generator->generateClass( + $controllerClassDetails->getFullName(), + 'crud/controller/Controller.tpl.php', + array_merge([ + 'entity_full_class_name' => $entityClassDetails->getFullName(), + 'entity_class_name' => $entityClassDetails->getShortName(), + 'form_full_class_name' => $formClassDetails->getFullName(), + 'form_class_name' => $formClassDetails->getShortName(), + 'route_path' => Str::asRoutePath($controllerClassDetails->getRelativeNameWithoutSuffix()), + 'route_name' => $routeName, + 'entity_var_plural' => $entityVarPlural, + 'entity_twig_var_plural' => $entityTwigVarPlural, + 'entity_var_singular' => $entityVarSingular, + 'entity_twig_var_singular' => $entityTwigVarSingular, + 'entity_identifier' => $entityDoctrineDetails->getIdentifier(), + ], + $repositoryVars + ) + ); + + $generator->generateClass( + $formClassDetails->getFullName(), + 'form/Type.tpl.php', + [ + 'bounded_full_class_name' => $entityClassDetails->getFullName(), + 'bounded_class_name' => $entityClassDetails->getShortName(), + 'form_fields' => $entityDoctrineDetails->getFormFields(), + ] + ); + + $templatesPath = Str::asFilePath($controllerClassDetails->getRelativeNameWithoutSuffix()); + + $templates = [ + '_delete_form' => [ + 'route_name' => $routeName, + 'entity_twig_var_singular' => $entityTwigVarSingular, + 'entity_identifier' => $entityDoctrineDetails->getIdentifier(), + ], + '_form' => [], + 'edit' => [ + 'entity_class_name' => $entityClassDetails->getShortName(), + 'entity_twig_var_singular' => $entityTwigVarSingular, + 'entity_identifier' => $entityDoctrineDetails->getIdentifier(), + 'route_name' => $routeName, + ], + 'index' => [ + 'entity_class_name' => $entityClassDetails->getShortName(), + 'entity_twig_var_plural' => $entityTwigVarPlural, + 'entity_twig_var_singular' => $entityTwigVarSingular, + 'entity_identifier' => $entityDoctrineDetails->getIdentifier(), + 'entity_fields' => $entityDoctrineDetails->getDisplayFields(), + 'route_name' => $routeName, + ], + 'new' => [ + 'entity_class_name' => $entityClassDetails->getShortName(), + 'route_name' => $routeName, + ], + 'show' => [ + 'entity_class_name' => $entityClassDetails->getShortName(), + 'entity_twig_var_singular' => $entityTwigVarSingular, + 'entity_identifier' => $entityDoctrineDetails->getIdentifier(), + 'entity_fields' => $entityDoctrineDetails->getDisplayFields(), + 'route_name' => $routeName, + ], + ]; + + foreach ($templates as $template => $variables) { + $generator->generateFile( + 'templates/'.$templatesPath.'/'.$template.'.html.twig', + 'crud/templates/'.$template.'.tpl.php', + $variables + ); + } + + $generator->writeChanges(); + + $this->writeSuccessMessage($io); + + $io->text(sprintf('Next: Check your new CRUD by going to %s/', Str::asRoutePath($controllerClassDetails->getRelativeNameWithoutSuffix()))); + } + + /** + * {@inheritdoc} + */ + public function configureDependencies(DependencyBuilder $dependencies) + { + $dependencies->addClassDependency( + Route::class, + 'annotations' + ); + + $dependencies->addClassDependency( + AbstractType::class, + 'form' + ); + + $dependencies->addClassDependency( + Validation::class, + 'validator' + ); + + $dependencies->addClassDependency( + TwigBundle::class, + 'twig-bundle' + ); + + $dependencies->addClassDependency( + DoctrineBundle::class, + 'orm-pack' + ); + + $dependencies->addClassDependency( + CsrfTokenManager::class, + 'security-csrf' + ); + } +} diff --git a/src/Maker/MakeForm.php b/src/Maker/MakeForm.php index c0f1c7c38..8d85bb801 100644 --- a/src/Maker/MakeForm.php +++ b/src/Maker/MakeForm.php @@ -11,8 +11,10 @@ namespace Symfony\Bundle\MakerBundle\Maker; +use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; use Symfony\Bundle\MakerBundle\ConsoleStyle; use Symfony\Bundle\MakerBundle\DependencyBuilder; +use Symfony\Bundle\MakerBundle\Doctrine\DoctrineEntityHelper; use Symfony\Bundle\MakerBundle\Generator; use Symfony\Bundle\MakerBundle\InputConfiguration; use Symfony\Bundle\MakerBundle\Str; @@ -20,6 +22,7 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Question\Question; use Symfony\Component\Form\AbstractType; use Symfony\Component\Validator\Validation; @@ -29,6 +32,13 @@ */ final class MakeForm extends AbstractMaker { + private $entityHelper; + + public function __construct(DoctrineEntityHelper $entityHelper) + { + $this->entityHelper = $entityHelper; + } + public static function getCommandName(): string { return 'make:form'; @@ -39,8 +49,27 @@ public function configureCommand(Command $command, InputConfiguration $inputConf $command ->setDescription('Creates a new form class') ->addArgument('name', InputArgument::OPTIONAL, sprintf('The name of the form class (e.g. %sType)', Str::asClassName(Str::getRandomTerm()))) + ->addArgument('bound-class', InputArgument::OPTIONAL, 'The name of Entity or custom model class that the new form will be bound to (empty for none)') ->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeForm.txt')) ; + + $inputConf->setArgumentAsNonInteractive('bound-class'); + } + + public function interact(InputInterface $input, ConsoleStyle $io, Command $command) + { + if (null === $input->getArgument('bound-class')) { + $argument = $command->getDefinition()->getArgument('bound-class'); + + $entities = $this->entityHelper->getEntitiesForAutocomplete(); + + $question = new Question($argument->getDescription()); + $question->setValidator(function ($answer) use ($entities) {return Validator::existsOrNull($answer, $entities); }); + $question->setAutocompleterValues($entities); + $question->setMaxAttempts(3); + + $input->setArgument('bound-class', $io->askQuestion($question)); + } } public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator) @@ -51,19 +80,33 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen 'Type' ); - $entityClassNameDetails = $generator->createClassNameDetails( - $formClassNameDetails->getRelativeNameWithoutSuffix(), - 'Entity\\' - ); + $formFields = ['field_name']; + $boundClassVars = []; + + $boundClass = $input->getArgument('bound-class'); + + if (null !== $boundClass) { + $boundClassDetails = $generator->createClassNameDetails( + $boundClass, + 'Entity\\' + ); + + $doctrineEntityDetails = $this->entityHelper->createDoctrineDetails($boundClassDetails->getFullName()); + + if (null !== $doctrineEntityDetails) { + $formFields = $doctrineEntityDetails->getFormFields(); + } + + $boundClassVars = [ + 'bounded_full_class_name' => $boundClassDetails->getFullName(), + 'bounded_class_name' => $boundClassDetails->getShortName(), + ]; + } $generator->generateClass( $formClassNameDetails->getFullName(), 'form/Type.tpl.php', - [ - 'entity_class_exists' => class_exists($entityClassNameDetails->getFullName()), - 'entity_full_class_name' => $entityClassNameDetails->getFullName(), - 'entity_class_name' => $entityClassNameDetails->getShortName(), - ] + array_merge(['form_fields' => $formFields], $boundClassVars) ); $generator->writeChanges(); @@ -90,5 +133,11 @@ public function configureDependencies(DependencyBuilder $dependencies) // add as an optional dependency: the user *probably* wants validation false ); + + $dependencies->addClassDependency( + DoctrineBundle::class, + 'orm', + false + ); } } diff --git a/src/Resources/config/makers.xml b/src/Resources/config/makers.xml index 13bded020..08497436e 100644 --- a/src/Resources/config/makers.xml +++ b/src/Resources/config/makers.xml @@ -20,6 +20,11 @@ + + + + + @@ -29,6 +34,7 @@ + diff --git a/src/Resources/config/services.xml b/src/Resources/config/services.xml index 7ccde453c..e6603bfcb 100644 --- a/src/Resources/config/services.xml +++ b/src/Resources/config/services.xml @@ -19,5 +19,9 @@ + + + + diff --git a/src/Resources/help/MakeCrud.txt b/src/Resources/help/MakeCrud.txt new file mode 100644 index 000000000..9ec8d05bc --- /dev/null +++ b/src/Resources/help/MakeCrud.txt @@ -0,0 +1,5 @@ +The %command.name% command generates crud controller with templates for selected entity. + +php %command.full_name% BlogPost + +If the argument is missing, the command will ask for the entity class name interactively. \ No newline at end of file diff --git a/src/Resources/skeleton/crud/controller/Controller.tpl.php b/src/Resources/skeleton/crud/controller/Controller.tpl.php new file mode 100644 index 000000000..3fb128a24 --- /dev/null +++ b/src/Resources/skeleton/crud/controller/Controller.tpl.php @@ -0,0 +1,105 @@ + + +namespace ; + +use ; +use ; + +use ; + +use Symfony\Bundle\FrameworkBundle\Controller\Controller; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Annotation\Route; + +/** + * @Route("") + */ +class extends Controller +{ + /** + * @Route("/", name="_index", methods="GET") + */ + + public function index( $): Response + { + return $this->render('/index.html.twig', ['' => $->findAll()]); + } + + public function index(): Response + { + $ = $this->getDoctrine() + ->getRepository(::class) + ->findAll(); + + return $this->render('/index.html.twig', ['' => $]); + } + + + /** + * @Route("/new", name="_new", methods="GET|POST") + */ + public function new(Request $request): Response + { + $ = new (); + $form = $this->createForm(::class, $); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $em = $this->getDoctrine()->getManager(); + $em->persist($); + $em->flush(); + + return $this->redirectToRoute('_index'); + } + + return $this->render('/new.html.twig', [ + '' => $, + 'form' => $form->createView(), + ]); + } + + /** + * @Route("/{}", name="_show", methods="GET") + */ + public function show( $): Response + { + return $this->render('/show.html.twig', ['' => $]); + } + + /** + * @Route("/{}/edit", name="_edit", methods="GET|POST") + */ + public function edit(Request $request, $): Response + { + $form = $this->createForm(::class, $); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $this->getDoctrine()->getManager()->flush(); + + return $this->redirectToRoute('_edit', ['' => $->get()]); + } + + return $this->render('/edit.html.twig', [ + '' => $, + 'form' => $form->createView(), + ]); + } + + /** + * @Route("/{}", name="_delete", methods="DELETE") + */ + public function delete(Request $request, $): Response + { + if (!$this->isCsrfTokenValid('delete'.$->get(), $request->request->get('_token'))) { + return $this->redirectToRoute('_index'); + } + + $em = $this->getDoctrine()->getManager(); + $em->remove($); + $em->flush(); + + return $this->redirectToRoute('_index'); + } +} diff --git a/src/Resources/skeleton/crud/templates/_delete_form.tpl.php b/src/Resources/skeleton/crud/templates/_delete_form.tpl.php new file mode 100644 index 000000000..b4f075c50 --- /dev/null +++ b/src/Resources/skeleton/crud/templates/_delete_form.tpl.php @@ -0,0 +1,5 @@ +
+ + + +
\ No newline at end of file diff --git a/src/Resources/skeleton/crud/templates/_form.tpl.php b/src/Resources/skeleton/crud/templates/_form.tpl.php new file mode 100644 index 000000000..92948ade0 --- /dev/null +++ b/src/Resources/skeleton/crud/templates/_form.tpl.php @@ -0,0 +1,4 @@ +{{ form_start(form) }} + {{ form_widget(form) }} + +{{ form_end(form) }} \ No newline at end of file diff --git a/src/Resources/skeleton/crud/templates/edit.tpl.php b/src/Resources/skeleton/crud/templates/edit.tpl.php new file mode 100644 index 000000000..073bd26f5 --- /dev/null +++ b/src/Resources/skeleton/crud/templates/edit.tpl.php @@ -0,0 +1,11 @@ +getHeadPrintCode('Edit '.$entity_class_name) ?> + +{% block body %} +

Edit

+ + {{ include('/_form.html.twig', {'button_label': 'Update'}) }} + + back to list + + {{ include('/_delete_form.html.twig') }} +{% endblock %} \ No newline at end of file diff --git a/src/Resources/skeleton/crud/templates/index.tpl.php b/src/Resources/skeleton/crud/templates/index.tpl.php new file mode 100644 index 000000000..0ffd18cac --- /dev/null +++ b/src/Resources/skeleton/crud/templates/index.tpl.php @@ -0,0 +1,35 @@ +getHeadPrintCode($entity_class_name.' index'); ?> + +{% block body %} +

index

+ + + + + + + + + + + + {% for in %} + + + + + + + {% else %} + + + + {% endfor %} + +
actions
{{ getEntityFieldPrintCode($entity_twig_var_singular, $field) ?> }} + show + edit +
no records found
+ + Create new +{% endblock %} \ No newline at end of file diff --git a/src/Resources/skeleton/crud/templates/new.tpl.php b/src/Resources/skeleton/crud/templates/new.tpl.php new file mode 100644 index 000000000..92b7d0a0e --- /dev/null +++ b/src/Resources/skeleton/crud/templates/new.tpl.php @@ -0,0 +1,9 @@ +getHeadPrintCode('New '.$entity_class_name) ?> + +{% block body %} +

Create new

+ + {{ include('/_form.html.twig') }} + + back to list +{% endblock %} \ No newline at end of file diff --git a/src/Resources/skeleton/crud/templates/show.tpl.php b/src/Resources/skeleton/crud/templates/show.tpl.php new file mode 100644 index 000000000..199e85e33 --- /dev/null +++ b/src/Resources/skeleton/crud/templates/show.tpl.php @@ -0,0 +1,22 @@ +getHeadPrintCode($entity_class_name) ?> + +{% block body %} +

+ + + + + + + + + + +
{{ getEntityFieldPrintCode($entity_twig_var_singular, $field) ?> }}
+ + back to list + + edit + + {{ include('/_delete_form.html.twig') }} +{% endblock %} \ No newline at end of file diff --git a/src/Resources/skeleton/form/Type.tpl.php b/src/Resources/skeleton/form/Type.tpl.php index 716f5ecb0..1d85ec8fa 100644 --- a/src/Resources/skeleton/form/Type.tpl.php +++ b/src/Resources/skeleton/form/Type.tpl.php @@ -1,10 +1,10 @@ -namespace ; +namespace ; - -use ; - + +use ; + use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -14,17 +14,20 @@ class extends AbstractType public function buildForm(FormBuilderInterface $builder, array $options) { $builder - ->add('field_name') + + ->add('') + ; } public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults([ - - // uncomment if you want to bind to a class - //'data_class' => ::class, - + + 'data_class' => ::class, + + // Configure your form options here + ]); } } diff --git a/src/Test/MakerTestCase.php b/src/Test/MakerTestCase.php index 66b5209bd..4dd13b016 100644 --- a/src/Test/MakerTestCase.php +++ b/src/Test/MakerTestCase.php @@ -288,4 +288,4 @@ private function processReplacements(array $replacements, $rootDir) file_put_contents($path, str_replace($replacement['find'], $replacement['replace'], $contents)); } } -} \ No newline at end of file +} diff --git a/src/Validator.php b/src/Validator.php index 443f12f9b..7ffec5e1e 100644 --- a/src/Validator.php +++ b/src/Validator.php @@ -46,4 +46,51 @@ public static function notBlank(string $value = null): string return $value; } + + public static function existsOrNull(string $className = null, array $entites = []) + { + if (null !== $className) { + self::validateClassName($className); + + if (0 === strpos($className, '\\')) { + self::classExists($className); + } else { + self::entityExists($className, $entites); + } + } + + return $className; + } + + public static function classExists(string $className, string $errorMessage = ''): string + { + self::notBlank($className); + + if (!class_exists($className)) { + $errorMessage = $errorMessage ?: sprintf('Class "%s" does\'t exists. Please enter existing full class name', $className); + + throw new RuntimeCommandException($errorMessage); + } + + return $className; + } + + public static function entityExists(string $className = null, array $entites = []): string + { + self::notBlank($className); + + if (empty($entites)) { + throw new RuntimeCommandException(sprintf('There is no registered entites. Please create entity before use this command', $className)); + } + + if (0 === strpos($className, '\\')) { + self::classExists($className, sprintf('Entity "%s" does\'t exists. Please enter existing one or create new', $className)); + } + + if (!in_array($className, $entites)) { + throw new RuntimeCommandException(sprintf('Entity "%s" does\'t exists. Please enter existing one or create new', $className)); + } + + return $className; + } } diff --git a/tests/Maker/FunctionalTest.php b/tests/Maker/FunctionalTest.php index 6e77b0c84..b6ba4c0cb 100644 --- a/tests/Maker/FunctionalTest.php +++ b/tests/Maker/FunctionalTest.php @@ -11,6 +11,7 @@ use Symfony\Bundle\MakerBundle\Maker\MakeAuthenticator; use Symfony\Bundle\MakerBundle\Maker\MakeCommand; use Symfony\Bundle\MakerBundle\Maker\MakeController; +use Symfony\Bundle\MakerBundle\Maker\MakeCrud; use Symfony\Bundle\MakerBundle\Maker\MakeEntity; use Symfony\Bundle\MakerBundle\Maker\MakeFixtures; use Symfony\Bundle\MakerBundle\Maker\MakeForm; @@ -167,12 +168,25 @@ public function getCommandTests() }) ]; - yield 'form' => [MakerTestDetails::createTest( + yield 'form_basic' => [MakerTestDetails::createTest( $this->getMakerInstance(MakeForm::class), [ // form name 'FooBar', + '', ]) + ->setFixtureFilesPath(__DIR__.'/../fixtures/MakeForm') + ]; + + yield 'form_with_entity' => [MakerTestDetails::createTest( + $this->getMakerInstance(MakeForm::class), + [ + // Entity name + 'SourFoodType', + 'SourFood', + ]) + ->addExtraDependencies('orm') + ->setFixtureFilesPath(__DIR__.'/../fixtures/MakeFormForEntity') ]; yield 'functional' => [MakerTestDetails::createTest( @@ -300,6 +314,65 @@ public function getCommandTests() $this->assertContains('No database changes were detected', $output); }) ]; + + yield 'crud_basic' => [MakerTestDetails::createTest( + $this->getMakerInstance(MakeCrud::class), + [ + // entity class name + 'SweetFood', + ]) + ->setFixtureFilesPath(__DIR__.'/../fixtures/MakeCrud') + // need for crud web tests + ->addExtraDependencies('symfony/css-selector') + ->addReplacement( + 'phpunit.xml.dist', + 'mysql://db_user:db_password@127.0.0.1:3306/db_name', + 'sqlite:///%kernel.project_dir%/var/app.db' + ) + ->addReplacement( + '.env', + 'mysql://db_user:db_password@127.0.0.1:3306/db_name', + 'sqlite:///%kernel.project_dir%/var/app.db' + ) + ->addPreMakeCommand('php bin/console doctrine:schema:create --env=test') + ->assert(function(string $output, string $directory) { + $this->assertFileExists($directory.'/src/Controller/SweetFoodController.php'); + $this->assertFileExists($directory.'/src/Form/SweetFoodType.php'); + + $this->assertContains('created: src/Controller/SweetFoodController.php', $output); + $this->assertContains('created: src/Form/SweetFoodType.php', $output); + }) + ]; + + yield 'crud_with_no_base' => [MakerTestDetails::createTest( + $this->getMakerInstance(MakeCrud::class), + [ + // entity class name + 'SweetFood', + ]) + ->setFixtureFilesPath(__DIR__.'/../fixtures/MakeCrud') + // need for crud web tests + ->addExtraDependencies('symfony/css-selector') + ->addReplacement( + 'phpunit.xml.dist', + 'mysql://db_user:db_password@127.0.0.1:3306/db_name', + 'sqlite:///%kernel.project_dir%/var/app.db' + ) + ->addReplacement( + '.env', + 'mysql://db_user:db_password@127.0.0.1:3306/db_name', + 'sqlite:///%kernel.project_dir%/var/app.db' + ) + ->addPreMakeCommand('php bin/console doctrine:schema:create --env=test') + ->addPreMakeCommand('rm templates/base.html.twig') + ->assert(function(string $output, string $directory) { + $this->assertFileExists($directory.'/src/Controller/SweetFoodController.php'); + $this->assertFileExists($directory.'/src/Form/SweetFoodType.php'); + + $this->assertContains('created: src/Controller/SweetFoodController.php', $output); + $this->assertContains('created: src/Form/SweetFoodType.php', $output); + }) + ]; } /** diff --git a/tests/fixtures/MakeCrud/src/Entity/SweetFood.php b/tests/fixtures/MakeCrud/src/Entity/SweetFood.php new file mode 100644 index 000000000..66590eaf7 --- /dev/null +++ b/tests/fixtures/MakeCrud/src/Entity/SweetFood.php @@ -0,0 +1,44 @@ +id; + } + + /** + * @return mixed + */ + public function getTitle() + { + return $this->title; + } + + /** + * @param mixed $title + */ + public function setTitle($title) + { + $this->title = $title; + } +} diff --git a/tests/fixtures/MakeCrud/tests/GeneratedCrudControllerTest.php b/tests/fixtures/MakeCrud/tests/GeneratedCrudControllerTest.php new file mode 100644 index 000000000..0b94c6b40 --- /dev/null +++ b/tests/fixtures/MakeCrud/tests/GeneratedCrudControllerTest.php @@ -0,0 +1,77 @@ +request('GET', '/sweet/food/'); + $this->assertTrue($client->getResponse()->isSuccessful()); + $this->assertContains('', $client->getResponse()->getContent()); + $this->assertContains('SweetFood index', $client->getResponse()->getContent()); + + $newLink = $crawler->filter('a:contains("Create new")')->eq(0)->link(); + + $crawler = $client->click($newLink); + $this->assertTrue($client->getResponse()->isSuccessful()); + $this->assertContains('', $client->getResponse()->getContent()); + $this->assertContains('New SweetFood', $client->getResponse()->getContent()); + + $newForm = $crawler->selectButton('Save')->form(); + $client->submit($newForm, ['sweet_food[title]' => 'Candy']); + $this->assertTrue($client->getResponse()->isRedirect()); + + $crawler = $client->followRedirect(); + $this->assertTrue($client->getResponse()->isSuccessful()); + $this->assertContains('', $client->getResponse()->getContent()); + $this->assertContains('SweetFood index', $client->getResponse()->getContent()); + $this->assertContains('Candy', $client->getResponse()->getContent()); + + $editLink = $crawler->filter('a:contains("edit")')->eq(0)->link(); + $crawler = $client->click($editLink); + $this->assertContains('', $client->getResponse()->getContent()); + $this->assertContains('Edit SweetFood', $client->getResponse()->getContent()); + $this->assertGreaterThan(0, $crawler->filter('input[type=text]')->count()); + + $editForm = $crawler->selectButton('Update')->form(); + $client->submit($editForm, ['sweet_food[title]' => 'Candy edited']); + $this->assertTrue($client->getResponse()->isRedirect()); + + $crawler = $client->followRedirect(); + $this->assertTrue($client->getResponse()->isSuccessful()); + $this->assertContains('', $client->getResponse()->getContent()); + $this->assertContains('Edit SweetFood', $client->getResponse()->getContent()); + $this->assertGreaterThan(0, $crawler->filter('input[type=text]')->count()); + $this->assertEquals('Candy edited', $crawler->filter('input[type=text]')->attr('value')); + + $backTolistLink = $crawler->filter('a:contains("back to list")')->eq(0)->link(); + + $crawler = $client->click($backTolistLink); + $this->assertTrue($client->getResponse()->isSuccessful()); + $this->assertContains('', $client->getResponse()->getContent()); + $this->assertContains('SweetFood index', $client->getResponse()->getContent()); + $this->assertContains('Candy edited', $client->getResponse()->getContent()); + + $showLink = $crawler->filter('a:contains("show")')->eq(0)->link(); + + $crawler = $client->click($showLink); + $this->assertTrue($client->getResponse()->isSuccessful()); + $this->assertContains('', $client->getResponse()->getContent()); + $this->assertContains('SweetFood', $client->getResponse()->getContent()); + $this->assertContains('Candy edited', $client->getResponse()->getContent()); + + $deleteForm = $crawler->selectButton('Delete')->form(); + $client->submit($deleteForm); + $this->assertTrue($client->getResponse()->isRedirect()); + + $client->followRedirect(); + $this->assertTrue($client->getResponse()->isSuccessful()); + $this->assertContains('', $client->getResponse()->getContent()); + $this->assertContains('SweetFood index', $client->getResponse()->getContent()); + $this->assertContains('no records found', $client->getResponse()->getContent()); + } +} diff --git a/tests/fixtures/MakeForm/tests/GeneratedFormTest.php b/tests/fixtures/MakeForm/tests/GeneratedFormTest.php new file mode 100644 index 000000000..0e9881d57 --- /dev/null +++ b/tests/fixtures/MakeForm/tests/GeneratedFormTest.php @@ -0,0 +1,29 @@ + 'field_value' + ]; + + $form = $this->factory->create(FooBarType::class); + $form->submit($formData); + + $this->assertTrue($form->isSynchronized()); + $this->assertEquals($formData, $form->getData()); + + $view = $form->createView(); + $children = $view->children; + + foreach (array_keys($formData) as $key) { + $this->assertArrayHasKey($key, $children); + } + } +} diff --git a/tests/fixtures/MakeFormForEntity/src/Entity/SourFood.php b/tests/fixtures/MakeFormForEntity/src/Entity/SourFood.php new file mode 100644 index 000000000..dd78930fa --- /dev/null +++ b/tests/fixtures/MakeFormForEntity/src/Entity/SourFood.php @@ -0,0 +1,47 @@ +id; + } + + /** + * @return mixed + */ + public function getTitle() + { + return $this->title; + } + + /** + * @param mixed $title + */ + public function setTitle($title) + { + $this->title = $title; + } +} diff --git a/tests/fixtures/MakeFormForEntity/tests/GeneratedFormTest.php b/tests/fixtures/MakeFormForEntity/tests/GeneratedFormTest.php new file mode 100644 index 000000000..5ccb84de9 --- /dev/null +++ b/tests/fixtures/MakeFormForEntity/tests/GeneratedFormTest.php @@ -0,0 +1,33 @@ + 'lemon' + ]; + + $form = $this->factory->create(SourFoodType::class); + $form->submit($formData); + + $object = new SourFood(); + $object->setTitle('lemon'); + + $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); + } + } +}