.. Copyright (C)  2014 Champs Libres Cooperative SCRLFS
   Permission is granted to copy, distribute and/or modify this document
   under the terms of the GNU Free Documentation License, Version 1.3
   or any later version published by the Free Software Foundation;
   with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.
   A copy of the license is included in the section entitled "GNU
   Free Documentation License".

.. _api:

API
###

Chill provides a basic framework to build REST api.

Basic configuration
*******************

Configure a route
=================

Follow those steps to build a REST api:

1. Create your model;
2. Configure the API;

You can also:

* hook into the controller to customize some steps;
* add more route and steps

.. note::

    Useful links:

    * `How to use annotation to configure serialization <https://symfony.com/doc/current/serializer.html>`_
    * `How to create your custom normalizer <https://symfony.com/doc/current/serializer/custom_normalizer.html>`_

Auto-loading the routes
=======================

Ensure that those lines are present in your file `app/config/routing.yml`:


.. code-block:: yaml

   chill_cruds:
       resource: 'chill_main_crud_route_loader:load'
       type: service


Create your model
=================

Create your model on the usual way:

.. code-block:: php

   namespace Chill\PersonBundle\Entity\AccompanyingPeriod;

   use Chill\PersonBundle\Entity\AccompanyingPeriod\OriginRepository;
   use Doctrine\ORM\Mapping as ORM;

   /**
    * @ORM\Entity(repositoryClass=OriginRepository::class)
    * @ORM\Table(name="chill_person_accompanying_period_origin")
    */
   class Origin
   {
       /**
        * @ORM\Id
        * @ORM\GeneratedValue
        * @ORM\Column(type="integer")
        */
       private $id;

       /**
        * @ORM\Column(type="json")
        */
       private $label;

       /**
        * @ORM\Column(type="date_immutable", nullable=true)
        */
       private $noActiveAfter;

       // .. getters and setters

   }


Configure api
=============

Configure the api using Yaml (see the full configuration: :ref:`api_full_configuration`):

.. code-block:: yaml

   # config/packages/chill_main.yaml
   chill_main:
       apis:
           accompanying_period_origin:
               base_path: '/api/1.0/person/accompanying-period/origin'
               class: 'Chill\PersonBundle\Entity\AccompanyingPeriod\Origin'
               name: accompanying_period_origin
               base_role: 'ROLE_USER'
               actions:
                   _index:
                       methods:
                           GET: true
                           HEAD: true
                   _entity:
                       methods:
                           GET: true
                           HEAD: true

.. note::

   If you are working on a shared bundle (aka "The chill bundles"), you should define your configuration inside the class :code:`ChillXXXXBundleExtension`, using the "prependConfig" feature:

   .. code-block:: php

      namespace Chill\PersonBundle\DependencyInjection;

      use Symfony\Component\DependencyInjection\ContainerBuilder;
      use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
      use Symfony\Component\HttpFoundation\Request;

      /**
       * Class ChillPersonExtension
       * Loads and manages your bundle configuration
       *
       * To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html}
       * @package Chill\PersonBundle\DependencyInjection
       */
      class ChillPersonExtension extends Extension implements PrependExtensionInterface
      {
          public function prepend(ContainerBuilder $container)
          {
              $this->prependCruds($container);
          }

          /**
           * @param ContainerBuilder $container
           */
          protected function prependCruds(ContainerBuilder $container)
          {
              $container->prependExtensionConfig('chill_main', [
                  'apis' => [
                      [
                          'class' => \Chill\PersonBundle\Entity\AccompanyingPeriod\Origin::class,
                          'name' => 'accompanying_period_origin',
                          'base_path' => '/api/1.0/person/accompanying-period/origin',
                          'controller' => \Chill\PersonBundle\Controller\OpeningApiController::class,
                          'base_role' => 'ROLE_USER',
                          'actions' => [
                              '_index' => [
                                  'methods' => [ 
                                      Request::METHOD_GET => true,
                                      Request::METHOD_HEAD => true
                                  ],
                              ],
                              '_entity' => [
                                  'methods' => [ 
                                      Request::METHOD_GET => true,
                                      Request::METHOD_HEAD => true
                                  ]
                              ],
                          ]
                      ]
                  ]
              ]);
          }
      }

The :code:`_index` and :code:`_entity` action
*********************************************

The :code:`_index` and :code:`_entity` action are default actions:

* they will call a specific method in the default controller;
* they will generate defined routes:

Index:
   Name: :code:`chill_api_single_accompanying_period_origin__index`

   Path: :code:`/api/1.0/person/accompanying-period/origin.{_format}`

Entity:
   Name: :code:`chill_api_single_accompanying_period_origin__entity`

   Path: :code:`/api/1.0/person/accompanying-period/origin/{id}.{_format}`

Role
****

