И, как вы уже наверное догадались, я буду использовать 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", создавать \ обновлять данные пользователей.
Комментарии (10)
lowadka
28.02.2016 23:43Если говорить в целом про бандл, то он наверное хорош, но если вы делаете исключительно json api, то он очень избыточен, как в скорости работы, так и в количестве кода. В реальном проекте такое:
/** * @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); }
Превратится в такое:
/** * @Common\Annotation\SerializationGroups({"manager_app__user_details"}) */ public function detailsAction(Common\Entity\User $entity) { return $entity }
Где вся повторяющаяся логика вынесена во всякие listener`ы и шорткатыFesor
29.02.2016 16:20+1Ну оно как бы и так выносится спокойно в листенеры и шорткаты, а почему у автора так как приведет — без понятия.
$user = $this->getDoctrine()->getRepository('AppBundle:User')->find($id); if (!$user instanceof User) { throw new NotFoundHttpException('User not found'); }
это вообще из коробки идет в виде ParamConverter
$view = $this->view($user, 200); return $this->handleView($view);
тут опять же непонятно почему бы просто не выплюнуть $user, у нас же все уже описано в аннотациях. Да и одной строчкой это делается.
BOLTIKUS
29.02.2016 10:08Стоит более подробно написать про группы сериализации. О том что их может быть несколько и это позволяет получать разные поля сущности в разных ситуациях.
Так же можно описать процесс десериализации, опять же группы, парам конвертер для получения и валидации сущностей из запросов клиентов.
И напоследок упомянуть про то как кастомизировать эксепшены через эксепшен врапперы и стратегию именования полей для сериализации.
Fesor
29.02.2016 15:09Не используйте JMS Serializer. Он привносит больше проблем чем пользы. Есть symfony/serializer на худой конец.
lowadka
29.02.2016 16:34Какие например? Не ради спора, действительно интересно
Fesor
29.02.2016 17:12+2Для начала посмотрим пульс пациента — он мертв. Проект активно не поддерживается уже более двух лет.
1) иногда просто так сериализует массив как хэш-мэпу, лечится кастылями в виде листенеров
2) надо замэпить json на существующий объект — лепим object creator
3) inline — работает только в одну сторону, без возможности задать кастомный префикс. бесполезная фича. В принципе очень много фич работающих наполовину.
4) шаг в право/шаг в лево — пишем свои хэндлеры или листенеры, много лишнего кода, не особо прогнозируется необходимость писать этот код. Много рисков.
5) версионизация API? переименовали пропертю? Пишите свои хэндлеры, мэппингами вы это так просто уже не разрулите.
6) в принципе для тупого CRUD сойдет, но чуть сложнее — и все… приплыли к морю кода. Быстрее руками мэппинги сделать.
7) медленно. У меня были проекты где jms serializer занимал 50% времени генерации респонса.
Лучше брать symfony/serializer. Он в этом плане намного более грамотная штука. Позволяет сделать все то же что и jms serializer но намного более грамотно (за счет выделения процесса нормализации и отделения от сериализации).
lowadka
Думаю в статье стоит упомянуть про параметр:
Многие сталкиваются с этим, так почему бы не сэкономить другим время
php_freelancer
А вот кстати да! Не подумал сначала почему-то. Вообще нужная вещица — сериализация null.
Добавил в статью, спасибо.
Fesor
Ну не сказать что оно особо нужная.
by25
Ну не сказал бы, 1С-ники (горите в аду) плачут часто, что нету ключа в json.