<?php
namespace App\EventSubscriber;
use App\DBAL\Types\NotificationCommandType;
use App\Entity\Category;
use App\Entity\Notification\Map\EventNotification;
use App\Entity\Notification\Map\StreamNotification;
use App\Entity\Notification\Map\TaskNotification;
use App\Entity\Plan;
use App\Entity\Position;
use App\Entity\Source;
use App\Entity\Tag;
use App\Entity\Template;
use App\Entity\Template\Column as TemplateColumn;
use App\Entity\Template\Task as TemplateTask;
use App\Entity\User;
use App\Entity\UserInterface;
use App\Entity\Workspace;
use App\Entity\Workspace\Member;
use App\Entity\Workspace\Stream;
use App\Entity\Workspace\Stream\Collaborator;
use App\Entity\Workspace\Stream\Column;
use App\Entity\Workspace\Stream\Comment;
use App\Entity\Workspace\Stream\Comment\Collaborator as CommentCollaborator;
use App\Entity\Workspace\Stream\Comment\Map\DocumentComment;
use App\Entity\Workspace\Stream\Comment\Map\EventComment;
use App\Entity\Workspace\Stream\Comment\Map\StreamComment;
use App\Entity\Workspace\Stream\Comment\Map\TaskComment;
use App\Entity\Workspace\Stream\Comment\Reaction as CommentReaction;
use App\Entity\Workspace\Stream\Document;
use App\Entity\Workspace\Stream\Event;
use App\Entity\Workspace\Stream\File;
use App\Entity\Workspace\Stream\File\Map\DocumentFile;
use App\Entity\Workspace\Stream\File\Map\EventFile;
use App\Entity\Workspace\Stream\File\Map\StreamFile;
use App\Entity\Workspace\Stream\File\Map\TaskFile;
use App\Entity\Workspace\Stream\Milestone;
use App\Entity\Workspace\Stream\Task;
use App\Entity\Workspace\Stream\Task\Item as TaskItem;
use App\Entity\Workspace\Stream\Whiteboard;
use App\Event\ObjectDeleteEvent;
use App\Event\ObjectEvent;
use App\Event\ObjectUpdateEvent;
use App\Events;
use App\Message;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Util\ClassUtils;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query\FilterCollection;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Security\Core\Security;
/**
* Process object events.
*/
class ObjectSubscriber implements EventSubscriberInterface
{
/** @var Security */
private $security;
/** @var RequestStack */
private $requestStack;
/** @var MessageBusInterface */
private $messageBus;
/** @var ManagerRegistry */
private $managerRegistry;
/**
* @param Security $security
* @param RequestStack $requestStack
* @param MessageBusInterface $messageBus
* @param ManagerRegistry $managerRegistry
*/
public function __construct(
Security $security,
RequestStack $requestStack,
MessageBusInterface $messageBus,
ManagerRegistry $managerRegistry
) {
$this->security = $security;
$this->requestStack = $requestStack;
$this->messageBus = $messageBus;
$this->managerRegistry = $managerRegistry;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array
{
return [
Events::OBJECT_CREATED => 'onObjectCreated',
Events::OBJECT_UPDATED => 'onObjectUpdated',
Events::OBJECT_DELETED => 'onObjectDeleted',
Events::OBJECT_DUE => 'onObjectDue',
];
}
/**
* @param ObjectEvent $event
*/
public function onObjectCreated(ObjectEvent $event): void
{
/** @var mixed $object */
$object = $event->getObject();
// Validate $object
switch (ClassUtils::getClass($object)) {
case Plan::class:
case Category::class:
case Position::class:
case Source::class:
case Tag::class:
$params = [
'sendAll' => true,
];
// no break
case Workspace::class:
case Member::class:
case Template::class:
case TemplateColumn::class:
case TemplateTask::class:
case Collaborator::class:
case StreamFile::class:
case DocumentFile::class:
case EventFile::class:
case TaskFile::class:
case TaskItem::class:
case Whiteboard::class:
case Milestone::class:
case Column::class:
$params = array_merge(
[
'ignoreLogging' => true,
],
$params ?? [],
);
// no break
case Stream::class:
if ($object instanceof Stream && $object->isDirectMessage()) {
$params = [
'ignoreLogging' => true,
];
}
// no break
case Document::class:
case Event::class:
case Task::class:
$this->triggerCommand(Message\NotifyObjectCreation::class, $object, $params ?? []);
break;
case StreamComment::class:
case DocumentComment::class:
case EventComment::class:
case TaskComment::class:
if (!$object->isInitialized()) {
$this->triggerCommand(Message\NotifyCommentCreation::class, $object);
}
break;
case CommentReaction::class:
if ($object->getComment() && $object->getComment()->getObject()) {
$this->triggerCommand(Message\NotifyCommentReaction::class, $object);
}
break;
default:
return;
}
}
/**
* @param ObjectUpdateEvent $event
*/
public function onObjectUpdated(ObjectUpdateEvent $event): void
{
/** @var mixed $object */
$object = $event->getObject();
/** @var array $objectChangeSet */
$objectChangeSet = $event->getObjectChangeSet();
// Validate $object
switch (ClassUtils::getClass($object)) {
case Plan::class:
case Category::class:
case Position::class:
case Source::class:
case Tag::class:
$params = [
'sendAll' => true,
];
break;
case StreamNotification::class:
case DocumentNotification::class:
case EventNotification::class:
case TaskNotification::class:
if (NotificationCommandType::COMMENT_ADDED !== $object->getCommand()) {
return;
}
break;
case User::class:
foreach (array_keys($objectChangeSet) as $key) {
switch ($key) {
case 'salt':
case 'password':
case 'lastLogin':
unset($objectChangeSet[$key]);
break;
}
}
// no break
case Workspace::class:
case Member::class:
case Template::class:
case TemplateColumn::class:
case TemplateTask::class:
case Stream::class:
case Collaborator::class:
case CommentCollaborator::class:
case StreamComment::class:
case DocumentComment::class:
case EventComment::class:
case TaskComment::class:
case StreamFile::class:
case DocumentFile::class:
case EventFile::class:
case TaskFile::class:
case TaskItem::class:
case Document::class:
case Whiteboard::class:
case Milestone::class:
case Column::class:
break;
case Event::class:
case Task::class:
if ($event->hasChangedField('enabled')) {
$this->triggerCommand(Message\NotifyObjectCompletion::class, $object);
}
break;
default:
return;
}
// Remove unfollowed fields
unset($objectChangeSet['comments']);
// Skip empty update
if (empty($objectChangeSet)) {
return;
}
// Prepare $objectChangeSet for notification
$transformObjectField = function (&$value) {
if (($value instanceof Proxy || \is_object($value)) && method_exists($value, 'getId')) {
$value = [
'id' => (string) $value->getId(),
'name' => (string) $value,
];
} elseif ($value instanceof \DateTime) {
$value = $value->format('c');
} elseif ($value instanceof Collection) {
$values = [];
foreach ($value as $v) {
$values[] = [
'id' => (string) $v->getId(),
'name' => (string) $v,
];
}
$value = $values;
}
};
foreach ($objectChangeSet as &$value) {
$transformObjectField($value[0]);
$transformObjectField($value[1]);
}
$this->triggerCommand(
Message\NotifyObjectUpdates::class,
$object,
array_merge(
[
'objectChangeSet' => $objectChangeSet,
],
$params ?? [],
)
);
}
/**
* @param ObjectDeleteEvent $event
*/
public function onObjectDeleted(ObjectDeleteEvent $event): void
{
/** @var EntityManagerInterface $entityManager */
$entityManager = $this->managerRegistry->getManager();
/** @var FilterCollection $filters */
$filters = $entityManager->getFilters();
/** @var mixed $object */
$object = $event->getObject();
// Skip when permanently deleting "soft-deleted" objects, except for Member or Collaborator
if (!$filters->isEnabled('soft_deleteable') && !($object instanceof Member || $object instanceof Collaborator)) {
return;
}
/** @var string $objectId */
$objectId = $event->getObjectId();
// Validate $object
switch (ClassUtils::getClass($object)) {
case Plan::class:
case Category::class:
case Position::class:
case Source::class:
case Tag::class:
$params = [
'sendAll' => true,
];
break;
case StreamNotification::class:
case DocumentNotification::class:
case EventNotification::class:
case TaskNotification::class:
$params = [
'ownerId' => $object->getOwner()->getId(),
];
break;
case User::class:
case Workspace::class:
case Template::class:
case Stream::class:
break;
case Member::class:
$parentObject = $object->getWorkspace();
if ($object->getUser()) {
$params = [
'userId' => $object->getUser()->getId(),
];
}
break;
case TemplateColumn::class:
case TemplateTask::class:
$parentObject = $object->getTemplate();
break;
case Collaborator::class:
if ($object->getUser()) {
$params = [
'userId' => $object->getUser()->getId(),
];
}
// no break
case Whiteboard::class:
case Milestone::class:
$ignoreLogging = true;
// no break
case Document::class:
case Event::class:
case Task::class:
case Column::class:
$ignoreLogging = $ignoreLogging ?? false;
// no break
case StreamComment::class:
case DocumentComment::class:
case EventComment::class:
case TaskComment::class:
if ($object instanceof Comment && $object->getParent()) {
$params = [
'commentParentId' => $object->getParent()->getId(),
];
}
// no break
case StreamFile::class:
case DocumentFile::class:
case EventFile::class:
case TaskFile::class:
$parentObject = $object->getStream();
break;
case TaskItem::class:
$parentObject = $object->getTask()->getStream();
break;
default:
return;
}
$this->triggerCommand(
Message\NotifyObjectDeletion::class,
$parentObject ?? $object,
array_merge(
[
'objectId' => (string) $objectId,
'ignoreLogging' => $ignoreLogging ?? true,
'params' => [
'objectName' => (string) $object,
'objectType' => $this->getObjectName($object),
],
],
$params ?? [],
)
);
}
/**
* @param ObjectEvent $event
*/
public function onObjectDue(ObjectEvent $event): void
{
/** @var mixed $object */
$object = $event->getObject();
// Validate $object
switch (ClassUtils::getClass($object)) {
case Document::class:
case Event::class:
case Task::class:
$this->triggerCommand(
Message\NotifyObjectDue::class,
$object,
[
'params' => [
'startDate' => $object->getStartDate() ? $object->getStartDate()->format('c') : null,
'endDate' => $object->getEndDate() ? $object->getEndDate()->format('c') : null,
],
]
);
break;
default:
return;
}
}
/**
* @param mixed $object
*
* @return string
*/
protected function getObjectName($object): string
{
if ($object instanceof Comment) {
return 'Comment';
} elseif ($object instanceof File) {
return 'File';
} elseif ($object instanceof TaskItem) {
return 'TaskItem';
} elseif ($object instanceof TemplateColumn) {
return 'TemplateColumn';
} elseif ($object instanceof TemplateTask) {
return 'TemplateTask';
}
return implode('', \array_slice(explode('\\', \get_class($object)), -1));
}
/**
* @param string $messageClass
* @param mixed $object
* @param array $params
*/
private function triggerCommand(string $messageClass, $object, array $params = []): void
{
/** @var UserInterface|null $user */
if ($user = $this->getUser()) {
$params['loginUser'] = (string) $user->getId();
}
// Dispatch message
$this->messageBus->dispatch(
new $messageClass(
$object->getId() ?? $param['objectId'] ?? null,
array_merge(
$params,
[
'objectClass' => ClassUtils::getClass($object),
]
)
)
);
}
/**
* @return UserInterface|null
*/
private function getUser(): ?UserInterface
{
try {
/** @var SessionInterface $session */
$session = $this->requestStack->getSession();
if ($user = $session->get('app_user')) {
return $user;
}
} catch (SessionNotFoundException $e) {
// Do nothing
}
return $this->security->getUser();
}
}