Symfony как использовать FOSRestBundle

от автора

В данном посте я бы хотел рассказать о том, как нужно правильно выстраивать RESTfull API для AngularJS и других фронтенд фреймворков с бекендом на Symfony.
И, как вы уже наверное догадались, я буду использовать FOSRestBundle — замечательный bundle, который и поможет нам реализовать backend.
Здесь не будет примеров как работать именно с Ангуляром, я буду описывать исключительно только работу с Symfony FosRestBundle.

Для работы нам так же понадобится JMSSerializerBundle для сериализации данных из Entity в JSON или другие форматы, исключить некоторые поля для той или иной сущности (например пароль для API метода получения списка пользователей) и многое другое, подробнее можете почитать в документации.

Установка и конфигурирование
1)Загружаем нужные зависимости в нашем composer.json

"friendsofsymfony/rest-bundle": "^1.7",
"jms/serializer-bundle": "^1.1"

2)Конфигурирование

// app/AppKernel.php class AppKernel extends Kernel {     public function registerBundles()     {         $bundles = array(             // ...             new JMS\SerializerBundle\JMSSerializerBundle(),             new FOS\RestBundle\FOSRestBundle(),         );          // ...     } } 

А теперь редактируем наш config.yml
Для начала будем настраивать наш FOSRestBundle

fos_rest:     body_listener: true     view:       view_response_listener: true     body_converter:         enabled: true         format_listener:         rules:             - { path: '^/api',  priorities: ['json'], fallback_format: json, exception_fallback_format: html, prefer_extension: true }             - { path: '^/', priorities: [ 'html', '*/*'], fallback_format: html, prefer_extension: true } 

body_listener включает EventListener для того, чтобы отслеживать какой формат ответа нужен пользователю, основываясь на его Accept-* заголовках
view_response_listener — эта настройка позволяет просто вернуть View для того или иного запроса
body_converter.rules — содержит массив для настроек, ориентированный на тот или иной адрес, в данном примере мы для всех запросов которые имеют префикс /api в адресе будет возвращаться JSON, во всех остальных случаях — html.

Теперь начнем настройку нашего JMSSerializeBundle

jms_serializer:     property_naming:         separator:  _         lower_case: true      metadata:         cache: file         debug: "%kernel.debug%"         file_cache:             dir: "%kernel.cache_dir%/serializer"         directories:             FOSUserBundle:                 namespace_prefix: FOS\UserBundle                 path: %kernel.root_dir%/config/serializer/FosUserBundle             AppBundle:                 namespace_prefix: AppBundle                 path: %kernel.root_dir%/config/serializer/AppBundle         auto_detection: true 

Здесь имеет смысл остановиться на моменте с jms_serializer.metadata.directories
Таким образом мы говорим serializer-у о том, что конфигурация для того или иного класса-сущности находится там-то или там-то 🙂
Вспомним, что нам требуется вывести весь список пользователей, я лично использую FosUserBundle в своих проектах и вот моя сущность:

<?php  namespace AppBundle\Entity;  use JMS\Serializer\Annotation\Expose; use JMS\Serializer\Annotation\Groups; use JMS\Serializer\Annotation\Exclude; use JMS\Serializer\Annotation\VirtualProperty; use JMS\Serializer\Annotation\ExclusionPolicy;  use Doctrine\ORM\Mapping as ORM; use FOS\UserBundle\Model\User as BaseUser; use FOS\UserBundle\Model\Group;  /**  * User  *  * @ORM\Table(name="user")  * @ORM\Entity(repositoryClass="AppBundle\Repository\UserRepository")  * @ExclusionPolicy("all")  */ class User extends BaseUser {     /**      * @ORM\Id      * @ORM\Column(type="integer")      * @ORM\GeneratedValue(strategy="AUTO")      * @Exclude      */     protected $id;      /**      * @ORM\Column(type="integer")      * @Groups({"user"})      * @Expose      */     private $balance = 0;      /**      * Set balance      *      * @param integer $balance      *      * @return User      */     public function setBalance($balance)     {         $this->balance = $balance;          return $this;     }      /**      * Get balance      *      * @return integer      */     public function getBalance()     {         return $this->balance;     } }  

Я привожу в пример именно эту сущность, которая наследуется от основной модели FosUserBundle. Это важно потому что оба класса придется конфигурировать для JmsSerializerBundle отдельно.
Итак, вернемся jms_serializer.metadata.directories:

directories:             FOSUserBundle:                 namespace_prefix: FOS\UserBundle                 path: %kernel.root_dir%/config/serializer/FosUserBundle             AppBundle:                 namespace_prefix: AppBundle                 path: %kernel.root_dir%/config/serializer/AppBundle 

