Implement context-specific voters for all current entities that can be linked to a document

For reusability an AbstractStoredObjectVoter was created and a StoredObjectVoterInterface.
A WorkflowDocumentService checks whether the StoredObject is involved in a workflow.
This commit is contained in:
Julie Lenaerts 2024-06-26 13:45:15 +02:00
parent 4607c36b57
commit c06e76a0ee
18 changed files with 456 additions and 88 deletions

View File

@ -12,6 +12,8 @@ declare(strict_types=1);
namespace Chill\ActivityBundle\Repository;
use Chill\ActivityBundle\Entity\Activity;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
@ -23,7 +25,7 @@ use Doctrine\Persistence\ManagerRegistry;
* @method Activity[] findAll()
* @method Activity[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class ActivityRepository extends ServiceEntityRepository
class ActivityRepository extends ServiceEntityRepository implements AssociatedEntityToStoredObjectInterface
{
public function __construct(ManagerRegistry $registry)
{
@ -97,4 +99,17 @@ class ActivityRepository extends ServiceEntityRepository
return $qb->getQuery()->getResult();
}
public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object
{
$qb = $this->createQueryBuilder('a');
$query = $qb
->join('a.documents', 'ad')
->join('ad.storedObject', 'so')
->where('so.id = :storedObjectId')
->setParameter('storedObjectId', $storedObject->getId())
->getQuery();
return $query->getResult();
}
}

View File

@ -19,7 +19,7 @@ use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ObjectRepository;
class AccompanyingCourseDocumentRepository implements ObjectRepository
class AccompanyingCourseDocumentRepository implements ObjectRepository, AssociatedEntityToStoredObjectInterface
{
private readonly EntityRepository $repository;
@ -46,12 +46,12 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository
return $qb->getQuery()->getSingleScalarResult();
}
public function findLinkedCourseDocument(StoredObject $storedObject): ?AccompanyingCourseDocument {
public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object
{
$qb = $this->repository->createQueryBuilder('d');
$query = $qb->where('d.storedObject = :storedObject')
->setParameter('storedObject', $storedObject)
->getQuery();
->setParameter('storedObject', $storedObject)
->getQuery();
return $query->getResult();
}
@ -66,7 +66,7 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository
return $this->repository->findAll();
}
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null)
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array
{
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
@ -76,7 +76,7 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository
return $this->findOneBy($criteria);
}
public function getClassName()
public function getClassName(): string
{
return AccompanyingCourseDocument::class;
}

View File

@ -0,0 +1,10 @@
<?php
namespace Chill\DocStoreBundle\Repository;
use Chill\DocStoreBundle\Entity\StoredObject;
interface AssociatedEntityToStoredObjectInterface
{
public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object;
}

View File

@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\Repository;
use Chill\DocStoreBundle\Entity\PersonDocument;
use Chill\DocStoreBundle\Entity\StoredObject;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ObjectRepository;
@ -19,7 +20,7 @@ use Doctrine\Persistence\ObjectRepository;
/**
* @template ObjectRepository<PersonDocument::class>
*/
readonly class PersonDocumentRepository implements ObjectRepository
readonly class PersonDocumentRepository implements ObjectRepository, AssociatedEntityToStoredObjectInterface
{
private EntityRepository $repository;
@ -53,4 +54,14 @@ readonly class PersonDocumentRepository implements ObjectRepository
{
return PersonDocument::class;
}
public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object
{
$qb = $this->repository->createQueryBuilder('d');
$query = $qb->where('d.storedObject = :storedObject')
->setParameter('storedObject', $storedObject)
->getQuery();
return $query->getResult();
}
}

View File

@ -1,61 +0,0 @@
<?php
namespace ChillDocStoreBundle\Security\Authorization;
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository;
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface;
use Chill\DocStoreBundle\Service\WorkflowDocumentService;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
class AccompanyingCourseDocumentStoredObjectVoter implements StoredObjectVoterInterface
{
final public const SEE_AND_EDIT = 'CHILL_ACCOMPANYING_COURSE_DOCUMENT_STORED_OBJECT_SEE_EDIT';
public function __construct(
private readonly AccompanyingCourseDocumentRepository $repository,
private readonly AccompanyingCourseDocumentVoter $accompanyingCourseDocumentVoter,
private readonly WorkflowDocumentService $workflowDocumentService
){
}
public function supports(string $attribute, StoredObject $subject): bool
{
// check if the stored object is linked to an AccompanyingCourseDocument
return $this->repository->findLinkedCourseDocument($subject) instanceof AccompanyingCourseDocument;
}
public function voteOnAttribute(string $attribute, StoredObject $subject, TokenInterface $token): bool
{
if (!$token->getUser() instanceof User) {
return false;
}
// Retrieve the related accompanying course document
$accompanyingCourseDocument = $this->repository->findLinkedCourseDocument($subject);
// Determine the attribute to pass to AccompanyingCourseDocumentVoter
$voterAttribute = match($attribute) {
self::SEE_AND_EDIT => AccompanyingCourseDocumentVoter::UPDATE,
default => AccompanyingCourseDocumentVoter::SEE_DETAILS,
};
// Check access using AccompanyingCourseDocumentVoter
if (false === $this->accompanyingCourseDocumentVoter->voteOnAttribute($voterAttribute, $accompanyingCourseDocument, $token)) {
return false;
}
// Check if entity is related to a workflow, if so, check if user can apply transition
$relatedWorkflow = $this->workflowDocumentService->getRelatedWorkflow($accompanyingCourseDocument);
if ($relatedWorkflow instanceof EntityWorkflow){
return $this->workflowDocumentService->canApplyTransition($relatedWorkflow);
}
return true;
}
}

