REST API в Symfony (без FosRestBundle) с использованием JWT аутентификации. Часть 1

Автор оригинала: Hicham Ben Kachoud
  • Перевод

Перевод статьи подготовлен в преддверии старта курса «Symfony Framework».





В первой части статьи мы рассмотрим самый простой способ реализации REST API в проекте Symfony без использования FosRestBundle. Во второй части, которую я опубликую следом, мы рассмотрим JWT аутентификацию. Прежде чем мы начнем, сперва мы должны понять, что на самом деле означает REST.


Что означает Rest?


REST (Representational State Transfer — передача состояния представления) — это архитектурный стиль разработки веб-сервисов, который невозможно игнорировать, потому что в современной экосистеме существует большая потребность в создании Restful-приложений. Это может быть связано с уверенным подъемом позиций JavaScript и связанных фреймворков.


REST API использует протокол HTTP. Это означает, что когда клиент делает какой-либо запрос к такому веб-сервису, он может использовать любой из стандартных HTTP-глаголов: GET, POST, DELETE и PUT. Ниже описано, что произойдет, если клиент укажет соответствующий глагол.


  • GET: будет использоваться для получения списка ресурсов или сведений о них.
  • POST: будет использоваться для создания нового ресурса.
  • PUT: будет использоваться для обновления существующего ресурса.
  • DELETE: будет использоваться для удаления существующего ресурса.

REST не имеет состояний (state), и это означает, что на стороне сервера тоже нет никаких состояний запроса. Состояния остаются на стороне клиента (пример — использование JWT для аутентификации, с помощью которого мы собираемся засекьюрить наш REST API). Таким образом, при использовании аутентификации в REST API нам нужно отправить аутентификационный заголовок, чтобы получить правильный ответ без хранения состояния.


Создание проекта Symfony:


Во-первых, мы предполагаем, что вы уже установили PHP и менеджер пакетов Сomposer для создания нового проекта Symfony. С этим всем в наличии создайте новый проект с помощью следующей команды в терминале:


composer create-project symfony/skeleton demo_rest_api


Создание проекта Symfony


Мы используем базовый скелет Symfony, который рекомендуется для микросервисов и API. Вот как выглядит структура каталогов:



Структура проекта


Config: содержит все настройки бандла и список бандлов в bundle.php.
Public: предоставляет доступ к приложению через index.php.
Src: содержит все контроллеры, модели и сервисы
Var: содержит системные логи и файлы кэша.
Vendor: содержит все внешние пакеты.


Теперь давайте установим некоторые необходимые пакеты с помощью Сomposer:


composer require symfony/orm-pack
composer require sensio/framework-extra-bundle

Мы установили sensio/framework-extra-bundle, который поможет нам упростить код, используя аннотации для определения наших маршрутов.


Нам также необходимо установить symphony/orm-pack для интеграции с Doctrine ORM, чтобы соединиться с базой данных. Ниже приведена конфигурация созданной мной базы данных, которая может быть задана в файле .env.



.env файл конфигурации


Теперь давайте создадим нашу первую сущность. Создайте новый файл с именем Post.php в папке src/Entity.