By default, the key `base_role` is used to check ACL. Take care of creating the :code:`Voter` required to take that into account.

For index action, the role will be called with :code:`NULL` as :code:`$subject`. The retrieved entity will be the subject for single queries.

You can also define a role for each method. In this case, this role is used for the given method, and, if any, the base role is taken into account.

.. code-block:: yaml

   # config/packages/chill_main.yaml
   chill_main:
       apis:
           accompanying_period_origin:
               base_path: '/api/1.0/person/bla/bla'
               class: 'Chill\PersonBundle\Entity\Blah'
               name: bla
               actions:
                   _entity:
                       methods:
                           GET: true
                           HEAD: true
                       roles:
                           GET: MY_ROLE_SEE
                           HEAD: MY ROLE_SEE

Customize the controller
************************

You can customize the controller by hooking into the default actions. Take care of extending :code:`Chill\MainBundle\CRUD\Controller\ApiController`.


.. code-block:: php


   namespace Chill\PersonBundle\Controller;

   use Chill\MainBundle\CRUD\Controller\ApiController;
   use Symfony\Component\HttpFoundation\Request;
   use Symfony\Component\HttpFoundation\Response;

   class OpeningApiController extends ApiController
   {
       protected function customizeQuery(string $action, Request $request, $qb): void
       {
           $qb->where($qb->expr()->gt('e.noActiveAfter', ':now'))
               ->orWhere($qb->expr()->isNull('e.noActiveAfter'));
           $qb->setParameter('now', new \DateTime('now'));
       }     
   }

And set your controller in configuration:

.. code-block:: yaml

   chill_main:
       apis:
           accompanying_period_origin:
               base_path: '/api/1.0/person/accompanying-period/origin'
               class: 'Chill\PersonBundle\Entity\AccompanyingPeriod\Origin'
               name: accompanying_period_origin
               # add a controller
               controller: 'Chill\PersonBundle\Controller\OpeningApiController'
               base_role: 'ROLE_USER'
               actions:
                   _index:
                       methods:
                           GET: true
                           HEAD: true
                   _entity:
                       methods:
                           GET: true
                           HEAD: true

Create your own actions
***********************

You can add your own actions:

.. code-block:: yaml

   chill_main:
       apis:
           -
               class: Chill\PersonBundle\Entity\AccompanyingPeriod
               name: accompanying_course
               base_path: /api/1.0/person/accompanying-course
               controller: Chill\PersonBundle\Controller\AccompanyingCourseApiController
               actions:
                   # add a custom participation:
                   participation:
                       methods:
                           POST: true
                           DELETE: true
                           GET: false
                           HEAD: false
                           PUT: false
                       roles:
                           POST: CHILL_PERSON_ACCOMPANYING_PERIOD_SEE
                           DELETE: CHILL_PERSON_ACCOMPANYING_PERIOD_SEE
                           GET: null
                           HEAD: null
                           PUT: null
                       single-collection: single

The key :code:`single-collection` with value :code:`single` will add a :code:`/{id}/ + "action name"` (in this example, :code:`/{id}/participation`) into the path, after the base path. If the value is :code:`collection`, no id will be set, but the action name will be append to the path.

Then, create the corresponding action into your controller:

.. code-block:: php

   namespace Chill\PersonBundle\Controller;

   use Chill\MainBundle\CRUD\Controller\ApiController;
   use Symfony\Component\HttpFoundation\Request;
   use Symfony\Component\HttpFoundation\Response;
   use Chill\PersonBundle\Entity\AccompanyingPeriod;
   use Symfony\Component\HttpFoundation\Exception\BadRequestException;
   use Symfony\Component\EventDispatcher\EventDispatcherInterface;
   use Symfony\Component\Validator\Validator\ValidatorInterface;
   use Chill\PersonBundle\Privacy\AccompanyingPeriodPrivacyEvent;
   use Chill\PersonBundle\Entity\Person;

   class AccompanyingCourseApiController extends ApiController
   {
       protected EventDispatcherInterface $eventDispatcher;

       protected ValidatorInterface $validator;

       public function __construct(EventDispatcherInterface $eventDispatcher, $validator)
       {
           $this->eventDispatcher = $eventDispatcher;
           $this->validator = $validator;
       }
       
       public function participationApi($id, Request $request, $_format)
       {
           /** @var AccompanyingPeriod $accompanyingPeriod */ 
           $accompanyingPeriod = $this->getEntity('participation', $id, $request);
           $person = $this->getSerializer()
               ->deserialize($request->getContent(), Person::class, $_format, []);

           if (NULL === $person) {
               throw new BadRequestException('person id not found');
           }

           $this->onPostCheckACL('participation', $request, $accompanyingPeriod, $_format);

           switch ($request->getMethod()) {
               case Request::METHOD_POST:
                   $participation = $accompanyingPeriod->addPerson($person);
                   break;
               case Request::METHOD_DELETE:
                   $participation = $accompanyingPeriod->removePerson($person);
                   break;
               default:
                   throw new BadRequestException("This method is not supported");
           }

           $errors = $this->validator->validate($accompanyingPeriod);

           if ($errors->count() > 0) {
               // only format accepted
               return $this->json($errors);
           }

           $this->getDoctrine()->getManager()->flush();

           return $this->json($participation);
       }
   }

