mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
653 lines
21 KiB
PHP
653 lines
21 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Chill is a software for social workers
|
|
*
|
|
* For the full copyright and license information, please view
|
|
* the LICENSE file that was distributed with this source code.
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Chill\MainBundle\CRUD\Controller;
|
|
|
|
use Chill\MainBundle\Pagination\PaginatorInterface;
|
|
use Chill\MainBundle\Serializer\Model\Collection;
|
|
use Exception;
|
|
use LogicException;
|
|
use RuntimeException;
|
|
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
use Symfony\Component\Serializer\Exception\NotEncodableValueException;
|
|
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
|
use Symfony\Component\Serializer\SerializerInterface;
|
|
use Symfony\Component\Validator\ConstraintViolationListInterface;
|
|
|
|
use function array_merge;
|
|
use function ucfirst;
|
|
|
|
class ApiController extends AbstractCRUDController
|
|
{
|
|
/**
|
|
* Base method for handling api action.
|
|
*
|
|
* @param mixed $id
|
|
* @param string $_format
|
|
*
|
|
* @return void
|
|
*/
|
|
public function entityApi(Request $request, $id, ?string $_format = 'json'): Response
|
|
{
|
|
switch ($request->getMethod()) {
|
|
case Request::METHOD_GET:
|
|
case Request::METHOD_HEAD:
|
|
return $this->entityGet('_entity', $request, $id, $_format);
|
|
|
|
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);
|
|
|
|
case Request::METHOD_DELETE:
|
|
return $this->entityDelete('_entity', $request, $id, $_format);
|
|
|
|
default:
|
|
throw new \Symfony\Component\HttpFoundation\Exception\BadRequestException('This method is not implemented');
|
|
}
|
|
}
|
|
|
|
public function entityDelete($action, Request $request, $id, string $_format): Response
|
|
{
|
|
$entity = $this->getEntity($action, $id, $request);
|
|
|
|
if (null === $entity) {
|
|
throw $this->createNotFoundException(sprintf('The %s with id %s '
|
|
. 'is not found', $this->getCrudName(), $id));
|
|
}
|
|
|
|
$response = $this->checkACL($action, $request, $_format, $entity);
|
|
|
|
if ($response instanceof Response) {
|
|
return $response;
|
|
}
|
|
|
|
$response = $this->onPostCheckACL($action, $request, $_format, $entity);
|
|
|
|
if ($response instanceof Response) {
|
|
return $response;
|
|
}
|
|
|
|
$response = $this->onBeforeSerialize($action, $request, $_format, $entity);
|
|
|
|
if ($response instanceof Response) {
|
|
return $response;
|
|
}
|
|
|
|
$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;
|
|
}
|
|
|
|
$this->getDoctrine()->getManager()->remove($entity);
|
|
$this->getDoctrine()->getManager()->flush();
|
|
|
|
$response = $this->onAfterFlush($action, $request, $_format, $entity, $errors);
|
|
|
|
if ($response instanceof Response) {
|
|
return $response;
|
|
}
|
|
|
|
return $this->json(Response::HTTP_OK);
|
|
}
|
|
|
|
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');
|
|
}
|
|
}
|
|
|
|
public function entityPut($action, Request $request, $id, string $_format): Response
|
|
{
|
|
$entity = $this->getEntity($action, $id, $request);
|
|
|
|
$postFetch = $this->onPostFetchEntity($action, $request, $entity, $_format);
|
|
|
|
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, $request, $_format, $entity);
|
|
|
|
if ($response instanceof Response) {
|
|
return $response;
|
|
}
|
|
|
|
$response = $this->onPostCheckACL($action, $request, $_format, $entity);
|
|
|
|
if ($response instanceof Response) {
|
|
return $response;
|
|
}
|
|
|
|
$response = $this->onBeforeSerialize($action, $request, $_format, $entity);
|
|
|
|
if ($response instanceof Response) {
|
|
return $response;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
$this->getDoctrine()->getManager()->flush();
|
|
|
|
$response = $this->onAfterFlush($action, $request, $_format, $entity, $errors);
|
|
|
|
if ($response instanceof Response) {
|
|
return $response;
|
|
}
|
|
|
|
return $this->json(
|
|
$entity,
|
|
Response::HTTP_OK,
|
|
[],
|
|
$this->getContextForSerializationPostAlter($action, $request, $_format, $entity)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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');
|
|
}
|
|
}
|
|
|
|
public function onBeforeSerialize(string $action, Request $request, $_format, $entity): ?Response
|
|
{
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Add or remove an associated entity, using `add` and `remove` methods.
|
|
*
|
|
* This method:
|
|
*
|
|
* 1. Fetch the base entity (throw 404 if not found)
|
|
* 2. checkACL,
|
|
* 3. run onPostCheckACL, return response if any,
|
|
* 4. deserialize posted data into the entity given by $postedDataType, with the context in $postedDataContext
|
|
* 5. run 'add+$property' for POST method, or 'remove+$property' for DELETE method
|
|
* 6. validate the base entity (not the deserialized one). Groups are fetched from getValidationGroups, validation is perform by `validate`
|
|
* 7. run onAfterValidation
|
|
* 8. if errors, return a 422 response with errors
|
|
* 9. if $forcePersist === true, persist the entity
|
|
* 10. flush the data
|
|
* 11. run onAfterFlush
|
|
* 12. return a 202 response for DELETE with empty body, or HTTP 200 for post with serialized posted entity
|
|
*
|
|
* @param string action
|
|
* @param mixed id
|
|
* @param string $property the name of the property. This will be used to make a `add+$property` and `remove+$property` method
|
|
* @param string $postedDataType the type of the posted data (the content)
|
|
* @param string $postedDataContext a context to deserialize posted data (the content)
|
|
* @param bool $forcePersist force to persist the created element (only for POST request)
|
|
* @param mixed $id
|
|
* @throw BadRequestException if unable to deserialize the posted data
|
|
* @throw BadRequestException if the method is not POST or DELETE
|
|
*/
|
|
protected function addRemoveSomething(string $action, $id, Request $request, string $_format, string $property, string $postedDataType, array $postedDataContext = [], bool $forcePersist = false): Response
|
|
{
|
|
$entity = $this->getEntity($action, $id, $request);
|
|
|
|
$postFetch = $this->onPostFetchEntity($action, $request, $entity, $_format);
|
|
|
|
if ($postFetch instanceof Response) {
|
|
return $postFetch;
|
|
}
|
|
|
|
$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;
|
|
}
|
|
|
|
$response = $this->onBeforeSerialize($action, $request, $_format, $entity);
|
|
|
|
if ($response instanceof Response) {
|
|
return $response;
|
|
}
|
|
|
|
try {
|
|
$postedData = $this->getSerializer()->deserialize($request->getContent(), $postedDataType, $_format, $postedDataContext);
|
|
} catch (\Symfony\Component\Serializer\Exception\UnexpectedValueException $e) {
|
|
throw new BadRequestException(sprintf('Unable to deserialize posted ' .
|
|
'data: %s', $e->getMessage()), 0, $e);
|
|
}
|
|
|
|
switch ($request->getMethod()) {
|
|
case Request::METHOD_DELETE:
|
|
// oups... how to use property accessor to remove element ?
|
|
$entity->{'remove' . ucfirst($property)}($postedData);
|
|
|
|
break;
|
|
|
|
case Request::METHOD_POST:
|
|
$entity->{'add' . ucfirst($property)}($postedData);
|
|
|
|
break;
|
|
|
|
default:
|
|
throw new BadRequestException('this method is not supported');
|
|
}
|
|
|
|
$errors = $this->validate($action, $request, $_format, $entity, [$postedData]);
|
|
|
|
$response = $this->onAfterValidation($action, $request, $_format, $entity, $errors, [$postedData]);
|
|
|
|
if ($response instanceof Response) {
|
|
return $response;
|
|
}
|
|
|
|
if ($errors->count() > 0) {
|
|
// only format accepted
|
|
return $this->json($errors, 422);
|
|
}
|
|
|
|
if ($forcePersist && $request->getMethod() === Request::METHOD_POST) {
|
|
$this->getDoctrine()->getManager()->persist($postedData);
|
|
}
|
|
|
|
$this->getDoctrine()->getManager()->flush();
|
|
|
|
$response = $this->onAfterFlush($action, $request, $_format, $entity, $errors, [$postedData]);
|
|
|
|
if ($response instanceof Response) {
|
|
return $response;
|
|
}
|
|
|
|
switch ($request->getMethod()) {
|
|
case Request::METHOD_DELETE:
|
|
return $this->json('', Response::HTTP_OK);
|
|
|
|
case Request::METHOD_POST:
|
|
return $this->json(
|
|
$postedData,
|
|
Response::HTTP_OK,
|
|
[],
|
|
$this->getContextForSerializationPostAlter($action, $request, $_format, $entity, [$postedData])
|
|
);
|
|
}
|
|
|
|
throw new Exception('Unable to handle such request method.');
|
|
}
|
|
|
|
/**
|
|
* Deserialize the content of the request into the class associated with the curd.
|
|
*
|
|
* @param mixed|null $entity
|
|
*/
|
|
protected function deserialize(string $action, Request $request, string $_format, $entity = null): object
|
|
{
|
|
$default = [];
|
|
|
|
if (null !== $entity) {
|
|
$default[AbstractNormalizer::OBJECT_TO_POPULATE] = $entity;
|
|
}
|
|
|
|
$context = array_merge(
|
|
$default,
|
|
$this->getContextForSerialization($action, $request, $_format, $entity)
|
|
);
|
|
|
|
return $this->getSerializer()->deserialize($request->getContent(), $this->getEntityClass(), $_format, $context);
|
|
}
|
|
|
|
/**
|
|
* 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. Serialize the entity and return the result. The serialization context is given by `getSerializationContext`
|
|
*
|
|
* @param mixed $id
|
|
* @param mixed $_format
|
|
*/
|
|
protected function entityGet(string $action, Request $request, $id, $_format = 'html'): Response
|
|
{
|
|
$entity = $this->getEntity($action, $id, $request);
|
|
|
|
$postFetch = $this->onPostFetchEntity($action, $request, $entity, $_format);
|
|
|
|
if ($postFetch instanceof Response) {
|
|
return $postFetch;
|
|
}
|
|
|
|
$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;
|
|
}
|
|
|
|
$response = $this->onBeforeSerialize($action, $request, $_format, $entity);
|
|
|
|
if ($response instanceof Response) {
|
|
return $response;
|
|
}
|
|
|
|
if ('json' === $_format) {
|
|
$context = $this->getContextForSerialization($action, $request, $_format, $entity);
|
|
|
|
return $this->json($entity, Response::HTTP_OK, [], $context);
|
|
}
|
|
|
|
throw new \Symfony\Component\HttpFoundation\Exception\BadRequestException('This format 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)
|
|
);
|
|
}
|
|
|
|
protected function getContextForSerialization(string $action, Request $request, string $_format, $entity): array
|
|
{
|
|
switch ($request->getMethod()) {
|
|
case Request::METHOD_GET:
|
|
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');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the context for serialization post alter query (in case of
|
|
* PATCH, PUT, or POST method).
|
|
*
|
|
* This is called **after** the entity was altered.
|
|
*
|
|
* @param mixed $entity
|
|
*/
|
|
protected function getContextForSerializationPostAlter(string $action, Request $request, string $_format, $entity, array $more = []): array
|
|
{
|
|
return ['groups' => ['read']];
|
|
}
|
|
|
|
/**
|
|
* get the role given from the config.
|
|
*
|
|
* @param mixed $entity
|
|
* @param mixed $_format
|
|
*/
|
|
protected function getRoleFor(string $action, Request $request, $entity, $_format): string
|
|
{
|
|
$actionConfig = $this->getActionConfig($action);
|
|
|
|
if (null !== $actionConfig['roles'][$request->getMethod()]) {
|
|
return $actionConfig['roles'][$request->getMethod()];
|
|
}
|
|
|
|
if ($this->crudConfig['base_role']) {
|
|
return $this->crudConfig['base_role'];
|
|
}
|
|
|
|
throw new RuntimeException(sprintf('the config does not have any role for the ' .
|
|
'method %s nor a global role for the whole action. Add those to your ' .
|
|
'configuration or override the required method', $request->getMethod()));
|
|
}
|
|
|
|
protected function getSerializer(): SerializerInterface
|
|
{
|
|
return $this->get('serializer');
|
|
}
|
|
|
|
protected function getValidationGroups(string $action, Request $request, string $_format, $entity): ?array
|
|
{
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* 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. Serialize the entities in a Collection, using `SerializeCollection`
|
|
*
|
|
* @param string $action
|
|
* @param mixed $_format
|
|
*/
|
|
protected function indexApiAction($action, Request $request, $_format)
|
|
{
|
|
$this->onPreIndex($action, $request, $_format);
|
|
|
|
$response = $this->checkACL($action, $request, $_format);
|
|
|
|
if ($response instanceof Response) {
|
|
return $response;
|
|
}
|
|
|
|
$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->serializeCollection($action, $request, $_format, $paginator, $entities);
|
|
}
|
|
|
|
protected function onAfterFlush(string $action, Request $request, string $_format, $entity, ConstraintViolationListInterface $errors, array $more = []): ?Response
|
|
{
|
|
return null;
|
|
}
|
|
|
|
protected function onAfterValidation(string $action, Request $request, string $_format, $entity, ConstraintViolationListInterface $errors, array $more = []): ?Response
|
|
{
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Serialize collections.
|
|
*
|
|
* @param mixed $entities
|
|
*/
|
|
protected function serializeCollection(string $action, Request $request, string $_format, PaginatorInterface $paginator, $entities): Response
|
|
{
|
|
$model = new Collection($entities, $paginator);
|
|
|
|
$context = $this->getContextForSerialization($action, $request, $_format, $entities);
|
|
|
|
return $this->json($model, Response::HTTP_OK, [], $context);
|
|
}
|
|
|
|
protected function validate(string $action, Request $request, string $_format, $entity, array $more = []): ConstraintViolationListInterface
|
|
{
|
|
$validationGroups = $this->getValidationGroups($action, $request, $_format, $entity);
|
|
|
|
return $this->getValidator()->validate($entity, null, $validationGroups);
|
|
}
|
|
}
|