Implement comments

This commit is contained in:
2024-06-10 20:51:42 +02:00
parent b4a1ae592f
commit 67ff9ff8db
20 changed files with 479 additions and 5 deletions

View File

@@ -2,7 +2,10 @@
namespace App\Controller;
use App\Entity\Comment;
use App\Entity\Post;
use App\Entity\User;
use App\Form\CommentType;
use App\Form\PostType;
use App\Repository\PostRepository;
use Doctrine\ORM\EntityManagerInterface;
@@ -10,7 +13,9 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\CurrentUser;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\UX\Turbo\TurboBundle;
class PostController extends AbstractController
{
@@ -62,8 +67,12 @@ class PostController extends AbstractController
#[Route('/post/{id}', name: 'app_post_show', methods: ['GET'])]
public function show(Post $post): Response
{
$form = $this->createForm(CommentType::class, new Comment(), [
'action' => $this->generateUrl('app_post_comment', ['id' => $post->getId()]),
]);
return $this->render('post/show.html.twig', [
'post' => $post,
'form' => $form,
]);
}
@@ -97,4 +106,71 @@ class PostController extends AbstractController
return $this->redirectToRoute('app_posts', [], Response::HTTP_SEE_OTHER);
}
#[Route('/post/{id}/comment', name: 'app_post_comment', methods: ['POST'])]
public function publishComment(Request $request, Post $post, EntityManagerInterface $entityManager, #[CurrentUser] User $user): Response
{
$comment = new Comment();
$form = $this->createForm(CommentType::class, $comment);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$comment->setRelatedPost($post)
->setAuthor($user);
$entityManager->persist($comment);
$entityManager->flush();
if (TurboBundle::STREAM_FORMAT === $request->getPreferredFormat()) {
$request->setRequestFormat(TurboBundle::STREAM_FORMAT);
return $this->renderBlock('comment/new.stream.html.twig', 'success_stream', ['comment' => $comment]);
}
}
return $this->redirectToRoute('app_post_show', ['id' => $post->getId()], Response::HTTP_SEE_OTHER);
}
#[Route('/comment/{id}/edit', name: 'app_post_comment_edit', methods: ['GET', 'POST'])]
public function editComment(Request $request, Comment $comment, EntityManagerInterface $entityManager): Response
{
$form = $this->createForm(CommentType::class, $comment, [
'action' => $this->generateUrl('app_post_comment_edit', ['id' => $comment->getId()]),
]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$entityManager->flush();
if ($comment->getRelatedPost() === null) {
return $this->redirectToRoute('app_posts', [], Response::HTTP_SEE_OTHER);
}
return $this->redirectToRoute('app_post_show', [
'id' => $comment->getRelatedPost()->getId()
], Response::HTTP_SEE_OTHER);
}
return $this->render('comment/edit.html.twig', [
'comment' => $comment,
'form' => $form,
]);
}
#[Route('/comment/{id}', name: 'app_post_comment_delete', methods: ['POST'])]
#[IsGranted('COMMENT_EDIT', subject: 'comment')]
public function deleteComment(Request $request, Comment $comment, EntityManagerInterface $entityManager): Response
{
if ($this->isCsrfTokenValid('delete'.$comment->getId(), (string) $request->getPayload()->get('_token'))) {
$id = $comment->getId();
$entityManager->remove($comment);
$entityManager->flush();
if (TurboBundle::STREAM_FORMAT === $request->getPreferredFormat()) {
$request->setRequestFormat(TurboBundle::STREAM_FORMAT);
return $this->renderBlock('comment/deleted.stream.html.twig', 'success_stream', ['comment' => $id]);
}
}
if ($comment->getRelatedPost() === null) {
return $this->redirectToRoute('app_posts');
}
return $this->redirectToRoute('app_post_show', ['id' => $comment->getRelatedPost()->getId()]);
}
}

119
src/Entity/Comment.php Normal file
View File

