Чат Telegram
Группа ВКонтакте
Реализуем CRUD в Symfony

Полноценный CRUD в Symfony

На прошлых уроках мы научились создавать сущности, контроллеры, отображения, формы. Познакомились с репозиториями и с тем, как выводить все данные и одну конкретную запись. Настало время объединить эти знания и написать первое CRUD приложение. Другими словами, мы научимся создавать, удалять, редактировать и просматривать одну запись.

Создавать и отображать мы уже умеем, но давайте пройдёмся по нашему контроллеру ещё раз.

<?php

declare(strict_types=1);

namespace App\Controller;

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

class PostsController extends AbstractController
{
    /** @var PostRepository $postRepository */
    private $postRepository;

    public function __construct(PostRepository $postRepository)
    {
        $this->postRepository = $postRepository;
    }

Инжектим наш репозиторий, который нам нужен для чтения данных, в конструктор. Ещё раз напоминаю вам, что репозиторий в Symfony работает с конкретной сущностью, позволяя нам выполнять различные выборки.

Вывод всех данных

Как выводить все данные - вы уже знаете. Если нет, напоминаю вам, как выглядит код:

   /**
     * @Route("/posts", name="blog_posts")
     */
    public function posts()
    {
        $posts = $this->postRepository->findAll();

        return $this->render('posts/index.html.twig', [
           'posts' => $posts
        ]);
    }

Методы findAll, find, findBy и другие довольно часто используются разработчиками, поэтому предоставляются фреймворком по умолчанию (мы ещё научимся с вами писать собственные запросы). Вы обращаетесь к репозиторию и просите его достать все данные, которые потом передаёте в шаблон.

Вывод одной записи

   /**
     * @Route("/posts/{slug}", name="blog_show")
     */
    public function show(Post $post)
    {
        return $this->render('posts/show.html.twig', [
            'post' => $post
        ]);
    }

Тут Symfony сравнивает {slug} со свойством slug в сущности Post. Если есть - возвращает нам данные. Теперь важное: этот метод вы должны написать ниже всех остальных! Мы уже создали метод для создания записи, роутинг которого выглядит следующим образом:

   /**
     * @Route("/posts/new", name="new_blog_post")
     */

Когда вы переходите по какому-то маршруту на сайте, Symfony начинает искать совпадение сверху вниз. Вы переходите по маршруту /posts/new, чтобы создать новую запись, но метод show() у нас находится вторым по счёту и его роутинг выглядит так:

   /**
     * @Route("/posts/{slug}", name="blog_show")
     */

Как вы думаете, что произойдёт, когда вы перейдёте по маршруту /posts/new? Правильно, Symfony начнёт искать его среди slug'ов нашей сущности и, не найдя совпадения, выкинет ошибку. Почему именно по slug? Потому что вы поставили этот action вторым по счёту, и Symfony именно с ним и найдёт совпадение, так как слово new ничем не отличается от обычного slug. Поэтому метод show() вы должны поставить самым последним в нашем контроллере.

Создание записи

Мы буквально на прошлом уроке научились создавать запись и научились работать с формами, но я ещё раз напомню вам код нашего метода:

   /**
     * @Route("/posts/new", name="new_blog_post")
     */
    public function addPost(Request $request, Slugify $slugify)
    {
        $post = new Post();
        $form = $this->createForm(PostType::class, $post);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            $post->setSlug($slugify->slugify($post->getTitle()));
            $post->setCreatedAt(new \DateTime());

            $em = $this->getDoctrine()->getManager();
            $em->persist($post);
            $em->flush();

            return $this->redirectToRoute('blog_posts');
        }
        return $this->render('posts/new.html.twig', [
            'form' => $form->createView()
        ]);
    }

Как вы помните из основ ООП, если наш метод от чего-то зависит, это можно передать или в качестве его аргументов, или в конструктор класса. Поскольку классы Request и Slugify нам нужны не во всех методах нашего класса, нет смысла перегружать конструктор ими. Собственно, что мы делаем: создаём объект класса Post, форму на основе его полей. Дальше мы проверяем, отправлена ли форма и валидна ли она. Если всё это выполняется, мы производим некоторые действия над созданием slug'а и текущего времени, потом подготавливаем данные и сохраняем. Делаем редирект на роут со всеми постами и рендерим нашу форму.

Редактирование записи

Метод edit(), который мы сейчас создадим, будет выглядеть несколько иначе, но в целом принцип ничем не отличается от создания.

   /**
     * @Route("/posts/{slug}/edit", name="blog_post_edit")
     */
    public function edit(Post $post, Request $request, Slugify $slugify)
    {
         $form = $this->createForm(PostType::class, $post);
         $form->handleRequest($request);

         if ($form->isSubmitted() && $form->isValid()) {
            $post->setSlug($slugify->slugify($post->getTitle()));
            $this->em->flush();

            return $this->redirectToRoute('blog_show', [
                'slug' => $post->getSlug()
            ]);
         }

         return $this->render('posts/new.html.twig', [
             'form' => $form->createView()
         ]);
    }

Начнём с первого - роутинга. Да, именно так нужно делать правильные ендпоинты: действие, которое мы выполняем в данный момент - удаление или редактирование - должно стоять в конце.

/posts/{slug}/edit
/posts/{slug}/delete

Объект Post нам создавать не нужно, поскольку он уже есть в форме. Мы просто создаём новый slug, если он изменился, и сохраняем данные. Поскольку мы обновляем данные, а не сохраняем их в первый раз, делать persist() нам не нужно. Обратите внимание, что мы делаем дальше, мы редиректим на роут blog_show с параметрами slug. То есть просматривая запись и решив её отредактировать, вы возвращаетесь туда же, но уже по новому slug. В конце мы рендерим ту же форму, которую использовали для создания. Вы вольны создать другую.

Также не забудьте в шаблоне, где вы отображаете данные (т.е., например, в show.html.twig), сделать ссылку на редактирование:

<a href="{{ path('blog_post_edit', {'slug': post.slug}) }}">Редактировать</a>

Удаление данных

В менеджере Doctrine уже есть готовые методы по удалению данных. Нам нужно всего лишь ими воспользоваться следующим образом:

   /**
     * @Route("/posts/{slug}/delete", name="blog_post_delete")
     */
    public function delete(Post $post)
    {     
        $em = $this->getDoctrine()->getManager();
        $em->remove($post);
        $em->flush();

        return $this->redirectToRoute('blog_posts');
    }

Опять передаём в качестве аргументов наш объект Post. В остальном здесь всё понятно: удаляем и делаем редирект на страницу всех постов. Также не забудьте сделать кнопку, аналогичную редактированию:

<a href="{{ path('blog_post_delete', {'slug': post.slug}) }}">Удалить</a>

На это всё, мы сделали CRUD. Но какое приложение существует без стилей? Наверняка вы уже втайне подключили bootstrap к своему проекту. В следующем уроке мы познакомимся с таким инструментом как вебпак.

loader
Логические задачи с собеседований