<?php
 namespace App\Entity;
 use Doctrine\ORM\Mapping as ORM;
 use Symfony\Component\Validator\Constraints as Assert;
 /**
  * @ORM\Entity
  * @ORM\Table(name="post")
  * @ORM\HasLifecycleCallbacks()
  */
 class Post implements \JsonSerializable {
  /**
   * @ORM\Column(type="integer")
   * @ORM\Id
   * @ORM\GeneratedValue(strategy="AUTO")
   */
  private $id;

  /**
   * @ORM\Column(type="string", length=100)
   *
   */
  private $name;
  /**
   * @ORM\Column(type="text")
   */
  private $description;

  /**
   * @ORM\Column(type="datetime")
   */
  private $create_date;

  /**
   * @return mixed
   */
  public function getId()
  {
   return $this->id;
  }
  /**
   * @param mixed $id
   */
  public function setId($id)
  {
   $this->id = $id;
  }
  /**
   * @return mixed
   */
  public function getName()
  {
   return $this->name;
  }
  /**
   * @param mixed $name
   */
  public function setName($name)
  {
   $this->name = $name;
  }
  /**
   * @return mixed
   */
  public function getDescription()
  {
   return $this->description;
  }
  /**
   * @param mixed $description
   */
  public function setDescription($description)
  {
   $this->description = $description;
  }

  /**
   * @return mixed
   */
  public function getCreateDate(): ?\DateTime
  {
   return $this->create_date;
  }

  /**
   * @param \DateTime $create_date
   * @return Post
   */
  public function setCreateDate(\DateTime $create_date): self
  {
   $this->create_date = $create_date;
   return $this;
  }

  /**
   * @throws \Exception
   * @ORM\PrePersist()
   */
  public function beforeSave(){

   $this->create_date = new \DateTime('now', new \DateTimeZone('Africa/Casablanca'));
  }

  /**
   * Specify data which should be serialized to JSON
   * @link https://php.net/manual/en/jsonserializable.jsonserialize.php
   * @return mixed data which can be serialized by <b>json_encode</b>,
   * which is a value of any type other than a resource.
   * @since 5.4.0
   */
  public function jsonSerialize()
  {
   return [
    "name" => $this->getName(),
    "description" => $this->getDescription()
   ];
  }
 }

И после этого выполните команду: php bin/console doctrine:schema:create для создания таблицы базы данных в соответствии с нашей сущностью Post.


Теперь давайте создадим PostController.php, куда мы добавим все методы, взаимодействующие с API. Он должен быть помещен в папку src/Controller.


<?php
 /**
  * Created by PhpStorm.
  * User: hicham benkachoud
  * Date: 02/01/2020
  * Time: 22:44
  */

 namespace App\Controller;

 use App\Entity\Post;
 use App\Repository\PostRepository;
 use Doctrine\ORM\EntityManagerInterface;
 use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
 use Symfony\Component\HttpFoundation\JsonResponse;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\Routing\Annotation\Route;

 /**
  * Class PostController
  * @package App\Controller
  * @Route("/api", name="post_api")
  */
 class PostController extends AbstractController
 {
  /**
   * @param PostRepository $postRepository
   * @return JsonResponse
   * @Route("/posts", name="posts", methods={"GET"})
   */
  public function getPosts(PostRepository $postRepository){
   $data = $postRepository->findAll();
   return $this->response($data);
  }

  /**
   * @param Request $request
   * @param EntityManagerInterface $entityManager
   * @param PostRepository $postRepository
   * @return JsonResponse
   * @throws \Exception
   * @Route("/posts", name="posts_add", methods={"POST"})
   */
  public function addPost(Request $request, EntityManagerInterface $entityManager, PostRepository $postRepository){

   try{
    $request = $this->transformJsonBody($request);

    if (!$request || !$request->get('name') || !$request->request->get('description')){
     throw new \Exception();
    }

    $post = new Post();
    $post->setName($request->get('name'));
    $post->setDescription($request->get('description'));
    $entityManager->persist($post);
    $entityManager->flush();

    $data = [
     'status' => 200,
     'success' => "Post added successfully",
    ];
    return $this->response($data);

   }catch (\Exception $e){
    $data = [
     'status' => 422,
     'errors' => "Data no valid",
    ];
    return $this->response($data, 422);
   }

  }

  /**
   * @param PostRepository $postRepository
   * @param $id
   * @return JsonResponse
   * @Route("/posts/{id}", name="posts_get", methods={"GET"})
   */
  public function getPost(PostRepository $postRepository, $id){
   $post = $postRepository->find($id);

   if (!$post){
    $data = [
     'status' => 404,
     'errors' => "Post not found",
    ];
    return $this->response($data, 404);
   }
   return $this->response($post);
  }

  /**
   * @param Request $request
   * @param EntityManagerInterface $entityManager
   * @param PostRepository $postRepository
   * @param $id
   * @return JsonResponse
   * @Route("/posts/{id}", name="posts_put", methods={"PUT"})
   */
  public function updatePost(Request $request, EntityManagerInterface $entityManager, PostRepository $postRepository, $id){

   try{
    $post = $postRepository->find($id);

    if (!$post){
     $data = [
      'status' => 404,
      'errors' => "Post not found",
     ];
     return $this->response($data, 404);
    }

    $request = $this->transformJsonBody($request);

    if (!$request || !$request->get('name') || !$request->request->get('description')){
     throw new \Exception();
    }

    $post->setName($request->get('name'));
    $post->setDescription($request->get('description'));
    $entityManager->flush();

    $data = [
     'status' => 200,
     'errors' => "Post updated successfully",
    ];
    return $this->response($data);

   }catch (\Exception $e){
    $data = [
     'status' => 422,
     'errors' => "Data no valid",
    ];
    return $this->response($data, 422);
   }

  }

  /**
   * @param PostRepository $postRepository
   * @param $id
   * @return JsonResponse
   * @Route("/posts/{id}", name="posts_delete", methods={"DELETE"})
   */
  public function deletePost(EntityManagerInterface $entityManager, PostRepository $postRepository, $id){
   $post = $postRepository->find($id);

   if (!$post){
    $data = [
     'status' => 404,
     'errors' => "Post not found",
    ];
    return $this->response($data, 404);
   }

   $entityManager->remove($post);
   $entityManager->flush();
   $data = [
    'status' => 200,
    'errors' => "Post deleted successfully",
   ];
   return $this->response($data);
  }

  /**
   * Returns a JSON response
   *
   * @param array $data
   * @param $status
   * @param array $headers
   * @return JsonResponse
   */
  public function response($data, $status = 200, $headers = [])
  {
   return new JsonResponse($data, $status, $headers);
  }

  protected function transformJsonBody(\Symfony\Component\HttpFoundation\Request $request)
  {
   $data = json_decode($request->getContent(), true);

   if ($data === null) {
    return $request;
   }

   $request->request->replace($data);

   return $request;
  }

 }

