Работа с доктриной. Многие ко многим: связываем посты с категориями

Привет! В этом уроке мы сделаем таблицу категорий для связывания их с постами. Давайте займемся определением отношения между категориями и публикациями. У одной категорий может быть много постов, как и у одной публикации - много категорий. Это называется отношением Многие ко Многим (Many-to-Many).

Чтобы определить связь, необходимо у сущности Post завести поле categories:

   /**
     * @var ArrayCollection
     * @ORM\ManyToMany(targetEntity="App\Entity\Category", cascade={"persist"})
     * @ORM\JoinTable(name="post_categories")
     * @ORM\JoinColumn(referencedColumnName="id", nullable=false)
     */
    private $categories;

Поле $categories будет типа ArrayCollection. В аннотации @ORM\ManyToMany мы указываем сущность для связи, в @ORM\JoinTable указываем имя пивот-таблицы, а @ORM\JoinColumn вам уже знакома. Можно такую же связь определить со стороны категории, чтобы через какой-нибудь $categories->getPosts()->toArray() достать все посты для конкретной категории, однако мы не будем так делать и не определим двухстороннюю связь. Вместо этого мы будем делать простой запрос через query builder.

Не забудем выполнить миграцию:

php bin/console doctrine:migrations:diff;
php bin/console doctrine:migrations:migrate;

После такого рефакторинга ваш проект перестанет работать, потому что репозитории у нас устроены по-старому. Давайте их немного перепишем:

<?php

declare(strict_types=1);

namespace App\Repository;

use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use App\Entity\Post;

class PostRepository
{
    /**
     * @var EntityManagerInterface
     */
    private $em;

    /**
     * @var EntityRepository 
     */
    private $repository;

    public function __construct(EntityManagerInterface $em)
    {
        $this->em = $em;
        $this->repository = $em->getRepository(Post::class);
    }

    public function add(Post $post)
    {
        $this->em->persist($post);
    }

    public function findOneBySlug(string $slug)
    {
        return $this->repository->findOneBy(['slug' => $slug]);
    }
}

Теперь мы не наследуемся от ServiceEntityRepository; наш репозиторий занимается добавлением на сохранение в базу сущности; а также теперь у нас нет доступа напрямую к методам ServiceEntityRepository и желания сделать findAll() не возникнет. Все прежние методы вроде findOne, findOneBy, findAll доступны внутри репозитория и вы можете закрыть их своими методами, где явно определить, что и по какому полю вы достаете.

Так же поступаем и с репозиторием категории:

<?php

declare(strict_types=1);

namespace App\Repository;

use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use App\Entity\Category;

class CategoryRepository
{
    /**
     * @var EntityManagerInterface
     */
    private $em;

    /**
     * @var EntityRepository
     */
    private $repository;

    public function __construct(EntityManagerInterface $em)
    {
        $this->em = $em;
        $this->repository = $em->getRepository(Category::class);
    }

    public function add(Category $category)
    {
        $this->em->persist($category);
    }

    public function findOneByName(string $name)
    {
        return $this->repository->findOneBy(['name' => $name]);
    }
}

Скоро мы отрефакторим старые контроллеры, а также напишем новые. На следующем уроке мы займемся сохранением сущности поста вместе с его тегами, сохранением тега, редактирование поста, посмотрим на различные ситуации и то, как из них выйти. У уроку прикладываю гист, если вам лень переписывать.

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