feat(notification) : notifier le nouvel assigné d'une tâche
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\EventListener;
|
||||
|
||||
use App\Entity\Notification;
|
||||
use App\Entity\Task;
|
||||
use App\Entity\User;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
|
||||
use Doctrine\ORM\Event\OnFlushEventArgs;
|
||||
use Doctrine\ORM\Event\PostFlushEventArgs;
|
||||
use Doctrine\ORM\Events;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
|
||||
#[AsDoctrineListener(event: Events::onFlush)]
|
||||
#[AsDoctrineListener(event: Events::postFlush)]
|
||||
final class TaskNotificationListener
|
||||
{
|
||||
/** @var list<array{user: User, type: string, task: Task}> */
|
||||
private array $pending = [];
|
||||
|
||||
public function __construct(private readonly Security $security) {}
|
||||
|
||||
public function onFlush(OnFlushEventArgs $args): void
|
||||
{
|
||||
$actor = $this->security->getUser();
|
||||
if (!$actor instanceof User) {
|
||||
return;
|
||||
}
|
||||
|
||||
$uow = $args->getObjectManager()->getUnitOfWork();
|
||||
|
||||
// Assignation sur une tâche nouvellement créée.
|
||||
foreach ($uow->getScheduledEntityInsertions() as $entity) {
|
||||
if (!$entity instanceof Task) {
|
||||
continue;
|
||||
}
|
||||
$assignee = $entity->getAssignee();
|
||||
if ($assignee instanceof User && $assignee !== $actor) {
|
||||
$this->pending[] = ['user' => $assignee, 'type' => 'task_assigned', 'task' => $entity];
|
||||
}
|
||||
}
|
||||
|
||||
// Changement d'assignation sur une tâche existante.
|
||||
foreach ($uow->getScheduledEntityUpdates() as $entity) {
|
||||
if (!$entity instanceof Task) {
|
||||
continue;
|
||||
}
|
||||
$changeSet = $uow->getEntityChangeSet($entity);
|
||||
if (!isset($changeSet['assignee'])) {
|
||||
continue;
|
||||
}
|
||||
$new = $changeSet['assignee'][1];
|
||||
if ($new instanceof User && $new !== $actor) {
|
||||
$this->pending[] = ['user' => $new, 'type' => 'task_assigned', 'task' => $entity];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function postFlush(PostFlushEventArgs $args): void
|
||||
{
|
||||
if ([] === $this->pending) {
|
||||
return;
|
||||
}
|
||||
|
||||
$pending = $this->pending;
|
||||
$this->pending = [];
|
||||
|
||||
$em = $args->getObjectManager();
|
||||
foreach ($pending as $item) {
|
||||
$em->persist($this->buildNotification($item['user'], $item['type'], $item['task']));
|
||||
}
|
||||
$em->flush();
|
||||
}
|
||||
|
||||
private function buildNotification(User $user, string $type, Task $task): Notification
|
||||
{
|
||||
[$title, $message] = $this->render($type, $task);
|
||||
|
||||
$notification = new Notification();
|
||||
$notification->setUser($user);
|
||||
$notification->setType($type);
|
||||
$notification->setTitle($title);
|
||||
$notification->setMessage($message);
|
||||
$notification->setCreatedAt(new DateTimeImmutable());
|
||||
|
||||
return $notification;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: string, 1: string}
|
||||
*/
|
||||
private function render(string $type, Task $task): array
|
||||
{
|
||||
$projectName = $task->getProject()?->getName() ?? '';
|
||||
$suffix = '' !== $projectName ? sprintf(' — %s', $projectName) : '';
|
||||
$context = sprintf('« %s »%s', (string) $task->getTitle(), $suffix);
|
||||
|
||||
return match ($type) {
|
||||
'task_assigned' => ['Nouvelle tâche assignée', $context],
|
||||
'task_collaborator_added' => ['Ajout à une tâche', $context],
|
||||
default => ['Notification', $context],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Functional\EventListener;
|
||||
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Task;
|
||||
use App\Entity\User;
|
||||
use App\Repository\NotificationRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class TaskNotificationListenerTest extends KernelTestCase
|
||||
{
|
||||
private EntityManagerInterface $em;
|
||||
private NotificationRepository $notifications;
|
||||
private TokenStorageInterface $tokenStorage;
|
||||
private Project $project;
|
||||
private User $actor;
|
||||
private User $alice;
|
||||
private User $bob;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
$c = self::getContainer();
|
||||
$this->em = $c->get(EntityManagerInterface::class);
|
||||
$this->notifications = $c->get(NotificationRepository::class);
|
||||
$this->tokenStorage = $c->get(TokenStorageInterface::class);
|
||||
|
||||
$project = $this->em->getRepository(Project::class)->findOneBy([]);
|
||||
self::assertNotNull($project, 'Les fixtures doivent fournir au moins un projet.');
|
||||
$this->project = $project;
|
||||
|
||||
$this->actor = $this->makeUser('actor');
|
||||
$this->alice = $this->makeUser('alice');
|
||||
$this->bob = $this->makeUser('bob');
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
public function testAssignmentToOtherUserCreatesNotification(): void
|
||||
{
|
||||
$this->loginAs($this->actor);
|
||||
|
||||
$task = $this->makeTask();
|
||||
$task->setAssignee($this->alice);
|
||||
$this->em->persist($task);
|
||||
$this->em->flush();
|
||||
|
||||
$rows = $this->notifications->findBy(['user' => $this->alice]);
|
||||
self::assertCount(1, $rows);
|
||||
self::assertSame('task_assigned', $rows[0]->getType());
|
||||
self::assertStringContainsString((string) $task->getTitle(), (string) $rows[0]->getMessage());
|
||||
}
|
||||
|
||||
public function testSelfAssignmentCreatesNoNotification(): void
|
||||
{
|
||||
$this->loginAs($this->actor);
|
||||
|
||||
$task = $this->makeTask();
|
||||
$task->setAssignee($this->actor);
|
||||
$this->em->persist($task);
|
||||
$this->em->flush();
|
||||
|
||||
self::assertCount(0, $this->notifications->findBy(['user' => $this->actor]));
|
||||
}
|
||||
|
||||
public function testReassignmentNotifiesOnlyNewAssignee(): void
|
||||
{
|
||||
$this->loginAs($this->actor);
|
||||
|
||||
$task = $this->makeTask();
|
||||
$task->setAssignee($this->alice);
|
||||
$this->em->persist($task);
|
||||
$this->em->flush();
|
||||
|
||||
$task->setAssignee($this->bob);
|
||||
$this->em->flush();
|
||||
|
||||
self::assertCount(1, $this->notifications->findBy(['user' => $this->alice]));
|
||||
self::assertCount(1, $this->notifications->findBy(['user' => $this->bob]));
|
||||
}
|
||||
|
||||
public function testAssigneeSetToNullCreatesNoNotificationForNull(): void
|
||||
{
|
||||
$this->loginAs($this->actor);
|
||||
|
||||
$task = $this->makeTask();
|
||||
$task->setAssignee($this->alice);
|
||||
$this->em->persist($task);
|
||||
$this->em->flush();
|
||||
|
||||
$task->setAssignee(null);
|
||||
$this->em->flush();
|
||||
|
||||
// alice a reçu la 1re notif, mais le passage à null n'en crée aucune autre.
|
||||
self::assertCount(1, $this->notifications->findBy(['user' => $this->alice]));
|
||||
}
|
||||
|
||||
public function testNoActorCreatesNoNotification(): void
|
||||
{
|
||||
$this->tokenStorage->setToken(null);
|
||||
|
||||
$task = $this->makeTask();
|
||||
$task->setAssignee($this->alice);
|
||||
$this->em->persist($task);
|
||||
$this->em->flush();
|
||||
|
||||
self::assertCount(0, $this->notifications->findBy(['user' => $this->alice]));
|
||||
}
|
||||
|
||||
private function makeUser(string $prefix): User
|
||||
{
|
||||
$user = new User();
|
||||
$user->setUsername($prefix.'-'.uniqid());
|
||||
$user->setPassword('x');
|
||||
$user->setRoles(['ROLE_USER']);
|
||||
$this->em->persist($user);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
private function makeTask(): Task
|
||||
{
|
||||
$task = new Task();
|
||||
$task->setNumber(random_int(100000, 999999));
|
||||
$task->setTitle('Tâche de test '.uniqid());
|
||||
$task->setProject($this->project);
|
||||
|
||||
return $task;
|
||||
}
|
||||
|
||||
private function loginAs(User $user): void
|
||||
{
|
||||
$this->tokenStorage->setToken(
|
||||
new UsernamePasswordToken($user, 'main', $user->getRoles()),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user