getEntity($action, $id, $request, $_format); $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 ($_format === 'json') { $context = $this->getContextForSerialization($action, $request, $_format, $entity); return $this->json($entity, Response::HTTP_OK, [], $context); } else { throw new \Symfony\Component\HttpFoundation\Exception\BadRequestException("This format is not implemented"); } } public function onBeforeSerialize(string $action, Request $request, $_format, $entity): ?Response { return null; } /** * Base method for handling api action * * @return void */ public function entityApi(Request $request, $id, $_format): 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, $_format); 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 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); $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) ); } public function entityDelete($action, Request $request, $id, string $_format): Response { $entity = $this->getEntity($action, $id, $request, $_format); 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); } protected function onAfterValidation(string $action, Request $request, string $_format, $entity, ConstraintViolationListInterface $errors, array $more = []): ?Response { return null; } protected function onAfterFlush(string $action, Request $request, string $_format, $entity, ConstraintViolationListInterface $errors, array $more = []): ?Response { return null; } protected function getValidationGroups(string $action, Request $request, string $_format, $entity): ?array { return null; } 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); } /** * Deserialize the content of the request into the class associated with the curd */ 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); } /** * 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. Serialize the entities in a Collection, using `SerializeCollection` * * @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->serializeCollection($action, $request, $_format, $paginator, $entities); } /** * 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 Request $request * @param string $_format * @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) * @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]) ); } } /** * Serialize collections * */ 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 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. */ protected function getContextForSerializationPostAlter(string $action, Request $request, string $_format, $entity, array $more = []): array { return [ 'groups' => [ 'read' ]]; } /** * get the role given from the config. */ 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'); } }