.. 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. 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 .. read-also:: * `How to use annotation to configure serialization `_ * `How to create your custom normalizer `_ 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 below): .. 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); } } .. api_full_config: 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