В данном посте я бы хотел рассказать о том, как нужно правильно выстраивать RESTfull API для AngularJS и других фронтенд фреймворков с бекендом на Symfony.
И, как вы уже наверное догадались, я буду использовать FOSRestBundle — замечательный bundle, который и поможет нам реализовать backend.
Здесь не будет примеров как работать именно с Ангуляром, я буду описывать исключительно только работу с Symfony FosRestBundle.
Для работы нам так же понадобится JMSSerializerBundle для сериализации данных из Entity в JSON или другие форматы, исключения некоторых полей для той или иной сущности (например пароль для API метода получения списка пользователей) и многое другое.
Подробнее можете почитать в документации.
Установка и конфигурирование
1) Загружаем нужные зависимости в нашем composer.json
2)Конфигурирование
А теперь редактируем наш config.yml
Для начала будем настраивать наш FOSRestBundle
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.metadata.directories, где мы говорим serializer-у о том, что конфигурация для того или иного класса (сущности) находится там-то или там-то :)
Условимся, что нам требуется вывести весь список пользователей, я лично использую FosUserBundle в своих проектах и вот моя сущность:
Я привожу в пример именно эту сущность, которая наследуется от основной модели FosUserBundle. Это важно потому что оба класса придется конфигурировать для JmsSerializerBundle отдельно.
Итак, вернемся jms_serializer.metadata.directories:
Здесь мы как раз и указываем, что для 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
app/config/serializer/FosUserBundle/Model.User.yml
Вот так просто мы смогли рассказать о том, что хотим видеть примерно следующий формат ответа от сервера при получении данных от 1 пользователя:
В принципе данную конфигурацию необязательно прописывать и сервер будет возвращать все данные о сущности. Только в данном случае нам нелогично показывать многие вещи, например такие, как пароль. Поэтому я посчитал нужным продемонстрировать в данном примере именно такую реализацию.
Теперь приступим к созданию контроллера
Первым делом создадим роут:
Обратите внимание на /api — не забывайте добавлять его, а если хотите изменить, то придется менять и конфигурацию для fos_rest в config.yml
Теперь сам BackendUserBundle/Resources/config/routing.yml:
Теперь можно приступать к созданию самого контроллера:
Заметим, что наследуемся мы теперь от FOS\RestBundle\Controller\FOSRestController.
Кстати, вы наверное обратили внимание на аннотацию View(serializerGroups={«user»}).
Дело в том, что т.к. мы мы хотим видеть и данные App\Entity\User и основной модели FosUserBundle, в которой хранятся все остальные поля, мы должны создать определенную группу, в данном случае — «user».
Итак, у нас есть 2 экшена getUserAction и getUsersAllAction. Сейчас вы поймете суть специфики названий методов контроллера.
Сделаем debug всех роутов:
Получаем:
Рассмотрим следующий пример с новыми методами:
Напоминает Laravel Resource Controller, правда?
В комментариях показано по какому адресу и методу запроса будет выполнен тот или иной метод.
В следующий раз я расскажу вам о том, как правильно использовать FOSRestBundle для, например, вывода комментариев определенного пользователя по адресу: "/users/{id}/comments", создавать \ обновлять данные пользователей.
И, как вы уже наверное догадались, я буду использовать 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 в своих проектах и вот моя сущность:
<?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" [GET] /users/{id}
public function deleteUserAction($id)
{} // "delete_user" [DELETE] /users/{id}
public function newUserAction($id)
{} // "new_user" [GET] /users/{id}/new
public function editUserAction($slug, $id)
{} // "edit_user" [GET] /users/{id}/edit
public function removeUserAction($slug)
{} // "remove_user" [GET] /users/{slug}/remove
}
Напоминает Laravel Resource Controller, правда?
В комментариях показано по какому адресу и методу запроса будет выполнен тот или иной метод.
В следующий раз я расскажу вам о том, как правильно использовать FOSRestBundle для, например, вывода комментариев определенного пользователя по адресу: "/users/{id}/comments", создавать \ обновлять данные пользователей.