mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-09-26 08:35:00 +00:00
Wrap signature state changes in transactions to prevent race conditions and ensure data integrity. Update controller and test class names to reflect broader state change capabilities. Enhance documentation with comments to clarify transaction requirements and procedure details for signature operations.
195 lines
7.6 KiB
PHP
195 lines
7.6 KiB
PHP
<?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 Chill\MainBundle\Workflow\Messenger\PostSignatureStateChangeMessage;
|
|
use Doctrine\DBAL\LockMode;
|
|
use Doctrine\ORM\EntityManagerInterface;
|
|
use Psr\Log\LoggerInterface;
|
|
use Symfony\Component\Clock\ClockInterface;
|
|
use Symfony\Component\Messenger\MessageBusInterface;
|
|
use Symfony\Component\Workflow\Registry;
|
|
|
|
/**
|
|
* Handles state changes for signature steps within a workflow.
|
|
*/
|
|
class SignatureStepStateChanger
|
|
{
|
|
private const LOG_PREFIX = '[SignatureStepStateChanger] ';
|
|
|
|
public function __construct(
|
|
private readonly Registry $registry,
|
|
private readonly ClockInterface $clock,
|
|
private readonly LoggerInterface $logger,
|
|
private readonly MessageBusInterface $messageBus,
|
|
private readonly EntityManagerInterface $entityManager,
|
|
) {}
|
|
|
|
/**
|
|
* Marks a signature as signed.
|
|
*
|
|
* This method will acquire a lock on the database side, so it must be wrapped into an explicit
|
|
* transaction.
|
|
*
|
|
* This method updates the state of the provided signature entity, sets the signature index,
|
|
* and logs the action. Additionally, it dispatches a message indicating that the signature
|
|
* state has changed.
|
|
*
|
|
* @param EntityWorkflowStepSignature $signature the signature entity to be marked as signed
|
|
* @param int|null $atIndex optional index position for the signature within the zone
|
|
*/
|
|
public function markSignatureAsSigned(EntityWorkflowStepSignature $signature, ?int $atIndex): void
|
|
{
|
|
$this->entityManager->refresh($signature, LockMode::PESSIMISTIC_WRITE);
|
|
|
|
$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]);
|
|
$this->messageBus->dispatch(new PostSignatureStateChangeMessage((int) $signature->getId()));
|
|
}
|
|
|
|
/**
|
|
* Marks a signature as canceled.
|
|
*
|
|
* This method will acquire a lock on the database side, so it must be wrapped into an explicit
|
|
* transaction.
|
|
*
|
|
* This method updates the signature state to 'canceled' and logs the action.
|
|
* It also dispatches a message to notify about the state change.
|
|
*/
|
|
public function markSignatureAsCanceled(EntityWorkflowStepSignature $signature): void
|
|
{
|
|
$this->entityManager->refresh($signature, LockMode::PESSIMISTIC_WRITE);
|
|
|
|
$signature
|
|
->setState(EntityWorkflowSignatureStateEnum::CANCELED)
|
|
->setStateDate($this->clock->now());
|
|
$this->logger->info(self::LOG_PREFIX.'Mark signature entity as canceled', ['signatureId' => $signature->getId()]);
|
|
$this->messageBus->dispatch(new PostSignatureStateChangeMessage((int) $signature->getId()));
|
|
}
|
|
|
|
/**
|
|
* Marks the given signature as rejected and updates its state and state date accordingly.
|
|
*
|
|
* This method will acquire a lock on the database side, so it must be wrapped into an explicit
|
|
* transaction.
|
|
*
|
|
* This method logs the rejection of the signature and dispatches a message indicating
|
|
* a state change has occurred.
|
|
*
|
|
* @param EntityWorkflowStepSignature $signature the signature entity to be marked as rejected
|
|
*/
|
|
public function markSignatureAsRejected(EntityWorkflowStepSignature $signature): void
|
|
{
|
|
$this->entityManager->refresh($signature, LockMode::PESSIMISTIC_WRITE);
|
|
|
|
$signature
|
|
->setState(EntityWorkflowSignatureStateEnum::REJECTED)
|
|
->setStateDate($this->clock->now());
|
|
$this->logger->info(self::LOG_PREFIX.'Mark signature entity as rejected', ['signatureId' => $signature->getId()]);
|
|
$this->messageBus->dispatch(new PostSignatureStateChangeMessage((int) $signature->getId()));
|
|
}
|
|
|
|
/**
|
|
* Executed after a signature has a new state.
|
|
*
|
|
* This method will acquire a lock on the database side, so it must be wrapped into an explicit
|
|
* transaction.
|
|
*
|
|
* This should be executed only by a system user (without any user registered)
|
|
*/
|
|
public function onPostMark(EntityWorkflowStepSignature $signature): void
|
|
{
|
|
$this->entityManager->refresh($signature, LockMode::PESSIMISTIC_READ);
|
|
|
|
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;
|
|
}
|
|
|
|
if ('person' === $signature->getSignerKind()) {
|
|
$futureUser = $this->getPreviousSender($signature->getStep());
|
|
} else {
|
|
$futureUser = $signature->getSigner();
|
|
}
|
|
|
|
if (null === $futureUser) {
|
|
$this->logger->info(self::LOG_PREFIX.'No previous user, will not apply a transition', ['signatureId' => $signature->getId()]);
|
|
|
|
return;
|
|
}
|
|
|
|
$transitionDto = new WorkflowTransitionContextDTO($entityWorkflow);
|
|
$transitionDto->futureDestUsers[] = $futureUser;
|
|
|
|
$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');
|
|
}
|
|
}
|