Implement comments
This commit is contained in:
@@ -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
119
src/Entity/Comment.php
Normal 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();
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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
29
src/Form/CommentType.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
43
src/Repository/CommentRepository.php
Normal file
43
src/Repository/CommentRepository.php
Normal 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()
|
||||
// ;
|
||||
// }
|
||||
}
|
@@ -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);
|
||||
|
||||
|
36
src/Security/Voter/CommentVoter.php
Normal file
36
src/Security/Voter/CommentVoter.php
Normal 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);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user