add post follow

This commit is contained in:
Simon Vieille 2022-02-06 19:36:10 +01:00
parent bf901ad799
commit 6607b03e3b
20 changed files with 621 additions and 2 deletions

1
.php-version Normal file
View file

@ -0,0 +1 @@
8.1

View file

@ -15,6 +15,8 @@ core:
- {name: 'Blog\PostController::rss', action: 'App\Controller\Blog\PostController::rss'}
- {name: 'Blog\PostController::jsonApi', action: 'App\Controller\Blog\PostController::jsonApi'}
- {name: 'Blog\CategoryController::categories', action: 'App\Controller\Blog\CategoryController::categories'}
- {name: 'Blog\PostFollowController::enable', action: 'App\Controller\Blog\PostFollowController::enable'}
- {name: 'Blog\PostFollowController::disable', action: 'App\Controller\Blog\PostFollowController::disable'}
- {name: 'StlMeshController::meshes', action: 'App\Controller\StlMeshController::meshes'}
pages:
App\Entity\Page\SimplePage:
@ -62,7 +64,7 @@ core:
templates:
- {name: "Par défaut", file: "page/text/default.txt.twig"}
App\Entity\Page\TitledPage:
name: '_'
name: 'Page avec titre'
templates:
- {name: "Par défaut", file: "page/text/default.txt.twig"}

View file

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20220205165823 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE post_follow (id INT AUTO_INCREMENT NOT NULL, post_id INT NOT NULL, comment_id INT NOT NULL, hash VARCHAR(255) NOT NULL, is_enabled TINYINT(1) NOT NULL, INDEX IDX_5816A0154B89032C (post_id), INDEX IDX_5816A015F8697D13 (comment_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
$this->addSql('ALTER TABLE post_follow ADD CONSTRAINT FK_5816A0154B89032C FOREIGN KEY (post_id) REFERENCES post (id)');
$this->addSql('ALTER TABLE post_follow ADD CONSTRAINT FK_5816A015F8697D13 FOREIGN KEY (comment_id) REFERENCES comment (id)');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP TABLE post_follow');
}
}

View file

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20220205172820 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE post_follow ADD created_at DATETIME NOT NULL, ADD updated_at DATETIME NOT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE post_follow DROP created_at, DROP updated_at');
}
}

View file

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20220206181024 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE post_follow DROP FOREIGN KEY FK_5816A015F8697D13');
$this->addSql('ALTER TABLE post_follow DROP FOREIGN KEY FK_5816A0154B89032C');
$this->addSql('ALTER TABLE post_follow ADD CONSTRAINT FK_5816A015F8697D13 FOREIGN KEY (comment_id) REFERENCES comment (id) ON DELETE CASCADE');
$this->addSql('ALTER TABLE post_follow ADD CONSTRAINT FK_5816A0154B89032C FOREIGN KEY (post_id) REFERENCES post (id) ON DELETE CASCADE');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE post_follow DROP FOREIGN KEY FK_5816A0154B89032C');
$this->addSql('ALTER TABLE post_follow DROP FOREIGN KEY FK_5816A015F8697D13');
$this->addSql('ALTER TABLE post_follow ADD CONSTRAINT FK_5816A0154B89032C FOREIGN KEY (post_id) REFERENCES post (id)');
$this->addSql('ALTER TABLE post_follow ADD CONSTRAINT FK_5816A015F8697D13 FOREIGN KEY (comment_id) REFERENCES comment (id)');
}
}

View file

