* * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ namespace Chill\MainBundle\CRUD\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Doctrine\ORM\QueryBuilder; use Chill\MainBundle\Pagination\PaginatorFactory; use Symfony\Component\Form\FormInterface; use Symfony\Component\Translation\TranslatorInterface; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Chill\MainBundle\Security\Authorization\AuthorizationHelper; use Symfony\Component\Security\Core\Role\Role; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Chill\MainBundle\CRUD\Resolver\Resolver; /** * * */ class CRUDController extends AbstractController { /** * * @var PaginatorFactory */ protected $paginatorFactory; /** * The crud configuration * * This configuration si defined by `chill_main['crud']`. * * @var array */ protected $crudConfig; public function setCrudConfig(array $config) { $this->crudConfig = $config; } protected function getDefaultOrdering(): array { return $this->orderingOptions(); } protected function processTemplateParameters($action): array { $configured = $this->getTemplateParameters($action); switch($action) { case 'index': $default = [ 'columns' => $this->getDoctrine()->getManager() ->getClassMetadata($this->getEntity()) ->getIdentifierFieldNames(), 'actions' => ['edit', 'delete'] ]; break; default: throw new \LogicException("this action is not supported: $action"); } $result = \array_merge($default, $configured); // add constants $result['class'] = $this->getEntity(); return $result; } protected function orderQuery(QueryBuilder $query, Request $request): QueryBuilder { $defaultOrdering = $this->getDefaultOrdering(); foreach ($defaultOrdering as $sort => $order) { $query->addOrderBy('e.'.$sort, $order); } return $query; } public function index(Request $request) { $totalItems = $this->getDoctrine()->getManager() ->createQuery("SELECT COUNT(e) FROM ".$this->getEntity()." e") ->getSingleScalarResult() ; $query = $this->getDoctrine()->getManager() ->createQueryBuilder() ->select('e') ->from($this->getEntity(), 'e'); $this->orderQuery($query, $request); $paginator = $this->paginatorFactory->create($totalItems); $query->setFirstResult($paginator->getCurrentPage()->getFirstItemNumber()) ->setMaxResults($paginator->getItemsPerPage()) ; $entities = $query->getQuery()->getResult(); return $this->render($this->getTemplate('index'), \array_merge([ 'entities' => $entities, ], $this->processTemplateParameters('index')) ); } public function edit(Request $request, $id): Response { return $this->formEditAction('edit', $request, $id); } public function new(Request $request): Response { return $this->formCreateAction('new', $request); } public function view(Request $request, $id): Response { return $this->viewAction('view', $request, $id); } protected function viewAction($action, Request $request, $id) { $entity = $this->getEntity($action, $id, $request); $postFetch = $this->onPostFetchEntity($action, $request, $entity); if ($postFetch instanceof Response) { return $postFetch; } $this->checkACL($action, $entity); $postCheckACL = $this->onPostCheckACL($action, $request, $entity); if ($postCheckACL instanceof Response) { return $postCheckACL; } $defaultTemplateParameters = [ 'entity' => $entity, 'crud_name' => $this->getCrudName() ]; return $this->render( $this->getTemplateFor($action, $entity, $request), $this->generateTemplateParameter($action, $entity, $request, $defaultTemplateParameters) ); } protected function formEditAction($action, Request $request, $id, $formClass = null, $formOptions = []): Response { $entity = $this->getEntity($action, $id, $request); if (NULL === $entity) { throw $this->createNotFoundException(sprintf("The %s with id %s " . "is not found"), $this->getCrudName(), $id); } $this->checkACL($action, $entity); $form = $this->createFormFor($action, $entity, $formClass, $formOptions); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { $this->onFormValid($entity, $form, $request); $em = $this->getDoctrine()->getManager(); $this->onPreFlush($action, $entity, $form, $request); $em->flush(); $this->onPostFlush($action, $entity, $form, $request); $this->addFlash('success', $this->generateFormSuccessMessage($action, $entity)); $result = $this->onBeforeRedirectAfterSubmission($action, $entity, $form, $request); if ($result instanceof Response) { return $result; } return $this->redirectToRoute('chill_crud_'.$this->getCrudName().'_index'); } elseif ($form->isSubmitted()) { $this->addFlash('error', $this->generateFormErrorMessage($action, $form)); } $defaultTemplateParameters = [ 'form' => $form->createView(), 'entity' => $entity, 'crud_name' => $this->getCrudName() ]; return $this->render( $this->getTemplateFor($action, $entity, $request), $this->generateTemplateParameter($action, $entity, $request, $defaultTemplateParameters) ); } protected function formCreateAction($action, Request $request, $formClass = null): Response { if ($request->query->has('duplicate')) { $entity = $this->duplicateEntity($action, $request); } else { $entity = $this->createEntity($action, $request); } $this->checkACL($action, $entity); $form = $this->createFormFor($action, $entity, $formClass); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { $this->onFormValid($entity, $form, $request); $em = $this->getDoctrine()->getManager(); $this->onPrePersist($action, $entity, $form, $request); $em->persist($entity); $this->onPostPersist($action, $entity, $form, $request); $this->onPreFlush($action, $entity, $form, $request); $em->flush(); $this->onPostFlush($action, $entity, $form, $request); $this->getPaginatorFactory(); $this->addFlash('success', $this->generateFormSuccessMessage($action, $entity)); $result = $this->onBeforeRedirectAfterSubmission($action, $entity, $form, $request); if ($result instanceof Response) { return $result; } return $this->redirectToRoute('chill_crud_'.$this->getCrudName().'_view', ['id' => $entity->getId()]); } elseif ($form->isSubmitted()) { $this->addFlash('error', $this->generateFormErrorMessage($action, $form)); } $defaultTemplateParameters = [ 'form' => $form->createView(), 'entity' => $entity, 'crud_name' => $this->getCrudName() ]; return $this->render( $this->getTemplateFor($action, $entity, $request), $this->generateTemplateParameter($action, $entity, $request, $defaultTemplateParameters) ); } /** * get the instance of the entity with the given id * * @param string $id * @return object */ protected function getEntity($action, $id, Request $request): ?object { return $this->getDoctrine() ->getRepository($this->getEntityClass()) ->find($id); } protected function duplicateEntity(string $action, Request $request) { $id = $request->query->get('duplicate_id', 0); $originalEntity = $this->getEntity($action, $id, $request); $this->getDoctrine()->getManager() ->detach($originalEntity); return $originalEntity; } protected function getEntityClass(): string { return $this->crudConfig['class']; } protected function getCrudName(): string { return $this->crudConfig['name']; } protected function checkACL($action, $entity) { $this->denyAccessUnlessGranted($this->getRoleFor($action), $entity); } protected function getRoleFor($action) { if (NULL !== ($this->getActionConfig($action)['role'])) { return $this->getActionConfig($action)['role']; } return $this->buildDefaultRole($action); } protected function buildDefaultRole($action) { return $this->getCrudResolver()->buildDefaultRole($this->getCrudName(), $action); } protected function getFormClassFor($action) { return $this->crudConfig[$action]['form_class'] ?? $this->crudConfig['form_class']; } protected function createFormFor($action, $entity, $formClass = null, $formOptions = []) { $formClass = $formClass ?? $this->getFormClassFor($action); $form = $this->createForm($formClass, $entity, $formOptions); $this->customizeForm($action, $form); return $form; } protected function customizeForm($action, FormInterface $form) { } protected function generateLabelForButton($action, $formName, $form) { return sprintf("crud.%s.button_action_form", $action); } protected function generateFormErrorMessage($action, FormInterface $form): string { $msg = 'This form contains errors'; return $this->getTranslator()->trans($msg); } protected function generateFormSuccessMessage($action, $entity): string { switch ($action) { case 'edit': $msg = "crud.edit.success"; break; case 'new': $msg = "crud.new.success"; break; default: $msg = "crud.default.success"; } return $this->getTranslator()->trans($msg); } protected function generateTemplateParameter( $action, $entity, Request $request, array $defaultTemplateParameters = [] ) { return $defaultTemplateParameters; } protected function createEntity($action, Request $request): object { $type = $this->getEntityClass(); return new $type; } protected function getTemplateFor($action, $entity, Request $request) { if ($this->hasCustomTemplate($action, $entity, $request)) { return $this->getActionConfig($action)['template']; } switch ($action) { case 'new': return '@ChillMain/CRUD/new.html.twig'; case 'edit': return '@ChillMain/CRUD/edit.html.twig'; case 'index': return '@ChillMain/CRUD/index.html.twig'; case 'view': return '@ChillMain/CRUD/view.html.twig'; default: throw new \LogicException("the view for action $action is not " . "defined. You should override ".__METHOD__." to add this " . "action"); } } protected function hasCustomTemplate($action, $entity, Request $request): bool { return !empty($this->getActionConfig($action)['template']); } protected function onPreFlush(string $action, $entity, FormInterface $form, Request $request) { } protected function onPostFlush(string $action, $entity, FormInterface $form, Request $request) { } protected function onPrePersist(string $action, $entity, FormInterface $form, Request $request) { } protected function onPostPersist(string $action, $entity, FormInterface $form, Request $request) { } protected function onPostFetchEntity($action, Request $request, $entity): ?Response { return null; } protected function onPostCheckACL($action, Request $request, $entity): ?Response { return null; } protected function onFormValid(object $entity, FormInterface $form, Request $request) { } protected function onBeforeRedirectAfterSubmission(string $action, $entity, FormInterface $form, Request $request) { $next = $request->request->get("submit", "save-and-close"); switch ($next) { case "save-and-close": return $this->redirectToRoute('chill_crud_'.$this->getCrudName().'_index'); case "save-and-new": return $this->redirectToRoute('chill_crud_'.$this->getCrudName().'_new'); default: return $this->redirectToRoute('chill_crud_'.$this->getCrudName().'_view', [ 'id' => $entity->getId() ]); } } protected function getActionConfig(string $action) { return $this->crudConfig['actions'][$action]; } protected function getPaginatorFactory(): PaginatorFactory { return $this->get(PaginatorFactory::class); } protected function getTranslator(): TranslatorInterface { return $this->container->get('translator'); } protected function getAuthorizationHelper(): AuthorizationHelper { return $this->container->get(AuthorizationHelper::class); } protected function getReachableCenters(Role $role, Scope $scope = null) { return $this->getAuthorizationHelper() ->getReachableCenters($this->getUser(), $role, $scope) ; } protected function getEventDispatcher(): EventDispatcherInterface { return $this->get(EventDispatcherInterface::class); } protected function getCrudResolver(): Resolver { return $this->get(Resolver::class); } public static function getSubscribedServices() { return \array_merge( parent::getSubscribedServices(), [ PaginatorFactory::class => PaginatorFactory::class, 'translator' => TranslatorInterface::class, AuthorizationHelper::class => AuthorizationHelper::class, EventDispatcherInterface::class => EventDispatcherInterface::class, Resolver::class => Resolver::class, ] ); } }