[recovery mode] 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
    serializer:
        serialize_null: 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 для того или иного запроса
serializer.serialize_null — эта настройка говорит о том, что мы так же хотим, чтобы NULL сериализовывался, как и все, если её не установить или установить как false, тогда все поля, что имеют null — просто напросто не будут отображаться в ответе сервера.
P.S.: спасибо, что напомнил lowadka
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 в своих проектах и вот моя сущность:

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

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

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} 

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



Напоминает Laravel Resource Controller, правда?

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

© Habrahabr.ru