Add event subscriber for document restoration on cancel

Implement an event subscriber to restore documents to their last kept version when a workflow transition ends in a non-positive final state. Includes corresponding unit tests and an unreleased feature change log entry.
This commit is contained in:
Julien Fastré 2025-02-14 15:08:42 +01:00
parent 739e0b1692
commit 0a34f9086f
Signed by: julienfastre
GPG Key ID: BDE2190974723FCB
3 changed files with 233 additions and 0 deletions

View File

@ -0,0 +1,6 @@
kind: Feature
body: Restore document to previous kept version when a workflow is canceled
time: 2025-02-14T15:03:28.707250207+01:00
custom:
Issue: "360"
SchemaChange: No schema change

View File

@ -0,0 +1,156 @@
<?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\Tests\Workflow\EventSubscriber;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Entity\StoredObjectPointInTime;
use Chill\DocStoreBundle\Entity\StoredObjectPointInTimeReasonEnum;
use Chill\DocStoreBundle\Service\StoredObjectRestoreInterface;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Chill\MainBundle\Workflow\EntityWorkflowMarkingStore;
use Chill\MainBundle\Workflow\EventSubscriber\OnCancelRestoreDocumentToEditableEventSubscriber;
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
use PHPUnit\Framework\TestCase;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\Workflow\DefinitionBuilder;
use Symfony\Component\Workflow\Metadata\InMemoryMetadataStore;
use Symfony\Component\Workflow\Registry;
use Symfony\Component\Workflow\SupportStrategy\WorkflowSupportStrategyInterface;
use Symfony\Component\Workflow\Transition;
use Symfony\Component\Workflow\Workflow;
use Symfony\Component\Workflow\WorkflowInterface;
/**
* @internal
*
* @coversNothing
*/
class OnCancelRestoreDocumentToEditableEventSubscriberTest extends TestCase
{
private function buildRegistry(StoredObjectRestoreInterface $storedObjectRestore, ?StoredObject $storedObject): Registry
{
$builder = new DefinitionBuilder(
['initial', 'intermediate', 'final', 'cancel'],
[
new Transition('to_intermediate', ['initial'], ['intermediate']),
new Transition('intermediate_to_final', ['intermediate'], ['final']),
new Transition('to_final', ['initial'], ['final']),
new Transition('to_cancel', ['initial'], ['cancel']),
]
);
$builder->setMetadataStore(
new InMemoryMetadataStore(
placesMetadata: [
'final' => ['isFinal' => true],
'cancel' => ['isFinal' => true, 'isFinalPositive' => false],
]
)
);
$registry = new Registry();
$workflow = new Workflow($builder->build(), new EntityWorkflowMarkingStore(), $eventDispatcher = new EventDispatcher(), 'dummy');
$manager = $this->createMock(EntityWorkflowManager::class);
$manager->method('getAssociatedStoredObject')->willReturn($storedObject);
$eventSubscriber = new OnCancelRestoreDocumentToEditableEventSubscriber(
$registry,
$manager,
$storedObjectRestore
);
$eventDispatcher->addSubscriber($eventSubscriber);
$registry->addWorkflow($workflow, new class () implements WorkflowSupportStrategyInterface {
public function supports(WorkflowInterface $workflow, object $subject): bool
{
return true;
}
});
return $registry;
}
public function testOnCancelRestoreDocumentToEditableExpectsRestoring(): void
{
$storedObject = new StoredObject();
$version = $storedObject->registerVersion();
new StoredObjectPointInTime($version, StoredObjectPointInTimeReasonEnum::KEEP_BEFORE_CONVERSION);
$storedObject->registerVersion();
$restore = $this->createMock(StoredObjectRestoreInterface::class);
$restore->expects($this->once())->method('restore')->with($version);
$registry = $this->buildRegistry($restore, $storedObject);
$entityWorkflow = (new EntityWorkflow())->setWorkflowName('dummy');
$workflow = $registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
$context = new WorkflowTransitionContextDTO($entityWorkflow);
$workflow->apply($entityWorkflow, 'to_cancel', [
'context' => $context,
'transition' => 'to_cancel',
'transitionAt' => new \DateTimeImmutable('now'),
]);
}
public function testOnCancelRestoreDocumentDoNotExpectRestoring(): void
{
$storedObject = new StoredObject();
$version = $storedObject->registerVersion();
new StoredObjectPointInTime($version, StoredObjectPointInTimeReasonEnum::KEEP_BEFORE_CONVERSION);
$storedObject->registerVersion();
$restore = $this->createMock(StoredObjectRestoreInterface::class);
$restore->expects($this->never())->method('restore')->withAnyParameters();
$registry = $this->buildRegistry($restore, $storedObject);
$entityWorkflow = (new EntityWorkflow())->setWorkflowName('dummy');
$workflow = $registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
$context = new WorkflowTransitionContextDTO($entityWorkflow);
$workflow->apply($entityWorkflow, 'to_intermediate', [
'context' => $context,
'transition' => 'to_intermediate',
'transitionAt' => new \DateTimeImmutable('now'),
]);
$workflow->apply($entityWorkflow, 'intermediate_to_final', [
'context' => $context,
'transition' => 'intermediate_to_final',
'transitionAt' => new \DateTimeImmutable('now'),
]);
}
public function testOnCancelRestoreDocumentToEditableToCancelStoredObjectWithoutKepts(): void
{
$storedObject = new StoredObject();
$storedObject->registerVersion();
$restore = $this->createMock(StoredObjectRestoreInterface::class);
$restore->expects($this->never())->method('restore')->withAnyParameters();
$registry = $this->buildRegistry($restore, $storedObject);
$entityWorkflow = (new EntityWorkflow())->setWorkflowName('dummy');
$workflow = $registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
$context = new WorkflowTransitionContextDTO($entityWorkflow);
$workflow->apply($entityWorkflow, 'to_cancel', [
'context' => $context,
'transition' => 'to_cancel',
'transitionAt' => new \DateTimeImmutable('now'),
]);
}
}

View File

@ -0,0 +1,71 @@
<?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\DocStoreBundle\Service\StoredObjectRestoreInterface;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Workflow\Event\TransitionEvent;
use Symfony\Component\Workflow\Registry;
final readonly class OnCancelRestoreDocumentToEditableEventSubscriber implements EventSubscriberInterface
{
public function __construct(
private Registry $registry,
private EntityWorkflowManager $manager,
private StoredObjectRestoreInterface $storedObjectRestore,
) {}
public static function getSubscribedEvents(): array
{
return ['workflow.transition' => ['onCancelRestoreDocumentToEditable', 0]];
}
public function onCancelRestoreDocumentToEditable(TransitionEvent $event): void
{
$entityWorkflow = $event->getSubject();
if (!$entityWorkflow instanceof EntityWorkflow) {
return;
}
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
foreach ($event->getTransition()->getTos() as $place) {
$metadata = $workflow->getMetadataStore()->getPlaceMetadata($place);
if (($metadata['isFinal'] ?? false) && !($metadata['isFinalPositive'] ?? true)) {
$this->restoreDocument($entityWorkflow);
return;
}
}
}
private function restoreDocument(EntityWorkflow $entityWorkflow): void
{
$storedObject = $this->manager->getAssociatedStoredObject($entityWorkflow);
if (null === $storedObject) {
return;
}
$version = $storedObject->getLastKeptBeforeConversionVersion();
if (null === $version) {
return;
}
$this->storedObjectRestore->restore($storedObject->getLastKeptBeforeConversionVersion());
}
}