Revert "Merge branch 'revert-671bb6d5' into 'master'"

This reverts merge request !732
This commit is contained in:
2024-09-19 13:40:09 +00:00
parent bfd7dc2270
commit 68688dd528
1701 changed files with 35022 additions and 14546 deletions

View File

@@ -22,9 +22,7 @@ use Symfony\Component\Workflow\Event\Event;
final readonly class WorkflowByUserCounter implements NotificationCounterInterface, EventSubscriberInterface
{
public function __construct(private EntityWorkflowStepRepository $workflowStepRepository, private CacheItemPoolInterface $cacheItemPool)
{
}
public function __construct(private EntityWorkflowStepRepository $workflowStepRepository, private CacheItemPoolInterface $cacheItemPool) {}
public function addNotification(UserInterface $u): int
{

View File

@@ -14,6 +14,9 @@ namespace Chill\MainBundle\Workflow;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
/**
* @template T of object
*/
interface EntityWorkflowHandlerInterface
{
/**
@@ -25,6 +28,9 @@ interface EntityWorkflowHandlerInterface
public function getEntityTitle(EntityWorkflow $entityWorkflow, array $options = []): string;
/**
* @return T|null
*/
public function getRelatedEntity(EntityWorkflow $entityWorkflow): ?object;
public function getRelatedObjects(object $object): array;
@@ -51,4 +57,9 @@ interface EntityWorkflowHandlerInterface
public function supports(EntityWorkflow $entityWorkflow, array $options = []): bool;
public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool;
/**
* @return list<EntityWorkflow>
*/
public function findByRelatedEntity(object $object): array;
}

View File

@@ -11,6 +11,7 @@ declare(strict_types=1);
namespace Chill\MainBundle\Workflow;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Workflow\Exception\HandlerNotFoundException;
use Symfony\Component\Workflow\Registry;
@@ -20,9 +21,7 @@ class EntityWorkflowManager
/**
* @param \Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface[] $handlers
*/
public function __construct(private readonly iterable $handlers, private readonly Registry $registry)
{
}
public function __construct(private readonly iterable $handlers, private readonly Registry $registry) {}
public function getHandler(EntityWorkflow $entityWorkflow, array $options = []): EntityWorkflowHandlerInterface
{
@@ -39,4 +38,29 @@ class EntityWorkflowManager
{
return $this->registry->all($entityWorkflow);
}
public function getAssociatedStoredObject(EntityWorkflow $entityWorkflow): ?StoredObject
{
foreach ($this->handlers as $handler) {
if ($handler instanceof EntityWorkflowWithStoredObjectHandlerInterface && $handler->supports($entityWorkflow)) {
return $handler->getAssociatedStoredObject($entityWorkflow);
}
}
return null;
}
/**
* @return list<EntityWorkflow>
*/
public function findByRelatedEntity(object $object): array
{
foreach ($this->handlers as $handler) {
if ([] !== $workflows = $handler->findByRelatedEntity($object)) {
return $workflows;
}
}
return [];
}
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Workflow;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Symfony\Component\Workflow\Marking;
use Symfony\Component\Workflow\MarkingStore\MarkingStoreInterface;
final readonly class EntityWorkflowMarkingStore implements MarkingStoreInterface
{
public function getMarking(object $subject): Marking
{
if (!$subject instanceof EntityWorkflow) {
throw new \UnexpectedValueException('Expected instance of EntityWorkflow');
}
$step = $subject->getCurrentStep();
return new Marking([$step->getCurrentStep() => 1]);
}
public function setMarking(object $subject, Marking $marking, array $context = []): void
{
if (!$subject instanceof EntityWorkflow) {
throw new \UnexpectedValueException('Expected instance of EntityWorkflow');
}
$places = $marking->getPlaces();
if (1 < count($places)) {
throw new \LogicException('Expected maximum one place');
}
$next = array_keys($places)[0];
$transitionDTO = $context['context'] ?? null;
$transition = $context['transition'];
$byUser = $context['byUser'] ?? null;
$at = $context['transitionAt'];
if (!$transitionDTO instanceof WorkflowTransitionContextDTO) {
throw new \UnexpectedValueException(sprintf('Expected instance of %s', WorkflowTransitionContextDTO::class));
}
$subject->setStep($next, $transitionDTO, $transition, $at, $byUser);
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Workflow;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
/**
* Add methods to handle workflows associated with @see{StoredObject}.
*
* @template T of object
*
* @template-extends EntityWorkflowHandlerInterface<T>
*/
interface EntityWorkflowWithStoredObjectHandlerInterface extends EntityWorkflowHandlerInterface
{
public function getAssociatedStoredObject(EntityWorkflow $entityWorkflow): ?StoredObject;
}

View File

@@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Workflow\EventSubscriber;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Security\Authorization\EntityWorkflowTransitionVoter;
use Chill\MainBundle\Templating\Entity\UserRender;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Workflow\Event\GuardEvent;
use Symfony\Component\Workflow\TransitionBlocker;
/**
* Prevent apply a transition on an entity workflow.
*
* This apply logic and rules to decide if a transition can be applyed.
*
* Those rules are:
*
* - if the transition is system-only or is allowed for user;
* - if the user is present in the dest users for a workflow;
* - or if the user have permission to apply all the transitions
*/
class EntityWorkflowGuardTransition implements EventSubscriberInterface
{
public function __construct(
private readonly UserRender $userRender,
private readonly Security $security,
) {}
public static function getSubscribedEvents(): array
{
return [
'workflow.guard' => [
['guardEntityWorkflow', 0],
],
];
}
public function guardEntityWorkflow(GuardEvent $event)
{
if (!$event->getSubject() instanceof EntityWorkflow) {
return;
}
/** @var EntityWorkflow $entityWorkflow */
$entityWorkflow = $event->getSubject();
if ($entityWorkflow->isFinal()) {
$event->addTransitionBlocker(
new TransitionBlocker(
'workflow.The workflow is finalized',
'd6306280-7535-11ec-a40d-1f7bee26e2c0'
)
);
return;
}
$user = $this->security->getUser();
$metadata = $event->getWorkflow()->getMetadataStore()->getTransitionMetadata($event->getTransition());
$systemTransitions = explode('+', $metadata['transitionGuard'] ?? 'only-dest');
if (null === $user) {
if (in_array('system', $systemTransitions, true)) {
// it is safe to apply this transition
return;
}
$event->addTransitionBlocker(
new TransitionBlocker(
'workflow.Transition is not allowed for system',
'd9e39a18-704c-11ef-b235-8fe0619caee7'
)
);
return;
}
// for users
if (!in_array('only-dest', $systemTransitions, true)) {
$event->addTransitionBlocker(
new TransitionBlocker(
'workflow.Only system can apply this transition',
'5b6b95e0-704d-11ef-a5a9-4b6fc11a8eeb'
)
);
}
if (
!$entityWorkflow->getCurrentStep()->getAllDestUser()->contains($user)
) {
if ($event->getMarking()->has('initial')) {
return;
}
if ($this->security->isGranted(EntityWorkflowTransitionVoter::APPLY_ALL_TRANSITIONS, $entityWorkflow->getCurrentStep())) {
return;
}
$event->addTransitionBlocker(new TransitionBlocker(
'workflow.You are not allowed to apply a transition on this workflow. Only those users are allowed: %users%',
'f3eeb57c-7532-11ec-9495-e7942a2ac7bc',
[
'%users%' => implode(
', ',
$entityWorkflow->getCurrentStep()->getAllDestUser()->map(fn (User $u) => $this->userRender->renderString($u, []))->toArray()
),
]
));
}
}
}

View File

@@ -13,41 +13,17 @@ namespace Chill\MainBundle\Workflow\EventSubscriber;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Templating\Entity\UserRender;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Workflow\Event\Event;
use Symfony\Component\Workflow\Event\GuardEvent;
use Symfony\Component\Workflow\TransitionBlocker;
class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterface
final readonly class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterface
{
public function __construct(private readonly LoggerInterface $chillLogger, private readonly Security $security, private readonly UserRender $userRender)
{
}
public function addDests(Event $event): void
{
if (!$event->getSubject() instanceof EntityWorkflow) {
return;
}
/** @var EntityWorkflow $entityWorkflow */
$entityWorkflow = $event->getSubject();
foreach ($entityWorkflow->futureCcUsers as $user) {
$entityWorkflow->getCurrentStep()->addCcUser($user);
}
foreach ($entityWorkflow->futureDestUsers as $user) {
$entityWorkflow->getCurrentStep()->addDestUser($user);
}
foreach ($entityWorkflow->futureDestEmails as $email) {
$entityWorkflow->getCurrentStep()->addDestEmail($email);
}
}
public function __construct(
private LoggerInterface $chillLogger,
private Security $security,
) {}
public static function getSubscribedEvents(): array
{
@@ -55,52 +31,16 @@ class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterfac
'workflow.transition' => 'onTransition',
'workflow.completed' => [
['markAsFinal', 2048],
['addDests', 2048],
],
'workflow.guard' => [
['guardEntityWorkflow', 0],
],
];
}
public function guardEntityWorkflow(GuardEvent $event)
{
if (!$event->getSubject() instanceof EntityWorkflow) {
return;
}
/** @var EntityWorkflow $entityWorkflow */
$entityWorkflow = $event->getSubject();
if ($entityWorkflow->isFinal()) {
$event->addTransitionBlocker(
new TransitionBlocker(
'workflow.The workflow is finalized',
'd6306280-7535-11ec-a40d-1f7bee26e2c0'
)
);
return;
}
if (!$entityWorkflow->getCurrentStep()->getAllDestUser()->contains($this->security->getUser())) {
if (!$event->getMarking()->has('initial')) {
$event->addTransitionBlocker(new TransitionBlocker(
'workflow.You are not allowed to apply a transition on this workflow. Only those users are allowed: %users%',
'f3eeb57c-7532-11ec-9495-e7942a2ac7bc',
[
'%users%' => implode(
', ',
$entityWorkflow->getCurrentStep()->getAllDestUser()->map(fn (User $u) => $this->userRender->renderString($u, []))->toArray()
),
]
));
}
}
}
public function markAsFinal(Event $event): void
{
// NOTE: it is not possible to move this method to the marking store, because
// there is dependency between the Workflow definition and the MarkingStoreInterface (the workflow
// constructor need a MarkingStoreInterface)
if (!$event->getSubject() instanceof EntityWorkflow) {
return;
}
@@ -125,18 +65,14 @@ class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterfac
/** @var EntityWorkflow $entityWorkflow */
$entityWorkflow = $event->getSubject();
$step = $entityWorkflow->getCurrentStep();
$step
->setTransitionAfter($event->getTransition()->getName())
->setTransitionAt(new \DateTimeImmutable('now'))
->setTransitionBy($this->security->getUser());
$user = $this->security->getUser();
$this->chillLogger->info('[workflow] apply transition on entityWorkflow', [
'relatedEntityClass' => $entityWorkflow->getRelatedEntityClass(),
'relatedEntityId' => $entityWorkflow->getRelatedEntityId(),
'transition' => $event->getTransition()->getName(),
'by_user' => $this->security->getUser(),
'by_user' => $user instanceof User ? $user->getId() : (string) $user?->getUserIdentifier(),
'entityWorkflow' => $entityWorkflow->getId(),
]);
}

View File

@@ -23,9 +23,13 @@ use Symfony\Component\Workflow\Registry;
class NotificationOnTransition implements EventSubscriberInterface
{
public function __construct(private readonly EntityManagerInterface $entityManager, private readonly \Twig\Environment $engine, private readonly MetadataExtractor $metadataExtractor, private readonly Security $security, private readonly Registry $registry)
{
}
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly \Twig\Environment $engine,
private readonly MetadataExtractor $metadataExtractor,
private readonly Security $security,
private readonly Registry $registry,
) {}
public static function getSubscribedEvents(): array
{
@@ -87,7 +91,10 @@ class NotificationOnTransition implements EventSubscriberInterface
'dest' => $subscriber,
'place' => $place,
'workflow' => $workflow,
'is_dest' => \in_array($subscriber->getId(), array_map(static fn (User $u) => $u->getId(), $entityWorkflow->futureDestUsers), true),
'is_dest' => \in_array($subscriber->getId(), array_map(
static fn (User $u) => $u->getId(),
$entityWorkflow->getCurrentStep()->getDestUser()->toArray()
), true),
];
$notification = new Notification();

View File

@@ -20,9 +20,13 @@ use Symfony\Component\Workflow\Registry;
class SendAccessKeyEventSubscriber
{
public function __construct(private readonly \Twig\Environment $engine, private readonly MetadataExtractor $metadataExtractor, private readonly Registry $registry, private readonly EntityWorkflowManager $entityWorkflowManager, private readonly MailerInterface $mailer)
{
}
public function __construct(
private readonly \Twig\Environment $engine,
private readonly MetadataExtractor $metadataExtractor,
private readonly Registry $registry,
private readonly EntityWorkflowManager $entityWorkflowManager,
private readonly MailerInterface $mailer,
) {}
public function postPersist(EntityWorkflowStep $step): void
{
@@ -34,7 +38,7 @@ class SendAccessKeyEventSubscriber
);
$handler = $this->entityWorkflowManager->getHandler($entityWorkflow);
foreach ($entityWorkflow->futureDestEmails as $emailAddress) {
foreach ($step->getDestEmail() as $emailAddress) {
$context = [
'entity_workflow' => $entityWorkflow,
'dest' => $emailAddress,

View File

@@ -11,6 +11,4 @@ declare(strict_types=1);
namespace Chill\MainBundle\Workflow\Exception;
class HandlerNotFoundException extends \RuntimeException
{
}
class HandlerNotFoundException extends \RuntimeException {}

View File

@@ -19,9 +19,7 @@ use Symfony\Component\Workflow\WorkflowInterface;
class MetadataExtractor
{
public function __construct(private readonly Registry $registry, private readonly TranslatableStringHelperInterface $translatableStringHelper)
{
}
public function __construct(private readonly Registry $registry, private readonly TranslatableStringHelperInterface $translatableStringHelper) {}
public function availableWorkflowFor(string $relatedEntityClass, ?int $relatedEntityId = 0): array
{

View File

@@ -20,9 +20,7 @@ use Symfony\Component\Security\Core\Security;
class WorkflowNotificationHandler implements NotificationHandlerInterface
{
public function __construct(private readonly EntityWorkflowRepository $entityWorkflowRepository, private readonly EntityWorkflowManager $entityWorkflowManager, private readonly Security $security)
{
}
public function __construct(private readonly EntityWorkflowRepository $entityWorkflowRepository, private readonly EntityWorkflowManager $entityWorkflowManager, private readonly Security $security) {}
public function getTemplate(Notification $notification, array $options = []): string
{

View File

@@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Workflow;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
use Psr\Log\LoggerInterface;
use Symfony\Component\Clock\ClockInterface;
use Symfony\Component\Workflow\Registry;
class SignatureStepStateChanger
{
private const LOG_PREFIX = '[SignatureStepStateChanger] ';
public function __construct(
private readonly Registry $registry,
private readonly ClockInterface $clock,
private readonly LoggerInterface $logger,
) {}
public function markSignatureAsSigned(EntityWorkflowStepSignature $signature, ?int $atIndex): void
{
$signature
->setState(EntityWorkflowSignatureStateEnum::SIGNED)
->setZoneSignatureIndex($atIndex)
->setStateDate($this->clock->now())
;
$this->logger->info(self::LOG_PREFIX.'Mark signature entity as signed', ['signatureId' => $signature->getId(), 'index' => (string) $atIndex]);
if (!EntityWorkflowStepSignature::isAllSignatureNotPendingForStep($signature->getStep())) {
$this->logger->info(self::LOG_PREFIX.'This is not the last signature, skipping transition to another place', ['signatureId' => $signature->getId()]);
return;
}
$this->logger->debug(self::LOG_PREFIX.'Continuing the process to find a transition', ['signatureId' => $signature->getId()]);
$entityWorkflow = $signature->getStep()->getEntityWorkflow();
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
$metadataStore = $workflow->getMetadataStore();
// find a transition
$marking = $workflow->getMarking($entityWorkflow);
$places = $marking->getPlaces();
$transition = null;
foreach ($places as $place => $int) {
$metadata = $metadataStore->getPlaceMetadata($place);
if (array_key_exists('onSignatureCompleted', $metadata)) {
$transition = $metadata['onSignatureCompleted']['transitionName'];
}
}
if (null === $transition) {
$this->logger->info(self::LOG_PREFIX.'The transition is not configured, will not apply a transition', ['signatureId' => $signature->getId()]);
return;
}
$previousUser = $this->getPreviousSender($signature->getStep());
if (null === $previousUser) {
$this->logger->info(self::LOG_PREFIX.'No previous user, will not apply a transition', ['signatureId' => $signature->getId()]);
return;
}
$transitionDto = new WorkflowTransitionContextDTO($entityWorkflow);
$transitionDto->futureDestUsers[] = $previousUser;
$workflow->apply($entityWorkflow, $transition, [
'context' => $transitionDto,
'transitionAt' => $this->clock->now(),
'transition' => $transition,
]);
$this->logger->info(self::LOG_PREFIX.'Transition automatically applied', ['signatureId' => $signature->getId()]);
}
private function getPreviousSender(EntityWorkflowStep $entityWorkflowStep): ?User
{
$stepsChained = $entityWorkflowStep->getEntityWorkflow()->getStepsChained();
foreach ($stepsChained as $stepChained) {
if ($stepChained === $entityWorkflowStep) {
if (null === $previous = $stepChained->getPrevious()) {
return null;
}
if (null !== $previousUser = $previous->getTransitionBy()) {
return $previousUser;
}
return $this->getPreviousSender($previous);
}
}
throw new \LogicException('no same step found');
}
}

View File

@@ -23,9 +23,7 @@ use Twig\Extension\RuntimeExtensionInterface;
class WorkflowTwigExtensionRuntime implements RuntimeExtensionInterface
{
public function __construct(private readonly EntityWorkflowManager $entityWorkflowManager, private readonly Registry $registry, private readonly EntityWorkflowRepository $repository, private readonly MetadataExtractor $metadataExtractor, private readonly NormalizerInterface $normalizer)
{
}
public function __construct(private readonly EntityWorkflowManager $entityWorkflowManager, private readonly Registry $registry, private readonly EntityWorkflowRepository $repository, private readonly MetadataExtractor $metadataExtractor, private readonly NormalizerInterface $normalizer) {}
public function getTransitionByString(EntityWorkflow $entityWorkflow, string $key): ?Transition
{

View File

@@ -17,9 +17,8 @@ namespace Chill\MainBundle\Workflow\Validator;
* * a handler exists;
* * a related entity does exists;
* * a workflow can be associated with this entity.
*
* @Annotation
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class EntityWorkflowCreation extends \Symfony\Component\Validator\Constraint
{
public string $messageEntityNotFound = 'Related entity is not found';

View File

@@ -21,9 +21,7 @@ use Symfony\Component\Workflow\WorkflowInterface;
class EntityWorkflowCreationValidator extends \Symfony\Component\Validator\ConstraintValidator
{
public function __construct(private readonly EntityWorkflowManager $entityWorkflowManager)
{
}
public function __construct(private readonly EntityWorkflowManager $entityWorkflowManager) {}
/**
* @param EntityWorkflow $value

View File

@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Workflow;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\PersonBundle\Entity\Person;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Workflow\Transition;
/**
* Context for a transition on an workflow entity.
*/
class WorkflowTransitionContextDTO
{
/**
* a list of future dest users for the next steps.
*
* This is in used in order to let controller inform who will be the future users which will validate
* the next step. This is necessary to perform some computation about the next users, before they are
* associated to the entity EntityWorkflowStep.
*
* @var array|User[]
*/
public array $futureDestUsers = [];
/**
* a list of future cc users for the next steps.
*
* @var array|User[]
*/
public array $futureCcUsers = [];
/**
* a list of future dest emails for the next steps.
*
* This is in used in order to let controller inform who will be the future emails which will validate
* the next step. This is necessary to perform some computation about the next emails, before they are
* associated to the entity EntityWorkflowStep.
*
* @var array|string[]
*/
public array $futureDestEmails = [];
/**
* A list of future @see{Person} with will sign the next step.
*
* @var list<Person>
*/
public array $futurePersonSignatures = [];
/**
* An eventual user which is requested to apply a signature.
*/
public ?User $futureUserSignature = null;
public ?Transition $transition = null;
public string $comment = '';
public function __construct(
public EntityWorkflow $entityWorkflow,
) {}
#[Assert\Callback()]
public function validateCCUserIsNotInDest(ExecutionContextInterface $context, $payload): void
{
foreach ($this->futureDestUsers as $u) {
if (in_array($u, $this->futureCcUsers, true)) {
$context
->buildViolation('workflow.The user in cc cannot be a dest user in the same workflow step')
->atPath('ccUsers')
->addViolation();
}
}
}
}