spasmie

Untitled

Aug 19th, 2025
82
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
PHP 19.80 KB | None | 0 0
  1. <?php
  2.  
  3. declare(strict_types=1);
  4.  
  5. namespace App\Service;
  6.  
  7. use App\Entity\Account;
  8. use App\Entity\BaseEntity;
  9. use App\Entity\Commentary;
  10. use App\Entity\Interface\AccountEntityInterface;
  11. use App\Entity\Interface\ArchivableEntityInterface;
  12. use App\Entity\Interface\BaseEntityInterface;
  13. use App\Entity\Interface\CommittedEntityInterface;
  14. use App\Entity\Interface\DeletedEntityInterface;
  15. use App\Entity\Interface\FilterAliasEntityInterface;
  16. use App\Entity\Interface\FilterExtendedEntityInterface;
  17. use App\Entity\Interface\SearchTextEntityInterface;
  18. use App\Entity\Interface\SnapshotableEntityInterface;
  19. use App\Entity\Interface\TrashableEntityInterface;
  20. use App\Entity\Model\FilterExtendedFieldValue;
  21. use App\Entity\Model\SearchTextFilter;
  22. use App\Enum\Entity\IsArchivedEnum;
  23. use App\Event\Entity\EntityDeletedEvent;
  24. use App\Exception\Entity\EntityTypeException;
  25. use App\Exception\Entity\EntityValidationException;
  26. use App\Exception\ObjectInUseException;
  27. use App\Exception\ObjectNotFoundException;
  28. use App\Exception\ObjectNotImplementException;
  29. use App\Helper\ArrayHelper;
  30. use App\Helper\SortingHelper;
  31. use App\Repository\EntityRepository;
  32. use App\Repository\Interface\EntityBuildFilterRepositoryInterface;
  33. use App\Repository\Interface\InvalidatebleCacheRepositoryInterface;
  34. use App\Repository\Interface\TotalsRepositoryInterface;
  35. use App\Service\Entity\Interface\EntityServiceInterface;
  36. use App\Service\Entity\Interface\FilterServiceInterface;
  37. use App\Service\Entity\Interface\TrashServiceInterface;
  38. use App\Service\Entity\TrashService;
  39. use App\Service\Locator\EntityServiceLocator;
  40. use App\Service\Utility\EntityHelperService;
  41. use App\Trait\Repository\EntityBuildFilterRepositoryTrait;
  42. use App\Trait\Service\GetServiceTrait;
  43. use Doctrine\ORM\EntityManagerInterface;
  44. use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
  45. use Symfony\Component\DependencyInjection\ServiceLocator;
  46. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  47. use Symfony\Component\Uid\UuidV7;
  48. use Symfony\Component\Validator\Constraint;
  49. use Symfony\Component\Validator\Validator\ValidatorInterface;
  50. use Symfony\Contracts\Service\Attribute\Required;
  51.  
  52. abstract class EntityService implements EntityServiceInterface
  53. {
  54.     use GetServiceTrait;
  55.  
  56.     /**
  57.      * @var EntityRepository<BaseEntityInterface>|null
  58.      */
  59.     protected ?EntityRepository $repository = null;
  60.  
  61.     /**
  62.      * @var ServiceLocator<EntityService>
  63.      */
  64.     protected ServiceLocator $locator;
  65.     protected EventDispatcherInterface $dispatcher;
  66.     protected EntityHelperService $entityHelperService;
  67.     protected TrashService $trashService;
  68.     protected ParameterBagInterface $params;
  69.     protected ValidatorInterface $validator;
  70.     protected EntityManagerInterface $em;
  71.  
  72.     protected ?string $entityName = null;
  73.  
  74.     #[Required]
  75.    public function setDependencies(
  76.         EntityServiceLocator $subscriber,
  77.         EventDispatcherInterface $dispatcher,
  78.         EntityHelperService $entityHelper,
  79.         TrashService $trashService,
  80.         ParameterBagInterface $params,
  81.         ValidatorInterface $validator,
  82.     ): void {
  83.         $this->locator = $subscriber->getLocator();
  84.         $this->dispatcher = $dispatcher;
  85.         $this->entityHelperService = $entityHelper;
  86.         $this->trashService = $trashService;
  87.         $this->params = $params;
  88.         $this->validator = $validator;
  89.     }
  90.  
  91.     #[\Override]
  92.    public function isExists(
  93.         UuidV7|string|null $id = null,
  94.         ?Account $account = null,
  95.         array $criteria = [],
  96.         bool $criteriaAlreadyPrepared = false,
  97.     ): bool {
  98.         if (!$criteriaAlreadyPrepared) {
  99.             $criteria = $this->prepareCriteria(
  100.                 criteria: $criteria,
  101.                 id: $id,
  102.                 account: $account,
  103.             );
  104.         } elseif (null !== $account) {
  105.             $criteria['account'] = $account;
  106.         }
  107.  
  108.         $repository = $this->getRepository();
  109.  
  110.         if (method_exists($repository, 'isExistsByFilter')) {
  111.             /* @var EntityBuildFilterRepositoryTrait $repository */
  112.             return $repository->isExistsByFilter($criteria);
  113.         }
  114.  
  115.         unset($criteria['filter']);
  116.  
  117.         return $this->getRepository()->isExists($criteria);
  118.     }
  119.  
  120.     #[\Override]
  121.    public function get(
  122.         UuidV7|string|null $id = null,
  123.         ?Account $account = null,
  124.         array $criteria = [],
  125.         array $orderBy = [],
  126.         bool $criteriaAlreadyPrepared = false,
  127.     ): BaseEntityInterface {
  128.         $entity = $this->find($id, $account, $criteria, $orderBy, $criteriaAlreadyPrepared);
  129.  
  130.         if (null === $entity) {
  131.             throw new ObjectNotFoundException($this->entityHelperService->getEntity($this), $id);
  132.         }
  133.  
  134.         return $entity;
  135.     }
  136.  
  137.     #[\Override]
  138.    public function find(
  139.         UuidV7|string|null $id = null,
  140.         ?Account $account = null,
  141.         array $criteria = [],
  142.         ?array $orderBy = [],
  143.         bool $criteriaAlreadyPrepared = false,
  144.     ): ?BaseEntityInterface {
  145.         if (null === $id && 0 === count($criteria)) {
  146.             return null;
  147.         }
  148.         if (is_string($id) && !empty($id) && !UuidV7::isValid($id)) {
  149.             $exception = new EntityValidationException();
  150.             $exception->addCustomError(sprintf('Error idetificator Uuid format: %s', $id));
  151.  
  152.             throw $exception;
  153.         }
  154.  
  155.         if (!$criteriaAlreadyPrepared) {
  156.             $criteria = $this->prepareCriteria(
  157.                 criteria: $criteria,
  158.                 id: $id,
  159.                 account: $account,
  160.             );
  161.         } elseif (null === $id) {
  162.             unset($criteria['id']);
  163.         } else {
  164.             $criteria['id'] = $id;
  165.         }
  166.         unset($criteria['filter']);
  167.  
  168.         if (null === $orderBy || 0 === count($orderBy)) {
  169.             $orderBy = $this->entityHelperService->getSortingDefault($this);
  170.         }
  171.  
  172.         $repository = $this->getRepository();
  173.  
  174.         if ($account && !isset($criteria['id']) && method_exists($repository, 'getByAccount')) {
  175.             $entity = $repository->getByAccount($criteria['account'], 1, 0, $orderBy);
  176.             $entity = $entity[0] ?? null;
  177.         } else {
  178.             $entityClass = $this->entityHelperService->getEntity($this);
  179.  
  180.             if (null !== $account && is_a($entityClass, AccountEntityInterface::class, true)) {
  181.                 $criteria['account'] = $account;
  182.             }
  183.  
  184.             if ($repository instanceof EntityBuildFilterRepositoryInterface) {
  185.                 $entity = $repository->findByFilter($criteria, $orderBy, 1)[0] ?? null;
  186.             } else {
  187.                 $entity = $repository->findOneBy($criteria, $orderBy);
  188.             }
  189.         }
  190.  
  191.         if (null !== $entity && !($entity instanceof BaseEntityInterface)) {
  192.             throw new EntityTypeException(BaseEntity::class);
  193.         }
  194.  
  195.         return $entity;
  196.     }
  197.  
  198.     #[\Override]
  199.    public function getAll(
  200.         array $criteria = [],
  201.         ?int $limit = null,
  202.         ?int $offset = null,
  203.         array $orderBy = [],
  204.         bool $criteriaAlreadyPrepared = false,
  205.     ): array {
  206.         if (!$criteriaAlreadyPrepared) {
  207.             $criteria = $this->prepareCriteria(
  208.                 criteria: $criteria,
  209.                 account: $criteria['account'] ?? null,
  210.             );
  211.         }
  212.  
  213.         $sortableProperties = $this->entityHelperService->getSortableProperties($this);
  214.         $sortingDefault = $this->entityHelperService->getSortingDefault($this);
  215.         $orderBy = SortingHelper::prepare($orderBy, $sortableProperties, $sortingDefault);
  216.         $repository = $this->getRepository();
  217.  
  218.         if (
  219.             (!($repository instanceof EntityBuildFilterRepositoryInterface) && isset($criteria['filter']))
  220.             || method_exists($repository, 'getByAccount')
  221.         ) {
  222.             unset($criteria['filter']);
  223.         }
  224.  
  225.         return match (true) {
  226.             $repository instanceof EntityBuildFilterRepositoryInterface => $repository->findByFilter(
  227.                 criteria: $criteria,
  228.                 orderBy: $orderBy,
  229.                 limit: $limit,
  230.                 offset: $offset,
  231.             ),
  232.             method_exists($repository, 'getByAccount') => $repository->getByAccount(
  233.                 $criteria['account'],
  234.                 $limit,
  235.                 $offset,
  236.                 $orderBy,
  237.             ),
  238.             default => $repository->findBy(
  239.                 $criteria,
  240.                 $orderBy,
  241.                 $limit,
  242.                 $offset,
  243.             ),
  244.         };
  245.     }
  246.  
  247.     /**
  248.      * @param int|null $chunkSize
  249.      *
  250.      * @see EntityBuildFilterRepositoryTrait::findByFilterIterable()
  251.      */
  252.     public function getAllIterable(
  253.         array $criteria = [],
  254.         ?int $limit = null,
  255.         ?int $offset = null,
  256.         array $orderBy = [],
  257.         bool $criteriaAlreadyPrepared = false,
  258.         ?int $chunkSize = null,
  259.     ): \Generator {
  260.         if (!$criteriaAlreadyPrepared) {
  261.             $criteria = $this->prepareCriteria(
  262.                 criteria: $criteria,
  263.                 account: $criteria['account'] ?? null,
  264.             );
  265.         }
  266.  
  267.         $repository = $this->getRepository();
  268.  
  269.         if (!($repository instanceof EntityBuildFilterRepositoryInterface)) {
  270.             throw new ObjectNotImplementException($repository::class, EntityBuildFilterRepositoryInterface::class);
  271.         }
  272.  
  273.         return $repository->findByFilterIterable(
  274.             criteria: $criteria,
  275.             orderBy: SortingHelper::prepare($orderBy, $this->entityHelperService->getSortableProperties($this), $this->entityHelperService->getSortingDefault($this)),
  276.             limit: $limit,
  277.             offset: $offset,
  278.             chunkSize: $chunkSize ?? 0,
  279.         );
  280.     }
  281.  
  282.     #[\Override]
  283.    public function getCount(
  284.         array $criteria = [],
  285.         bool $criteriaAlreadyPrepared = false,
  286.     ): int {
  287.         if (!$criteriaAlreadyPrepared) {
  288.             $criteria = $this->prepareCriteria(
  289.                 criteria: $criteria,
  290.                 account: $criteria['account'] ?? null,
  291.             );
  292.         }
  293.  
  294.         $repository = $this->getRepository();
  295.  
  296.         if (
  297.             method_exists($repository, 'countAllByAccount')
  298.             || (!method_exists($repository, 'getCountByFilter') && isset($criteria['filter']))
  299.         ) {
  300.             unset($criteria['filter']);
  301.         }
  302.  
  303.         return match (true) {
  304.             method_exists($repository, 'countAllByAccount') => $repository->countAllByAccount($criteria['account']),
  305.             method_exists($repository, 'getCountByFilter') => $repository->getCountByFilter($criteria),
  306.             default => $repository->count($criteria),
  307.         };
  308.     }
  309.  
  310.     #[\Override]
  311.    public function getSum(
  312.         string $fieldName,
  313.         array $criteria = [],
  314.         bool $criteriaAlreadyPrepared = false,
  315.     ): int {
  316.         if (!$criteriaAlreadyPrepared) {
  317.             $criteria = $this->prepareCriteria(
  318.                 criteria: $criteria,
  319.                 account: $criteria['account'] ?? null,
  320.             );
  321.         }
  322.  
  323.         $repository = $this->getRepository();
  324.  
  325.         unset($criteria['filter']);
  326.  
  327.         return match (true) {
  328.             method_exists($repository, 'getSumByFilter') => $repository->getSumByFilter($fieldName, $criteria),
  329.             default => throw new \LogicException(sprintf('Count sum method not found for repository "%s".', $repository::class)),
  330.         };
  331.     }
  332.  
  333.     #[\Override]
  334.    public function getTotals(
  335.         ?Account $account,
  336.         array $criteria = [],
  337.     ): ?array {
  338.         $repository = $this->getRepository();
  339.  
  340.         if ($account) {
  341.             $criteria['account'] = $account;
  342.         }
  343.  
  344.         if ($repository instanceof TotalsRepositoryInterface) {
  345.             return $repository->getTotals($criteria);
  346.         }
  347.  
  348.         return null;
  349.     }
  350.  
  351.     #[\Override]
  352.    public function delete(
  353.         BaseEntityInterface $entity,
  354.         ?Account $account = null,
  355.         ?bool $ignoreTrash = false,
  356.         bool $flush = true,
  357.     ): void {
  358.  
  359.         if (
  360.             !$ignoreTrash
  361.             && null !== $account
  362.             && $entity instanceof TrashableEntityInterface
  363.             && $this instanceof TrashServiceInterface
  364.             && !$entity->getIsTrashed()
  365.         ) {
  366.             $this->trash($entity, $account);
  367.  
  368.             return;
  369.         }
  370.  
  371.         if (!$this->canDelete($entity)) {
  372.             throw new ObjectInUseException($entity::class);
  373.         }
  374.  
  375.         $isBeenCommitted = false;
  376.         if ($entity instanceof CommittedEntityInterface) {
  377.             $isBeenCommitted = $entity->getIsCommitted();
  378.         }
  379.  
  380.         $payload = ($entity instanceof SnapshotableEntityInterface) ? $this->getSnapshot($entity) : [];
  381.  
  382.         $repository = $this->getRepository();
  383.  
  384.         if ($entity instanceof DeletedEntityInterface) {
  385.             $entity->delete();
  386.             $repository->update(entity: $entity, flush: $flush);
  387.         } else {
  388.             $repository->delete(entity: $entity, flush: $flush);
  389.         }
  390.  
  391.         if ($entity instanceof TrashableEntityInterface && $entity->getIsTrashed()) {
  392.             $this->trashService->deleteByEntity($entity, $account);
  393.         }
  394.  
  395.         $this->dispatcher->dispatch(new EntityDeletedEvent(
  396.             entity: $entity,
  397.             account: $account,
  398.             isBeenCommitted: $isBeenCommitted,
  399.             payload: $payload,
  400.         ));
  401.     }
  402.  
  403.     #[\Override]
  404.    public function prepareCriteria(
  405.         array $criteria = [],
  406.         UuidV7|string|null $id = null,
  407.         ?Account $account = null,
  408.         string $search = '',
  409.         array $exclude = [],
  410.         bool $isAccurateSearch = false,
  411.     ): array {
  412.         $entityFqcn = $this->entityHelperService->getEntity($this);
  413.         $entity = new $entityFqcn();
  414.  
  415.         if (null !== $id) {
  416.             $criteria['id'] = $id;
  417.         }
  418.  
  419.         if ($entity instanceof FilterExtendedEntityInterface) {
  420.             foreach ($entity->getFilterExtendedFields() as $extendedField) {
  421.                 if (isset($criteria[$extendedField])) {
  422.                     $criteria[$extendedField] = new FilterExtendedFieldValue($criteria[$extendedField]);
  423.                 }
  424.             }
  425.         }
  426.  
  427.         if ($entity instanceof FilterAliasEntityInterface) {
  428.             foreach ($entity->getFilterAliases() as $alias => $normal) {
  429.                 if (isset($criteria[$alias])) {
  430.                     $val = $criteria[$alias];
  431.                     ArrayHelper::assignArrayByPathAndValue(
  432.                         arr: $criteria,
  433.                         path: $normal,
  434.                         value: is_string($val) ? explode(',', $val) : [$val])
  435.                     ;
  436.                     unset($criteria[$alias]);
  437.                 }
  438.             }
  439.         }
  440.  
  441.         if ($entity instanceof ArchivableEntityInterface) {
  442.             if (!array_key_exists('isArchived', $criteria)) {
  443.                 if (null === $id) {
  444.                     $criteria['isArchived'] = IsArchivedEnum::false;
  445.                 }
  446.             } else {
  447.                 $criteria['isArchived'] = IsArchivedEnum::tryFromValue($criteria['isArchived']);
  448.             }
  449.         }
  450.  
  451.         if ($this instanceof FilterServiceInterface) {
  452.             $criteria = $this->getFilter($entityFqcn, $criteria, ['account']);
  453.         }
  454.  
  455.         if (null !== $account && $entity instanceof AccountEntityInterface) {
  456.             if (array_key_exists('account', $criteria) && null === $criteria['account']) {
  457.                 unset($criteria['account']);
  458.             } else {
  459.                 $criteria['account'] = $account;
  460.             }
  461.         }
  462.  
  463.         if (!empty($search)) {
  464.             if ($entity instanceof SearchTextEntityInterface) {
  465.                 $isCommentary = $entity instanceof Commentary;
  466.                 $criteria['searchText'] = new SearchTextFilter(
  467.                     accountId: $account?->getId()?->toRfc4122(),
  468.                     entity: $isCommentary ? ($criteria['entityId'][0] ?? null) : $this->entityHelperService->getName($entityFqcn),
  469.                     searchText: $search,
  470.                     entityId: $criteria['entityId'][0] ?? null,
  471.                     isAccurateSearch: $isAccurateSearch,
  472.                 );
  473.             } else {
  474.                 $criteria['searchText'] = $search;
  475.             }
  476.         }
  477.  
  478.         if (property_exists($entity, 'deleted')) {
  479.             if (!array_key_exists('deleted', $criteria)) {
  480.                 $criteria['deleted'] = false;
  481.             } elseif (null === $criteria['deleted']) {
  482.                 unset($criteria['deleted']);
  483.             }
  484.         }
  485.  
  486.         if ($entity instanceof TrashableEntityInterface) {
  487.             if (!array_key_exists('isTrashed', $criteria)) {
  488.                 if (null === $id) {
  489.                     $criteria['isTrashed'] = false;
  490.                 }
  491.             } elseif (null === $criteria['isTrashed']) {
  492.                 unset($criteria['isTrashed']);
  493.             }
  494.         }
  495.  
  496.         return $criteria;
  497.     }
  498.  
  499.     /**
  500.      * @param Constraint[]|Constraint|null $constraints
  501.      */
  502.     #[\Override]
  503.    public function validate(
  504.         BaseEntity $entity,
  505.         Constraint|array|null $constraints = null,
  506.     ): void {
  507.         if (empty($constraints)) {
  508.             return;
  509.         }
  510.  
  511.         if (!is_array($constraints)) {
  512.             $constraints = [$constraints];
  513.         }
  514.  
  515.         $exception = new EntityValidationException();
  516.  
  517.         foreach ($constraints as $constraint) {
  518.             $validationErrors = $this->validator->validate($entity, $constraint);
  519.  
  520.             if ($validationErrors->count() > 0) {
  521.                 foreach ($validationErrors as $error) {
  522.                     $exception->addCustomError($error->getMessage());
  523.                 }
  524.             }
  525.         }
  526.  
  527.         if (count($exception->getErrors())) {
  528.             throw $exception;
  529.         }
  530.     }
  531.  
  532.     public function invalidateCache(
  533.         BaseEntityInterface $entity,
  534.     ): void {
  535.         $repository = $this->entityHelperService->findRepository($this);
  536.         $cache = $repository->getCache();
  537.  
  538.         if (null !== $cache && $repository instanceof InvalidatebleCacheRepositoryInterface) {
  539.             $repository->invalidateCache($cache, $entity);
  540.         }
  541.     }
  542.  
  543.     public function refresh(
  544.         BaseEntityInterface $entity,
  545.     ): void {
  546.         $this->getRepository()->refresh(entity: $entity, lock: false);
  547.     }
  548.  
  549.     /**
  550.      * Write all entities to repository and clear ObjectManager.
  551.      */
  552.     public function detach(
  553.         BaseEntityInterface $entity,
  554.     ): void {
  555.         $this->getRepository()->detach($entity);
  556.     }
  557.  
  558.     /**
  559.      * Write all entities to repository.
  560.      */
  561.     public function flushAll(): void
  562.     {
  563.         $this->getRepository()->update(flush: true);
  564.     }
  565.  
  566.     #[\Override]
  567.    public function getSnapshot(
  568.         SnapshotableEntityInterface $entity,
  569.     ): array {
  570.         $data = $this->getRepository()->getPreviousData($entity);
  571.         $fields = $entity->getSnapshotFields();
  572.  
  573.         foreach (array_keys($data) as $field) {
  574.             if (!in_array($field, $fields, true)) {
  575.                 unset($data[$field]);
  576.             }
  577.         }
  578.  
  579.         return $data;
  580.     }
  581.  
  582.     /**
  583.      * @return EntityRepository<BaseEntityInterface>
  584.      */
  585.     protected function getRepository(): EntityRepository
  586.     {
  587.         if (null === $this->repository) {
  588.             $this->repository = $this->entityHelperService->findRepository($this);
  589.         }
  590.  
  591.         return $this->repository;
  592.     }
  593.  
  594.     public function with(string|array|null $assocList = null): void
  595.     {
  596.         $this->getRepository()->with($assocList);
  597.     }
  598.  
  599.     protected function canDelete(
  600.         BaseEntityInterface $entity,
  601.     ): bool {
  602.         return true;
  603.     }
  604. }
  605.  
Advertisement
Add Comment
Please, Sign In to add comment