Allow logged users to comment posts. Edition is not allowed via the API since it doesn't support auth yet. Symfony UX is used with Turbo to avoid full pages reload, without JavaScript™. Co-authored-by: clfreville2 <clement.freville2@etu.uca.fr> Reviewed-on: #10pull/13/head
parent
b4a1ae592f
commit
8859cd0000
@ -0,0 +1 @@
|
||||
import './bootstrap.js';
|
@ -0,0 +1,3 @@
|
||||
import { startStimulusApp } from '@symfony/stimulus-bundle';
|
||||
|
||||
const app = startStimulusApp();
|
@ -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();
|
||||
}
|
||||
}
|
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
@ -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()
|
||||
// ;
|
||||
// }
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
<form method="post" action="{{ path('app_post_comment_delete', {'id': comment.id}) }}" onsubmit="return confirm('Are you sure you want to delete this comment?');">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('delete' ~ comment.id) }}">
|
||||
<button class="btn btn-danger">Delete</button>
|
||||
</form>
|
@ -0,0 +1,17 @@
|
||||
<turbo-frame id="comment_{{ comment.id }}">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
{{ comment.author.email }} le {{ comment.createdAt | date }}
|
||||
{% if comment.createdAt != comment.editedAt %}
|
||||
(modifié le {{ comment.editedAt | date }})
|
||||
{% endif %}
|
||||
</h5>
|
||||
<p class="card-text">{{ comment.content }}</p>
|
||||
{% if is_granted('COMMENT_EDIT', comment) %}
|
||||
<a href="{{ path('app_post_comment_edit', {'id': comment.id}) }}" class="btn btn-primary">Modifier</a>
|
||||
{{ include('comment/_delete_form.html.twig') }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</turbo-frame>
|
@ -0,0 +1,3 @@
|
||||
{% block success_stream %}
|
||||
<turbo-stream action="remove" target="comment_{{ comment }}"></turbo-stream>
|
||||
{% endblock %}
|
@ -0,0 +1,11 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Edit Comment{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<h1>Edit Comment</h1>
|
||||
|
||||
<turbo-frame id="comment_{{ comment.id }}">
|
||||
{{ include('post/_form.html.twig', {'button_label': 'Update'}) }}
|
||||
</turbo-frame>
|
||||
{% endblock %}
|
@ -0,0 +1,7 @@
|
||||
{% block success_stream %}
|
||||
<turbo-stream action="append" targets="#comments">
|
||||
<template>
|
||||
{{ include('comment/comment.html.twig') }}
|
||||
</template>
|
||||
</turbo-stream>
|
||||
{% endblock %}
|
@ -1,4 +1,4 @@
|
||||
{{ form_start(form) }}
|
||||
{{ form_widget(form) }}
|
||||
<button class="btn">{{ button_label|default('Save') }}</button>
|
||||
<button class="btn btn-primary">{{ button_label|default('Save') }}</button>
|
||||
{{ form_end(form) }}
|
||||
|
Loading…
Reference in new issue