@@ -0,0 +1,119 @@
<?php
namespace App\Entity;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use App\Repository\CommentRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: CommentRepository::class)]
#[ORM\HasLifecycleCallbacks]
#[ApiResource(operations: [new GetCollection()])]
#[ApiFilter(filterClass: SearchFilter::class, properties: ['author', 'related_post'])]
class Comment
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'comments')]
#[ORM\JoinColumn(nullable: false)]
private ?User $author = null;
#[ORM\ManyToOne(inversedBy: 'comments')]
private ?Post $related_post = null;
#[ORM\Column]
private ?\DateTimeImmutable $created_at = null;
#[ORM\Column]
private ?\DateTimeImmutable $edited_at = null;
#[ORM\Column(length: 255)]
private ?string $content = null;
public function getId(): ?int
{
return $this->id;
}
public function getAuthor(): ?User
{
return $this->author;
}
public function setAuthor(?User $author): static
{
$this->author = $author;
return $this;
}
public function getRelatedPost(): ?Post
{
return $this->related_post;
}
public function setRelatedPost(?Post $related_post): static
{
$this->related_post = $related_post;
return $this;
}
public function getCreatedAt(): ?\DateTimeImmutable
{
return $this->created_at;
}
public function setCreatedAt(\DateTimeImmutable $created_at): static
{
$this->created_at = $created_at;
return $this;
}
public function getEditedAt(): ?\DateTimeImmutable
{
return $this->edited_at;
}
public function setEditedAt(\DateTimeImmutable $edited_at): static
{
$this->edited_at = $edited_at;
return $this;
}
public function getContent(): ?string
{
return $this->content;
}
public function setContent(string $content): static
{
$this->content = $content;
return $this;
}
#[ORM\PrePersist]
public function setCreatedAtDate(): void
{
if ($this->created_at === null) {
$this->created_at = new \DateTimeImmutable();
$this->edited_at = $this->created_at;
}
}
#[ORM\PreUpdate]
public function setEditedAtDate(): void
{
$this->edited_at = new \DateTimeImmutable();
}
}

View File

@@ -8,6 +8,8 @@ use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GetCollection;
use App\Repository\PostRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
@@ -61,6 +63,17 @@ class Post
#[Groups(['post:collection:read'])]
private ?Species $species = null;
/**
* @var Collection<int, Comment>
*/
#[ORM\OneToMany(targetEntity: Comment::class, mappedBy: 'related_post', fetch: 'EXTRA_LAZY')]
private Collection $comments;
public function __construct()
{
$this->comments = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
@@ -158,4 +171,34 @@ class Post
$this->publicationDate = new \DateTimeImmutable();
}
}
/**
* @return Collection<int, Comment>
*/
public function getComments(): Collection
{
return $this->comments;
}
public function addComment(Comment $comment): static
{
if (!$this->comments->contains($comment)) {
$this->comments->add($comment);
$comment->setRelatedPost($this);
}
return $this;
}
public function removeComment(Comment $comment): static
{
if ($this->comments->removeElement($comment)) {
// set the owning side to null (unless already changed)
if ($comment->getRelatedPost() === $this) {
$comment->setRelatedPost(null);
}
}
return $this;
}
}

View File

@@ -10,6 +10,8 @@ use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Put;
use App\Repository\UserRepository;
use App\State\UserPasswordHasher;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
@@ -60,6 +62,17 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
#[Groups(['user:create', 'user:update'])]
private ?string $plainPassword = null;
/**
* @var Collection<int, Comment>
*/
#[ORM\OneToMany(targetEntity: Comment::class, mappedBy: 'author')]
private Collection $comments;
public function __construct()
{
$this->comments = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
@@ -146,4 +159,34 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
}
/**
* @return Collection<int, Comment>
*/
public function getComments(): Collection
{
return $this->comments;
}
public function addComment(Comment $comment): static
{
if (!$this->comments->contains($comment)) {
$this->comments->add($comment);
$comment->setAuthor($this);
}
return $this;
}
public function removeComment(Comment $comment): static
{
if ($this->comments->removeElement($comment)) {
// set the owning side to null (unless already changed)
if ($comment->getAuthor() === $this) {
$comment->setAuthor(null);
}
}
return $this;
}
}

29
src/Form/CommentType.php Normal file
View File

@@ -0,0 +1,29 @@
<?php
namespace App\Form;
use App\Entity\Comment;
use App\Entity\Post;
use App\Entity\User;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class CommentType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('content', TextareaType::class)
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Comment::class,
]);
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Repository;
use App\Entity\Comment;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Comment>
*/
class CommentRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Comment::class);
}
// /**
// * @return Comment[] Returns an array of Comment objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('c')
// ->andWhere('c.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('c.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?Comment
// {
// return $this->createQueryBuilder('c')
// ->andWhere('c.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

View File

@@ -25,6 +25,8 @@ class PostRepository extends ServiceEntityRepository
public function findPaginatedPosts(int $page, int $limit): Paginator
{
$query = $this->createQueryBuilder('p')
->addSelect('species')
->leftJoin('p.species', 'species')
->setFirstResult(($page - 1) * $limit)
->setMaxResults($limit);

View File

@@ -0,0 +1,36 @@
<?php
namespace App\Security\Voter;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\User\UserInterface;
class CommentVoter extends Voter
{
public const EDIT = 'COMMENT_EDIT';
protected function supports(string $attribute, mixed $subject): bool
{
// replace with your own logic
// https://symfony.com/doc/current/security/voters.html
return $attribute === self::EDIT
&& $subject instanceof \App\Entity\Comment;
}
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
$user = $token->getUser();
// if the user is anonymous, do not grant access
if (!$user instanceof UserInterface) {
return false;
}
if ($subject->getAuthor() === $user) {
return true;
}
return in_array('ROLE_ADMIN', $user->getRoles(), true);
}
}