[crud] clean and add documentation

This commit is contained in:
Julien Fastré 2020-03-12 22:31:21 +01:00
parent ad3ced9683
commit 13e81b3b49
5 changed files with 465 additions and 150 deletions

View File

@ -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");

View File

@ -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)));
}
}

View File

@ -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();

View File

@ -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);

View File

@ -1,4 +0,0 @@
name: 'country'
actions: ['index', 'new', 'edit', 'delete']
base_path: '/admin/country'
controller: 'Chill\MainBundle\CRUD\Controller\CRUDController'