diff --git a/src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php b/src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php index f4dfc0fc5..14b0473da 100644 --- a/src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php +++ b/src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php @@ -42,11 +42,6 @@ class ApiController extends AbstractCRUDController 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; @@ -100,7 +95,6 @@ class ApiController extends AbstractCRUDController $entity = $this->getEntity($action, $id, $request, $_format); $postFetch = $this->onPostFetchEntity($action, $request, $entity, $_format); - if ($postFetch instanceof Response) { return $postFetch; } @@ -147,6 +141,11 @@ class ApiController extends AbstractCRUDController $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, @@ -155,7 +154,13 @@ class ApiController extends AbstractCRUDController ); } - protected function onAfterValidation(string $action, Request $request, string $_format, $entity, ConstraintViolationListInterface $errors): ?Response + 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; } @@ -165,13 +170,16 @@ class ApiController extends AbstractCRUDController return null; } - protected function validate(string $action, Request $request, string $_format, $entity): ConstraintViolationListInterface + 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 = []; @@ -273,6 +281,110 @@ class ApiController extends AbstractCRUDController 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. flush the data + * 10. run onAfterFlush + * 11. 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) + * @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, $postedDataContext = []): 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); + } + + $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 @@ -307,7 +419,7 @@ class ApiController extends AbstractCRUDController * * This is called **after** the entity was altered. */ - protected function getContextForSerializationPostAlter(string $action, Request $request, string $_format, $entity): array + protected function getContextForSerializationPostAlter(string $action, Request $request, string $_format, $entity, array $more = []): array { return [ 'groups' => [ 'read' ]]; } diff --git a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseApiController.php b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseApiController.php index 046af597d..c6cc0a515 100644 --- a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseApiController.php +++ b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseApiController.php @@ -14,6 +14,7 @@ use Chill\PersonBundle\Privacy\AccompanyingPeriodPrivacyEvent; use Chill\PersonBundle\Entity\Person; use Chill\ThirdPartyBundle\Entity\ThirdParty; use Symfony\Component\Serializer\Exception\RuntimeException; +use Chill\PersonBundle\Entity\AccompanyingPeriod\Resource; class AccompanyingCourseApiController extends ApiController { @@ -67,53 +68,9 @@ class AccompanyingCourseApiController extends ApiController public function resourceApi($id, Request $request, string $_format): Response { - return $this->addRemoveSomething('resource', $id, $request, $_format, 'resource', - \Chill\PersonBundle\Entity\AccompanyingPeriod\Resource::class); + return $this->addRemoveSomething('resource', $id, $request, $_format, 'resource', Resource::class); } - public function addRemoveSomething(string $action, $id, Request $request, string $_format, $property, string $postedDataType, $postedDataContext = []): Response - { - $entity = $this->getEntity($action, $id, $request); - - $this->checkACL($action, $request, $_format, $entity); - $this->onPostCheckACL($action, $request, $_format, $entity); - - 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->validator->validate($entity); - - if ($errors->count() > 0) { - // only format accepted - return $this->json($errors, 422); - } - - $this->getDoctrine()->getManager()->flush(); - - switch ($request->getMethod()) { - case Request::METHOD_DELETE: - return $this->json('', 202); - case Request::METHOD_POST: - return $this->json($postedData, 200, [], ['groups' => [ 'read' ]]); - } - } - public function requestorApi($id, Request $request, string $_format): Response { diff --git a/src/Bundle/ChillPersonBundle/Tests/Controller/AccompanyingCourseApiControllerTest.php b/src/Bundle/ChillPersonBundle/Tests/Controller/AccompanyingCourseApiControllerTest.php index 7c60c899a..bdeb5dba6 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Controller/AccompanyingCourseApiControllerTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Controller/AccompanyingCourseApiControllerTest.php @@ -33,6 +33,7 @@ use Chill\MainBundle\Entity\Center; use Doctrine\Common\Collections\Criteria; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\Request; +use Chill\PersonBundle\Entity\AccompanyingPeriod\Resource; /** * Test api for AccompanyingCourseControllerTest @@ -86,6 +87,84 @@ class AccompanyingCourseApiControllerTest extends WebTestCase $this->assertEquals(404, $response->getStatusCode(), "Test that the response of rest api has a status code 'not found' (404)"); } + /** + * @dataProvider dataGenerateRandomRequestorValidData + */ + public function testResourceWithValidData(AccompanyingPeriod $period, $personId, $thirdPartyId) + { + $em = self::$container->get(EntityManagerInterface::class); + + // post a person + $this->client->request( + Request::METHOD_POST, + sprintf('/api/1.0/person/accompanying-course/%d/resource.json', $period->getId()), + [], // parameters + [], // files + [], // server parameters + \json_encode([ 'type' => 'accompanying_period_resource', 'resource' => [ 'type' => 'person', 'id' => $personId ]]) + ); + $response = $this->client->getResponse(); + $data = \json_decode($response->getContent(), true); + + $this->assertEquals(200, $response->getStatusCode()); + $this->assertArrayHasKey('id', $data); + $this->assertEquals($personId, $data['resource']['id']); + + // check into database + $resource = $em->getRepository(Resource::class) + ->find($data['id']); + $this->assertInstanceOf(Resource::class, $resource); + $this->assertInstanceOf(Person::class, $resource->getResource()); + $this->assertEquals($personId, $resource->getResource()->getId()); + + // remove the resource + $this->client->request( + Request::METHOD_DELETE, + sprintf('/api/1.0/person/accompanying-course/%d/requestor.json', $period->getId()), + [], + [], + [], //server + \json_encode([ 'type' => 'accompanying_period_resource', 'id' => $resource->getId()]) + ); + $response = $this->client->getResponse(); + $this->assertEquals(200, $response->getStatusCode()); + + // post a third party + $this->client->request( + Request::METHOD_POST, + sprintf('/api/1.0/person/accompanying-course/%d/resource.json', $period->getId()), + [], // parameters + [], // files + [], // server parameters + \json_encode([ 'type' => 'accompanying_period_resource', 'resource' => [ 'type' => 'thirdparty', 'id' => $thirdPartyId ]]) + ); + $response = $this->client->getResponse(); + $data = \json_decode($response->getContent(), true); + + $this->assertEquals(200, $response->getStatusCode()); + $this->assertArrayHasKey('id', $data); + $this->assertEquals($thirdPartyId, $data['resource']['id']); + + // check into database + $resource = $em->getRepository(Resource::class) + ->find($data['id']); + $this->assertInstanceOf(Resource::class, $resource); + $this->assertInstanceOf(ThirdParty::class, $resource->getResource()); + $this->assertEquals($thirdPartyId, $resource->getResource()->getId()); + + // remove the resource + $this->client->request( + Request::METHOD_DELETE, + sprintf('/api/1.0/person/accompanying-course/%d/requestor.json', $period->getId()), + [], + [], + [], //server + \json_encode([ 'type' => 'accompanying_period_resource', 'id' => $resource->getId()]) + ); + $response = $this->client->getResponse(); + $this->assertEquals(200, $response->getStatusCode()); + } + /** * @dataProvider dataGenerateRandomRequestorValidData */