diff --git a/src/Bundle/ChillMainBundle/CRUD/Controller/AbstractCRUDController.php b/src/Bundle/ChillMainBundle/CRUD/Controller/AbstractCRUDController.php index df7d065cb..71476fb78 100644 --- a/src/Bundle/ChillMainBundle/CRUD/Controller/AbstractCRUDController.php +++ b/src/Bundle/ChillMainBundle/CRUD/Controller/AbstractCRUDController.php @@ -40,6 +40,20 @@ class AbstractCRUDController extends AbstractController return $e; } + /** + * Create an entity. + * + * @param string $action + * @param Request $request + * @return object + */ + protected function createEntity(string $action, Request $request): object + { + $type = $this->getEntityClass(); + + return new $type; + } + /** * Count the number of entities * diff --git a/src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php b/src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php index 14b0473da..9686a7b98 100644 --- a/src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php +++ b/src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php @@ -85,11 +85,75 @@ class ApiController extends AbstractCRUDController 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, $_format); + default: + throw new \Symfony\Component\HttpFoundation\Exception\BadRequestException("This method is not implemented"); + } + } + 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"); } } + 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) + ); + } public function entityPut($action, Request $request, $id, string $_format): Response { $entity = $this->getEntity($action, $id, $request, $_format); @@ -407,6 +471,7 @@ class ApiController extends AbstractCRUDController 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"); diff --git a/src/Bundle/ChillMainBundle/CRUD/Routing/CRUDRoutesLoader.php b/src/Bundle/ChillMainBundle/CRUD/Routing/CRUDRoutesLoader.php index 8cbf81c5a..d48973350 100644 --- a/src/Bundle/ChillMainBundle/CRUD/Routing/CRUDRoutesLoader.php +++ b/src/Bundle/ChillMainBundle/CRUD/Routing/CRUDRoutesLoader.php @@ -189,6 +189,20 @@ class CRUDRoutesLoader extends Loader "or allow, at least, one method"); } + if ('_entity' === $name && \in_array(Request::METHOD_POST, $methods)) { + unset($methods[\array_search(Request::METHOD_POST, $methods)]); + $entityPostRoute = $this->createEntityPostRoute($name, $crudConfig, $action, + $controller, $requirements); + $collection->add("chill_api_single_{$crudConfig['name']}_{$name}_create", + $entityPostRoute); + } + + if (count($methods) === 0) { + // the only method was POST, + // continue to next + continue; + } + $route = new Route($path, $defaults, $requirements); $route->setMethods($methods); @@ -197,4 +211,17 @@ class CRUDRoutesLoader extends Loader return $collection; } + + private function createEntityPostRoute(string $name, $crudConfig, array $action, $controller, $requirements): Route + { + $localPath = $action['path'].'.{_format}'; + $defaults = [ + '_controller' => $controller.':'.($action['controller_action'] ?? 'entityPost') + ]; + $path = $crudConfig['base_path'].$localPath; + $route = new Route($path, $defaults, $requirements); + $route->setMethods([ Request::METHOD_POST ]); + + return $route; + } } diff --git a/src/Bundle/ChillMainBundle/Serializer/Normalizer/CenterNormalizer.php b/src/Bundle/ChillMainBundle/Serializer/Normalizer/CenterNormalizer.php index 26182d936..f1681c60a 100644 --- a/src/Bundle/ChillMainBundle/Serializer/Normalizer/CenterNormalizer.php +++ b/src/Bundle/ChillMainBundle/Serializer/Normalizer/CenterNormalizer.php @@ -20,19 +20,32 @@ namespace Chill\MainBundle\Serializer\Normalizer; use Chill\MainBundle\Entity\Center; +use Chill\MainBundle\Repository\CenterRepository; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Symfony\Component\Serializer\Exception\InvalidArgumentException; +use Symfony\Component\Serializer\Exception\UnexpectedValueException; /** * * */ -class CenterNormalizer implements NormalizerInterface +class CenterNormalizer implements NormalizerInterface, DenormalizerInterface { + private CenterRepository $repository; + + + public function __construct(CenterRepository $repository) + { + $this->repository = $repository; + } + public function normalize($center, string $format = null, array $context = array()) { /** @var Center $center */ return [ 'id' => $center->getId(), + 'type' => 'center', 'name' => $center->getName() ]; } @@ -41,4 +54,30 @@ class CenterNormalizer implements NormalizerInterface { return $data instanceof Center; } + + public function denormalize($data, string $type, string $format = null, array $context = []) + { + if (FALSE === \array_key_exists('type', $data)) { + throw new InvalidArgumentException('missing "type" key in data'); + } + if ('center' !== $data['type']) { + throw new InvalidArgumentException('type should be equal to "center"'); + } + if (FALSE === \array_key_exists('id', $data)) { + throw new InvalidArgumentException('missing "id" key in data'); + } + + $center = $this->repository->find($data['id']); + + if (null === $center) { + throw new UnexpectedValueException("The type with id {$data['id']} does not exists"); + } + + return $center; + } + + public function supportsDenormalization($data, string $type, string $format = null) + { + return $type === Center::class; + } } diff --git a/src/Bundle/ChillMainBundle/chill.api.specs.yaml b/src/Bundle/ChillMainBundle/chill.api.specs.yaml index ada65b08a..68a3eb764 100644 --- a/src/Bundle/ChillMainBundle/chill.api.specs.yaml +++ b/src/Bundle/ChillMainBundle/chill.api.specs.yaml @@ -9,15 +9,14 @@ servers: description: "Your current dev server" components: - parameters: - _format: - name: _format - in: path - required: true - schema: - type: string - enum: - - json + schemas: + Center: + type: object + properties: + id: + type: integer + name: + type: string paths: /1.0/search.json: diff --git a/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php b/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php index 8851ad6d6..1112cdfcd 100644 --- a/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php +++ b/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php @@ -501,11 +501,13 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac '_entity' => [ 'methods' => [ Request::METHOD_GET => true, - Request::METHOD_HEAD => true + Request::METHOD_HEAD => true, + Request::METHOD_POST=> true, ], 'roles' => [ Request::METHOD_GET => \Chill\PersonBundle\Security\Authorization\PersonVoter::SEE, Request::METHOD_HEAD => \Chill\PersonBundle\Security\Authorization\PersonVoter::SEE, + Request::METHOD_POST => \Chill\PersonBundle\Security\Authorization\PersonVoter::CREATE, ] ], diff --git a/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonNormalizer.php b/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonNormalizer.php index 4a9f874de..c4b6a7118 100644 --- a/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonNormalizer.php +++ b/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonNormalizer.php @@ -18,7 +18,11 @@ */ namespace Chill\PersonBundle\Serializer\Normalizer; +use Chill\MainBundle\Entity\Center; use Chill\PersonBundle\Entity\Person; +use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface; +use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; @@ -27,6 +31,7 @@ use Symfony\Component\Serializer\Exception\RuntimeException; use Symfony\Component\Serializer\Exception\UnexpectedValueException; use Chill\MainBundle\Templating\Entity\ChillEntityRenderExtension; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; +use Symfony\Component\Serializer\Normalizer\ObjectToPopulateTrait; /** * Serialize a Person entity @@ -34,13 +39,18 @@ use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; */ class PersonNormalizer implements NormalizerInterface, - NormalizerAwareInterface + NormalizerAwareInterface, + DenormalizerInterface, + DenormalizerAwareInterface { - - protected NormalizerInterface $normalizer; - private ChillEntityRenderExtension $render; + use NormalizerAwareTrait; + + use ObjectToPopulateTrait; + + use DenormalizerAwareTrait; + public function __construct(ChillEntityRenderExtension $render) { $this->render = $render; @@ -59,7 +69,9 @@ class PersonNormalizer implements 'center' => $this->normalizer->normalize($person->getCenter()), 'phonenumber' => $person->getPhonenumber(), 'mobilenumber' => $person->getMobilenumber(), - 'altNames' => $this->normalizeAltNames($person->getAltNames()) + 'altNames' => $this->normalizeAltNames($person->getAltNames()), + 'gender' => $person->getGender(), + 'gender_numeric' => $person->getGenderNumeric(), ]; } @@ -80,9 +92,39 @@ class PersonNormalizer implements return $data instanceof Person; } - - public function setNormalizer(NormalizerInterface $normalizer) + public function denormalize($data, string $type, string $format = null, array $context = []) { - $this->normalizer = $normalizer; + $person = $this->extractObjectToPopulate($type, $context); + + if (null === $person) { + $person = new Person(); + } + + foreach (['firstName', 'lastName', 'phonenumber', 'mobilenumber', 'gender'] + as $item) { + if (\array_key_exists($item, $data)) { + $person->{'set'.\ucfirst($item)}($data[$item]); + } + } + + foreach ([ + 'birthdate' => \DateTime::class, + 'center' => Center::class + ] as $item => $class) { + if (\array_key_exists($item, $data)) { + $object = $this->denormalizer->denormalize($data[$item], $class, $format, $context); + dump($item, $data, $object, $class); + if ($object instanceof $class) { + $person->{'set'.\ucfirst($item)}($object); + } + } + } + + return $person; + } + + public function supportsDenormalization($data, string $type, string $format = null) + { + return $type === Person::class && ($data['type'] ?? NULL) === 'person'; } } diff --git a/src/Bundle/ChillPersonBundle/chill.api.specs.yaml b/src/Bundle/ChillPersonBundle/chill.api.specs.yaml index 111c55b32..c3067be55 100644 --- a/src/Bundle/ChillPersonBundle/chill.api.specs.yaml +++ b/src/Bundle/ChillPersonBundle/chill.api.specs.yaml @@ -41,6 +41,11 @@ components: properties: id: type: integer + readOnly: true + type: + type: string + enum: + - 'person' firstName: type: string lastName: @@ -48,12 +53,23 @@ components: text: type: string description: a canonical representation for the person name + readOnly: true birthdate: $ref: '#/components/schemas/Date' phonenumber: type: string mobilenumber: type: string + gender: + type: string + enum: + - man + - woman + - both + gender_numeric: + type: integer + description: a numerical representation of gender + readOnly: true PersonById: type: object properties: @@ -201,6 +217,29 @@ paths: $ref: "#/components/schemas/Person" 403: description: "Unauthorized" + /1.0/person/person.json: + post: + tags: + - person + summary: Create a single person + requestBody: + description: "A person" + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Person' + responses: + 200: + description: "OK" + content: + application/json: + schema: + $ref: "#/components/schemas/Person" + 403: + description: "Unauthorized" + 422: + description: "Invalid data: the data is a valid json, could be deserialized, but does not pass validation" /1.0/person/social-work/social-issue.json: get: