getMethod()) { Request::METHOD_GET, Request::METHOD_HEAD => $this->entityGet('_entity', $request, $id, $_format), Request::METHOD_PUT, Request::METHOD_PATCH => $this->entityPut('_entity', $request, $id, $_format), Request::METHOD_POST => $this->entityPostAction('_entity', $request, $id), Request::METHOD_DELETE => $this->entityDelete('_entity', $request, $id, $_format), default => throw new BadRequestHttpException('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 { return match ($request->getMethod()) { Request::METHOD_POST => $this->entityPostAction('_entity', $request, $_format), default => throw new BadRequestHttpException('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 BadRequestHttpException('invalid json', $e, 400); } $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()->getManagerForClass($this->getEntityClass())->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) { return match ($request->getMethod()) { Request::METHOD_GET, Request::METHOD_HEAD => $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) * * @throw BadRequestException if unable to deserialize the posted data * @throw BadRequestException if the method is not POST or DELETE */ protected function addRemoveSomething(string $action, mixed $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 BadRequestHttpException(sprintf('Unable to deserialize posted data: %s', $e->getMessage()), $e, 0); } match ($request->getMethod()) { /* @phpstan-ignore-next-line */ Request::METHOD_DELETE => $entity->{'remove'.\ucfirst($property)}($postedData), /* @phpstan-ignore-next-line */ Request::METHOD_POST => $entity->{'add'.\ucfirst($property)}($postedData), default => throw new BadRequestHttpException('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::METHOD_POST === $request->getMethod()) { $this->getDoctrine()->getManager()->persist($postedData); } $this->getDoctrine()->getManager()->flush(); $response = $this->onAfterFlush($action, $request, $_format, $entity, $errors, [$postedData]); if ($response instanceof Response) { return $response; } return match ($request->getMethod()) { Request::METHOD_DELETE => $this->json('', Response::HTTP_OK), Request::METHOD_POST => $this->json( $postedData, Response::HTTP_OK, [], $this->getContextForSerializationPostAlter($action, $request, $_format, $entity, [$postedData]) ), default => 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` */ protected function entityGet(string $action, Request $request, mixed $id, mixed $_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 BadRequestHttpException('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 BadRequestHttpException('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 { return match ($request->getMethod()) { Request::METHOD_GET => ['groups' => ['read']], Request::METHOD_PUT, Request::METHOD_PATCH, Request::METHOD_POST => ['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, mixed $entity, array $more = []): array { return ['groups' => ['read']]; } 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 */ protected function indexApiAction($action, Request $request, mixed $_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. */ protected function serializeCollection(string $action, Request $request, string $_format, PaginatorInterface $paginator, mixed $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); } }