Создание Rest Api на Symfony 4
Полный код проекта доступен в репозитории https://github.com/symfonyst/rest
Что нужно установить?
Для удобства ниже приведена пошаговая инструкция
1. Ставим Symfony 4
$ composer create-project symfony/website-skeleton rest-api '4.*'
2. Запускаем локальный сервер
$ php -S 127.0.0.1:9001 -t public
При этом в браузере по запросу http://127.0.0.1:9001/ должна отобразиться страница приветсвие Symfony
3. Установка FOSRestBundle
В Symfony есть отличный бандл для работы с Rest Controller'ами, он называется FOSRestBundle
Для него есть хорошая
официальная документация
$ composer require friendsofsymfony/rest-bundle:2.5
Во время установки будет выведено следующее уведомление о проблеме:
Package zendframework/zend-code is abandoned, you should avoid using it. Use laminas/laminas-code instead.
Package zendframework/zend-eventmanager is abandoned, you should avoid using it. Use laminas/laminas-eventmanager instead.
Также будет предложен выбор для решения данной пробемы, выбираем yes и установка продолжается далее.
После успешного завершения установки необходимый бандл должен автоматически добавится в файл с подключенными бандлами (/config/bundles.php)
return [
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true],
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true],
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
FOS\RestBundle\FOSRestBundle::class => ['all' => true],
];
Установка сериалайзера, JMSSerializerBundle
Для корректной работы бандла нам также необходимо установить сериалайзер, я буду использовать JMSSerializerBundle
$ composer require jms/serializer-bundle
По ходу установки также возникнет предупреждение об устаревших компонентах и предложением их заменить, соглашаемся и едем дальше.
После успешного завершения установки в файл с бандлами добавится новая строка:
JMS\SerializerBundle\JMSSerializerBundle::class => ['all' => true],
Конфигурирование FOSRestBundle
Теперь нам необходимо скофигурировать бандл. За конфигурацию отвечает файл:
config/packages/fos_rest.yaml
Он создается автоматически при установке бандла
Раскомментируем в нем некоторые строчки, вот какой результат получился у меня
# Read the documentation: https://symfony.com/doc/master/bundles/FOSRestBundle/index.html
fos_rest:
param_fetcher_listener: true
allowed_methods_listener: true
view:
view_response_listener: true
exception:
exception_controller: 'fos_rest.exception.controller:showAction'
format_listener:
rules:
- { path: ^/api, prefer_extension: false, fallback_format: json, priorities: [ json ] }
- { path: ^/, prefer_extension: true, fallback_format: html, priorities: [ html ] }
Создание класса REST контроллера
Часто возникает необходимость отдавать данные не только на своем сайте, но и на сторонние ресурсы. Для решение данной задачи как раз и служит технология REST
Допустим мы хотим создать сервис новостей, который можно использовать на стороне клиента, с помощью JavaScript
Нам понадобится создать новый класс, который наследуется от AbstractFOSRestController
namespace App\Controller\Api;
use FOS\RestBundle\Controller\AbstractFOSRestController;
use FOS\RestBundle\View\View;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use FOS\RestBundle\Controller\Annotations as Rest;
/**
* Class NewsController
* @package App\Controller
* @Rest\RouteResource("news")
*/
class NewsController extends AbstractFOSRestController
{
public function getAllAction(Request $request){
$currentDate = new \DateTime();
$secondDate = clone $currentDate;
$data = [
[
"date" => $currentDate,
"name" => "Тестовая новость",
"description" => "Описание новости",
"content" => "Контент",
],
[
"date" => $secondDate->modify('2 day'),
"name" => "Вторая новость",
"description" => "Описание новости",
"content" => "Контент",
],
];
return View::create($data, Response::HTTP_OK);
}
}
Конфигурация маршрутов
Нам также необходимо создать маршруты для нашего нового контроллера
Для этого в папке config/routes создаем файл api.yaml со следующим содержимым:
app.api.news:
type: rest
resource: App\Controller\Api\NewsController
name_prefix: api_ # Our precious parameter
prefix: api
Теперь давайте проверим автоматические роуты, которые были созданы
Для этого набираем в консоли:
php bin/console debug:route
И видим в одной из строк
api_get_news_all GET ANY ANY /api/news/all.{_format}
Набираем в строке браузера следующий адрес: http://127.0.0.1:9001/api/news/all
И нам отображается следующий результат
Подобным образом можно создать множество различных REST контроллеров, решив тем самым задачу передачи данных.
Далее мы создадим методы в контроллере, которые будут отвечать за создание, редактирование и удаление записей.
Но сначала нам потребуется создать класс формы новостей NewsType
// src/Form/NewsType.php
namespace App\Form;
use App\Entity\News;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class NewsType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('active')
->add('name')
->add('anons')
->add('content')
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => News::class,
'csrf_protection' => false,
]);
}
}
Создание записи, POST запрос.
public function postAction(Request $request){
try {
$entity = new News();
$form = $this->createForm(NewsType::class, $entity);
$form->submit($request->request->all());
if (
$form->isValid()
) {
$this->removeExtraFields($request, $form);
$em = $this->get('doctrine')->getManager();
$em->persist($entity);
$em->flush();
return $this->getSuccessResponse($entity);
}
throw new \InvalidArgumentException('Error! Invalid form data.');
} catch (\Exception $exception) {
return $this->getFailResponse($exception);
}
}
Обновление записи, методы PUT, PATCH
public function putAction($id, Request $request)
{
return $this->patchAction($id, $request);
}
public function patchAction($id, Request $request)
{
try {
$em = $this->get('doctrine')->getManager();
$entity = $em->getRepository(News::class)->findOneBy([
'id' => $id,
'deleted' => 0
]);
if (!$entity) throw new EntityNotFoundException('News not found');
$form = $this->createForm(NewsType::class, $entity);
$this->removeExtraFields($request, $form);
$form->submit($request->request->all());
if ($form->isValid()) {
$em->persist($entity);
$em->flush();
return $this->getSuccessResponse($entity);
}
throw new \InvalidArgumentException("Invalid form");
} catch (\Exception $exception) {
return $this->getFailResponse($exception);
}
}
Удаление записи, метод DELETE
Удалять запись из базы мы не будем, просто проставим соответствующий флаг и сохраним дату удаления.
public function deleteAction ($id, Request $request) {
try {
$em = $this->get('doctrine')->getManager();
/**
* @var News $entity
*/
$entity = $em->getRepository(News::class)->findOneBy([
'id' => $id,
'deleted' => 0
]);
if (!$entity) throw new EntityNotFoundException('News not found');
$entity->setDeleted(1);
$entity->setDeletedDate(new \DateTime());
$em->persist($entity);
$em->flush();
return $this->getSuccessResponse($entity);
} catch ( \Exception $exception) {
return $this->getFailResponse($exception);
}
}
Тесты
Напишем тесты для проверки полученных методов
// tests/Controller/Api/NewsControllerTest.php
namespace App\Tests\Controller\Api;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\HttpFoundation\Response;
class NewsControllerTest extends WebTestCase
{
public function testPost(){
static::$kernel = static::createKernel();
static::$kernel->boot();
$client = static::createClient();
$client->request("POST", "/api/news.json", [
"active" => 1,
"name" => "Свежая новость",
"anons" => "Анонс новости",
"content" => "Контент новости",
]);
$this->assertEquals($client->getResponse()->getStatusCode(), Response::HTTP_OK);
}
}
public function testPut()
{
static::$kernel = static::createKernel();
static::$kernel->boot();
$client = static::createClient();
$client->request("POST", "/api/news.json", [
"active" => 1,
"name" => "Начало учебного года",
"anons" => "1 сентября начался учебный год",
"content" => "С утра прошли праздничные линейки во всех школах",
]);
$this->assertEquals($client->getResponse()->getStatusCode(), Response::HTTP_OK);
$res = json_decode($client->getResponse()->getContent());
$this->assertTrue(isset($res->data->id));
$id = $res->data->id;
$client->request("PUT", "/api/news/" . $id . ".json", [
"name" => "Начало учебного года 2021",
"anons" => "1 сентября начался учебный год, поздравляем всех!",
"content" => "С утра прошли праздничные линейки во всех школах. Фотоотчет на сайте.",
]);
$this->assertTrue($client->getResponse()->getStatusCode() == Response::HTTP_OK);
$res = json_decode($client->getResponse()->getContent());
$this->assertEquals($res->data->name, "Начало учебного года 2021");
$client->request('DELETE', "/api/news/" . $id . ".json");
$this->assertTrue($client->getResponse()->getStatusCode() == Response::HTTP_OK);
}
Запустим тесты
php bin/console doctrine:fixtures:load
Careful, database "dev_rest_api" will be purged. Do you want to continue? (yes/no) [no]:
> y
> purging database
> loading App\DataFixtures\AppFixtures
> loading App\DataFixtures\UserFixtures
phpunit --filter NewsControllerTest