deleteAction('delete', $request, $id); } /** * BAse method for edit action. * * IMplemented by the method formEditAction, with action as 'edit' */ public function edit(Request $request, mixed $id): Response { return $this->formEditAction('edit', $request, $id); } /** * Get the context for the serialization. */ public function getContextForSerialization(string $action, Request $request, mixed $entity, string $_format): array { return []; } public static function getSubscribedServices(): array { return \array_merge( parent::getSubscribedServices(), [ 'chill_main.paginator_factory' => PaginatorFactory::class, 'translator' => TranslatorInterface::class, AuthorizationHelper::class => AuthorizationHelper::class, EventDispatcherInterface::class => EventDispatcherInterface::class, Resolver::class => Resolver::class, SerializerInterface::class => SerializerInterface::class, FilterOrderHelperFactoryInterface::class => FilterOrderHelperFactoryInterface::class, ManagerRegistry::class => ManagerRegistry::class, ] ); } /** * Base method called by index action. * * @return type */ public function index(Request $request) { return $this->indexEntityAction('index', $request); } /** * Base method for new action. * * Implemented by the method formNewAction, with action as 'new' */ public function new(Request $request): Response { return $this->formCreateAction('new', $request); } public function setCrudConfig(array $config): void { $this->crudConfig = $config; } /** * Base method for the view action. * * Implemented by the method viewAction, with action as 'view' */ public function view(Request $request, mixed $id): Response { return $this->viewAction('view', $request, $id); } /** * build a default role name, using the crud resolver. * * This method should not be overriden. Override `getRoleFor` instead. * * @param string $action * * @return string */ protected function buildDefaultRole($action) { return $this->getCrudResolver()->buildDefaultRole( $this->getCrudName(), $action ); } protected function buildFilterOrderHelper(string $action, Request $request): ?FilterOrderHelper { return null; } /** * Build the base query for listing all entities, normally use in a listing * page. * * This base query does not contains any `WHERE` or `SELECT` clauses. Those * are added by other methods, like `queryEntities` and `countQueries`. * * @return QueryBuilder */ protected function buildQueryEntities(string $action, Request $request) { $query = $this->getManagerRegistry() ->getManager() ->createQueryBuilder() ->select('e') ->from($this->getEntityClass(), 'e'); $this->customizeQuery($action, $request, $query); return $query; } /** * check the acl. Called by every action. * * By default, check the role given by `getRoleFor` for the value given in * $entity. * * Throw an \Symfony\Component\Security\Core\Exception\AccessDeniedHttpException * if not accessible. * * @throws \Symfony\Component\Security\Core\Exception\AccessDeniedHttpException */ protected function checkACL(string $action, mixed $entity) { $this->denyAccessUnlessGranted($this->getRoleFor($action), $entity); } /** * Count the number of entities. */ protected function countEntities(string $action, Request $request, ?FilterOrderHelper $filterOrder = null): int { return $this->buildQueryEntities($action, $request) ->select('COUNT(e)') ->getQuery() ->getSingleScalarResult(); } /** * Create an entity. */ protected function createEntity(string $action, Request $request): object { $type = $this->getEntityClass(); return new $type(); } /** * Create a form. * * use the method `getFormClassFor` * * A hook is available: `customizeForm` allow you to customize the form * if needed. * * It is preferable to override customizeForm instead of overriding * this method. */ protected function createFormFor(string $action, mixed $entity, ?string $formClass = null, array $formOptions = []): FormInterface { $formClass ??= $this->getFormClassFor($action); $form = $this->createForm($formClass, $entity, $formOptions); $this->customizeForm($action, $form); return $form; } /** * Customize the form created by createFormFor. */ protected function customizeForm(string $action, FormInterface $form) {} protected function customizeQuery(string $action, Request $request, $query): void {} /** * @param null $formClass */ protected function deleteAction(string $action, Request $request, $id, $formClass = null): Response { $this->onPreDelete($action, $request); $entity = $this->getEntity($action, $id, $request); $postFetch = $this->onPostFetchEntity($action, $request, $entity); if ($postFetch instanceof Response) { return $postFetch; } if (null === $entity) { throw $this->createNotFoundException(sprintf('The %s with id %s is not found', $this->getCrudName(), $id)); } $response = $this->checkACL($action, $entity); if ($response instanceof Response) { return $response; } $response = $this->onPostCheckACL($action, $request, $entity); if ($response instanceof Response) { return $response; } $form = $this->createFormFor($action, $entity, $formClass); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { $this->onFormValid($action, $entity, $form, $request); $em = $this->getManagerRegistry()->getManager(); $this->onPreRemove($action, $entity, $form, $request); $this->removeEntity($action, $entity, $form, $request); $this->onPostRemove($action, $entity, $form, $request); $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().'_view', ['id' => $entity->getId()]); } if ($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) ); } /** * Duplicate an entity. */ protected function duplicateEntity(string $action, Request $request) { $id = $request->query->get('duplicate_id', 0); $originalEntity = $this->getEntity($action, $id, $request); $this->getManagerRegistry()->getManager() ->detach($originalEntity); return $originalEntity; } /** * The new (or creation) action. * * Some steps may be overriden during this process of rendering: * * This method: * * 1. Create or duplicate an entity: * * If the `duplicate` parameter is present, the entity is duplicated * using the `duplicate` method. * * If not, the entity is created using the `create` method. * 3. check ACL using `checkACL` ; * 4. launch `onPostCheckACL`. If the result is an instance of Response, * this response is returned ; * 5. generate a form using `createFormFor`, and handle request on this form; * * If the form is valid, the entity is stored and flushed, and a redirection * is returned. * * In this case, those hooks are available: * * * onFormValid * * onPreFlush * * onPostFlush * * onBeforeRedirectAfterSubmission. If this method return an instance of * Response, this response is returned. * * 5. generate default template parameters: * * * `entity`: the fetched entity ; * * `crud_name`: the crud name ; * * `form`: the formview instance. * * 6. Launch rendering, the parameter is fetch using `getTemplateFor` * The parameters may be personnalized using `generateTemplateParameter`. * * @param type $formClass */ protected function formCreateAction(string $action, Request $request, $formClass = null): Response { if ($request->query->has('duplicate')) { $entity = $this->duplicateEntity($action, $request); } else { $entity = $this->createEntity($action, $request); } $response = $this->checkACL($action, $entity); if ($response instanceof Response) { return $response; } $response = $this->onPostCheckACL($action, $request, $entity); if ($response instanceof Response) { return $response; } $form = $this->createFormFor($action, $entity, $formClass); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { $this->onFormValid($action, $entity, $form, $request); $em = $this->getManagerRegistry()->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->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()]); } if ($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) ); } /** * The edit action. * * Some steps may be overriden during this process of rendering: * * This method: * * 1. fetch the entity, using `getEntity` * 2. launch `onPostFetchEntity`. If postfetch is an instance of Response, * this response is returned. * 2. throw an HttpNotFoundException if entity is null * 3. check ACL using `checkACL` ; * 4. launch `onPostCheckACL`. If the result is an instance of Response, * this response is returned ; * 5. generate a form using `createFormFor`, and handle request on this form; * * If the form is valid, the entity is stored and flushed, and a redirection * is returned. * * In this case, those hooks are available: * * * onFormValid * * onPreFlush * * onPostFlush * * onBeforeRedirectAfterSubmission. If this method return an instance of * Response, this response is returned. * * 5. generate default template parameters: * * * `entity`: the fetched entity ; * * `crud_name`: the crud name ; * * `form`: the formview instance. * * 6. Launch rendering, the parameter is fetch using `getTemplateFor` * The parameters may be personnalized using `generateTemplateParameter`. * * @param class-string $formClass * * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException */ protected function formEditAction(string $action, Request $request, mixed $id, ?string $formClass = null, array $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)); } $response = $this->checkACL($action, $entity); if ($response instanceof Response) { return $response; } $response = $this->onPostCheckACL($action, $request, $entity); if ($response instanceof Response) { return $response; } $form = $this->createFormFor($action, $entity, $formClass, $formOptions); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { $this->onFormValid($action, $entity, $form, $request); $em = $this->getManagerRegistry()->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'); } if ($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) ); } /** * Generate a message which explains an error about the form. * * Used in form actions */ protected function generateFormErrorMessage(string $action, FormInterface $form): string { $msg = 'This form contains errors'; return $this->getTranslator()->trans($msg); } /** * Generate a success message when a form could be flushed successfully. * * @param string $action */ protected function generateFormSuccessMessage($action, mixed $entity): string { $msg = match ($action) { 'edit' => 'crud.edit.success', 'new' => 'crud.new.success', 'delete' => 'crud.delete.success', default => 'crud.default.success', }; return $this->getTranslator()->trans($msg); } /** * Customize template parameters. * * @return array */ protected function generateTemplateParameter( string $action, mixed $entity, Request $request, array $defaultTemplateParameters = [] ) { return $defaultTemplateParameters; } /** * Include services. */ protected function getActionConfig(string $action) { return $this->crudConfig['actions'][$action]; } protected function getAuthorizationHelper(): AuthorizationHelper { return $this->container->get(AuthorizationHelper::class); } /** * @return string the crud name */ protected function getCrudName(): string { return $this->crudConfig['name']; } protected function getCrudResolver(): Resolver { return $this->get(Resolver::class); } protected function getDefaultDeleteFormClass($action) { return CRUDDeleteEntityForm::class; } /** * get the instance of the entity with the given id. * * @param string $id */ protected function getEntity(mixed $action, $id, Request $request): ?object { return $this->getManagerRegistry() ->getRepository($this->getEntityClass()) ->find($id); } /** * @return string the complete fqdn of the class */ protected function getEntityClass(): string { return $this->crudConfig['class']; } protected function getEventDispatcher(): EventDispatcherInterface { return $this->get(EventDispatcherInterface::class); } protected function getFilterOrderHelperFactory(): FilterOrderHelperFactoryInterface { return $this->get(FilterOrderHelperFactoryInterface::class); } /** * get the default form class from config. * * @param string $action * * @return string the FQDN of the form class */ protected function getFormClassFor($action) { if ('delete' === $action) { return $this->crudConfig[$action]['form_class'] ?? $this->getDefaultDeleteFormClass($action); } return $this->crudConfig[$action]['form_class'] ?? $this->crudConfig['form_class']; } /** * @todo (check how to do this with dependency injection and make changes...) */ protected function getPaginatorFactory(): PaginatorFactory { return $this->container->get('chill_main.paginator_factory'); } protected function getManagerRegistry(): ManagerRegistry { return $this->container->get(ManagerRegistry::class); } /** * Get the result of the query. */ protected function getQueryResult( string $action, Request $request, int $totalItems, PaginatorInterface $paginator, ?FilterOrderHelper $filterOrder = null ) { $query = $this->queryEntities($action, $request, $paginator, $filterOrder); return $query->getQuery()->getResult(); } /** * @return \Chill\MainBundle\Entity\Center[] */ protected function getReachableCenters(string $role, ?Scope $scope = null) { return $this->getAuthorizationHelper() ->getReachableCenters($this->getUser(), $role, $scope); } /** * get the role given from the config. * * @param string $action * * @return string */ protected function getRoleFor($action) { if (\array_key_exists('role', $this->getActionConfig($action))) { return $this->getActionConfig($action)['role']; } return $this->buildDefaultRole($action); } /** * Get the template for the current crud. * * This template may be defined in configuration. If any template are * defined, return the default template for the actions new, edit, index, * and view. * * @param string $action * * @return string the path to the template * * @throws \LogicException if no template are available */ protected function getTemplateFor($action, mixed $entity, Request $request) { if ($this->hasCustomTemplate($action, $entity, $request)) { return $this->getActionConfig($action)['template']; } return match ($action) { 'new' => '@ChillMain/CRUD/new.html.twig', 'edit' => '@ChillMain/CRUD/edit.html.twig', 'index' => '@ChillMain/CRUD/index.html.twig', 'view' => '@ChillMain/CRUD/view.html.twig', 'delete' => '@ChillMain/CRUD/delete.html.twig', default => throw new \LogicException("the view for action {$action} is not ".'defined. You should override '.__METHOD__.' to add this action'), }; } protected function getTranslator(): TranslatorInterface { return $this->container->get('translator'); } protected function hasCustomTemplate($action, $entity, Request $request): bool { return !empty($this->getActionConfig($action)['template']); } /** * Build an index page. * * Some steps may be overriden during this process of rendering. * * This method: * * 1. Launch `onPreIndex` * 2. check acl. If it does return a response instance, return it * 3. launch `onPostCheckACL`. If it does return a response instance, return it * 4. count the items, using `countEntities` * 5. build a paginator element from the the number of entities ; * 6. Launch `onPreIndexQuery`. If it does return a response instance, return it * 7. fetch the results, using `getQueryResult` * * Internally, this build a query, using `queryEntities` * * 8. Launch `onPostIndexFetchQuery`. If it does return a response instance, return it * 9. create default parameters: * * The default parameters are: * * * entities: the list en entities ; * * crud_name: the name of the crud ; * * paginator: a paginator element ; * 10. Launch rendering, the parameter is fetch using `getTemplateFor` * The parameters may be personnalized using `generateTemplateParameter`. * * @param string $action */ protected function indexEntityAction($action, Request $request) { $this->onPreIndex($action, $request); $response = $this->checkACL($action, null); if ($response instanceof Response) { return $response; } $entity = ''; $response = $this->onPostCheckACL($action, $request, $entity); if ($response instanceof Response) { return $response; } $filterOrder = $this->buildFilterOrderHelper($action, $request); $totalItems = $this->countEntities($action, $request, $filterOrder); $paginator = $this->getPaginatorFactory()->create($totalItems); $response = $this->onPreIndexBuildQuery( $action, $request, $totalItems, $paginator ); if ($response instanceof Response) { return $response; } $entities = $this->getQueryResult($action, $request, $totalItems, $paginator, $filterOrder); $response = $this->onPostIndexFetchQuery( $action, $request, $totalItems, $paginator, $entities ); if ($response instanceof Response) { return $response; } $defaultTemplateParameters = [ 'entities' => $entities, 'crud_name' => $this->getCrudName(), 'paginator' => $paginator, 'filter_order' => $filterOrder, ]; return $this->render( $this->getTemplateFor($action, $entities, $request), $this->generateTemplateParameter($action, $entities, $request, $defaultTemplateParameters) ); } /** * Return a redirect response depending on the value of submit button. * * The handled values are : * * * save-and-close: return to index of current crud ; * * save-and-new: return to new page of current crud ; * * save-and-view: return to view page of current crud ; */ protected function onBeforeRedirectAfterSubmission(string $action, mixed $entity, FormInterface $form, Request $request): ?Response { $next = $request->request->get('submit', 'save-and-close'); return match ($next) { 'save-and-close' => $this->redirectToRoute('chill_crud_'.$this->getCrudName().'_index'), 'save-and-new' => $this->redirectToRoute('chill_crud_'.$this->getCrudName().'_new', $request->query->all()), default => $this->redirectToRoute('chill_crud_'.$this->getCrudName().'_view', [ 'id' => $entity->getId(), ]), }; } protected function onFormValid(string $action, object $entity, FormInterface $form, Request $request) {} protected function onPostCheckACL($action, Request $request, $entity): ?Response { return null; } protected function onPostFetchEntity($action, Request $request, $entity): ?Response { return null; } protected function onPostFlush(string $action, $entity, FormInterface $form, Request $request) {} /** * method used by indexAction. */ protected function onPostIndexBuildQuery(string $action, Request $request, int $totalItems, PaginatorInterface $paginator, mixed $query) {} /** * method used by indexAction. */ protected function onPostIndexFetchQuery(string $action, Request $request, int $totalItems, PaginatorInterface $paginator, mixed $entities) {} protected function onPostPersist(string $action, $entity, FormInterface $form, Request $request) {} protected function onPostRemove(string $action, $entity, FormInterface $form, Request $request) {} protected function onPreDelete(string $action, Request $request) {} protected function onPreFlush(string $action, $entity, FormInterface $form, Request $request) {} protected function onPreIndex(string $action, Request $request) {} /** * method used by indexAction. */ protected function onPreIndexBuildQuery(string $action, Request $request, int $totalItems, PaginatorInterface $paginator) {} protected function onPrePersist(string $action, $entity, FormInterface $form, Request $request) {} protected function onPreRemove(string $action, $entity, FormInterface $form, Request $request) {} /** * Add ordering fields in the query build by self::queryEntities. * * @param mixed|QueryBuilder $query by default, an instance of QueryBuilder * * @return mixed|QueryBuilder */ protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator) { return $query; } /** * Query the entity. * * By default, get all entities. * * The method `orderEntity` is called internally to order entities. * * It returns, by default, a query builder. * * @return type */ protected function queryEntities(string $action, Request $request, PaginatorInterface $paginator, ?FilterOrderHelper $filterOrder = null) { $query = $this->buildQueryEntities($action, $request) ->setFirstResult($paginator->getCurrentPage()->getFirstItemNumber()) ->setMaxResults($paginator->getItemsPerPage()); // allow to order queries and return the new query return $this->orderQuery($action, $query, $request, $paginator); } protected function removeEntity(string $action, $entity, FormInterface $form, Request $request) { $this->getManagerRegistry() ->getManager() ->remove($entity); } /** * The view action. * * Some steps may be overriden during this process of rendering: * * This method: * * 1. fetch the entity, using `getEntity` * 2. launch `onPostFetchEntity`. If postfetch is an instance of Response, * this response is returned. * 2. throw an HttpNotFoundException if entity is null * 3. check ACL using `checkACL` ; * 4. launch `onPostCheckACL`. If the result is an instance of Response, * this response is returned ; * 5. generate default template parameters: * * * `entity`: the fetched entity ; * * `crud_name`: the crud name * 6. Launch rendering, the parameter is fetch using `getTemplateFor` * The parameters may be personnalized using `generateTemplateParameter`. */ protected function viewAction(string $action, Request $request, mixed $id, mixed $_format = 'html'): Response { $entity = $this->getEntity($action, $id, $request); $postFetch = $this->onPostFetchEntity($action, $request, $entity); if ($postFetch instanceof Response) { return $postFetch; } if (null === $entity) { throw $this->createNotFoundException(sprintf('The %s with id %s is not found', $this->getCrudName(), $id)); } $response = $this->checkACL($action, $entity); if ($response instanceof Response) { return $response; } $response = $this->onPostCheckACL($action, $request, $entity); if ($response instanceof Response) { return $response; } if ('html' === $_format) { $defaultTemplateParameters = [ 'entity' => $entity, 'crud_name' => $this->getCrudName(), ]; return $this->render( $this->getTemplateFor($action, $entity, $request), $this->generateTemplateParameter($action, $entity, $request, $defaultTemplateParameters) ); } if ('json' === $_format) { $context = $this->getContextForSerialization($action, $request, $entity, $_format); return $this->json($entity, Response::HTTP_OK, [], $context); } throw new BadRequestHttpException('This format is not implemented'); } }