Add audit functionality for StoredObject actions and integrate StoredObjectSubjectConverter and StoredObjectSubjectDisplayer

- Introduced `StoredObjectSubjectConverter` for audit conversion logic and `StoredObjectSubjectDisplayer` for display logic.
- Added corresponding Twig template and French translations for audit messages.
- Created unit tests to ensure proper functionality of the converter and displayer.
This commit is contained in:
2026-03-02 12:09:38 +01:00
parent 847610f679
commit 4c8eb4b3b9
6 changed files with 300 additions and 0 deletions

View File

@@ -0,0 +1,51 @@
<?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\DocStoreBundle\Audit\Displayer;
use Chill\DocStoreBundle\Repository\StoredObjectRepositoryInterface;
use Chill\MainBundle\Audit\Subject;
use Chill\MainBundle\Audit\SubjectDisplayerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Twig\Environment;
final readonly class StoredObjectSubjectDisplayer implements SubjectDisplayerInterface
{
public function __construct(
private StoredObjectRepositoryInterface $storedObjectRepository,
private Environment $twig,
private TranslatorInterface $translator,
) {}
public function supportsDisplay(Subject $subject, array $options = []): bool
{
return 'stored_object' === $subject->type;
}
public function display(Subject $subject, string $format = 'html', array $options = []): string
{
$id = $subject->identifiers['id'];
$storedObject = $this->storedObjectRepository->find($id);
if ('html' === $format) {
return $this->twig->render('@ChillDocStore/Audit/stored_object.html.twig', [
'id' => $id,
'storedObject' => $storedObject,
]);
}
if (null !== $storedObject) {
return $this->translator->trans('audit.stored_object.display_with_title', ['{id}' => $id, '{title}' => $storedObject->getTitle()]);
}
return $this->translator->trans('audit.stored_object.display', ['{id}' => $id]);
}
}

View File

@@ -0,0 +1,54 @@
<?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\DocStoreBundle\Audit\SubjectConverter;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectRepository;
use Chill\MainBundle\Audit\Subject;
use Chill\MainBundle\Audit\SubjectBag;
use Chill\MainBundle\Audit\SubjectConverterInterface;
use Chill\MainBundle\Audit\SubjectConverterManagerAwareInterface;
use Chill\MainBundle\Audit\SubjectConverterManagerAwareTrait;
/**
* @implements SubjectConverterInterface<StoredObject>
*/
final class StoredObjectSubjectConverter implements SubjectConverterInterface, SubjectConverterManagerAwareInterface
{
use SubjectConverterManagerAwareTrait;
public function __construct(private readonly AssociatedEntityToStoredObjectRepository $associatedEntityToStoredObjectRepository) {}
public function convert(mixed $subject, bool $includeAssociated = false): SubjectBag
{
$main = new SubjectBag(new Subject('stored_object', ['id' => $subject->getId()]));
$associated = $this->associatedEntityToStoredObjectRepository->findAssociatedEntityToStoredObject($subject);
if (null !== $associated) {
$main->append(
$this->subjectConverterManager->getSubjectsForEntity($associated, $includeAssociated)
);
}
return $main;
}
public function supportsConvert(mixed $subject): bool
{
return $subject instanceof StoredObject;
}
public static function getDefaultPriority(): int
{
return 0;
}
}

View File

@@ -0,0 +1 @@
<span>{{ 'audit.stored_object.display'|trans({'{{id}': id }) }}{% if storedObject is not null %} - {{ storedObject.title}}{% endif %}</span>

View File

