diff --git a/CRUD/Controller/CRUDController.php b/CRUD/Controller/CRUDController.php index 7a9c1e76b..c9047783b 100644 --- a/CRUD/Controller/CRUDController.php +++ b/CRUD/Controller/CRUDController.php @@ -54,50 +54,88 @@ class CRUDController extends AbstractController $this->crudConfig = $config; } - protected function processTemplateParameters($action): array - { - throw new Exception('is this method used ?'); - - $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($action, QueryBuilder $query, Request $request, PaginatorInterface $paginator): QueryBuilder - { - return $query; - } - + /** + * Base method called by index action. + * + * @param Request $request + * @return type + */ public function index(Request $request) { return $this->indexEntityAction('index', $request); } + /** + * Build an index page. + * + * Some steps may be overriden during this process of rendering. + * + * This method: + * + * 1. Launch `onPreIndex` + * x. check acl. If it does return a response instance, return it + * x. launch `onPostCheckACL`. If it does return a response instance, return it + * 1. count the items, using `countEntities` + * 2. build a paginator element from the the number of entities ; + * 3. Launch `onPreIndexQuery`. If it does return a response instance, return it + * 3. build a query, using `queryEntities` + * x. fetch the results, using `getQueryResult` + * x. Launch `onPostIndexFetchQuery`. If it does return a response instance, return it + * 4. create default parameters: + * + * The default parameters are: + * + * * entities: the list en entities ; + * * crud_name: the name of the crud ; + * * paginator: a paginator element ; + * 5. Launch rendering, the parameter is fetch using `getTemplateFor` + * The parameters may be personnalized using `generateTemplateParameter`. + * + * @param string $action + * @param Request $request + * @return type + */ protected function indexEntityAction($action, Request $request) { + $this->onPreIndex($action, $request); + + $response = $this->checkACL($action, null); + if ($response instanceof Response) { + return $response; + } + + $response = $this->onPostCheckACL($action, $request, $entity); + if ($response instanceof Response) { + return $response; + } + $totalItems = $this->countEntities($action, $request); $paginator = $this->getPaginatorFactory()->create($totalItems); + + $response = $this->onPreIndexBuildQuery($action, $request, $totalItems, + $paginator); + + if ($response instanceof Response) { + return $response; + } + $query = $this->queryEntities($action, $request, $paginator); - $entities = $query->getQuery()->getResult(); + $response = $this->onPostIndexBuildQuery($action, $request, $totalItems, + $paginator, $query); + + if ($response instanceof Response) { + return $response; + } + + $entities = $this->getQueryResult($action, $request, $totalItems, $paginator, $query); + + $response = $this->onPostIndexFetchQuery($action, $request, $totalItems, + $paginator, $entities); + + if ($response instanceof Response) { + return $response; + } $defaultTemplateParameters = [ 'entities' => $entities, @@ -111,7 +149,60 @@ class CRUDController extends AbstractController ); } - protected function queryEntities($action, Request $request, PaginatorInterface $paginator) + /** + * + * @param string $action + * @param Request $request + */ + protected function onPreIndex(string $action, Request $request) { } + + /** + * method used by indexAction + * + * @param string $action + * @param Request $request + * @param int $totalItems + * @param PaginatorInterface $paginator + */ + protected function onPreIndexBuildQuery(string $action, Request $request, int $totalItems, PaginatorInterface $paginator) { } + + /** + * method used by indexAction + * + * @param string $action + * @param Request $request + * @param int $totalItems + * @param PaginatorInterface $paginator + * @param mixed $query + */ + protected function onPostIndexBuildQuery(string $action, Request $request, int $totalItems, PaginatorInterface $paginator, $query) { } + + /** + * method used by indexAction + * + * @param string $action + * @param Request $request + * @param int $totalItems + * @param PaginatorInterface $paginator + * @param mixed $entities + */ + protected function onPostIndexFetchQuery(string $action, Request $request, int $totalItems, PaginatorInterface $paginator, $entities) { } + + /** + * Query the entity. + * + * By default, get all entities. + * + * The method `orderEntity` is called internally to order entities. + * + * It returns, by default, a query builder. + * + * @param string $action + * @param Request $request + * @param PaginatorInterface $paginator + * @return type + */ + protected function queryEntities(string $action, Request $request, PaginatorInterface $paginator) { $query = $this->getDoctrine()->getManager() ->createQueryBuilder() @@ -126,7 +217,44 @@ class CRUDController extends AbstractController return $query; } - protected function countEntities($action, Request $request) + + /** + * Add ordering fields in the query build by self::queryEntities + * + * @param string $action + * @param QueryBuilder|mixed $query by default, an instance of QueryBuilder + * @param Request $request + * @param PaginatorInterface $paginator + * @return QueryBuilder + */ + protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator): QueryBuilder + { + return $query; + } + + /** + * Get the result of the query + * + * @param string $action + * @param Request $request + * @param int $totalItems + * @param PaginatorInterface $paginator + * @param mixed $query + * @return mixed + */ + protected function getQueryResult(string $action, Request $request, int $totalItems, PaginatorInterface $paginator, $query) + { + return $query->getQuery()->getResult(); + } + + /** + * Count the number of entities + * + * @param string $action + * @param Request $request + * @return int + */ + protected function countEntities(string $action, Request $request): int { return $this->getDoctrine()->getManager() ->createQuery("SELECT COUNT(e) FROM ".$this->getEntityClass()." e") @@ -134,22 +262,74 @@ class CRUDController extends AbstractController ; } + /** + * BAse method for edit action + * + * IMplemented by the method formEditAction, with action as 'edit' + * + * @param Request $request + * @param mixed $id + * @return Response + */ public function edit(Request $request, $id): Response { return $this->formEditAction('edit', $request, $id); } + /** + * Base method for new action + * + * Implemented by the method formNewAction, with action as 'new' + * + * @param Request $request + * @return Response + */ public function new(Request $request): Response { return $this->formCreateAction('new', $request); } + /** + * Base method for the view action. + * + * Implemented by the method viewAction, with action as 'view' + * + * @param Request $request + * @param mixed $id + * @return Response + */ public function view(Request $request, $id): Response { return $this->viewAction('view', $request, $id); } - protected function viewAction($action, Request $request, $id) + /** + * 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`. + * + * @param string $action + * @param Request $request + * @param mixed $id + * @return Response + */ + protected function viewAction(string $action, Request $request, $id) { $entity = $this->getEntity($action, $id, $request); @@ -159,12 +339,19 @@ class CRUDController extends AbstractController return $postFetch; } - $this->checkACL($action, $entity); + if (NULL === $entity) { + throw $this->createNotFoundException(sprintf("The %s with id %s " + . "is not found"), $this->getCrudName(), $id); + } - $postCheckACL = $this->onPostCheckACL($action, $request, $entity); + $response = $this->checkACL($action, $entity); + if ($response instanceof Response) { + return $response; + } - if ($postCheckACL instanceof Response) { - return $postCheckACL; + $response = $this->onPostCheckACL($action, $request, $entity); + if ($response instanceof Response) { + return $response; } $defaultTemplateParameters = [ @@ -178,7 +365,51 @@ class CRUDController extends AbstractController ); } - protected function formEditAction($action, Request $request, $id, $formClass = null, $formOptions = []): Response + /** + * 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 string $action + * @param Request $request + * @param mixed $id + * @param string $formClass + * @param array $formOptions + * @return Response + * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException + */ + protected function formEditAction(string $action, Request $request, $id, string $formClass = null, array $formOptions = []): Response { $entity = $this->getEntity($action, $id, $request); @@ -187,7 +418,15 @@ class CRUDController extends AbstractController . "is not found"), $this->getCrudName(), $id); } - $this->checkACL($action, $entity); + $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); @@ -227,7 +466,50 @@ class CRUDController extends AbstractController ); } - protected function formCreateAction($action, Request $request, $formClass = null): Response + /** + * 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 string $action + * @param Request $request + * @param type $formClass + * @return Response + */ + protected function formCreateAction(string $action, Request $request, $formClass = null): Response { if ($request->query->has('duplicate')) { $entity = $this->duplicateEntity($action, $request); @@ -235,7 +517,15 @@ class CRUDController extends AbstractController $entity = $this->createEntity($action, $request); } - $this->checkACL($action, $entity); + $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); @@ -292,6 +582,13 @@ class CRUDController extends AbstractController ->find($id); } + /** + * Duplicate an entity + * + * @param string $action + * @param Request $request + * @return mixed + */ protected function duplicateEntity(string $action, Request $request) { $id = $request->query->get('duplicate_id', 0); @@ -303,21 +600,48 @@ class CRUDController extends AbstractController return $originalEntity; } + /** + * + * @return string the complete fqdn of the class + */ protected function getEntityClass(): string { return $this->crudConfig['class']; } + /** + * + * @return string the crud name + */ protected function getCrudName(): string { return $this->crudConfig['name']; } - protected function checkACL($action, $entity) + /** + * 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. + * + * @param string $action + * @param mixed $entity + * @throws \Symfony\Component\Security\Core\Exception\AccessDeniedHttpException + */ + protected function checkACL(string $action, $entity) { $this->denyAccessUnlessGranted($this->getRoleFor($action), $entity); } + /** + * get the role given from the config. + * + * @param string $action + * @return string + */ protected function getRoleFor($action) { if (NULL !== ($this->getActionConfig($action)['role'])) { @@ -327,19 +651,50 @@ class CRUDController extends AbstractController return $this->buildDefaultRole($action); } + /** + * 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); } + /** + * get the default form class from config + * + * @param string $action + * @return string the FQDN of the form class + */ protected function getFormClassFor($action) { return $this->crudConfig[$action]['form_class'] ?? $this->crudConfig['form_class']; } - protected function createFormFor($action, $entity, $formClass = null, $formOptions = []) + /** + * 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. + * + * @param string $action + * @param mixed $entity + * @param string $formClass + * @param array $formOptions + * @return FormInterface + */ + protected function createFormFor(string $action, $entity, string $formClass = null, array $formOptions = []): FormInterface { $formClass = $formClass ?? $this->getFormClassFor($action); @@ -350,23 +705,40 @@ class CRUDController extends AbstractController return $form; } - protected function customizeForm($action, FormInterface $form) + /** + * Customize the form created by createFormFor. + * + * @param string $action + * @param FormInterface $form + */ + protected function customizeForm(string $action, FormInterface $form) { } - protected function generateLabelForButton($action, $formName, $form) - { - return sprintf("crud.%s.button_action_form", $action); - } - - protected function generateFormErrorMessage($action, FormInterface $form): string + /** + * Generate a message which explains an error about the form. + * + * Used in form actions + * + * @param string $action + * @param FormInterface $form + * @return string + */ + 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 + * @param mixed $entity + * @return string + */ protected function generateFormSuccessMessage($action, $entity): string { switch ($action) { @@ -383,8 +755,17 @@ class CRUDController extends AbstractController return $this->getTranslator()->trans($msg); } + /** + * Customize template parameters. + * + * @param string $action + * @param mixed $entity + * @param Request $request + * @param array $defaultTemplateParameters + * @return array + */ protected function generateTemplateParameter( - $action, + string $action, $entity, Request $request, array $defaultTemplateParameters = [] @@ -392,7 +773,14 @@ class CRUDController extends AbstractController return $defaultTemplateParameters; } - protected function createEntity($action, Request $request): object + /** + * Create an entity. + * + * @param string $action + * @param Request $request + * @return object + */ + protected function createEntity(string $action, Request $request): object { $type = $this->getEntityClass(); @@ -400,12 +788,17 @@ class CRUDController extends AbstractController } /** + * 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 * @param mixed $entity the entity for the current request, or an array of entities * @param Request $request - * @return string - * @throws \LogicException + * @return string the path to the template + * @throws \LogicException if no template are available */ protected function getTemplateFor($action, $entity, Request $request) { @@ -464,6 +857,21 @@ class CRUDController extends AbstractController { } + /** + * 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 ; + * + * @param string $action + * @param mixed $entity + * @param FormInterface $form + * @param Request $request + * @return \Symfony\Component\HttpFoundation\RedirectResponse + */ protected function onBeforeRedirectAfterSubmission(string $action, $entity, FormInterface $form, Request $request) { $next = $request->request->get("submit", "save-and-close"); diff --git a/CRUD/Resolver/Resolver.php b/CRUD/Resolver/Resolver.php index 66ad75e82..86e0ecbca 100644 --- a/CRUD/Resolver/Resolver.php +++ b/CRUD/Resolver/Resolver.php @@ -69,29 +69,6 @@ class Resolver foreach($crudConfig as $conf) { $this->crudConfig[$conf['name']] = $conf; } - - $this->buildPropertyAccess(); - } - - - private function buildPropertyAccess() - { - $this->propertyAccess = PropertyAccess::createPropertyAccessorBuilder() - ->enableExceptionOnInvalidIndex() - ->getPropertyAccessor(); - } - - /** - * Return the data at given path. - * - * Path are given to - * - * @param object $entity - * @param string $path - */ - public function getData($entity, $path) - { - return $this->propertyAccess->getValue($entity, $path); } public function getConfigValue($key, $crudName, $action = null) @@ -116,43 +93,4 @@ class Resolver '_'. $action); } - - public function getTwigTemplate($entity, $path): string - { - list($focusEntity, $subPath) = $this->getFocusedEntity($entity, $path); - - $classMetadata = $this->em->getClassMetadata(\get_class($focusEntity)); - $type = $classMetadata->getTypeOfField($subPath); - - switch ($type) { - - default: - return '@ChillMain/CRUD/_inc/default.html.twig'; - } - } - - /** - * Get the object on which the path apply - * - * This methods recursively parse the path and entity and return the entity - * which will deliver the info, and the last path. - * - * @param object $entity - * @param string $path - * @return array [$focusedEntity, $lastPath] - */ - private function getFocusedEntity($entity, $path) - { - if (\strpos($path, '.') === FALSE) { - return [$entity, $path]; - } - - $exploded = \explode('.', $path); - - $subEntity = $this->propertyAccess - ->getValue($entity, $exploded[0]); - - return $this->getFocusedEntity($subEntity, - \implode('.', \array_slice($exploded, 1))); - } } diff --git a/CRUD/Routing/CRUDRoutesLoader.php b/CRUD/Routing/CRUDRoutesLoader.php index aef4aec69..12f261d21 100644 --- a/CRUD/Routing/CRUDRoutesLoader.php +++ b/CRUD/Routing/CRUDRoutesLoader.php @@ -37,17 +37,6 @@ class CRUDRoutesLoader $this->config = $config; } - protected function addDummyConfig() - { - $this->config[] = [ - 'name' => 'country', - 'actions' => ['index'],//, 'new', 'edit', 'delete'], - 'base_path' => '/admin/country', - 'controller' => \Chill\MainBundle\Controller\AdminCountryCRUDController::class - ]; - } - - public function load() { $collection = new RouteCollection(); diff --git a/CRUD/Templating/TwigCRUDResolver.php b/CRUD/Templating/TwigCRUDResolver.php index 9c83df864..f962038b4 100644 --- a/CRUD/Templating/TwigCRUDResolver.php +++ b/CRUD/Templating/TwigCRUDResolver.php @@ -43,14 +43,6 @@ class TwigCRUDResolver extends AbstractExtension $this->resolver = $resolver; } - public function getFilters() - { - return [ - new TwigFilter('chill_crud_display', [$this, 'display'], - ['needs_environment' => true, 'is_safe' => ['html']]) - ]; - } - public function getFunctions() { return [ @@ -59,14 +51,6 @@ class TwigCRUDResolver extends AbstractExtension ]; } - public function display(Environment $env, $entity, $path): string - { - $data = $this->resolver->getData($entity, $path); - $template = $this->resolver->getTwigTemplate($entity, $path); - - return $env->render($template, ['data' => $data, 'entity' => $entity, ]); - } - public function getConfig($configKey, $crudName, $action = null) { return $this->resolver->getConfigValue($configKey, $crudName, $action); diff --git a/Resources/config/crud/country.yml b/Resources/config/crud/country.yml deleted file mode 100644 index 751529b12..000000000 --- a/Resources/config/crud/country.yml +++ /dev/null @@ -1,4 +0,0 @@ -name: 'country' -actions: ['index', 'new', 'edit', 'delete'] -base_path: '/admin/country' -controller: 'Chill\MainBundle\CRUD\Controller\CRUDController' \ No newline at end of file