Managing association
********************

ManyToOne association
=====================

In ManyToOne association, you can add associated entities using the :code:`PATCH` request. By default, the serializer deserialize entities only with their id and discriminator type, if any.

Example:

.. code-block:: bash

   curl -X 'PATCH' \
     'http://localhost:8001/api/1.0/person/accompanying-course/2668.json' \
     -H 'accept: */*' \
     -H 'Content-Type: application/json' \
      # see the data sent to the server: \
     -d '{
     "type": "accompanying_period",
     "id": 2668,
     "origin": { "id": 11 }
   }'

ManyToMany associations
=======================

In OneToMany association, you can easily create route for adding and removing entities, using :code:`POST` and :code:`DELETE` requests.

Prepare your entity, creating the methods :code:`addYourEntity` and :code:`removeYourEntity`:

.. code-block:: php

   namespace Chill\PersonBundle\Entity;

   use Chill\MainBundle\Entity\Scope;
   use Doctrine\Common\Collections\ArrayCollection;
   use Doctrine\Common\Collections\Collection;
   use Doctrine\ORM\Mapping as ORM;
   use Symfony\Component\Serializer\Annotation\Groups;
   use Symfony\Component\Serializer\Annotation\DiscriminatorMap;

   /**
    * AccompanyingPeriod Class
    *
    * @ORM\Entity
    * @ORM\Table(name="chill_person_accompanying_period")
    * @DiscriminatorMap(typeProperty="type", mapping={
    *  "accompanying_period"=AccompanyingPeriod::class
    *  })
    */
   class AccompanyingPeriod
   {
       /**
        * @var Collection
        * @ORM\ManyToMany(
        *     targetEntity=Scope::class,
        *     cascade={}
        *     )
        * @Groups({"read"})
        */
       private $scopes;

       public function addScope(Scope $scope): self
       {
           $this->scopes[] = $scope;

           return $this;
       }

       public function removeScope(Scope $scope): void
       {
           $this->scopes->removeElement($scope);
       }


Create your route into the configuration:

.. code-block:: yaml

   chill_main:
       apis:
           -
               class: Chill\PersonBundle\Entity\AccompanyingPeriod
               name: accompanying_course
               base_path: /api/1.0/person/accompanying-course
               controller: Chill\PersonBundle\Controller\AccompanyingCourseApiController
               actions:
                   scope:
                       methods:
                           POST: true
                           DELETE: true
                           GET: false
                           HEAD: false
                           PUT: false
                           PATCH: false
                       roles:
                           POST: CHILL_PERSON_ACCOMPANYING_PERIOD_SEE
                           DELETE: CHILL_PERSON_ACCOMPANYING_PERIOD_SEE
                           GET: null
                           HEAD: null
                           PUT: null
                           PATCH: null
                       controller_action: null
                       path: null
                       single-collection: single

This will create a new route, which will accept two methods: DELETE and POST:

.. code-block:: raw

   +--------------+---------------------------------------------------------------------------------------+
   | Property     | Value                                                                                 |
   +--------------+---------------------------------------------------------------------------------------+
   | Route Name   | chill_api_single_accompanying_course_scope                                            |
   | Path         | /api/1.0/person/accompanying-course/{id}/scope.{_format}                              |
   | Path Regex   | {^/api/1\.0/person/accompanying\-course/(?P<id>[^/]++)/scope\.(?P<_format>[^/]++)$}sD |
   | Host         | ANY                                                                                   |
   | Host Regex   |                                                                                       |
   | Scheme       | ANY                                                                                   |
   | Method       | POST|DELETE                                                                           |
   | Requirements | {id}: \d+                                                                             |
   | Class        | Symfony\Component\Routing\Route                                                       |
   | Defaults     | _controller: csapi_accompanying_course_controller:scopeApi                            |
   | Options      | compiler_class: Symfony\Component\Routing\RouteCompiler                               |
   +--------------+---------------------------------------------------------------------------------------+



Then, create the controller action. Call the method:

.. code-block:: php

   namespace Chill\PersonBundle\Controller;

   use Chill\MainBundle\CRUD\Controller\ApiController;
   use Symfony\Component\HttpFoundation\Request;
   use Symfony\Component\HttpFoundation\Response;
   use Chill\MainBundle\Entity\Scope;

   class MyController extends ApiController
   {
       public function scopeApi($id, Request $request, string $_format): Response
       {
           return $this->addRemoveSomething('scope', $id, $request, $_format, 'scope', Scope::class, [ 'groups' => [ 'read' ] ]);
       }
   }

This will allow to add a scope by his id, and delete them.

Curl requests:

.. code-block:: bash

   # add a scope with id 5
   curl -X 'POST' \
     'http://localhost:8001/api/1.0/person/accompanying-course/2868/scope.json' \
     -H 'accept: */*' \
     -H 'Content-Type: application/json' \
     -d '{
     "type": "scope",
     "id": 5
   }'

   # remove a scope with id 5
   curl -X 'DELETE' \
     'http://localhost:8001/api/1.0/person/accompanying-course/2868/scope.json' \
     -H 'accept: */*' \
     -H 'Content-Type: application/json' \
     -d '{
     "id": 5,
     "type": "scope"
   }'