Здесь мы как раз и указываем, что для AppBundle классов мы будем искать конфигурацию для сериализации в app/config/serializer/AppBundle, а для FosUserBundle — в app/config/serializer/FosUserBundle.
Конфигурация для класса будет находиться автоматически в формате:
Для класса AppBundle\Entity\User — app/config/serializer/AppBundle/Entity.User.(yml|xml|php)
Для класса базовой модели FosUserBundle — app/config/serializer/FosUserBundle/Model.User.(yml|xml|php)

Лично я предпочитаю использовать YAML. Начнем наконец-таки рассказывать JMSSerializer каким образом нам нужно чтобы он настраивал тот или иной класс.
app/config/serializer/AppBundle/Entity.User.yml

AppBundle\Entity\User:     exclusion_policy: ALL     properties:         balance:             expose: true 

app/config/serializer/FosUserBundle/Model.User.yml

FOS\UserBundle\Model\User:     exclusion_policy: ALL     group: user     properties:         id:             expose: true         username:             expose: true         email:             expose: true         balance:             expose: true 

Вот так просто мы смогли рассказать о том, что хотим видеть примерно следующий формат ответа от сервера при получении данных от 1 пользователя:

{"id":1,"username":"admin","email":"admin","balance":0}

В принципе данную конфигурацию необязательно прописывать, только тогда сервер будет возвращать все данные о сущности. В данном случае нам нелогично показывать многие вещи, например такие, как пароль. Поэтому я посчитал нужным продемонстрировать в данном примере и такую реализацию.

Теперь приступим к созданию контроллера
Первым делом создадим роут:

backend_user:     resource: "@BackendUserBundle/Resources/config/routing.yml"     prefix:   /api 

Обратите внимание на /api — не забывайте добавлять его, а если хотите изменить, то придется менять и конфигурацию для fos_rest в config.yml

Теперь сам BackendUserBundle/Resources/config/routing.yml:

backend_user_users:   type: rest   resource: "@BackendUserBundle/Controller/UsersController.php"   prefix: /v1 

Теперь можно приступать к созданию самого контроллера:

<?php  namespace Backend\UserBundle\Controller;  use AppBundle\Entity\User; use FOS\RestBundle\Controller\FOSRestController; use FOS\RestBundle\Controller\Annotations as Rest; use FOS\RestBundle\Controller\Annotations\View; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;  /**  * Class UsersController  * @package Backend\UserBundle\Controller  */ class UsersController extends FOSRestController {     /**      * @return \Symfony\Component\HttpFoundation\Response      * @View(serializerGroups={"user"})      */     public function getUsersAllAction()     {         $users = $this->getDoctrine()->getRepository('AppBundle:User')->findAll();          $view = $this->view($users, 200);         return $this->handleView($view);     }       /**      * @param $id      * @return \Symfony\Component\HttpFoundation\Response      * @View(serializerGroups={"user"})      */     public function getUserAction($id)     {         $user = $this->getDoctrine()->getRepository('AppBundle:User')->find($id);          if (!$user instanceof User) {             throw new NotFoundHttpException('User not found');         }          $view = $this->view($user, 200);         return $this->handleView($view);     } }  

Заметим, что наследуемся мы теперь от FOS\RestBundle\Controller\FOSRestController.
Кстати, вы наверное обратили внимание на аннотацию View(serializerGroups={«user»}).
Дело в том, что т.к. мы мы хотим видеть и данные App\Entity\User и основной модели FosUserBundle, в которой хранятся все остальные поля, мы должны создать определенную группу, в данном случае — «user».

Итак, у нас есть 2 экшена getUserAction и getUsersAllAction. Сейчас вы поймете суть специфики названий методов контроллера.
Сделаем debug всех роутов:
$ app/console debug:route | grep api
Получаем:

get_users_all                              GET        ANY      ANY    /api/v1/users/all.{_format}                          get_user                                      GET        ANY      ANY    /api/v1/users/{id}.{_format}  

Рассмотрим следующий пример с новыми методами:

<?php class UsersComment extends Controller {     public function postUser($id)     {} // "post_user_comment_vote" [POST] /users/{id}      public function getUser($id)     {} // "get_user_comments"   [GET] /users/{id}      public function deleteUserAction($id)     {} // "delete_user_comment" [DELETE] /users/{id}      public function newUserAction($id)     {} // "new_user_comments"   [GET] /users/{id}/new      public function editUserAction($slug, $id)     {} // "edit_user_comment"   [GET] /users/{id}/edit      public function removeUserAction($slug)     {} // "remove_user_comment" [GET] /users/{slug}/remove } 

В комментариях показано по какому адресу и методу запроса будет выполнен тот или иной метод.
В следующий раз я расскажу вам о том, как правильно использовать FOSRestBundle для, например, вывода комментариев определенного пользователя по адресу: "/users/{id}/comments" и т.п.

ссылка на оригинал статьи https://habrahabr.ru/post/278123/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *