Doctrine ORM: первая сущность, миграции и фикстуры

Итак, начиная с этого урока, мы приступаем изучать концепции, которые вы не использовали при создании собственного MVC фреймворка, а именно - сущности, миграции и фикстуры. Как я говорил в одном из прошлых уроков, Symfony - не MVC фреймворк. Пусть вас не пугает этот факт, это совсем не значит, что, изучив Symfony, вы не сможете писать на Laravel или, упаси бог, на Yii2. Нет, на раннем этапе вы будете обращаться с Symfony как с MVC фреймворком, и только потом, с опытом, поймёте фундаментальную разницу. Например, вы можете попробовать вернуть из action что-то другое, не объект Response, а массив данных, и сразу словите ошибку

The controller must return a "Symfony\Component\HttpFoundation\Response" object

Поскольку Symfony - это HTTP фреймворк, он получает данные из Request'a и отправляет в виде Response.

Сущность в Symfony - это одна конкретная таблица. Если вы будете работать с постами, очевидно, вам нужна сущность Post.

Сущность можно создать двумя способами: через терминал, выполнив команду make:entity, ну или же просто вручную в папке Entity. На раннем этапе предлагаю использовать make:entity, ведь команда создаст не только сущность, но и класс-репозиторий этой сущности по работе с базой данных. Таким образом, если в том же Laravel Eloquent ORM использует паттерн ActiveRecord для работы с базой данных, то Symfony использует паттерн Репозиторий, позволяющий абстрагироваться от конкретного типа базы данных. По умолчанию, репозиторий для каждой сущности предлагает несколько простых методов выборки: findAll, find, findBy и другие.

Итак, выполните в корне проекта bin/console make:entity. Вы сразу можете передавать в качестве ключа название сущности, как это сделал я

php bin/console make:entity Post 

Если вам будет проще, пока можете думать о сущности в качестве модели. Выполнив команду, вы заметите, что на этом консоль продолжает работать и предлагает ввести имя поля таблицы, тип данных поля, размер и проч. Мы так делать не будем, а взамен познакомимся с аннотациями. Создав сущность, просто прервите операцию с помощью Ctrl+C.

Дальше зайдите в класс Entity/Post.php, вы должны увидеть следующее:

По умолчанию, Doctrine генерирует id. Вы могли заметить нечто необычное - обширные аннотации над классом и свойствами класса. Да, именно так выглядит маппинг Doctrine ORM. Над свойствами класса, которые, я напоминаю, являются полями вашей таблицы Post, вы пишете тип данных, размер, может ли быть null и многое другое, с чем мы позже познакомимся. Над самим классом стоит следующая аннотация:

/**
 * @ORM\Entity(repositoryClass="App\Repository\PostRepository")
 */

Она указывает, какой репозиторий обрабатывает нашу сущность.
Давайте начнём заполнять наш класс. Для начала определимся, какие у нас будут поля: title, body, slug и created_at. Этих полей будет достаточно, чтобы познакомиться с разными типами аннотаций. Тут вам не обойтись без автокомплита PhpStorm, так что советую скачать его немедленно.

Аннотация Column принимает тип данных, можно ещё указать, может ли быть поле nullable. В аннотации Length мы указываем длину, также можно указать с помощью minMessage и maxMessage, какое сообщение об ошибке будет получать пользователь, если его title не будет соответствовать допустимой длине. Над id вы могли заметить ORM\Id и ORM\GeneratedValue(), это специальные аннотации для первичного ключа.

/**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", nullable=false)
     * @Assert\Length(min=10, max=255)
     */
    private $title;

    /**
     * @ORM\Column(type="text", nullable=false)
     */
    private $body;

    /**
     * @ORM\Column(type="string", nullable=false)
     */
    private $slug;

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

Также вы должны сделать сеттеры и геттеры для каждого свойства класса, кроме id - для него нужен только геттер. В конце наш класс должен выглядеть следующим образом:

<?php

declare(strict_types=1);

namespace App\Entity;

use Doctrine\DBAL\Types\DateType;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * @ORM\Entity(repositoryClass="App\Repository\PostRepository")
 */
class Post
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", nullable=false)
     * @Assert\Length(min=10, max=255)
     */
    private $title;

    /**
     * @ORM\Column(type="text", nullable=false)
     */
    private $body;

    /**
     * @ORM\Column(type="string", nullable=false)
     */
    private $slug;

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

    public function getId(): ?int
    {
        return $this->id;
    }

    /**
     * @return mixed
     */
    public function getTitle()
    {
        return $this->title;
    }

    /**
     * @param mixed $title
     */
    public function setTitle($title): void
    {
        $this->title = $title;
    }

    /**
     * @return mixed
     */
    public function getBody()
    {
        return $this->body;
    }

    /**
     * @param mixed $body
     */
    public function setBody($body): void
    {
        $this->body = $body;
    }

    /**
     * @return mixed
     */
    public function getSlug()
    {
        return $this->slug;
    }

    /**
     * @param mixed $slug
     */
    public function setSlug($slug): void
    {
        $this->slug = $slug;
    }

    /**
     * @return \DateTimeInterface
     */
    public function getCreatedAt(): ?\DateTimeInterface
    {
        return $this->created_at;
    }

    /**
     * @param \DateTimeInterface $created_at
     */
    public function setCreatedAt(\DateTimeInterface $created_at): void
    {
        $this->created_at = $created_at;
    }
}

На данном этапе мы закончили работу с сущностью. Теперь мы должны выполнить миграцию, чтобы у нас появилась таблица в нашей базе! Открываем терминал и выполняем следующую команду:

php bin/console make:migration

У вас сгенерируется миграция в папке Migrations. Вот как она выглядит:

Внизу вы могли заметить, как Symfony вам подсказывает, что нужно сделать, чтобы запустить миграцию, а именно выполнить команду

php bin/console doctrine:migrations:migrate

Также вы должны будете подтвердить её, введя y. Вуаля, у вас появилась первая таблица! Однако, у вас нет данных. Давайте же введём вручную пару десятков значений. Шучу конечно, ничего вручную мы вводить не будем, ведь у нас есть фикстуры (fixtures)! Фикстуры - это фейковые данные, которые используются в dev-режиме, чтобы заполнить базу и проверить её работоспособность. Однако для начала нам надо скачать DoctrineFixturesBundle следующей командой:

composer require --dev doctrine/doctrine-fixtures-bundle

Ещё скачаем популярную библиотеку Faker для генерирования рандомных данных.

composer require fzaninotto/faker

Объясню разницу: DoctrineFixturesBundle отправляет фейковые данные, позволяет выбирать, в какой последовательности они будут генерироваться (допустим, сначала пользователи, а потом посты, если у нас связь один-ко-многим), а Faker только создаёт случайные данные. Так же работают сиды в Laravel'е, если вы с ними знакомы.

Если помните, у нас есть поле slug, обычно slug генерируется на основе названия статьи. На packagist можно найти библиотеку cocur/slugify, делающая slug как из английских слов, так и из русских. Давайте скачаем её тоже:

composer require cocur/slugify

После установки DoctrineFixturesBundle у вас появится папка DataFixtures и в ней класс AppFixtures.php. В нём-то мы и будем загружать наши фейковые данные. Поначалу файл выглядит следующим образом:

ObjectManager - это класс, подготавливающий и сохраняющий данные в базе, к нему мы вернёмся позже. Мы не будем загружать фикстуры в методе load, в нём мы будем вызывать приватные методы, которые сейчас создадим. Поскольку мы работаем с постами, создадим приватный метод loadPosts, который вызовем в методе load. Для начала определимся с зависимостями: нам нужен класс Factory из библиотеки Faker и Slugify. Создадим для них свойства и заинжектим в конструктор:

    private $faker;

    private $slug;

    public function __construct(Slugify $slugify)
    {
        $this->faker = Factory::create();
        $this->slug = $slugify;
    }

Дальше я приведу реализацию метода loadPosts и объясню, что мы делаем:

    public function loadPosts(ObjectManager $manager)
    {
        for ($i = 1; $i < 20; $i++) {
            $post = new Post();
            $post->setTitle($this->faker->text(100));
            $post->setSlug($this->slug->slugify($post->getTitle()));
            $post->setBody($this->faker->text(1000));
            $post->setCreatedAt($this->faker->dateTime);

            $manager->persist($post);
        }
        $manager->flush();
    }

Итак, мы запускаем обычный цикл, из которого понятно, что мы создадим 20 записей. Создаём класс Post. Сначала сеттим title, куда отправляем значения, сгенерированные для нас faker. Далее генерируем slug с помощью метода slugify класса Slugify, куда передаём title с помощью метода getTitle(). Поскольку сейчас мы работаем с первой записью ($i = 1), то именно title под id, равному 1, у нас и попадёт в setSlug. Это нам и нужно. Дальше мы просто сеттим body и created_at. Метод persist готовит данные, а flush всё сохраняет в конце цикла. Таким образом, наш класс выглядит следующим образом:

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

php bin/console doctrine:fixtures:load

Подтвердите действие с помощью Y и бегите смотреть базу. Вы увидите, что все поля заполнены правильно и в поле slug у нас находится title, но с маленькими буквами и разделителями. Собственно, всё работает так, как мы этого и хотели. Во второй части статьи мы выведем все данные в шаблоне, данные по id или slug и поработаем с репозиториями!

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