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