Перевод статьи подготовлен в преддверии старта курса «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»