handling multi types and acc-period/repositories endpoint

This commit is contained in:
Julien Fastré 2021-05-13 00:54:32 +02:00
parent 4a04628d5b
commit 87e2ac9386
9 changed files with 434 additions and 6 deletions

View File

@ -0,0 +1,71 @@
<?php
namespace Chill\MainBundle\Serializer\Normalizer;
use Symfony\Component\Serializer\Normalizer\ContextAwareDenormalizerInterface;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
use Symfony\Component\Serializer\Exception\RuntimeException;
/**
* Denormalize an object given a list of supported class
*/
class DiscriminatedObjectDenormalizer implements ContextAwareDenormalizerInterface, DenormalizerAwareInterface
{
use DenormalizerAwareTrait;
/**
* The type to set for enabling this type
*/
public const TYPE = '@multi';
/**
* Should be present in context and contains an array of
* allowed types.
*/
public const ALLOWED_TYPES = 'denormalize_multi.allowed_types';
/**
* {@inheritDoc}
*/
public function denormalize($data, string $type, string $format = null, array $context = [])
{
foreach ($context[self::ALLOWED_TYPES] as $localType) {
if ($this->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;
}
}

View File

@ -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 */

View File

@ -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,

View File

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

View File

@ -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()
{

View File

@ -0,0 +1,82 @@
<?php
namespace Chill\PersonBundle\Serializer\Normalizer;
use Chill\PersonBundle\Entity\Person;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Resource;
use Chill\PersonBundle\Repository\AccompanyingPeriod\ResourceRepository;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\ObjectToPopulateTrait;
use Symfony\Component\Serializer\Exception;
use Chill\MainBundle\Serializer\Normalizer\DiscriminatedObjectDenormalizer;
class AccompanyingPeriodResourceNormalizer implements DenormalizerInterface, DenormalizerAwareInterface
{
use DenormalizerAwareTrait;
use ObjectToPopulateTrait;
public function __construct(ResourceRepository $repository)
{
$this->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;
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace Chill\PersonBundle\Tests\Entity\AccompanyingPeriod;
use PHPUnit\Framework\TestCase;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Resource;
use Chill\PersonBundle\Entity\Person;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
class ResourceTest extends TestCase
{
public function testSetResource()
{
$person = new Person();
$thirdParty = new ThirdParty();
$resource = new Resource();
$resource->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());
}
}

View File

@ -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"

View File

@ -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]