Здесь мы определили пять маршрутов:


● GET /api/posts: вернет список постов.



api получения всех постов


● POST /api/posts: создаст новый пост.



api добавления нового поста


● GET /api/posts/id: вернет пост, соответствующий определенному идентификатору,



получение конкретного поста


● PUT /api/posts/id: обновит пост.



обновление поста


Это результат после обновления:



пост после обновления


● DELETE /api/posts/id: удалит пост.



удаление поста


Это результат получения всех постов после удаления поста с идентификатором 3:



все посты после удаления


Исходный код можно найти здесь


Заключение


Итак, теперь мы понимаем, что такое REST и Restful. Restful API должен быть без состояний. Мы знаем, как создать Restful-приложение, используя HTTP-глаголы. В общем, теперь мы хорошо понимаем REST и готовы профессионально создавать Restful-приложения.


В следующей статье мы рассмотрим, как обеспечить секьюрность API с помощью JWT аутентификации.




Узнать подробнее о курсе «Symfony Framework»



OTUS. Онлайн-образование
Цифровые навыки от ведущих экспертов

Похожие публикации

Комментарии 4

    +4
    PUT: будет использоваться для обновления существующего ресурса.

    Частичный PUT нарушает спецификацию HTTP. Он предназначен для полной замены ресурса, а не для обновления. В чем-то сродни операции присваивания. Для частичных обновлений подходит или POST, или PATCH из RFC 5789.

      +1
      Логика в контроллерах. Как мило.
        +3
        Во-первых, нужно уже переходить на современный пхп, убрать горы типов в аннотациях и заменить всё на тайпхинты. Во-вторых, ловятся все ексепшены и конвертятся в 4xx, т.е. пятисотые не предвидятся и в лог они не попадут. Не стриктовые сравнения типа if (!post) {… } или if (!request) {… .}, советую воспользоваться phpstan со стрикт правилами. В контроллере навалено всё, и обработка реквеста, и логика работы с ентити, а если нам нужно будет тоже самое сделать через cli например, получим дублирование.
        Советую посмотреть этот пример
          +1

          POST, GET — это конечно "глаголы", но в контексте http правильнее переводить как "методы" https://developer.mozilla.org/ru/docs/Web/HTTP/Methods

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое