diff --git a/src/Bundle/ChillMainBundle/CRUD/CompilerPass/CRUDControllerCompilerPass.php b/src/Bundle/ChillMainBundle/CRUD/CompilerPass/CRUDControllerCompilerPass.php index 43424d4f9..d8a46837c 100644 --- a/src/Bundle/ChillMainBundle/CRUD/CompilerPass/CRUDControllerCompilerPass.php +++ b/src/Bundle/ChillMainBundle/CRUD/CompilerPass/CRUDControllerCompilerPass.php @@ -64,6 +64,7 @@ class CRUDControllerCompilerPass implements CompilerPassInterface $controller->addTag('controller.service_arguments'); if (FALSE === $alreadyDefined) { $controller->setAutoconfigured(true); + $controller->setPublic(true); } $param = 'chill_main_'.$apiOrCrud.'_config_'.$crudEntry['name']; diff --git a/src/Bundle/ChillMainBundle/CRUD/Controller/AbstractCRUDController.php b/src/Bundle/ChillMainBundle/CRUD/Controller/AbstractCRUDController.php index 31141a857..5cc055f26 100644 --- a/src/Bundle/ChillMainBundle/CRUD/Controller/AbstractCRUDController.php +++ b/src/Bundle/ChillMainBundle/CRUD/Controller/AbstractCRUDController.php @@ -7,6 +7,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; use Chill\MainBundle\Pagination\PaginatorFactory; +use Chill\MainBundle\Pagination\PaginatorInterface; class AbstractCRUDController extends AbstractController { @@ -32,6 +33,122 @@ class AbstractCRUDController extends AbstractController ->find($id); } + /** + * Count the number of entities + * + * By default, count all entities. You can customize the query by + * using the method `customizeQuery`. + * + * @param string $action + * @param Request $request + * @return int + */ + protected function countEntities(string $action, Request $request, $_format): int + { + return $this->buildQueryEntities($action, $request) + ->select('COUNT(e)') + ->getQuery() + ->getSingleScalarResult() + ; + } + + /** + * Query the entity. + * + * By default, get all entities. You can customize the query by using the + * method `customizeQuery`. + * + * The method `orderEntity` is called internally to order entities. + * + * It returns, by default, a query builder. + * + */ + protected function queryEntities(string $action, Request $request, string $_format, PaginatorInterface $paginator) + { + $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, $_format); + } + + /** + * Add ordering fields in the query build by self::queryEntities + * + */ + protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator, $_format) + { + return $query; + } + + /** + * Build the base query for listing all entities. + * + * This method is used internally by `countEntities` `queryEntities` + * + * This base query does not contains any `WHERE` or `SELECT` clauses. You + * can add some by using the method `customizeQuery`. + * + * The alias for the entity is "e". + * + * @param string $action + * @param Request $request + * @return QueryBuilder + */ + protected function buildQueryEntities(string $action, Request $request) + { + $qb = $this->getDoctrine()->getManager() + ->createQueryBuilder() + ->select('e') + ->from($this->getEntityClass(), 'e') + ; + + $this->customizeQuery($action, $request, $qb); + + return $qb; + } + + protected function customizeQuery(string $action, Request $request, $query): void {} + + /** + * Get the result of the query + */ + protected function getQueryResult(string $action, Request $request, string $_format, int $totalItems, PaginatorInterface $paginator, $query) + { + return $query->getQuery()->getResult(); + } + + protected function onPreIndex(string $action, Request $request, string $_format): ?Response + { + return null; + } + + /** + * method used by indexAction + */ + protected function onPreIndexBuildQuery(string $action, Request $request, string $_format, int $totalItems, PaginatorInterface $paginator): ?Response + { + return null; + } + + /** + * method used by indexAction + */ + protected function onPostIndexBuildQuery(string $action, Request $request, string $_format, int $totalItems, PaginatorInterface $paginator, $query): ?Response + { + return null; + } + + /** + * method used by indexAction + */ + protected function onPostIndexFetchQuery(string $action, Request $request, string $_format, int $totalItems, PaginatorInterface $paginator, $entities): ?Response + { + return null; + } + + /** * Get the complete FQDN of the class * @@ -53,7 +170,7 @@ class AbstractCRUDController extends AbstractController /** * Called on post check ACL */ - protected function onPostCheckACL(string $action, Request $request, $entity, $_format): ?Response + protected function onPostCheckACL(string $action, Request $request, string $_format, $entity): ?Response { return null; } @@ -69,7 +186,7 @@ class AbstractCRUDController extends AbstractController * * @throws \Symfony\Component\Security\Core\Exception\AccessDeniedHttpException */ - protected function checkACL(string $action, Request $request, $entity, $_format) + protected function checkACL(string $action, Request $request, string $_format, $entity = null) { $this->denyAccessUnlessGranted($this->getRoleFor($action, $request, $entity, $_format), $entity); } @@ -103,22 +220,6 @@ class AbstractCRUDController extends AbstractController */ protected function getPaginatorFactory(): PaginatorFactory { - return $this->container->get(PaginatorFactory::class); - } - - /** - * Defined the services necessary for this controller - * - * @return array - */ - public static function getSubscribedServices(): array - { - return \array_merge( - parent::getSubscribedServices(), - [ - PaginatorFactory::class => PaginatorFactory::class, - - ] - ); + return $this->container->get('chill_main.paginator_factory'); } } diff --git a/src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php b/src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php index 7256720bb..eb1de49d3 100644 --- a/src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php +++ b/src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php @@ -6,6 +6,8 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Serializer\SerializerInterface; +use Chill\MainBundle\Serializer\Model\Collection; +use Chill\MainBundle\Pagination\PaginatorInterface; class ApiController extends AbstractCRUDController { @@ -41,7 +43,7 @@ class ApiController extends AbstractCRUDController . "is not found", $this->getCrudName(), $id)); } - $response = $this->checkACL($action, $request, $entity, $_format); + $response = $this->checkACL($action, $request, $_format, $entity); if ($response instanceof Response) { return $response; } @@ -86,6 +88,110 @@ class ApiController extends AbstractCRUDController } } + /** + * Base action for indexing entities + */ + public function indexApi(Request $request, string $_format) + { + switch ($request->getMethod()) { + case Request::METHOD_GET: + case REQUEST::METHOD_HEAD: + return $this->indexApiAction('_index', $request, $_format); + default: + throw $this->createNotFoundException("This method is not supported"); + } + } + + /** + * 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 indexApiAction($action, Request $request, $_format) + { + $this->onPreIndex($action, $request, $_format); + + $response = $this->checkACL($action, $request, $_format); + if ($response instanceof Response) { + return $response; + } + + if (!isset($entity)) { + $entity = ''; + } + + $response = $this->onPostCheckACL($action, $request, $_format, $entity); + if ($response instanceof Response) { + return $response; + } + + $totalItems = $this->countEntities($action, $request, $_format); + $paginator = $this->getPaginatorFactory()->create($totalItems); + + $response = $this->onPreIndexBuildQuery($action, $request, $_format, $totalItems, + $paginator); + + if ($response instanceof Response) { + return $response; + } + + $query = $this->queryEntities($action, $request, $_format, $paginator); + + $response = $this->onPostIndexBuildQuery($action, $request, $_format, $totalItems, + $paginator, $query); + + if ($response instanceof Response) { + return $response; + } + + $entities = $this->getQueryResult($action, $request, $_format, $totalItems, $paginator, $query); + + $response = $this->onPostIndexFetchQuery($action, $request, $_format, $totalItems, + $paginator, $entities); + + if ($response instanceof Response) { + return $response; + } + + return $this->serializeCollectionItems($action, $request, $_format, $paginator, $entities); + } + + /** + * Serialize collections + * + */ + protected function serializeCollectionItems(string $action, Request $request, string $_format, PaginatorInterface $paginator, $entities): Response + { + $model = new Collection($entities, $paginator); + + return $this->json($model); + } + protected function getContextForSerialization(string $action, Request $request, $entity, $_format): array { @@ -103,8 +209,8 @@ class ApiController extends AbstractCRUDController return $actionConfig['roles'][$request->getMethod()]; } - if ($this->crudConfig['role']) { - return $this->crudConfig['role']; + if ($this->crudConfig['base_role']) { + return $this->crudConfig['base_role']; } throw new \RuntimeException(sprintf("the config does not have any role for the ". diff --git a/src/Bundle/ChillMainBundle/CRUD/Routing/CRUDRoutesLoader.php b/src/Bundle/ChillMainBundle/CRUD/Routing/CRUDRoutesLoader.php index b09bbc55b..32068e518 100644 --- a/src/Bundle/ChillMainBundle/CRUD/Routing/CRUDRoutesLoader.php +++ b/src/Bundle/ChillMainBundle/CRUD/Routing/CRUDRoutesLoader.php @@ -90,8 +90,7 @@ class CRUDRoutesLoader extends Loader $collection->addCollection($this->loadCrudConfig($crudConfig)); } foreach ($this->apiConfig as $crudConfig) { - $collection->addCollection($this->loadApiSingle($crudConfig)); - //$collection->addCollection($this->loadApiMulti($crudConfig)); + $collection->addCollection($this->loadApi($crudConfig)); } return $collection; @@ -140,7 +139,7 @@ class CRUDRoutesLoader extends Loader * @param $crudConfig * @return RouteCollection */ - protected function loadApiSingle(array $crudConfig): RouteCollection + protected function loadApi(array $crudConfig): RouteCollection { $collection = new RouteCollection(); $controller ='csapi_'.$crudConfig['name'].'_controller'; @@ -149,6 +148,65 @@ class CRUDRoutesLoader extends Loader // filter only on single actions $singleCollection = $action['single-collection'] ?? $name === '_entity' ? 'single' : NULL; if ('collection' === $singleCollection) { +// continue; + } + + // compute default action + switch ($name) { + case '_entity': + $controllerAction = 'entityApi'; + break; + case '_index': + $controllerAction = 'indexApi'; + break; + default: + $controllerAction = $name.'Api'; + break; + } + + $defaults = [ + '_controller' => $controller.':'.($action['controller_action'] ?? $controllerAction) + ]; + + // path are rewritten + // if name === 'default', we rewrite it to nothing :-) + $localName = \in_array($name, [ '_entity', '_index' ]) ? '' : '/'.$name; + if ('collection' === $action['single-collection'] || '_index' === $name) { + $localPath = $action['path'] ?? $localName.'.{_format}'; + } else { + $localPath = $action['path'] ?? '/{id}'.$localName.'.{_format}'; + } + $path = $crudConfig['base_path'].$localPath; + + $requirements = $action['requirements'] ?? [ '{id}' => '\d+' ]; + + $methods = \array_keys(\array_filter($action['methods'], function($value, $key) { return $value; }, + ARRAY_FILTER_USE_BOTH)); + + $route = new Route($path, $defaults, $requirements); + $route->setMethods($methods); + + $collection->add('chill_api_single_'.$crudConfig['name'].'_'.$name, $route); + } + + return $collection; + } + + /** + * Load routes for api multi + * + * @param $crudConfig + * @return RouteCollection + */ + protected function loadApiMultiConfig(array $crudConfig): RouteCollection + { + $collection = new RouteCollection(); + $controller ='csapi_'.$crudConfig['name'].'_controller'; + + foreach ($crudConfig['actions'] as $name => $action) { + // filter only on single actions + $singleCollection = $action['single-collection'] ?? $name === '_index' ? 'collection' : NULL; + if ('single' === $singleCollection) { continue; } @@ -175,33 +233,4 @@ class CRUDRoutesLoader extends Loader return $collection; } - - /** - * Load routes for api multi - * - * @param $crudConfig - * @return RouteCollection - */ - protected function loadApiMultiConfig(array $crudConfig): RouteCollection - { - $collection = new RouteCollection(); - foreach ($crudConfig['actions_multi'] as $name => $action) { - // we compute the data from configuration to a local form - $defaults = [ - '_controller' => 'cscrud_'.$crudConfig['name'].'_controller'.':'.($action['controller_action'] ?? $name.'Api') - ]; - // path are rewritten - // if name === 'index', we rewrite it to nothing :-) - $localName = 'index' === $name ? '' : $name; - $localPath = $action['path'] ?? '.{_format}'; - $path = $crudConfig['base_path'].$localPath.$name; - $requirements = $action['requirements'] ?? [ '{id}' => '\d+' ]; - $methods = $name === 'default' ? self::ALL_MULTI_METHODS: []; - $route = new Route($path, $defaults, $requirements); - - $collection->add('chill_api_multi'.$crudConfig['name'].'_'.$name, $route); - } - - return $collection; - } } diff --git a/src/Bundle/ChillMainBundle/DependencyInjection/Configuration.php b/src/Bundle/ChillMainBundle/DependencyInjection/Configuration.php index a7b15a069..c7e4c00ef 100644 --- a/src/Bundle/ChillMainBundle/DependencyInjection/Configuration.php +++ b/src/Bundle/ChillMainBundle/DependencyInjection/Configuration.php @@ -189,14 +189,16 @@ class Configuration implements ConfigurationInterface ->children() ->scalarNode('controller_action') ->defaultNull() - ->info('the method name to call in the route. Will be set to the concatenation of action name + \'Api\' if left empty.') + ->info('the method name to call in the controller. Will be set to the concatenation '. + 'of action name + \'Api\' if left empty.') ->example("showApi") ->end() ->scalarNode('path') ->defaultNull() - ->info('the path that will be **appended** after the base path. Do not forget to add ' - . 'arguments for the method. By default, will set to the action name, including an `{id}` ' - . 'parameter. A suffix of action name will be appended, except if the action name is "entity".') + ->info('the path that will be **appended** after the base path. Do not forget to add ' . + 'arguments for the method. By default, will set to the action name, including an `{id}` '. + 'parameter. A suffix of action name will be appended, except if the action name '. + 'is "_entity".') ->example('/{id}/my-action') ->end() ->arrayNode('requirements') @@ -205,7 +207,10 @@ class Configuration implements ConfigurationInterface ->end() ->enumNode('single-collection') ->values(['single', 'collection']) - ->info('indicates if the returned object is a single element or a collection') + ->defaultValue('single') + ->info('indicates if the returned object is a single element or a collection. '. + 'If the action name is `_index`, this value will always be considered as '. + '`collection`') ->end() ->arrayNode('methods') ->addDefaultsIfNotSet() @@ -219,6 +224,7 @@ class Configuration implements ConfigurationInterface ->end() ->end() ->arrayNode('roles') + ->addDefaultsIfNotSet() ->info("The role require for each http method") ->children() ->scalarNode(Request::METHOD_GET)->defaultNull()->end() diff --git a/src/Bundle/ChillMainBundle/Serializer/Model/Collection.php b/src/Bundle/ChillMainBundle/Serializer/Model/Collection.php new file mode 100644 index 000000000..9983d3595 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Serializer/Model/Collection.php @@ -0,0 +1,28 @@ +items = $items; + $this->paginator = $paginator; + } + + public function getPaginator(): PaginatorInterface + { + return $this->paginator; + } + + public function getItems() + { + return $this->items; + } +} diff --git a/src/Bundle/ChillMainBundle/Serializer/Normalizer/CollectionNormalizer.php b/src/Bundle/ChillMainBundle/Serializer/Normalizer/CollectionNormalizer.php new file mode 100644 index 000000000..b21bf6326 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Serializer/Normalizer/CollectionNormalizer.php @@ -0,0 +1,39 @@ +getPaginator(); + + $data['count'] = $paginator->getTotalItems(); + $data['first'] = $paginator->getCurrentPageFirstItemNumber(); + $data['items_per_page'] = $paginator->getItemsPerPage(); + $data['next'] = $paginator->hasNextPage() ? + $paginator->getNextPage()->generateUrl() : null; + $data['previous'] = $paginator->hasPreviousPage() ? + $paginator->getPreviousPage()->generateUrl() : null; + + // normalize results + $data['results'] = $this->normalizer->normalize($collection->getItems(), + $format, $context); + + return $data; + } +} diff --git a/src/Bundle/ChillMainBundle/config/services/pagination.yaml b/src/Bundle/ChillMainBundle/config/services/pagination.yaml index c7c8a89a9..f6282a39f 100644 --- a/src/Bundle/ChillMainBundle/config/services/pagination.yaml +++ b/src/Bundle/ChillMainBundle/config/services/pagination.yaml @@ -6,6 +6,7 @@ services: - "@request_stack" - "@router" - "%chill_main.pagination.item_per_page%" + Chill\MainBundle\Pagination\PaginatorFactory: '@chill_main.paginator_factory' chill_main.paginator.twig_extensions: diff --git a/src/Bundle/ChillMainBundle/config/services/serializer.yaml b/src/Bundle/ChillMainBundle/config/services/serializer.yaml index 763576a5c..fb5f57b7e 100644 --- a/src/Bundle/ChillMainBundle/config/services/serializer.yaml +++ b/src/Bundle/ChillMainBundle/config/services/serializer.yaml @@ -11,3 +11,7 @@ services: Chill\MainBundle\Serializer\Normalizer\UserNormalizer: tags: - { name: 'serializer.normalizer', priority: 64 } + + Chill\MainBundle\Serializer\Normalizer\CollectionNormalizer: + tags: + - { name: 'serializer.normalizer', priority: 64 } diff --git a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/Origin.php b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/Origin.php index 42c75efca..9227a8d58 100644 --- a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/Origin.php +++ b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/Origin.php @@ -53,7 +53,7 @@ class Origin return $this->id; } - public function getLabel(): ?string + public function getLabel() { return $this->label; }