diff --git a/src/Bundle/ChillMainBundle/CRUD/Controller/AbstractCRUDController.php b/src/Bundle/ChillMainBundle/CRUD/Controller/AbstractCRUDController.php index df7d065cb..71476fb78 100644 --- a/src/Bundle/ChillMainBundle/CRUD/Controller/AbstractCRUDController.php +++ b/src/Bundle/ChillMainBundle/CRUD/Controller/AbstractCRUDController.php @@ -40,6 +40,20 @@ class AbstractCRUDController extends AbstractController return $e; } + /** + * Create an entity. + * + * @param string $action + * @param Request $request + * @return object + */ + protected function createEntity(string $action, Request $request): object + { + $type = $this->getEntityClass(); + + return new $type; + } + /** * Count the number of entities * diff --git a/src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php b/src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php index 14b0473da..9686a7b98 100644 --- a/src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php +++ b/src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php @@ -85,11 +85,75 @@ class ApiController extends AbstractCRUDController case Request::METHOD_PUT: case Request::METHOD_PATCH: return $this->entityPut('_entity', $request, $id, $_format); + case Request::METHOD_POST: + return $this->entityPostAction('_entity', $request, $id, $_format); + default: + throw new \Symfony\Component\HttpFoundation\Exception\BadRequestException("This method is not implemented"); + } + } + public function entityPost(Request $request, $_format): Response + { + switch($request->getMethod()) { + case Request::METHOD_POST: + return $this->entityPostAction('_entity', $request, $_format); default: throw new \Symfony\Component\HttpFoundation\Exception\BadRequestException("This method is not implemented"); } } + protected function entityPostAction($action, Request $request, string $_format): Response + { + $entity = $this->createEntity($action, $request); + + try { + $entity = $this->deserialize($action, $request, $_format, $entity); + } catch (NotEncodableValueException $e) { + throw new BadRequestException("invalid json", 400, $e); + } + + $errors = $this->validate($action, $request, $_format, $entity); + + $response = $this->onAfterValidation($action, $request, $_format, $entity, $errors); + if ($response instanceof Response) { + return $response; + } + + if ($errors->count() > 0) { + $response = $this->json($errors); + $response->setStatusCode(Response::HTTP_UNPROCESSABLE_ENTITY); + + return $response; + } + + $response = $this->checkACL($action, $request, $_format, $entity); + if ($response instanceof Response) { + return $response; + } + + $response = $this->onPostCheckACL($action, $request, $_format, $entity); + if ($response instanceof Response) { + return $response; + } + + $this->getDoctrine()->getManager()->persist($entity); + $this->getDoctrine()->getManager()->flush(); + + $response = $this->onAfterFlush($action, $request, $_format, $entity, $errors); + if ($response instanceof Response) { + return $response; + } + $response = $this->onBeforeSerialize($action, $request, $_format, $entity); + if ($response instanceof Response) { + return $response; + } + + return $this->json( + $entity, + Response::HTTP_OK, + [], + $this->getContextForSerializationPostAlter($action, $request, $_format, $entity) + ); + } public function entityPut($action, Request $request, $id, string $_format): Response { $entity = $this->getEntity($action, $id, $request, $_format); @@ -407,6 +471,7 @@ class ApiController extends AbstractCRUDController return [ 'groups' => [ 'read' ]]; case Request::METHOD_PUT: case Request::METHOD_PATCH: + case Request::METHOD_POST: return [ 'groups' => [ 'write' ]]; default: throw new \LogicException("get context for serialization is not implemented for this method"); diff --git a/src/Bundle/ChillMainBundle/CRUD/Controller/CRUDController.php b/src/Bundle/ChillMainBundle/CRUD/Controller/CRUDController.php index 8338af8c6..25b7e5860 100644 --- a/src/Bundle/ChillMainBundle/CRUD/Controller/CRUDController.php +++ b/src/Bundle/ChillMainBundle/CRUD/Controller/CRUDController.php @@ -43,16 +43,16 @@ use Symfony\Component\Serializer\SerializerInterface; */ class CRUDController extends AbstractController { - + /** - * The crud configuration + * The crud configuration * * This configuration si defined by `chill_main['crud']`. * * @var array */ protected $crudConfig; - + /** * @param array $config */ @@ -60,7 +60,7 @@ class CRUDController extends AbstractController { $this->crudConfig = $config; } - + /** * @param $parameter * @return Response @@ -69,7 +69,7 @@ class CRUDController extends AbstractController { return new Response($parameter); } - + /** * @param Request $request * @param $id @@ -79,7 +79,7 @@ class CRUDController extends AbstractController { return $this->deleteAction('delete', $request, $id); } - + /** * @param string $action * @param Request $request @@ -90,78 +90,78 @@ class CRUDController extends AbstractController protected function deleteAction(string $action, Request $request, $id, $formClass = null) { $this->onPreDelete($action, $request, $id); - + $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($entity, $form, $request); $em = $this->getDoctrine()->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()]); - + } 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->getTemplateFor($action, $entity, $request), $this->generateTemplateParameter($action, $entity, $request, $defaultTemplateParameters) ); } - + /** * @param string $action * @param Request $request */ protected function onPreDelete(string $action, Request $request) {} - + /** * @param string $action * @param $entity @@ -169,7 +169,7 @@ class CRUDController extends AbstractController * @param Request $request */ protected function onPreRemove(string $action, $entity, FormInterface $form, Request $request) {} - + /** * @param string $action * @param $entity @@ -177,7 +177,7 @@ class CRUDController extends AbstractController * @param Request $request */ protected function onPostRemove(string $action, $entity, FormInterface $form, Request $request) {} - + /** * @param string $action * @param $entity @@ -190,10 +190,10 @@ class CRUDController extends AbstractController ->getManager() ->remove($entity); } - + /** * Base method called by index action. - * + * * @param Request $request * @return type */ @@ -201,14 +201,14 @@ class CRUDController extends AbstractController { 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 @@ -219,15 +219,15 @@ class CRUDController extends AbstractController * 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: - * + * + * 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` + * 5. Launch rendering, the parameter is fetch using `getTemplateFor` * The parameters may be personnalized using `generateTemplateParameter`. - * + * * @param string $action * @param Request $request * @return type @@ -235,80 +235,80 @@ class CRUDController extends AbstractController protected function indexEntityAction($action, Request $request) { $this->onPreIndex($action, $request); - + $response = $this->checkACL($action, null); if ($response instanceof Response) { return $response; } - + if (!isset($entity)) { $entity = ''; } - + $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, + + $response = $this->onPreIndexBuildQuery($action, $request, $totalItems, $paginator); - + if ($response instanceof Response) { return $response; } - + $query = $this->queryEntities($action, $request, $paginator); - - $response = $this->onPostIndexBuildQuery($action, $request, $totalItems, + + $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, + + $response = $this->onPostIndexFetchQuery($action, $request, $totalItems, $paginator, $entities); - + if ($response instanceof Response) { return $response; } - + $defaultTemplateParameters = [ 'entities' => $entities, 'crud_name' => $this->getCrudName(), 'paginator' => $paginator ]; - + return $this->render( - $this->getTemplateFor($action, $entities, $request), + $this->getTemplateFor($action, $entities, $request), $this->generateTemplateParameter($action, $entities, $request, $defaultTemplateParameters) ); } - + /** * @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 @@ -316,10 +316,10 @@ class CRUDController extends AbstractController * @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 @@ -327,14 +327,14 @@ class CRUDController extends AbstractController * @param mixed $entities */ protected function onPostIndexFetchQuery(string $action, Request $request, int $totalItems, PaginatorInterface $paginator, $entities) { } - + /** * 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`. - * + * * @param string $action * @param Request $request * @return QueryBuilder @@ -347,16 +347,16 @@ class CRUDController extends AbstractController ->from($this->getEntityClass(), 'e') ; } - + /** * 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 @@ -367,15 +367,15 @@ class CRUDController extends AbstractController $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); } - - + + /** * 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 @@ -386,10 +386,10 @@ class CRUDController extends AbstractController { return $query; } - + /** * Get the result of the query - * + * * @param string $action * @param Request $request * @param int $totalItems @@ -397,14 +397,14 @@ class CRUDController extends AbstractController * @param mixed $query * @return mixed */ - protected function getQueryResult(string $action, Request $request, int $totalItems, PaginatorInterface $paginator, $query) + 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 @@ -417,12 +417,12 @@ class CRUDController extends AbstractController ->getSingleScalarResult() ; } - + /** * BAse method for edit action - * + * * IMplemented by the method formEditAction, with action as 'edit' - * + * * @param Request $request * @param mixed $id * @return Response @@ -431,12 +431,12 @@ class CRUDController extends AbstractController { 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 */ @@ -444,12 +444,12 @@ class CRUDController extends AbstractController { 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 @@ -458,28 +458,28 @@ class CRUDController extends AbstractController { return $this->viewAction('view', $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, + * 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` + * 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 @@ -488,23 +488,23 @@ class CRUDController extends AbstractController protected function viewAction(string $action, Request $request, $id, $_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; @@ -515,9 +515,9 @@ class CRUDController extends AbstractController 'entity' => $entity, 'crud_name' => $this->getCrudName() ]; - + return $this->render( - $this->getTemplateFor($action, $entity, $request), + $this->getTemplateFor($action, $entity, $request), $this->generateTemplateParameter($action, $entity, $request, $defaultTemplateParameters) ); } elseif ($_format === 'json') { @@ -537,45 +537,45 @@ class CRUDController extends AbstractController { return []; } - - + + /** * 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, + * 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 + * * 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` + * + * 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 @@ -587,98 +587,98 @@ class CRUDController extends AbstractController protected function formEditAction(string $action, Request $request, $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); + . "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($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->getTemplateFor($action, $entity, $request), $this->generateTemplateParameter($action, $entity, $request, $defaultTemplateParameters) ); } - + /** * 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 + * + * 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, + * 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 + * * 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` + * + * 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 @@ -691,62 +691,62 @@ class CRUDController extends AbstractController } 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($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->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 */ @@ -756,10 +756,10 @@ class CRUDController extends AbstractController ->getRepository($this->getEntityClass()) ->find($id); } - + /** * Duplicate an entity - * + * * @param string $action * @param Request $request * @return mixed @@ -768,24 +768,24 @@ class CRUDController extends AbstractController { $id = $request->query->get('duplicate_id', 0); $originalEntity = $this->getEntity($action, $id, $request); - + $this->getDoctrine()->getManager() ->detach($originalEntity); - + 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 @@ -795,13 +795,13 @@ class CRUDController extends AbstractController /** * check the acl. Called by every action. - * - * By default, check the role given by `getRoleFor` for the value given in + * + * 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 @@ -810,10 +810,10 @@ class CRUDController extends AbstractController { $this->denyAccessUnlessGranted($this->getRoleFor($action), $entity); } - + /** * get the role given from the config. - * + * * @param string $action * @return string */ @@ -822,27 +822,27 @@ class CRUDController extends AbstractController if (\array_key_exists('role', $this->getActionConfig($action))) { return $this->getActionConfig($action)['role']; } - + 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(), + 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 */ @@ -852,27 +852,27 @@ class CRUDController extends AbstractController return $this->crudConfig[$action]['form_class'] ?? $this->getDefaultDeleteFormClass($action); } - + return $this->crudConfig[$action]['form_class'] ?? $this->crudConfig['form_class']; } - + protected function getDefaultDeleteFormClass($action) { return CRUDDeleteEntityForm::class; } - + /** * 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 @@ -882,17 +882,17 @@ class CRUDController extends AbstractController protected function createFormFor(string $action, $entity, string $formClass = null, array $formOptions = []): FormInterface { $formClass = $formClass ?? $this->getFormClassFor($action); - + $form = $this->createForm($formClass, $entity, $formOptions); - + $this->customizeForm($action, $form); - + return $form; } - + /** * Customize the form created by createFormFor. - * + * * @param string $action * @param FormInterface $form */ @@ -900,12 +900,12 @@ class CRUDController extends AbstractController { } - + /** * Generate a message which explains an error about the form. - * + * * Used in form actions - * + * * @param string $action * @param FormInterface $form * @return string @@ -913,13 +913,13 @@ class CRUDController extends AbstractController 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 @@ -939,13 +939,13 @@ class CRUDController extends AbstractController default: $msg = "crud.default.success"; } - + return $this->getTranslator()->trans($msg); } - + /** * Customize template parameters. - * + * * @param string $action * @param mixed $entity * @param Request $request @@ -953,9 +953,9 @@ class CRUDController extends AbstractController * @return array */ protected function generateTemplateParameter( - string $action, - $entity, - Request $request, + string $action, + $entity, + Request $request, array $defaultTemplateParameters = [] ) { return $defaultTemplateParameters; @@ -963,7 +963,7 @@ class CRUDController extends AbstractController /** * Create an entity. - * + * * @param string $action * @param Request $request * @return object @@ -971,17 +971,17 @@ class CRUDController extends AbstractController protected function createEntity(string $action, Request $request): object { $type = $this->getEntityClass(); - + return new $type; } - + /** * 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, + * + * 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 @@ -993,11 +993,11 @@ class CRUDController extends AbstractController if ($this->hasCustomTemplate($action, $entity, $request)) { return $this->getActionConfig($action)['template']; } - + switch ($action) { case 'new': return '@ChillMain/CRUD/new.html.twig'; - case 'edit': + case 'edit': return '@ChillMain/CRUD/edit.html.twig'; case 'index': return '@ChillMain/CRUD/index.html.twig'; @@ -1011,7 +1011,7 @@ class CRUDController extends AbstractController . "action"); } } - + /** * @param $action * @param $entity @@ -1022,7 +1022,7 @@ class CRUDController extends AbstractController { return !empty($this->getActionConfig($action)['template']); } - + /** * @param string $action * @param $entity @@ -1032,7 +1032,7 @@ class CRUDController extends AbstractController protected function onPreFlush(string $action, $entity, FormInterface $form, Request $request) { } - + /** * @param string $action * @param $entity @@ -1042,7 +1042,7 @@ class CRUDController extends AbstractController protected function onPostFlush(string $action, $entity, FormInterface $form, Request $request) { } - + /** * @param string $action * @param $entity @@ -1052,7 +1052,7 @@ class CRUDController extends AbstractController protected function onPrePersist(string $action, $entity, FormInterface $form, Request $request) { } - + /** * @param string $action * @param $entity @@ -1062,7 +1062,7 @@ class CRUDController extends AbstractController protected function onPostPersist(string $action, $entity, FormInterface $form, Request $request) { } - + /** * @param $action * @param Request $request @@ -1073,7 +1073,7 @@ class CRUDController extends AbstractController { return null; } - + /** * @param $action * @param Request $request @@ -1084,7 +1084,7 @@ class CRUDController extends AbstractController { return null; } - + /** * @param object $entity * @param FormInterface $form @@ -1093,16 +1093,16 @@ class CRUDController extends AbstractController protected function onFormValid(object $entity, FormInterface $form, Request $request) { } - + /** * 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 @@ -1124,7 +1124,7 @@ class CRUDController extends AbstractController ]); } } - + /** * Include services * @@ -1135,7 +1135,7 @@ class CRUDController extends AbstractController { return $this->crudConfig['actions'][$action]; } - + /** * @return PaginatorFactory */ @@ -1143,7 +1143,7 @@ class CRUDController extends AbstractController { return $this->container->get('chill_main.paginator_factory'); } - + /** * @return TranslatorInterface */ @@ -1151,7 +1151,7 @@ class CRUDController extends AbstractController { return $this->container->get('translator'); } - + /** * @return AuthorizationHelper */ @@ -1159,19 +1159,19 @@ class CRUDController extends AbstractController { return $this->container->get(AuthorizationHelper::class); } - + /** * @param Role $role * @param Scope|null $scope * @return \Chill\MainBundle\Entity\Center[] */ - protected function getReachableCenters(Role $role, Scope $scope = null) + protected function getReachableCenters(Role $role, Scope $scope = null) { return $this->getAuthorizationHelper() ->getReachableCenters($this->getUser(), $role, $scope) ; } - + /** * @return EventDispatcherInterface */ @@ -1179,7 +1179,7 @@ class CRUDController extends AbstractController { return $this->get(EventDispatcherInterface::class); } - + /** * @return Resolver */ @@ -1187,7 +1187,7 @@ class CRUDController extends AbstractController { return $this->get(Resolver::class); } - + /** * @return array */ diff --git a/src/Bundle/ChillMainBundle/CRUD/Routing/CRUDRoutesLoader.php b/src/Bundle/ChillMainBundle/CRUD/Routing/CRUDRoutesLoader.php index 32068e518..9d61d6233 100644 --- a/src/Bundle/ChillMainBundle/CRUD/Routing/CRUDRoutesLoader.php +++ b/src/Bundle/ChillMainBundle/CRUD/Routing/CRUDRoutesLoader.php @@ -183,48 +183,26 @@ class CRUDRoutesLoader extends Loader $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); - } + if (count($methods) === 0) { + throw new \RuntimeException("The api configuration named \"{$crudConfig['name']}\", action \"{$name}\", ". + "does not have any allowed methods. You should remove this action from the config ". + "or allow, at least, one method"); + } - return $collection; - } + if ('_entity' === $name && \in_array(Request::METHOD_POST, $methods)) { + unset($methods[\array_search(Request::METHOD_POST, $methods)]); + $entityPostRoute = $this->createEntityPostRoute($name, $crudConfig, $action, + $controller); + $collection->add("chill_api_single_{$crudConfig['name']}_{$name}_create", + $entityPostRoute); + } - /** - * 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) { + if (count($methods) === 0) { + // the only method was POST, + // continue to next continue; } - $defaults = [ - '_controller' => $controller.':'.($action['controller_action'] ?? '_entity' === $name ? 'entityApi' : $name.'Api') - ]; - - // path are rewritten - // if name === 'default', we rewrite it to nothing :-) - $localName = '_entity' === $name ? '' : '/'.$name; - $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); @@ -233,4 +211,18 @@ class CRUDRoutesLoader extends Loader return $collection; } + + private function createEntityPostRoute(string $name, $crudConfig, array $action, $controller): Route + { + $localPath = $action['path'].'.{_format}'; + $defaults = [ + '_controller' => $controller.':'.($action['controller_action'] ?? 'entityPost') + ]; + $path = $crudConfig['base_path'].$localPath; + $requirements = $action['requirements'] ?? []; + $route = new Route($path, $defaults, $requirements); + $route->setMethods([ Request::METHOD_POST ]); + + return $route; + } } diff --git a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php index 277e916a8..5644138e6 100644 --- a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php +++ b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php @@ -35,6 +35,8 @@ use Chill\MainBundle\Doctrine\DQL\OverlapsI; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; use Chill\MainBundle\Doctrine\DQL\Replace; +use Chill\MainBundle\Doctrine\Type\NativeDateIntervalType; +use Chill\MainBundle\Doctrine\Type\PointType; use Symfony\Component\HttpFoundation\Request; /** @@ -167,37 +169,49 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface, $container->prependExtensionConfig('twig', $twigConfig); //add DQL function to ORM (default entity_manager) - $container->prependExtensionConfig('doctrine', array( - 'orm' => array( - 'dql' => array( - 'string_functions' => array( - 'unaccent' => Unaccent::class, - 'GET_JSON_FIELD_BY_KEY' => GetJsonFieldByKey::class, - 'AGGREGATE' => JsonAggregate::class, - 'REPLACE' => Replace::class, - ), - 'numeric_functions' => [ - 'JSONB_EXISTS_IN_ARRAY' => JsonbExistsInArray::class, - 'SIMILARITY' => Similarity::class, - 'OVERLAPSI' => OverlapsI::class - ] - ) - ) - )); + $container + ->prependExtensionConfig( + 'doctrine', + [ + 'orm' => [ + 'dql' => [ + 'string_functions' => [ + 'unaccent' => Unaccent::class, + 'GET_JSON_FIELD_BY_KEY' => GetJsonFieldByKey::class, + 'AGGREGATE' => JsonAggregate::class, + 'REPLACE' => Replace::class, + ], + 'numeric_functions' => [ + 'JSONB_EXISTS_IN_ARRAY' => JsonbExistsInArray::class, + 'SIMILARITY' => Similarity::class, + 'OVERLAPSI' => OverlapsI::class, + ], + ], + ], + ], + ); //add dbal types (default entity_manager) - $container->prependExtensionConfig('doctrine', array( - 'dbal' => [ - 'types' => [ - 'dateinterval' => [ - 'class' => \Chill\MainBundle\Doctrine\Type\NativeDateIntervalType::class - ], - 'point' => [ - 'class' => \Chill\MainBundle\Doctrine\Type\PointType::class - ] - ] - ] - )); + $container + ->prependExtensionConfig( + 'doctrine', + [ + 'dbal' => [ + // This is mandatory since we are using postgis as database. + 'mapping_types' => [ + 'geometry' => 'string', + ], + 'types' => [ + 'dateinterval' => [ + 'class' => NativeDateIntervalType::class + ], + 'point' => [ + 'class' => PointType::class + ] + ] + ] + ] + ); //add current route to chill main $container->prependExtensionConfig('chill_main', array( diff --git a/src/Bundle/ChillMainBundle/Serializer/Normalizer/CenterNormalizer.php b/src/Bundle/ChillMainBundle/Serializer/Normalizer/CenterNormalizer.php index 26182d936..f1681c60a 100644 --- a/src/Bundle/ChillMainBundle/Serializer/Normalizer/CenterNormalizer.php +++ b/src/Bundle/ChillMainBundle/Serializer/Normalizer/CenterNormalizer.php @@ -20,19 +20,32 @@ namespace Chill\MainBundle\Serializer\Normalizer; use Chill\MainBundle\Entity\Center; +use Chill\MainBundle\Repository\CenterRepository; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Symfony\Component\Serializer\Exception\InvalidArgumentException; +use Symfony\Component\Serializer\Exception\UnexpectedValueException; /** * * */ -class CenterNormalizer implements NormalizerInterface +class CenterNormalizer implements NormalizerInterface, DenormalizerInterface { + private CenterRepository $repository; + + + public function __construct(CenterRepository $repository) + { + $this->repository = $repository; + } + public function normalize($center, string $format = null, array $context = array()) { /** @var Center $center */ return [ 'id' => $center->getId(), + 'type' => 'center', 'name' => $center->getName() ]; } @@ -41,4 +54,30 @@ class CenterNormalizer implements NormalizerInterface { return $data instanceof Center; } + + public function denormalize($data, string $type, string $format = null, array $context = []) + { + if (FALSE === \array_key_exists('type', $data)) { + throw new InvalidArgumentException('missing "type" key in data'); + } + if ('center' !== $data['type']) { + throw new InvalidArgumentException('type should be equal to "center"'); + } + if (FALSE === \array_key_exists('id', $data)) { + throw new InvalidArgumentException('missing "id" key in data'); + } + + $center = $this->repository->find($data['id']); + + if (null === $center) { + throw new UnexpectedValueException("The type with id {$data['id']} does not exists"); + } + + return $center; + } + + public function supportsDenormalization($data, string $type, string $format = null) + { + return $type === Center::class; + } } diff --git a/src/Bundle/ChillMainBundle/chill.api.specs.yaml b/src/Bundle/ChillMainBundle/chill.api.specs.yaml index ada65b08a..68a3eb764 100644 --- a/src/Bundle/ChillMainBundle/chill.api.specs.yaml +++ b/src/Bundle/ChillMainBundle/chill.api.specs.yaml @@ -9,15 +9,14 @@ servers: description: "Your current dev server" components: - parameters: - _format: - name: _format - in: path - required: true - schema: - type: string - enum: - - json + schemas: + Center: + type: object + properties: + id: + type: integer + name: + type: string paths: /1.0/search.json: diff --git a/src/Bundle/ChillPersonBundle/Controller/ApiPersonController.php b/src/Bundle/ChillPersonBundle/Controller/ApiPersonController.php deleted file mode 100644 index bfaf22d7b..000000000 --- a/src/Bundle/ChillPersonBundle/Controller/ApiPersonController.php +++ /dev/null @@ -1,30 +0,0 @@ - - * - * 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\PersonBundle\Controller; - -use Symfony\Bundle\FrameworkBundle\Controller\Controller; -use Symfony\Component\HttpFoundation\JsonResponse; - - -class ApiPersonController extends Controller -{ - public function viewAction($id, $_format) - { - - } -} diff --git a/src/Bundle/ChillPersonBundle/Controller/PersonApiController.php b/src/Bundle/ChillPersonBundle/Controller/PersonApiController.php new file mode 100644 index 000000000..84f1ebf66 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Controller/PersonApiController.php @@ -0,0 +1,50 @@ + + * + * 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\PersonBundle\Controller; + +use Chill\PersonBundle\Security\Authorization\PersonVoter; +use Chill\MainBundle\Security\Authorization\AuthorizationHelper; +use Symfony\Component\Security\Core\Role\Role; +use Chill\MainBundle\CRUD\Controller\ApiController; +use Symfony\Component\HttpFoundation\Request; + + +class PersonApiController extends ApiController +{ + private AuthorizationHelper $authorizationHelper; + + /** + * @param AuthorizationHelper $authorizationHelper + */ + public function __construct(AuthorizationHelper $authorizationHelper) + { + $this->authorizationHelper = $authorizationHelper; + } + + protected function createEntity(string $action, Request $request): object + { + $person = parent::createEntity($action, $request); + + // TODO temporary hack to allow creation of person with fake center + $centers = $this->authorizationHelper->getReachableCenters($this->getUser(), + new Role(PersonVoter::CREATE)); + $person->setCenter($centers[0]); + + return $person; + } +} diff --git a/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadSocialActions.php b/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadSocialActions.php index c12156fc7..ec47c6e0b 100644 --- a/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadSocialActions.php +++ b/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadSocialActions.php @@ -37,32 +37,32 @@ class LoadSocialActions extends AbstractFixture implements OrderedFixtureInterfa return 10020; } - public static $socialActions = array( - 'social_action_info_conseil' => array( - 'title' => array( + public static $socialActions = [ + 'social_action_info_conseil' => [ + 'title' => [ 'fr' => 'Informer, conseiller' - ), + ], 'issue' => 'social_issue_prev_prot' - ), - 'social_action_instruire' => array( - 'title' => array( + ], + 'social_action_instruire' => [ + 'title' => [ 'fr' => 'Instruire l\'imprime unique pour des impayés' - ), + ], 'issue' => 'social_issue_prev_prot' - ), - 'social_action_MASP' => array( - 'title' => array( + ], + 'social_action_MASP' => [ + 'title' => [ 'fr' => 'MASP' - ), + ], 'issue' => 'social_issue_diff_fin' - ), - 'social_action_protection_enfant' => array( - 'title' => array( + ], + 'social_action_protection_enfant' => [ + 'title' => [ 'fr' => 'Protection Enfant confié dans le cadre judiciaire' - ), + ], 'issue' => 'social_issue_enfant_protection' - ), - ); + ], + ]; public function load(ObjectManager $manager) { diff --git a/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadSocialGoals.php b/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadSocialGoals.php index f71d6cf3a..a6c2b0ef0 100644 --- a/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadSocialGoals.php +++ b/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadSocialGoals.php @@ -38,20 +38,20 @@ class LoadSocialGoals extends AbstractFixture implements OrderedFixtureInterface return 10030; } - public static $socialGoals = array( - 'social_goal_instuire_dossier' => array( - 'title' => array( + public static $socialGoals = [ + 'social_goal_instuire_dossier' => [ + 'title' => [ 'fr' => 'Instruire le dossier de surendettement' - ), + ], 'action' => 'social_action_MASP' - ), - 'social_goal_proteger' => array( - 'title' => array( + ], + 'social_goal_proteger' => [ + 'title' => [ 'fr' => 'Protéger via une assistance educative placement' - ), + ], 'action' => 'social_action_protection_enfant' - ), - ); + ], + ]; public function load(ObjectManager $manager) { diff --git a/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadSocialIssues.php b/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadSocialIssues.php index c5b635f5b..eb9c303ed 100644 --- a/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadSocialIssues.php +++ b/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadSocialIssues.php @@ -37,36 +37,36 @@ class LoadSocialIssues extends AbstractFixture implements OrderedFixtureInterfac return 10010; } - public static $socialIssues = array( - 'social_issue_diff_fin_or_admin' => array( - 'title' => array( + public static $socialIssues = [ + 'social_issue_diff_fin_or_admin' => [ + 'title' => [ 'fr' => 'ADULTE - DIFFICULTES FINANCIERES ET/OU ADMINISTRATIVES' - ) - ), - 'social_issue_prev_prot' => array( - 'title' => array( + ] + ], + 'social_issue_prev_prot' => [ + 'title' => [ 'fr' => 'ADULTE PREVENTION/PROTECTION' - ), + ], 'parent' => 'social_issue_diff_fin_or_admin' - ), - 'social_issue_diff_fin' => array( - 'title' => array( + ], + 'social_issue_diff_fin' => [ + 'title' => [ 'fr' => 'Difficulté financière' - ), + ], 'parent' => 'social_issue_diff_fin_or_admin' - ), - 'social_issue_enfant_famille' => array( - 'title' => array( + ], + 'social_issue_enfant_famille' => [ + 'title' => [ 'fr' => 'Enfant / famille' - ) - ), - 'social_issue_enfant_protection' => array( - 'title' => array( + ] + ], + 'social_issue_enfant_protection' => [ + 'title' => [ 'fr' => 'enfant - protection' - ), + ], 'parent' => 'social_issue_enfant_famille' - ), - ); + ], + ]; public function load(ObjectManager $manager) { diff --git a/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadSocialResults.php b/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadSocialResults.php index 33cff5eea..573313573 100644 --- a/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadSocialResults.php +++ b/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadSocialResults.php @@ -38,34 +38,34 @@ class LoadSocialResults extends AbstractFixture implements OrderedFixtureInterfa return 10040; } - public static $socialResults = array( - 'social_result_FSL_acces' => array( - 'title' => array( + public static $socialResults = [ + 'social_result_FSL_acces' => [ + 'title' => [ 'fr' => 'FSL - accès cautionnement' - ), + ], 'action' => 'social_action_instruire' - ), - 'social_result_FSL_maintien' => array( - 'title' => array( + ], + 'social_result_FSL_maintien' => [ + 'title' => [ 'fr' => 'FSL maintien - impayés de loyer' - ), + ], 'action' => 'social_action_MASP' - ), - 'social_result_soutien_parental' => array( - 'title' => array( + ], + 'social_result_soutien_parental' => [ + 'title' => [ 'fr' => 'Soutien parental' - ), + ], // 'action' => 'social_action_protection_enfant', (via le goal) 'goal' => 'social_goal_proteger' - ), - 'social_result_accompagnement_mineur' => array( - 'title' => array( + ], + 'social_result_accompagnement_mineur' => [ + 'title' => [ 'fr' => 'Accompagnement du mineur' - ), + ], // 'action' => 'social_action_protection_enfant', (via le goal) 'goal' => 'social_goal_proteger', - ), - ); + ], + ]; public function load(ObjectManager $manager) { diff --git a/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php b/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php index 0d6346c01..27721012d 100644 --- a/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php +++ b/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php @@ -476,7 +476,6 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac 'class' => \Chill\PersonBundle\Entity\SocialWork\SocialIssue::class, 'name' => 'social_work_social_issue', 'base_path' => '/api/1.0/person/social-work/social-issue', -// 'controller' => \Chill\PersonBundle\Controller\OpeningApiController::class, 'base_role' => 'ROLE_USER', 'actions' => [ '_index' => [ @@ -493,6 +492,28 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac ], ] ], + [ + 'class' => \Chill\PersonBundle\Entity\Person::class, + 'name' => 'person', + 'base_path' => '/api/1.0/person/person', + 'base_role' => \Chill\PersonBundle\Security\Authorization\PersonVoter::SEE, + 'controller' => \Chill\PersonBundle\Controller\PersonApiController::class, + 'actions' => [ + '_entity' => [ + 'methods' => [ + Request::METHOD_GET => true, + Request::METHOD_HEAD => true, + Request::METHOD_POST=> true, + ], + 'roles' => [ + Request::METHOD_GET => \Chill\PersonBundle\Security\Authorization\PersonVoter::SEE, + Request::METHOD_HEAD => \Chill\PersonBundle\Security\Authorization\PersonVoter::SEE, + Request::METHOD_POST => \Chill\PersonBundle\Security\Authorization\PersonVoter::CREATE, + + ] + ], + ] + ], ] ]); } diff --git a/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonNormalizer.php b/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonNormalizer.php index 4a9f874de..98edcec14 100644 --- a/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonNormalizer.php +++ b/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonNormalizer.php @@ -18,7 +18,11 @@ */ namespace Chill\PersonBundle\Serializer\Normalizer; +use Chill\MainBundle\Entity\Center; use Chill\PersonBundle\Entity\Person; +use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface; +use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; @@ -27,6 +31,7 @@ use Symfony\Component\Serializer\Exception\RuntimeException; use Symfony\Component\Serializer\Exception\UnexpectedValueException; use Chill\MainBundle\Templating\Entity\ChillEntityRenderExtension; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; +use Symfony\Component\Serializer\Normalizer\ObjectToPopulateTrait; /** * Serialize a Person entity @@ -34,16 +39,24 @@ use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; */ class PersonNormalizer implements NormalizerInterface, - NormalizerAwareInterface + NormalizerAwareInterface, + DenormalizerInterface, + DenormalizerAwareInterface { - - protected NormalizerInterface $normalizer; - private ChillEntityRenderExtension $render; - public function __construct(ChillEntityRenderExtension $render) + private PersonRepository $repository; + + use NormalizerAwareTrait; + + use ObjectToPopulateTrait; + + use DenormalizerAwareTrait; + + public function __construct(ChillEntityRenderExtension $render, PersonRepository $repository) { $this->render = $render; + $this->repository = $repository; } public function normalize($person, string $format = null, array $context = array()) @@ -59,7 +72,9 @@ class PersonNormalizer implements 'center' => $this->normalizer->normalize($person->getCenter()), 'phonenumber' => $person->getPhonenumber(), 'mobilenumber' => $person->getMobilenumber(), - 'altNames' => $this->normalizeAltNames($person->getAltNames()) + 'altNames' => $this->normalizeAltNames($person->getAltNames()), + 'gender' => $person->getGender(), + 'gender_numeric' => $person->getGenderNumeric(), ]; } @@ -80,9 +95,50 @@ class PersonNormalizer implements return $data instanceof Person; } - - public function setNormalizer(NormalizerInterface $normalizer) + public function denormalize($data, string $type, string $format = null, array $context = []) { - $this->normalizer = $normalizer; + $person = $this->extractObjectToPopulate($type, $context); + + if (\array_key_exists('id', $data)) { + $person = $this->repository->find($data['id']); + + if (null === $person) { + throw new UnexpectedValueException("The person with id \"{$data['id']}\" does ". + "not exists"); + } + // currently, not allowed to update a person through api + // if instantiated with id + return $person; + } + + if (null === $person) { + $person = new Person(); + } + + foreach (['firstName', 'lastName', 'phonenumber', 'mobilenumber', 'gender'] + as $item) { + if (\array_key_exists($item, $data)) { + $person->{'set'.\ucfirst($item)}($data[$item]); + } + } + + foreach ([ + 'birthdate' => \DateTime::class, + 'center' => Center::class + ] as $item => $class) { + if (\array_key_exists($item, $data)) { + $object = $this->denormalizer->denormalize($data[$item], $class, $format, $context); + if ($object instanceof $class) { + $person->{'set'.\ucfirst($item)}($object); + } + } + } + + return $person; + } + + public function supportsDenormalization($data, string $type, string $format = null) + { + return $type === Person::class && ($data['type'] ?? NULL) === 'person'; } } diff --git a/src/Bundle/ChillPersonBundle/Tests/Controller/PersonApiControllerTest.php b/src/Bundle/ChillPersonBundle/Tests/Controller/PersonApiControllerTest.php new file mode 100644 index 000000000..d261f4294 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Tests/Controller/PersonApiControllerTest.php @@ -0,0 +1,83 @@ +getClientAuthenticated(); + + $client->request(Request::METHOD_GET, "/api/1.0/person/person/{$personId}.json"); + $response = $client->getResponse(); + + $this->assertEquals(403, $response->getStatusCode()); + } + + /** + * @dataProvider dataGetPersonFromCenterA + */ + public function testPersonGet($personId): void + { + $client = $this->getClientAuthenticated(); + + $client->request(Request::METHOD_GET, "/api/1.0/person/person/{$personId}.json"); + $response = $client->getResponse(); + + $this->assertResponseIsSuccessful(); + + $data = \json_decode($client->getResponse()->getContent(), true); + + $this->assertArrayHasKey('type', $data); + $this->assertArrayHasKey('id', $data); + $this->assertEquals('person', $data['type']); + $this->assertEquals($personId, $data['id']); + } + + public function dataGetPersonFromCenterA(): \Iterator + { + self::bootKernel(); + $em = self::$container->get(EntityManagerInterface::class); + $personIds= $em->createQuery("SELECT p.id FROM ".Person::class." p ". + "JOIN p.center c ". + "WHERE c.name = :center") + ->setParameter('center', 'Center A') + ->setMaxResults(100) + ->getScalarResult() + ; + + \shuffle($personIds); + + yield \array_pop($personIds); + yield \array_pop($personIds); + } + + public function dataGetPersonFromCenterB(): \Iterator + { + self::bootKernel(); + $em = self::$container->get(EntityManagerInterface::class); + $personIds= $em->createQuery("SELECT p.id FROM ".Person::class." p ". + "JOIN p.center c ". + "WHERE c.name = :center") + ->setParameter('center', 'Center B') + ->setMaxResults(100) + ->getScalarResult() + ; + + \shuffle($personIds); + + yield \array_pop($personIds); + yield \array_pop($personIds); + } +} diff --git a/src/Bundle/ChillPersonBundle/chill.api.specs.yaml b/src/Bundle/ChillPersonBundle/chill.api.specs.yaml index a5636d93e..c3067be55 100644 --- a/src/Bundle/ChillPersonBundle/chill.api.specs.yaml +++ b/src/Bundle/ChillPersonBundle/chill.api.specs.yaml @@ -41,6 +41,11 @@ components: properties: id: type: integer + readOnly: true + type: + type: string + enum: + - 'person' firstName: type: string lastName: @@ -48,12 +53,23 @@ components: text: type: string description: a canonical representation for the person name + readOnly: true birthdate: $ref: '#/components/schemas/Date' phonenumber: type: string mobilenumber: type: string + gender: + type: string + enum: + - man + - woman + - both + gender_numeric: + type: integer + description: a numerical representation of gender + readOnly: true PersonById: type: object properties: @@ -178,6 +194,53 @@ components: readOnly: true paths: + /1.0/person/person/{id}.json: + get: + tags: + - person + summary: Get a single person + parameters: + - name: id + in: path + required: true + description: The person's id + schema: + type: integer + format: integer + minimum: 1 + responses: + 200: + description: "OK" + content: + application/json: + schema: + $ref: "#/components/schemas/Person" + 403: + description: "Unauthorized" + /1.0/person/person.json: + post: + tags: + - person + summary: Create a single person + requestBody: + description: "A person" + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Person' + responses: + 200: + description: "OK" + content: + application/json: + schema: + $ref: "#/components/schemas/Person" + 403: + description: "Unauthorized" + 422: + description: "Invalid data: the data is a valid json, could be deserialized, but does not pass validation" + /1.0/person/social-work/social-issue.json: get: tags: diff --git a/src/Bundle/ChillPersonBundle/config/services/controller.yaml b/src/Bundle/ChillPersonBundle/config/services/controller.yaml index fd5e1f952..311950883 100644 --- a/src/Bundle/ChillPersonBundle/config/services/controller.yaml +++ b/src/Bundle/ChillPersonBundle/config/services/controller.yaml @@ -53,3 +53,9 @@ services: $validator: '@Symfony\Component\Validator\Validator\ValidatorInterface' $registry: '@Symfony\Component\Workflow\Registry' tags: ['controller.service_arguments'] + + Chill\PersonBundle\Controller\PersonApiController: + arguments: + $authorizationHelper: '@Chill\MainBundle\Security\Authorization\AuthorizationHelper' + tags: ['controller.service_arguments'] +