Работа с доктриной. Связь один ко многим: связываем комментарии с постами
На прошлых уроках мы написали первую сущность, поработали с формой, стандартной авторизацией и авторизацией через соц сети. Однако в реальных приложениях все намного сложнее: есть многочисленные связи между сущностями, которые надо правильно обрабатывать. К счастью, Symfony в качестве инструмента с базой данных использует Doctrine, которая очень удобна как раз для решения вопросов отношений. Сегодня мы с вами познакомимся с первым типом отношений - ManyToOne/OnyToMany.
Основы ассоциаций в Doctrine
Мы уже делали сущность Post на одном из первых уроков, поэтому мы не будем ничего переписывать, а прямо в ней сделаем отношение с другой сущностью, Comment. Для начала давайте создадим ее.
<?php
declare(strict_types=1);
namespace App\Entity;
use DateTimeImmutable;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity()
*/
class Comment
{
/**
* @ORM\Id()
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @var string
*
* @ORM\Column(type="text", nullable=false)
*/
private $comment;
/**
* @var Post
*
* @ORM\ManyToOne(targetEntity=Post::class, inversedBy="comments")
* @ORM\JoinColumn(referencedColumnName="id")
*/
private $post;
/**
* @var User
*
* @ORM\ManyToOne(targetEntity=User::class, inversedBy="comments")
* @ORM\JoinColumn(referencedColumnName="id")
*/
private $user;
/**
* @var DateTimeImmutable
*
* @ORM\Column(type="date_immutable")
*/
private $createdAt;
/**
* @var DateTimeImmutable
*
* @ORM\Column(type="date_immutable")
*/
private $updatedAt;
private function __construct()
{
$this->createdAt = new DateTimeImmutable();
$this->updatedAt = new DateTimeImmutable();
}
/**
* @param string $content
* @param User $user
* @param Post $post
*
* @return Comment
*/
public static function create(string $content, User $user, Post $post): Comment
{
$comment = new self();
$comment->comment = $content;
$comment->post = $post;
$comment->user = $user;
return $comment;
}
/**
* @return string
*/
public function getComment(): string
{
return $this->comment;
}
/**
* @return User
*/
public function getUser(): User
{
return $this->user;
}
/**
* @return Post
*/
public function getPost(): Post
{
return $this->post;
}
/**
* @param Post $post
*/
public function setPost(Post $post)
{
$this->post = $post;
}
/**
* @param User $user
*/
public function setUser(User $user)
{
$this->user = $user;
}
}
Давайте сначала поразмышляем, как правильно определить отношение между сущностями. У нас есть комментарии и есть публикация. У публикации может быть много комментариев, но комментарий может принадлежать только одной сущности. Отсюда и возникает, что мы должны определить к таблице Post и User отношение ManyToOne, потому как только одному посту и одному пользователю могут принадлежать много комментариев, но не наоборот.
Аннотация ManyToOne принимает несколько аргументов, один из них - targetEntity - является ссылкой (можно указывать как Entity::class, так и полный неймспейс до сущности) на класс для связи, другой inversedBy, где указывается поле для связи. В нашем случае к сущностям User и Post мы позже добавим поле comments. Важно заметить, что связь с двух сторон можно не указывать, потому как доктрина умная и додумает сама, как соединить (а именно по id), но если вам понадобится получить комментарии для всех постов, то удобно достать их через публикацию, хоть это и не очень быстро.
Кстати, @JoinColumn() можно не указывать, доктрина все равно создаст нам такие же поля, как и с ней, однако если вы хотите изменить название поля или референс, то надо использовать данную аннотацию.
Теперь давайте укажем связи в сущностях Post и User, делается это следующим образом:
// Post.php
/**
* @var Comment[]
*
* @ORM\OneToMany(targetEntity=Comment::class, mappedBy="post")
*/
private $comments;
// User.php
/**
* @var Comment[]
*
* @ORM\OneToMany(targetEntity=Comment::class, mappedBy="user")
*/
private $comments;
При этом в конструкторе классов Post и User надо определить поле comments как коллекцию объектов:
public function __construct()
{
$this->comments = new ArrayCollection();
}
Вы могли заметить небольшие отличия в аннотациях ManyToOne и OneToMany, а именно на то, что у них разные атрибуты - inversedBy и mappedBy. inversedBy указывается в аннотации ManyToOne и указывает на родителя связи, mappedBy же указывают на обратную сторону двунаправленной связи.
При этом не надо указывать @ORM\Column() для полей связи, так как тип связующего поля возьмется по id, и если вы укажете, то аннотация @ManyToOne или @OneToMany просто не выполнится. Теперь можете запускать миграции:
php bin/console doctrine:migrations:diff
php bin/console doctrine:migrations:migrate
Если вы посмотрите в базу, то увидите таблицу comment со следующими полями:
id | post_id | user_id | comment | created_at | updated_at
Все сработало ровно так, как нам и нужно было.
Добавление комментария
Для добавления комментария к посту мы будем работать с коллекциями. Для этого нам сначала нужно добавить следующие методы в сущность Post:
public function addComment(Comment $comment): void
{
$comment->setPost($this);
if (!$this->comments->contains($comment)) {
$this->comments->add($comment);
}
}
public function removeComment(Comment $comment): void
{
$this->comments->removeElement($comment);
}
У ArrayCollection достаточно простой API, советую познакомиться с ним по ссылке.
В первом методе мы сеттим Post и проверяем, содержится ли уже в коллекции комментарий с таким id, если нет, добавляем, если да, ничего не делаем. Ну и удаление работает так же просто: вызываем метод коллекции removeElement, куда передаем сущность Comment. Теперь давайте создадим форму:
class CommentType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('comment', TextareaType::class, [
'label' => 'Новый комментарий',
])
;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Comment::class,
]);
}
Кажется, все готово, чтобы можно было под постом добавить комментарий. Для этого сначала напишем экшен контроллера PostsController, а потом отрендерим шаблон:
/**
* @Route("/post/{slug}", methods={"POST"}, name="comment_new")
*
*/
public function commentNew(Post $post, Request $request)
{
$comment = new Comment();
$comment->setUser($this->getUser());
$post->addComment($comment);
$form = $this->createForm(CommentType::class, $comment);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($comment);
$em->flush();
return $this->redirectToRoute('post_show', ['slug' => $post->getSlug()]);
}
return $this->render('post/show.html.twig', [
'post' => $post,
'form' => $form->createView()
]);
}
Как вы могли заметить, мы по одному роуту и показываем публикацию, и создаем комментарий. Примерно так может выглядеть итоговый шаблон:
{% extends 'base.html.twig' %}
{% block title %}{{ post.title }}{% endblock %}
{% block body %}
<br>
<div class="container col-sm-12">
<h4>{{ post.title }}</h4>
<p>{{ post.body }}</p><hr>
<span>{{ post.getCreatedAt() | date("m/d/Y") }}</span><br><hr>
<div class="container">
{{ form_start(form) }}
{{ form_row(form.comment) }}
<button type="submit" class="btn btn-primary">Отправить</button>
{{ form_end(form) }}
</div>
{% for comment in post.comments %}
<p>{{ comment.comment }}</p><hr>
{% endfor %}
</div>
{% endblock %}
И вот теперь нам понадобилась связь в сущности Post, так как через post.comments мы достали все комментарии, принадлежащие конкретному посту. Так же вы можете достать все комментария юзера через user.comments в шаблоне или $this->getUser()->getComments() в коде.
Итого
На этом пока все. Еще больше концепций и правил по работе с отношениями мы рассмотрим в следующих уроках.
P.S.
Если у вас будут проблемы с последней версией Symfony или с пониманием работы отношений, пишите вопросы в комментариях.
Комментарии