@ -17,6 +17,8 @@ use App\UrlGenerator\PostGenerator;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use App\Factory\Blog\PostFollowFactory;
use App\Manager\PostFollowManager;
class PostController extends PageController
{
@ -36,6 +38,7 @@ class PostController extends PageController
Post $post,
string $slug,
CommentFactory $commentFactory,
PostFollowManager $postFollowManager,
Request $request,
EntityManager $entityManager
): Response {
@ -61,6 +64,7 @@ class PostController extends PageController
if ($form->isValid()) {
$data = $request->request->get($form->getName());
$parentCommentId = (int) $data['parentCommentId'];
$follow = (bool) ($data['follow'] ?? false);
foreach ($post->getComments(['id' => $parentCommentId]) as $comment) {
$form->getData()->setParentComment($comment);
@ -70,8 +74,13 @@ class PostController extends PageController
$this->addFlash('success', 'Commentaire publié !');
if ($follow && $form->getData()->getEmail()) {
$postFollowManager->create($post, $form->getData());
}
return $this->redirect($request->getUri());
}
$this->addFlash('error', 'Le formulaire n\'est pas valide.');
}

View file

@ -0,0 +1,47 @@
<?php
namespace App\Controller\Blog;
use App\Core\Controller\Site\PageController;
use App\Core\Site\SiteRequest;
use App\Core\Site\SiteStore;
use App\Entity\Blog\PostFollow;
use App\Manager\PostFollowManager;
class PostFollowController extends PageController
{
protected PostFollowManager $manager;
public function __construct(PostFollowManager $manager, SiteRequest $siteRequest, SiteStore $siteStore)
{
parent::__construct($siteRequest, $siteStore);
$this->manager = $manager;
}
public function enable(PostFollow $postFollow)
{
$this->manager->enable($postFollow);
$this->addFlash('success', 'Votre e-mail a bien été confirmé.');
return $this->redirectToRoute('blog_menu_post', [
'post' => $postFollow->getPost()->getId(),
'slug' => $postFollow->getPost()->getSlug(),
'_domain' => $this->siteRequest->getDomain(),
]);
}
public function disable(PostFollow $postFollow)
{
$this->manager->disable($postFollow);
$this->addFlash('success', 'Vous ne recevrez plus de notification pour cet article.');
return $this->redirectToRoute('blog_menu_post', [
'post' => $postFollow->getPost()->getId(),
'slug' => $postFollow->getPost()->getSlug(),
'_domain' => $this->siteRequest->getDomain(),
]);
}
}

View file

@ -66,9 +66,15 @@ class Comment implements EntityInterface
*/
private $comments;
/**
* @ORM\OneToMany(targetEntity=PostFollow::class, mappedBy="comment", orphanRemoval=true)
*/
private $postFollows;
public function __construct()
{
$this->comments = new ArrayCollection();
$this->postFollows = new ArrayCollection();
}
public function getId(): ?int
@ -220,4 +226,34 @@ class Comment implements EntityInterface
substr($this->getContent(), 0, 20).'…'
);
}
/**
* @return Collection|PostFollow[]
*/
public function getPostFollows(): Collection
{
return $this->postFollows;
}
public function addPostFollow(PostFollow $postFollow): self
{
if (!$this->postFollows->contains($postFollow)) {
$this->postFollows[] = $postFollow;
$postFollow->setComment($this);
}
return $this;
}
public function removePostFollow(PostFollow $postFollow): self
{
if ($this->postFollows->removeElement($postFollow)) {
// set the owning side to null (unless already changed)
if ($postFollow->getComment() === $this) {
$postFollow->setComment(null);
}
}
return $this;
}
}

View file

@ -118,10 +118,16 @@ class Post implements EntityInterface
*/
private $notebook;
/**
* @ORM\OneToMany(targetEntity=PostFollow::class, mappedBy="post", orphanRemoval=true)
*/
private $postFollows;
public function __construct()
{
$this->categories = new ArrayCollection();
$this->comments = new ArrayCollection();
$this->postFollows = new ArrayCollection();
}
public function getId(): ?int
@ -410,4 +416,34 @@ class Post implements EntityInterface
return $this;
}
/**
* @return Collection|PostFollow[]
*/
public function getPostFollows(): Collection
{
return $this->postFollows;
}
public function addPostFollow(PostFollow $postFollow): self
{
if (!$this->postFollows->contains($postFollow)) {
$this->postFollows[] = $postFollow;
$postFollow->setPost($this);
}
return $this;
}
public function removePostFollow(PostFollow $postFollow): self
{
if ($this->postFollows->removeElement($postFollow)) {
// set the owning side to null (unless already changed)
if ($postFollow->getPost() === $this) {
$postFollow->setPost(null);
}
}
return $this;
}
}

