From 87e2ac93862d52dd3e2fe1c1b8f6ddbcaa79a45b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 13 May 2021 00:54:32 +0200 Subject: [PATCH] handling multi types and acc-period/repositories endpoint --- .../DiscriminatedObjectDenormalizer.php | 71 ++++++++++ .../AccompanyingCourseApiController.php | 51 ++++++- .../ChillPersonExtension.php | 12 ++ .../Entity/AccompanyingPeriod.php | 6 +- .../Entity/AccompanyingPeriod/Resource.php | 38 ++++- .../AccompanyingPeriodResourceNormalizer.php | 82 +++++++++++ .../AccompanyingPeriod/ResourceTest.php | 40 ++++++ .../ChillPersonBundle/chill.api.specs.yaml | 134 +++++++++++++++++- .../config/services/repository.yaml | 6 + 9 files changed, 434 insertions(+), 6 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/Serializer/Normalizer/DiscriminatedObjectDenormalizer.php create mode 100644 src/Bundle/ChillPersonBundle/Serializer/Normalizer/AccompanyingPeriodResourceNormalizer.php create mode 100644 src/Bundle/ChillPersonBundle/Tests/Entity/AccompanyingPeriod/ResourceTest.php diff --git a/src/Bundle/ChillMainBundle/Serializer/Normalizer/DiscriminatedObjectDenormalizer.php b/src/Bundle/ChillMainBundle/Serializer/Normalizer/DiscriminatedObjectDenormalizer.php new file mode 100644 index 000000000..25b2c5017 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Serializer/Normalizer/DiscriminatedObjectDenormalizer.php @@ -0,0 +1,71 @@ +denormalizer->supportsDenormalization($data, $localType, $format)) { + try { + return $this->denormalizer->denormalize($data, $localType, $format, $context); } catch (RuntimeException $e) { + $lastException = $e; + } + } + } + + throw new RuntimeException(sprintf("Could not find any denormalizer for those ". + "ALLOWED_TYPES: %s", \implode(", ", $context[self::ALLOWED_TYPES]))); + } + + /** + * {@inheritDoc} + */ + public function supportsDenormalization($data, string $type, string $format = null, array $context = []) + { + if (self::TYPE !== $type) { + return false; + } + + if (0 === count($context[self::ALLOWED_TYPES] ?? [])) { + throw new \LogicException("The context should contains a list of + allowed types"); + } + + foreach ($context[self::ALLOWED_TYPES] as $localType) { + if ($this->denormalizer->supportsDenormalization($data, $localType, $format)) { + return true; + } + } + + return false; + } + +} diff --git a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseApiController.php b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseApiController.php index e9c31fd5d..046af597d 100644 --- a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseApiController.php +++ b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseApiController.php @@ -48,7 +48,6 @@ class AccompanyingCourseApiController extends ApiController break; case Request::METHOD_DELETE: $participation = $accompanyingPeriod->removePerson($person); - $participation->setEndDate(new \DateTimeImmutable('now')); break; default: throw new BadRequestException("This method is not supported"); @@ -66,6 +65,56 @@ class AccompanyingCourseApiController extends ApiController return $this->json($participation, 200, [], ['groups' => [ 'read' ]]); } + public function resourceApi($id, Request $request, string $_format): Response + { + return $this->addRemoveSomething('resource', $id, $request, $_format, 'resource', + \Chill\PersonBundle\Entity\AccompanyingPeriod\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 { /** @var AccompanyingPeriod $accompanyingPeriod */ diff --git a/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php b/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php index 230207f6b..7c54ad90f 100644 --- a/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php +++ b/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php @@ -342,6 +342,18 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac Request::METHOD_DELETE=> \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE ] ], + 'resource' => [ + 'methods' => [ + Request::METHOD_POST => true, + Request::METHOD_DELETE => true, + Request::METHOD_GET => false, + Request::METHOD_HEAD => false, + ], + 'roles' => [ + Request::METHOD_POST => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE, + Request::METHOD_DELETE=> \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE + ] + ], 'requestor' => [ 'methods' => [ Request::METHOD_POST => true, diff --git a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php index 6882210e4..dab4b1b71 100644 --- a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php +++ b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php @@ -231,7 +231,9 @@ class AccompanyingPeriod * * @ORM\OneToMany( * targetEntity="Chill\PersonBundle\Entity\AccompanyingPeriod\Resource", - * mappedBy="accompanyingPeriod" + * mappedBy="accompanyingPeriod", + * cascade={"persist", "remove"}, + * orphanRemoval=true * ) * @Groups({"read"}) */ @@ -692,6 +694,7 @@ class AccompanyingPeriod public function addResource(Resource $resource): self { + $resource->setAccompanyingPeriod($this); $this->resources[] = $resource; return $this; @@ -699,6 +702,7 @@ class AccompanyingPeriod public function removeResource(Resource $resource): void { + $resource->setAccompanyingPeriod(null); $this->resources->removeElement($resource); } diff --git a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/Resource.php b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/Resource.php index ec13fcad6..38ff33af6 100644 --- a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/Resource.php +++ b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/Resource.php @@ -28,10 +28,15 @@ use Chill\PersonBundle\Entity\AccompanyingPeriod\Comment; use Chill\PersonBundle\Entity\Person; use Chill\ThirdPartyBundle\Entity\ThirdParty; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\DiscriminatorMap; +use Symfony\Component\Serializer\Annotation\Groups; /** * @ORM\Entity(repositoryClass=ResourceRepository::class) * @ORM\Table(name="chill_person_accompanying_period_resource") + * @DiscriminatorMap(typeProperty="type", mapping={ + * "accompanying_period_resource"=Resource::class + * }) */ class Resource { @@ -39,6 +44,7 @@ class Resource * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") + * @Groups({"read"}) */ private $id; @@ -91,7 +97,7 @@ class Resource return $this->thirdParty; } - public function setThirdParty(?ThirdParty $thirdParty): self + private function setThirdParty(?ThirdParty $thirdParty): self { $this->thirdParty = $thirdParty; @@ -103,7 +109,7 @@ class Resource return $this->person; } - public function setPerson(?Person $person): self + private function setPerson(?Person $person): self { $this->person = $person; @@ -121,9 +127,35 @@ class Resource return $this; } + + /** + * + * @param $resource Person|ThirdParty + */ + public function setResource($resource): self + { + if ($resource instanceof ThirdParty) { + $this->setThirdParty($resource); + $this->setPerson(NULL); + } elseif ($resource instanceof Person) { + $this->setPerson($resource); + $this->setThirdParty(NULL); + } elseif (NULL === $resource) { + $this->setPerson(NULL); + $this->setThirdParty(NULL); + } else { + throw new \UnexpectedValueException(sprintf("the resource ". + "should be an instance of %s or %s", Person::class, + ThirdParty::class)); + } + + return $this; + } + /** - * @return Person|ThirdParty + * @return ThirdParty|Person + * @Groups({"read", "write"}) */ public function getResource() { diff --git a/src/Bundle/ChillPersonBundle/Serializer/Normalizer/AccompanyingPeriodResourceNormalizer.php b/src/Bundle/ChillPersonBundle/Serializer/Normalizer/AccompanyingPeriodResourceNormalizer.php new file mode 100644 index 000000000..0ac49c905 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Serializer/Normalizer/AccompanyingPeriodResourceNormalizer.php @@ -0,0 +1,82 @@ +repository = $repository; + } + + public function denormalize($data, string $type, string $format = null, array $context = []) + { + $resource = $this->extractObjectToPopulate($type, $context); + + if ('accompanying_period_resource' !== ($data['type'] ?? NULL)) { + throw new Exception\InvalidArgumentException("the key type must be present in data and set to 'accompanying_period_resource'"); + } + + if ($resource === NULL && \array_key_exists('id', $data)) { + $resource = $this->repository->find($data['id']); + + if (NULL === $resource) { + throw new Exception\UnexpectedValueException(sprintf("the resource with". + "id %d is not found", $data['id'])); + } + + // if resource found, available only for read-only + if (count($data) > 2) { + unset($data['id']); + unset($data['type']); + throw new Exception\ExtraAttributesException($data); + } + } + + if ($resource === NULL) { + $resource = new Resource(); + } + + if (\array_key_exists('resource', $data)) { + $res = $this->denormalizer->denormalize( + $data['resource'], + DiscriminatedObjectDenormalizer::TYPE, + $format, + \array_merge( + $context, + [ + DiscriminatedObjectDenormalizer::ALLOWED_TYPES => + [ + Person::class, ThirdParty::class + ] + ] + ) + ); + + $resource->setResource($res); + } + + return $resource; + } + + + public function supportsDenormalization($data, string $type, string $format = null) + { + return $type === Resource::class; + } +} diff --git a/src/Bundle/ChillPersonBundle/Tests/Entity/AccompanyingPeriod/ResourceTest.php b/src/Bundle/ChillPersonBundle/Tests/Entity/AccompanyingPeriod/ResourceTest.php new file mode 100644 index 000000000..2fef00ec6 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Tests/Entity/AccompanyingPeriod/ResourceTest.php @@ -0,0 +1,40 @@ +setResource($person); + + $this->assertSame($person, $resource->getResource()); + $this->assertNull($resource->getThirdParty()); + + $resource->setResource($thirdParty); + + $this->assertSame($thirdParty, $resource->getResource()); + $this->assertNull($resource->getPerson()); + + // we repeat adding a person, to ensure that third party is + // well reset + $resource->setResource($person); + $this->assertSame($person, $resource->getResource()); + $this->assertNull($resource->getThirdParty()); + + $resource->setResource(null); + $this->assertNull($resource->getThirdParty()); + $this->assertNull($resource->getPerson()); + $this->assertNull($resource->getResource()); + } +} diff --git a/src/Bundle/ChillPersonBundle/chill.api.specs.yaml b/src/Bundle/ChillPersonBundle/chill.api.specs.yaml index fdec7f92f..e6029c366 100644 --- a/src/Bundle/ChillPersonBundle/chill.api.specs.yaml +++ b/src/Bundle/ChillPersonBundle/chill.api.specs.yaml @@ -1,5 +1,29 @@ components: schemas: + Date: + type: object + properties: + datetime: + type: string + format: date-time + Person: + type: object + properties: + id: + type: integer + firstName: + type: string + lastName: + type: string + text: + type: string + description: a canonical representation for the person name + birthdate: + $ref: '#/components/schemas/Date' + phonenumber: + type: string + mobilenumber: + type: string PersonById: type: object properties: @@ -12,6 +36,12 @@ components: required: - id - type + # should go to third party + ThirdParty: + type: object + properties: + text: + type: string ThirdPartyById: type: object properties: @@ -35,7 +65,33 @@ components: type: integer requestorAnonymous: type: boolean - + Resource: + type: object + properties: + type: + type: string + enum: + - 'accompanying_period_resource' + readOnly: true + id: + type: integer + readOnly: true + resource: + anyOf: + - $ref: '#/components/schemas/PersonById' + - $ref: '#/components/schemas/ThirdPartyById' + ResourceById: + type: object + properties: + id: + type: integer + type: + type: string + enum: + - 'accompanying_period_resource' + required: + - id + - type paths: /1.0/person/accompanying-course/{id}.json: @@ -213,3 +269,79 @@ paths: description: "OK" 422: description: "object with validation errors" + + /1.0/person/accompanying-course/{id}/resource.json: + post: + tags: + - person + summary: "Add a resource to the accompanying course" + parameters: + - name: id + in: path + required: true + description: The accompanying period's id + schema: + type: integer + format: integer + minimum: 1 + requestBody: + description: "A resource" + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Resource' + examples: + add person with id 50: + summary: "a person with id 50" + value: + type: accompanying_period_resource + resource: + type: person + id: 50 + add thirdparty with id 100: + summary: "a third party with id 100" + value: + type: accompanying_period_resource + resource: + type: thirdparty + id: 100 + + responses: + 401: + description: "Unauthorized" + 404: + description: "Not found" + 200: + description: "OK" + 422: + description: "object with validation errors" + delete: + tags: + - person + summary: "Remove the resource" + parameters: + - name: id + in: path + required: true + description: The accompanying period's id + schema: + type: integer + format: integer + minimum: 1 + requestBody: + description: "A resource" + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ResourceById' + responses: + 401: + description: "Unauthorized" + 404: + description: "Not found" + 200: + description: "OK" + 422: + description: "object with validation errors" diff --git a/src/Bundle/ChillPersonBundle/config/services/repository.yaml b/src/Bundle/ChillPersonBundle/config/services/repository.yaml index b99402bcf..7163b9d65 100644 --- a/src/Bundle/ChillPersonBundle/config/services/repository.yaml +++ b/src/Bundle/ChillPersonBundle/config/services/repository.yaml @@ -1,5 +1,11 @@ services: + # temporary, waiting for https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/34 + Chill\PersonBundle\Repository\AccompanyingPeriod\: + resource: './../../Repository/AccompanyingPeriod' + autowire: true + tags: [ doctrine.repository_service ] + chill.person.repository.person: class: Chill\PersonBundle\Repository\PersonRepository factory: ['@doctrine.orm.entity_manager', getRepository]