Deserializing an association where multiple types are allowed
=============================================================

Sometimes, multiples types are allowed as association to one entity:

.. code-block:: php

   namespace Chill\PersonBundle\Entity\AccompanyingPeriod;

   use Chill\PersonBundle\Entity\Person;
   use Chill\ThirdPartyBundle\Entity\ThirdParty;
   use Doctrine\ORM\Mapping as ORM;

   class Resource
   {


       /**
        * @ORM\ManyToOne(targetEntity=ThirdParty::class)
        * @ORM\JoinColumn(nullable=true)
        */
       private $thirdParty;

       /**
        * @ORM\ManyToOne(targetEntity=Person::class)
        * @ORM\JoinColumn(nullable=true)
        */
       private $person;


       /**
        *
        * @param $resource Person|ThirdParty
        */
       public function setResource($resource): self
       {
          // ...
       }
       
       
       /**
        * @return ThirdParty|Person
        * @Groups({"read", "write"})
        */
       public function getResource()
       {
           return $this->person ?? $this->thirdParty;
       }
   }

This is not well taken into account by the Symfony serializer natively.

You must, then, create your own CustomNormalizer. You can help yourself using this:

.. code-block:: 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 = [])
       {
           // .. snipped for brevity

           if ($resource === NULL) {
               $resource = new Resource();
           }

           if (\array_key_exists('resource', $data)) {
               $res = $this->denormalizer->denormalize(
                   $data['resource'],
                   // call for a "multiple type"
                   DiscriminatedObjectDenormalizer::TYPE,
                   $format,
                   // into the context, we add the list of allowed types:
                   [ 
                       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;
       }  
   }

Serialization for collection
****************************

A specific model has been defined for returning collection:

.. code-block:: json

   {
       "count": 49,
       "results": [
       ],
       "pagination": {
           "more": true,
           "next": "/api/1.0/search.json&q=xxxx......&page=2",
           "previous": null,
           "first": 0,
           "items_per_page": 1
       }
   }

Where this is relevant, this model should be re-used in custom controller actions.

In custom actions, this can be achieved quickly by assembling results into a :code:`Chill\MainBundle\Serializer\Model\Collection`. The pagination information is given by using :code:`Paginator` (see :ref:`Pagination <pagination-ref>`).

.. code-block:: php

   use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
   use Chill\MainBundle\Pagination\PaginatorInterface;

   class MyController extends AbstractController
   {

       protected function serializeCollection(PaginatorInterface $paginator, $entities): Response
       {
           $model = new Collection($entities, $paginator);

           return $this->json($model, Response::HTTP_OK, [], $context);
       }
   }


.. _api_full_configuration:

Full configuration example
**************************

.. code-block:: yaml

       apis:
           -
               class: Chill\PersonBundle\Entity\AccompanyingPeriod
               name: accompanying_course
               base_path: /api/1.0/person/accompanying-course
               controller: Chill\PersonBundle\Controller\AccompanyingCourseApiController
               actions:
                   _entity:
                       roles:
                           GET: CHILL_PERSON_ACCOMPANYING_PERIOD_SEE
                           HEAD: null
                           POST: null
                           DELETE: null
                           PUT: null
                       controller_action: null
                       path: null
                       single-collection: single
                       methods:
                           GET: true
                           HEAD: true
                           POST: false
                           DELETE: false
                           PUT: false
                   participation:
                       methods:
                           POST: true
                           DELETE: true
                           GET: false
                           HEAD: false
                           PUT: false
                       roles:
                           POST: CHILL_PERSON_ACCOMPANYING_PERIOD_SEE
                           DELETE: CHILL_PERSON_ACCOMPANYING_PERIOD_SEE
                           GET: null
                           HEAD: null
                           PUT: null
                       controller_action: null
                       # the requirements for the route. Will be set to `[ 'id' => '\d+' ]` if left empty.
                       requirements:         []
                       path: null
                       single-collection: single
               base_role: null