View file

@ -0,0 +1,99 @@
<?php
namespace App\Entity\Blog;
use App\Core\Doctrine\Timestampable;
use App\Core\Entity\EntityInterface;
use App\Repository\Blog\PostFollowRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass=PostFollowRepository::class)
* @ORM\HasLifecycleCallbacks
*/
class PostFollow implements EntityInterface
{
use Timestampable;
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=255)
*/
private $hash;
/**
* @ORM\Column(type="boolean")
*/
private $isEnabled;
/**
* @ORM\ManyToOne(targetEntity=Post::class, inversedBy="postFollows")
* @ORM\JoinColumn(nullable=false, onDelete="CASCADE")
*/
private $post;
/**
* @ORM\ManyToOne(targetEntity=Comment::class, inversedBy="postFollows")
* @ORM\JoinColumn(nullable=false, onDelete="CASCADE")
*/
private $comment;
public function getId(): ?int
{
return $this->id;
}
public function getHash(): ?string
{
return $this->hash;
}
public function setHash(string $hash): self
{
$this->hash = $hash;
return $this;
}
public function getIsEnabled(): ?bool
{
return $this->isEnabled;
}
public function setIsEnabled(bool $isEnabled): self
{
$this->isEnabled = $isEnabled;
return $this;
}
public function getPost(): ?Post
{
return $this->post;
}
public function setPost(?Post $post): self
{
$this->post = $post;
return $this;
}
public function getComment(): ?Comment
{
return $this->comment;
}
public function setComment(?Comment $comment): self
{
$this->comment = $comment;
return $this;
}
}

View file