@@ -0,0 +1,104 @@
<?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\DocStoreBundle\Tests\Audit\Displayer;
use Chill\DocStoreBundle\Audit\Displayer\StoredObjectSubjectDisplayer;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Repository\StoredObjectRepositoryInterface;
use Chill\MainBundle\Audit\Subject;
use PHPUnit\Framework\TestCase;
use Symfony\Contracts\Translation\TranslatorInterface;
use Twig\Environment;
/**
* @internal
*
* @coversNothing
*/
class StoredObjectSubjectDisplayerTest extends TestCase
{
private StoredObjectRepositoryInterface $repository;
private Environment $twig;
private StoredObjectSubjectDisplayer $displayer;
private TranslatorInterface $translator;
protected function setUp(): void
{
$this->repository = $this->createMock(StoredObjectRepositoryInterface::class);
$this->twig = $this->createMock(Environment::class);
$this->translator = $this->createMock(TranslatorInterface::class);
$this->displayer = new StoredObjectSubjectDisplayer($this->repository, $this->twig, $this->translator);
}
public function testSupportsDisplay(): void
{
$this->assertTrue($this->displayer->supportsDisplay(new Subject('stored_object', ['id' => 1])));
$this->assertFalse($this->displayer->supportsDisplay(new Subject('person', ['id' => 1])));
}
public function testDisplayHtml(): void
{
$subject = new Subject('stored_object', ['id' => 123]);
$storedObject = $this->createMock(StoredObject::class);
$this->repository->expects($this->once())
->method('find')
->with(123)
->willReturn($storedObject);
$this->twig->expects($this->once())
->method('render')
->with('@ChillDocStore/Audit/stored_object.html.twig', [
'id' => 123,
'storedObject' => $storedObject,
])
->willReturn('Rendered HTML');
$result = $this->displayer->display($subject, 'html');
$this->assertSame('Rendered HTML', $result);
}
public function testDisplayString(): void
{
$subject = new Subject('stored_object', ['id' => 123]);
$storedObject = $this->createMock(StoredObject::class);
$storedObject->method('getTitle')->willReturn('Document Title');
$this->translator->method('trans')->with('audit.stored_object.display_with_title', ['{id}' => 123, '{title}' => 'Document Title'])
->willReturn('translated');
$this->repository->expects($this->once())
->method('find')
->with(123)
->willReturn($storedObject);
$result = $this->displayer->display($subject, 'string');
$this->assertSame('translated', $result);
}
public function testDisplayStringWithNotFoundObject(): void
{
$this->translator->method('trans')->with('audit.stored_object.display', ['{id}' => 123])
->willReturn('translated');
$subject = new Subject('stored_object', ['id' => 123]);
$this->repository->expects($this->once())
->method('find')
->with(123)
->willReturn(null);
$result = $this->displayer->display($subject, 'string');
$this->assertSame('translated', $result);
}
}

View File

@@ -0,0 +1,85 @@
<?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\DocStoreBundle\Tests\Audit\SubjectConverter;
use Chill\DocStoreBundle\Audit\SubjectConverter\StoredObjectSubjectConverter;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectRepository;
use Chill\MainBundle\Audit\Subject;
use Chill\MainBundle\Audit\SubjectBag;
use Chill\MainBundle\Audit\SubjectConverterManagerInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
/**
* @internal
*
* @coversNothing
*/
class StoredObjectSubjectConverterTest extends TestCase
{
use ProphecyTrait;
public function testSupportsConvert(): void
{
$associatedEntity = $this->prophesize(AssociatedEntityToStoredObjectRepository::class);
$subjectConverterManager = $this->prophesize(SubjectConverterManagerInterface::class);
$converter = new StoredObjectSubjectConverter($associatedEntity->reveal());
$converter->setSubjectConverterManager($subjectConverterManager->reveal());
$this->assertTrue($converter->supportsConvert($this->createMock(StoredObject::class)));
$this->assertFalse($converter->supportsConvert(new \stdClass()));
}
public function testConvertWithoutAssociated(): void
{
$storedObject = new StoredObject();
$reflection = new \ReflectionClass($storedObject);
$id = $reflection->getProperty('id');
$id->setValue($storedObject, 123);
$associatedEntity = $this->prophesize(AssociatedEntityToStoredObjectRepository::class);
$subjectConverterManager = $this->prophesize(SubjectConverterManagerInterface::class);
$associatedEntity->findAssociatedEntityToStoredObject($storedObject)->willReturn(null)->shouldBeCalled();
$converter = new StoredObjectSubjectConverter($associatedEntity->reveal());
$converter->setSubjectConverterManager($subjectConverterManager->reveal());
$subjectBag = $converter->convert($storedObject);
$this->assertSame('stored_object', $subjectBag->subject->type);
$this->assertSame(['id' => 123], $subjectBag->subject->identifiers);
}
public function testConvertWithAssociated(): void
{
$storedObject = new StoredObject();
$reflection = new \ReflectionClass($storedObject);
$id = $reflection->getProperty('id');
$id->setValue($storedObject, 123);
$associatedEntity = $this->prophesize(AssociatedEntityToStoredObjectRepository::class);
$subjectConverterManager = $this->prophesize(SubjectConverterManagerInterface::class);
$associatedEntity->findAssociatedEntityToStoredObject($storedObject)->willReturn($associated = new \stdClass());
$subjectConverterManager->getSubjectsForEntity($associated, true)->willReturn(new SubjectBag($s = new Subject('associated', ['id' => 456])));
$converter = new StoredObjectSubjectConverter($associatedEntity->reveal());
$converter->setSubjectConverterManager($subjectConverterManager->reveal());
$subjectBag = $converter->convert($storedObject, true);
$this->assertSame('stored_object', $subjectBag->subject->type);
$this->assertSame(['id' => 123], $subjectBag->subject->identifiers);
$this->assertCount(1, $subjectBag->associatedSubjects);
$this->assertSame($s, $subjectBag->associatedSubjects[0]);
}
}

View File

@@ -128,3 +128,8 @@ signatures:
see_all_pages: Voir toutes les pages
all_pages: Toutes les pages
go_to_signature_unique: Aller à la zone de signature
audit:
stored_object:
display: Document n°{id}
display_with_title: Document n°{id} - {title}