View File

@ -48,15 +48,19 @@ class StoredObjectVoter extends Voter
return false;
}
$attributeAsEnum = StoredObjectRoleEnum::from($attribute);
// Loop through context-specific voters
foreach ($this->storedObjectVoters as $storedObjectVoter) {
if ($storedObjectVoter->supports($attribute, $subject)) {
return $storedObjectVoter->voteOnAttribute($attribute, $subject, $token);
if ($storedObjectVoter->supports($attributeAsEnum, $subject)) {
return $storedObjectVoter->voteOnAttribute($attributeAsEnum, $subject, $token);
}
}
// User role-based fallback
if ($this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN')) {
// TODO: this maybe considered as a security issue, as all authenticated users can reach a stored object which
// is potentially detached from an existing entity.
return true;
}

View File

@ -12,12 +12,13 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\Security\Authorization;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
interface StoredObjectVoterInterface {
public function supports(string $attribute, StoredObject $subject): bool;
public function supports(StoredObjectRoleEnum $attribute, StoredObject $subject): bool;
public function voteOnAttribute(string $attribute, StoredObject $subject, TokenInterface $token): bool;
public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool;
}

View File

@ -0,0 +1,67 @@
<?php
namespace Chill\DocStoreBundle\Security\Authorization\StoredObjectVoters;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface;
use Chill\DocStoreBundle\Service\WorkflowDocumentService;
use Chill\MainBundle\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Security;
abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface
{
abstract protected function getRepository(): AssociatedEntityToStoredObjectInterface;
/**
* @return class-string
*/
abstract protected function getClass(): string;
abstract protected function attributeToRole(StoredObjectRoleEnum $attribute): string;
abstract protected function canBeAssociatedWithWorkflow(): bool;
function __construct(
private readonly Security $security,
private readonly ?WorkflowDocumentService $workflowDocumentService = null,
)
{
}
public function supports(StoredObjectRoleEnum $attribute, StoredObject $subject): bool
{
$class = $this->getClass();
return $this->getRepository()->findAssociatedEntityToStoredObject($subject) instanceof $class;
}
public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool
{
if (!$token->getUser() instanceof User) {
return false;
}
// Retrieve the related accompanying course document
$entity = $this->getRepository()->findAssociatedEntityToStoredObject($subject);
// Determine the attribute to pass to AccompanyingCourseDocumentVoter
$voterAttribute = $this->attributeToRole($attribute);
if (false === $this->security->isGranted($voterAttribute, $entity)) {
return false;
}
if ($this->canBeAssociatedWithWorkflow()) {
if (null === $this->workflowDocumentService) {
throw new \LogicException("Provide a workflow document service");
}
return $this->workflowDocumentService->notBlockedByWorkflow($entity);
}
return true;
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace ChillDocStoreBundle\Security\Authorization\StoredObjectVoters;
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository;
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoters\AbstractStoredObjectVoter;
use Chill\DocStoreBundle\Service\WorkflowDocumentService;
use Symfony\Component\Security\Core\Security;
final class AccompanyingCourseStoredObjectVoter extends AbstractStoredObjectVoter
{
public function __construct(
private readonly AccompanyingCourseDocumentRepository $repository,
Security $security,
WorkflowDocumentService $workflowDocumentService
){
parent::__construct($security, $workflowDocumentService);
}
protected function getRepository(): AssociatedEntityToStoredObjectInterface
{
return $this->repository;
}
protected function attributeToRole(StoredObjectRoleEnum $attribute): string
{
return match ($attribute) {
StoredObjectRoleEnum::EDIT => AccompanyingCourseDocumentVoter::UPDATE,
StoredObjectRoleEnum::SEE => AccompanyingCourseDocumentVoter::SEE_DETAILS,
};
}
protected function getClass(): string
{
return AccompanyingCourseDocument::class;
}
protected function canBeAssociatedWithWorkflow(): bool
{
return true;
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace src\Bundle\ChillDocStoreBundle\Security\Authorization\StoredObjectVoters;
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoters\AbstractStoredObjectVoter;
use Chill\DocStoreBundle\Service\WorkflowDocumentService;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocumentRepository;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkEvaluationDocumentVoter;
use Symfony\Component\Security\Core\Security;
class AccompanyingPeriodWorkEvaluationStoredObjectVoter extends AbstractStoredObjectVoter
{
public function __construct(
private readonly AccompanyingPeriodWorkEvaluationDocumentRepository $repository,
Security $security,
WorkflowDocumentService $workflowDocumentService
){
parent::__construct($security, $workflowDocumentService);
}
protected function getRepository(): AssociatedEntityToStoredObjectInterface
{
return $this->repository;
}
/**
* @inheritDoc
*/
protected function getClass(): string
{
return AccompanyingPeriodWorkEvaluationDocument::class;
}
protected function attributeToRole(StoredObjectRoleEnum $attribute): string
{
//Question: there is no update/edit check in AccompanyingPeriodWorkEvaluationDocumentVoter, so for both SEE and EDIT of the
// stored object I check with SEE right in AccompanyingPeriodWorkEvaluationDocumentVoter, correct?
return match ($attribute) {
StoredObjectRoleEnum::SEE, StoredObjectRoleEnum::EDIT => AccompanyingPeriodWorkEvaluationDocumentVoter::SEE,
};
}
protected function canBeAssociatedWithWorkflow(): bool
{
return true;
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace src\Bundle\ChillDocStoreBundle\Security\Authorization\StoredObjectVoters;
use Chill\ActivityBundle\Entity\Activity;
use Chill\ActivityBundle\Repository\ActivityDocumentACLAwareRepository;
use Chill\ActivityBundle\Repository\ActivityRepository;
use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoters\AbstractStoredObjectVoter;
use Chill\DocStoreBundle\Service\WorkflowDocumentService;
use Symfony\Component\Security\Core\Security;
class ActivityStoredObjectVoter extends AbstractStoredObjectVoter
{
public function __construct(
private readonly ActivityRepository $repository,
Security $security,
WorkflowDocumentService $workflowDocumentService
){
parent::__construct($security, $workflowDocumentService);
}
protected function getRepository(): AssociatedEntityToStoredObjectInterface
{
return $this->repository;
}
/**
* @inheritDoc
*/
protected function getClass(): string
{
return Activity::class;
}
protected function attributeToRole(StoredObjectRoleEnum $attribute): string
{
return match ($attribute) {
StoredObjectRoleEnum::EDIT => ActivityVoter::UPDATE,
StoredObjectRoleEnum::SEE => ActivityVoter::SEE_DETAILS,
};
}
protected function canBeAssociatedWithWorkflow(): bool
{
return false;
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace src\Bundle\ChillDocStoreBundle\Security\Authorization\StoredObjectVoters;
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoters\AbstractStoredObjectVoter;
use Chill\DocStoreBundle\Service\WorkflowDocumentService;
use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Repository\EventRepository;
use Chill\EventBundle\Security\Authorization\EventVoter;
use Symfony\Component\Security\Core\Security;
class EventStoredObjectVoter extends AbstractStoredObjectVoter
{
public function __construct(
private readonly EventRepository $repository,
Security $security,
WorkflowDocumentService $workflowDocumentService
){
parent::__construct($security, $workflowDocumentService);
}
protected function getRepository(): AssociatedEntityToStoredObjectInterface
{
return $this->repository;
}
/**
* @inheritDoc
*/
protected function getClass(): string
{
return Event::class;
}
protected function attributeToRole(StoredObjectRoleEnum $attribute): string
{
return match ($attribute) {
StoredObjectRoleEnum::EDIT => EventVoter::UPDATE,
StoredObjectRoleEnum::SEE => EventVoter::SEE_DETAILS,
};
}
protected function canBeAssociatedWithWorkflow(): bool
{
return false;
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace src\Bundle\ChillDocStoreBundle\Security\Authorization\StoredObjectVoters;
use Chill\DocStoreBundle\Entity\PersonDocument;
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\DocStoreBundle\Repository\PersonDocumentRepository;
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoters\AbstractStoredObjectVoter;
use Chill\DocStoreBundle\Service\WorkflowDocumentService;
use Symfony\Component\Security\Core\Security;
class PersonStoredObjectVoter extends AbstractStoredObjectVoter
{
public function __construct(
private readonly PersonDocumentRepository $repository,
Security $security,
WorkflowDocumentService $workflowDocumentService
){
parent::__construct($security, $workflowDocumentService);
}
protected function getRepository(): AssociatedEntityToStoredObjectInterface
{
return $this->repository;
}
/**
* @inheritDoc
*/
protected function getClass(): string
{
return PersonDocument::class;
}
protected function attributeToRole(StoredObjectRoleEnum $attribute): string
{
return match ($attribute) {
StoredObjectRoleEnum::EDIT => PersonDocumentVoter::UPDATE,
StoredObjectRoleEnum::SEE => PersonDocumentVoter::SEE_DETAILS,
};
}
protected function canBeAssociatedWithWorkflow(): bool
{
return true;
}
}

View File

@ -13,19 +13,19 @@ class WorkflowDocumentService
{
}
public function getRelatedWorkflow($entity): ?EntityWorkflow
public function notBlockedByWorkflow($entity): bool
{
return $this->repository->findByRelatedEntity(get_class($entity), $entity->getId());
}
/**
* @var EntityWorkflow
*/
$workflow = $this->repository->findByRelatedEntity(get_class($entity), $entity->getId());
public function canApplyTransition(EntityWorkflow $entityWorkflow): bool
{
if ($entityWorkflow->isFinal()) {
if ($workflow->isFinal()) {
return false;
}
$currentUser = $this->security->getUser();
if ($entityWorkflow->getCurrentStep()->getAllDestUser()->contains($currentUser)) {
if ($workflow->getCurrentStep()->getAllDestUser()->contains($currentUser)) {
return true;
}

View File

@ -31,7 +31,7 @@ use Symfony\Component\Validator\Constraints as Assert;
/**
* Class Event.
*/
#[ORM\Entity(repositoryClass: \Chill\EventBundle\Repository\EventRepository::class)]
#[ORM\Entity]
#[ORM\HasLifecycleCallbacks]
#[ORM\Table(name: 'chill_event_event')]
class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInterface, TrackUpdateInterface

View File

@ -11,17 +11,66 @@ declare(strict_types=1);
namespace Chill\EventBundle\Repository;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\EventBundle\Entity\Event;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ObjectRepository;
/**
* Class EventRepository.
*/
class EventRepository extends ServiceEntityRepository
class EventRepository implements ObjectRepository, AssociatedEntityToStoredObjectInterface
{
public function __construct(ManagerRegistry $registry)
private readonly EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager)
{
parent::__construct($registry, Event::class);
$this->repository = $entityManager->getRepository(Event::class);
}
public function createQueryBuilder(string $alias, ?string $indexBy = null): QueryBuilder
{
return $this->repository->createQueryBuilder($alias, $indexBy);
}
public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object
{
$qb = $this->createQueryBuilder('e');
$query = $qb
->join('e.documents', 'ed')
->join('ed.storedObject', 'so')
->where('so.id = :storedObjectId')
->setParameter('storedObjectId', $storedObject->getId())
->getQuery();
return $query->getResult();
}
public function find($id)
{
return $this->repository->find($id);
}
public function findAll(): array
{
return $this->repository->findAll();
}
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
{
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
public function findOneBy(array $criteria)
{
return $this->repository->findOneBy($criteria);
}
public function getClassName(): string
{
return Event::class;
}
}

View File

@ -99,6 +99,22 @@ class EntityWorkflowRepository implements ObjectRepository
return $this->repository->findAll();
}
public function findByRelatedEntity($entityClass, $relatedEntityId): ?EntityWorkflow
{
$qb = $this->repository->createQueryBuilder('w');
$query = $qb->where(
$qb->expr()->andX(
$qb->expr()->eq('w.relatedEntityClass', ':entity_class'),
$qb->expr()->eq('w.relatedEntityId', ':entity_id'),
)
)->setParameter('entity_class', $entityClass)
->setParameter('entity_id', $relatedEntityId);
return $query->getQuery()->getResult();
}
/**
* @param mixed|null $limit
* @param mixed|null $offset

View File

@ -11,12 +11,14 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Repository\AccompanyingPeriod;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ObjectRepository;
class AccompanyingPeriodWorkEvaluationDocumentRepository implements ObjectRepository
class AccompanyingPeriodWorkEvaluationDocumentRepository implements ObjectRepository, AssociatedEntityToStoredObjectInterface
{
private readonly EntityRepository $repository;
@ -58,4 +60,14 @@ class AccompanyingPeriodWorkEvaluationDocumentRepository implements ObjectReposi
{
return AccompanyingPeriodWorkEvaluationDocument::class;
}
public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object
{
$qb = $this->repository->createQueryBuilder('ed');
$query = $qb->where('ed.storedObject = :storedObject')
->setParameter('storedObject', $storedObject)
->getQuery();
return $query->getResult();
}
}