@ -9,6 +9,8 @@ use App\Core\Notification\MailNotifier;
use App\Entity\Blog\Comment;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use App\Core\Setting\SettingManager;
use App\Repository\Blog\PostFollowRepositoryQuery;
use App\Core\Site\SiteRequest;
/**
* class CommentEventSubscriber.
@ -20,12 +22,22 @@ class CommentEventSubscriber extends EntityManagerEventSubscriber
protected MailNotifier $notifier;
protected UrlGeneratorInterface $urlGenerator;
protected SettingManager $settingManager;
protected PostFollowRepository $postFollowRepository;
protected SiteRequest $siteRequest;
public function __construct(MailNotifier $notifier, UrlGeneratorInterface $urlGenerator, SettingManager $settingManager)
public function __construct(
MailNotifier $notifier,
UrlGeneratorInterface $urlGenerator,
SettingManager $settingManager,
PostFollowRepositoryQuery $postFollowRepositoryQuery,
SiteRequest $siteRequest
)
{
$this->notifier = $notifier;
$this->urlGenerator = $urlGenerator;
$this->settingManager = $settingManager;
$this->postFollowRepositoryQuery = $postFollowRepositoryQuery;
$this->siteRequest = $siteRequest;
}
public function support(EntityInterface $entity)
@ -50,9 +62,41 @@ class CommentEventSubscriber extends EntityManagerEventSubscriber
'post' => $this->urlGenerator->generate('blog_menu_post', [
'post' => $event->getEntity()->getPost()->getId(),
'slug' => $event->getEntity()->getPost()->getSlug(),
'_domain' => $this->siteRequest->getDomain(),
], UrlGeneratorInterface::ABSOLUTE_URL).'#review-'.$event->getEntity()->getId(),
],
], 'text/plain')
;
$postFollows = $this->postFollowRepositoryQuery->create()
->where('.post = :post')
->andWhere('.isEnabled = 1')
->setParameter(':post', $event->getEntity()->getPost()->getId())
->find();
foreach ($postFollows as $postFollow) {
if ($event->getEntity()->getEmail() !== $postFollow->getComment()->getEmail()) {
$this->notifier
->init()
->setFrom($this->settingManager->get('email_sender')->getValue())
->addRecipient($this->settingManager->get('email_comment')->getValue())
->setSubject('[Deblan] Nouveau commentaire')
->notify('mail/post_follow_comment.html.twig', [
'post' => $event->getEntity()->getPost(),
'links' => [
'post' => $this->urlGenerator->generate('blog_menu_post', [
'post' => $event->getEntity()->getPost()->getId(),
'slug' => $event->getEntity()->getPost()->getSlug(),
'_domain' => $this->siteRequest->getDomain(),
], UrlGeneratorInterface::ABSOLUTE_URL).'#review-'.$event->getEntity()->getId(),
'disable' => $this->urlGenerator->generate('blog_tech_follow_disable', [
'hash' => $postFollow->getHash(),
'_domain' => $this->siteRequest->getDomain(),
], UrlGeneratorInterface::ABSOLUTE_URL),
],
], 'text/plain')
;
}
}
}
}

View file

@ -0,0 +1,75 @@
<?php
namespace App\EventSuscriber\Blog;
use App\Core\Entity\EntityInterface;
use App\Core\Event\EntityManager\EntityManagerEvent;
use App\Core\EventSuscriber\EntityManagerEventSubscriber;
use App\Core\Notification\MailNotifier;
use App\Core\Setting\SettingManager;
use App\Entity\Blog\PostFollow;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use App\Core\Site\SiteRequest;
/**
* class PostFollowEventSubscriber.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class PostFollowEventSubscriber extends EntityManagerEventSubscriber
{
protected MailNotifier $notifier;
protected UrlGeneratorInterface $urlGenerator;
protected SettingManager $settingManager;
protected SiteRequest $siteRequest;
public function __construct(
MailNotifier $notifier,
UrlGeneratorInterface $urlGenerator,
SettingManager $settingManager,
SiteRequest $siteRequest
)
{
$this->notifier = $notifier;
$this->urlGenerator = $urlGenerator;
$this->settingManager = $settingManager;
$this->siteRequest = $siteRequest;
}
public function support(EntityInterface $entity)
{
return $entity instanceof PostFollow;
}
public function onCreate(EntityManagerEvent $event)
{
if (!$this->support($event->getEntity())) {
return;
}
$this->notifier
->init()
->setFrom($this->settingManager->get('email_sender')->getValue())
->addRecipient($event->getEntity()->getComment()->getEmail())
->setSubject('[Deblan] Confirmer votre e-mail')
->notify('mail/post_follow_subscription.html.twig', [
'entity' => $event->getEntity(),
'links' => [
'post' => $this->urlGenerator->generate('blog_menu_post', [
'post' => $event->getEntity()->getPost()->getId(),
'slug' => $event->getEntity()->getPost()->getSlug(),
'_domain' => $this->siteRequest->getDomain(),
], UrlGeneratorInterface::ABSOLUTE_URL),
'enable' => $this->urlGenerator->generate('blog_tech_follow_enable', [
'hash' => $event->getEntity()->getHash(),
'_domain' => $this->siteRequest->getDomain(),
], UrlGeneratorInterface::ABSOLUTE_URL),
'disable' => $this->urlGenerator->generate('blog_tech_follow_disable', [
'hash' => $event->getEntity()->getHash(),
'_domain' => $this->siteRequest->getDomain(),
], UrlGeneratorInterface::ABSOLUTE_URL),
],
], 'text/plain')
;
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace App\Factory\Blog;
use App\Core\Factory\FactoryInterface;
use App\Entity\Blog\Comment;
use App\Entity\Blog\Post;
use App\Entity\Blog\PostFollow as Entity;
class PostFollowFactory implements FactoryInterface
{
public function create(Post $post, Comment $comment): Entity
{
$entity = new Entity();
$entity
->setPost($post)
->setComment($comment)
->setIsEnabled(false)
->setHash(hash('sha256', bin2hex(random_bytes(32))))
;
return $entity;
}
}

View file

@ -14,6 +14,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Url;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
class UserCommentType extends AbstractType
{
@ -75,6 +76,16 @@ class UserCommentType extends AbstractType
]
);
$builder->add(
'follow',
CheckboxType::class,
[
'label' => 'Recevoir une notification par e-mail si un commentaire est déposé sur cet article',
'mapped' => false,
'required' => false,
]
);
$builder->add(
'parentCommentId',
HiddenType::class,

View file

@ -0,0 +1,70 @@
<?php
namespace App\Manager;
use App\Core\Manager\EntityManager;
use App\Entity\Blog\Comment;
use App\Entity\Blog\Post;
use App\Entity\Blog\PostFollow;
use App\Factory\Blog\PostFollowFactory;
use App\Repository\Blog\PostFollowRepositoryQuery;
/**
* class PostFollowManager.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class PostFollowManager
{
protected PostFollowFactory $factory;
protected PostFollowRepositoryQuery $repository;
protected EntityManager $manager;
public function __construct(
PostFollowFactory $factory,
PostFollowRepositoryQuery $repository,
EntityManager $manager
) {
$this->factory = $factory;
$this->repository = $repository;
$this->manager = $manager;
}
public function create(Post $post, Comment $comment): PostFollow
{
$postFollow = $this->find($post, $comment);
if (!$postFollow) {
$postFollow = $this->factory->create($post, $comment);
$this->manager->create($postFollow);
}
return $postFollow;
}
public function find(Post $post, Comment $comment): ?PostFollow
{
return $this->repository->create()
->where('.post = :post')
->andWhere('.comment = :comment')
->setParameters([
':post' => $post->getId(),
':comment' => $comment->getId(),
])
->findOne()
;
}
public function enable(PostFollow $postFollow): void
{
$postFollow->setIsEnabled(true);
$this->manager->update($postFollow);
}
public function disable(PostFollow $postFollow): void
{
$this->manager->delete($postFollow);
}
}

View file

@ -0,0 +1,20 @@
<?php
namespace App\Repository\Blog;
use App\Entity\Blog\PostFollow;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
class PostFollowRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, PostFollow::class);
}
public function getEm()
{
return $this->getEntityManager();
}
}

View file

@ -0,0 +1,15 @@
<?php
namespace App\Repository\Blog;
use App\Core\Repository\RepositoryQuery;
use Knp\Component\Pager\PaginatorInterface;
use App\Repository\Blog\PostFollowRepository as Repository;
class PostFollowRepositoryQuery extends RepositoryQuery
{
public function __construct(Repository $repository, PaginatorInterface $paginator)
{
parent::__construct($repository, 'p', $paginator);
}
}

View file

@ -0,0 +1,8 @@
Un nouveau commentaire a été déposé sur l'article suivant :
{{ post.title|raw }}
{{ links.post }}
Vous pourrez supprimer les notifications de cet article via ce lien :
{{ links.disable }}

View file

@ -0,0 +1,14 @@
Vous avez décidé de recevoir les notifications de cet article :
{{ entity.post.title|raw }}
{{ links.post }}
Si vous n'êtes pas à l'origine de cette demande, vous pouvez ignorer ce message et votre e-mail sera automatiquement supprimé dans quelques jours.
Veuillez cliquer sur le lien de confirmation pour valider votre e-mail :
{{ links.enable }}
Vous pourrez vous désinscrire à tout moment via ce lien :
{{ links.disable }}

View file

@ -148,6 +148,13 @@
{{ form_errors(form.email) }}
</div>
</div>
<div class="row">
<div class="field col-12">
{{ form_label(form.follow) }}
{{ form_widget(form.follow) }}
{{ form_errors(form.follow) }}
</div>
</div>
<div class="field">
<p class="no-margin">
{{- 'Votre commentaire - Vous pouvez utiliser du markdown ' }}