Kaynağa Gözat

Ajout du dateTimeConstraint Aware Filter

Vincent GUFFON 4 yıl önce
ebeveyn
işleme
240e955c82

+ 4 - 0
config/packages/doctrine.yaml

@@ -21,6 +21,10 @@ doctrine:
         auto_generate_proxy_classes: true
         entity_managers:
             default:
+                filters:
+                    date_time_filter:
+                        class: App\Filter\DoctrineFilter\DateTimeFilter
+                        enabled: true
                 connection: default
                 auto_mapping: true
                 mappings:

+ 16 - 0
src/Annotation/DateTimeConstraintAware.php

@@ -0,0 +1,16 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Annotation;
+
+use Attribute;
+
+/**
+ * Classe DateTimeConstraintAware qui gère l'annotation pour le Doctrine filter
+ */
+#[Attribute(Attribute::TARGET_CLASS)]
+final class DateTimeConstraintAware
+{
+    public string $startDateFieldName;
+    public string $endDateFieldName;
+}

+ 25 - 0
src/Doctrine/Access/Extensions/DateTimeConstraintExtension.php

@@ -0,0 +1,25 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Doctrine\Access\Extensions;
+
+use App\Doctrine\Access\AccessExtensionInterface;
+use Doctrine\ORM\QueryBuilder;
+use Symfony\Component\HttpFoundation\RequestStack;
+
+class DateTimeConstraintExtension implements AccessExtensionInterface {
+    public function __construct(
+        private RequestStack $requestStack
+    ){
+    }
+    public function support(string $name): bool
+    {
+        return $this->requestStack->getMainRequest()->get('_time_constraint', true) == true;
+    }
+
+    public function addWhere(QueryBuilder $queryBuilder)
+    {
+        $rootAlias = $queryBuilder->getRootAliases()[0];
+        $queryBuilder->innerJoin(sprintf('%s.organizationFunction', $rootAlias), 'organization_function');
+    }
+}

+ 3 - 0
src/Entity/Access/OrganizationFunction.php

@@ -4,6 +4,7 @@ declare(strict_types=1);
 namespace App\Entity\Access;
 
 use ApiPlatform\Core\Annotation\ApiResource;
+use App\Annotation\DateTimeConstraintAware;
 use App\Entity\Traits\ActivityPeriodTrait;
 use App\Repository\Access\OrganizationFunctionRepository;
 use Doctrine\ORM\Mapping as ORM;
@@ -13,7 +14,9 @@ use Symfony\Component\Validator\Constraints as Assert;
  * Fonction d'un Access dans une Organization sur une période donnée
  */
 #[ApiResource]
+
 #[ORM\Entity(repositoryClass: OrganizationFunctionRepository::class)]
+#[DateTimeConstraintAware(startDateFieldName: "startDate", endDateFieldName: "endDate")]
 class OrganizationFunction
 {
     use ActivityPeriodTrait;

+ 1 - 0
src/Entity/Traits/ActivityPeriodTrait.php

@@ -3,6 +3,7 @@ declare(strict_types=1);
 
 namespace App\Entity\Traits;
 
+use App\Annotation\DateTimeAware;
 use Doctrine\ORM\Mapping as ORM;
 
 trait ActivityPeriodTrait

+ 38 - 0
src/EventListener/DoctrineFilter/DoctrineFilterListener.php

@@ -0,0 +1,38 @@
+<?php
+declare(strict_types=1);
+
+namespace App\EventListener\DoctrineFilter;
+
+use App\Service\Utils\DateTimeConstraint;
+use Doctrine\ORM\EntityManagerInterface;
+use Symfony\Component\HttpFoundation\RequestStack;
+use Symfony\Component\HttpKernel\Event\RequestEvent;
+use Symfony\Component\Security\Core\Security;
+
+/**
+ * Classe DoctrineFilterListener qui permet d'assurer l'injection de dépendance pour le SQL Filter
+ */
+class DoctrineFilterListener
+{
+    public function __construct(
+        private EntityManagerInterface $entityManager,
+        private DateTimeConstraint $dateTimeConstraint,
+        private Security $security,
+        private RequestStack $requestStack
+    )
+    {
+    }
+
+    public function onKernelRequest(RequestEvent $event)
+    {
+        if (!$event->isMainRequest()) {
+            // don't do anything if it's not the main request
+            return;
+        }
+        $filter = $this->entityManager->getFilters()->getFilter('date_time_filter');
+        $filter->setParameter('accessId', $this->security->getUser()?->getId() ?? null);
+        $filter->setParameter('_time_constraint', $this->requestStack->getMainRequest()->get('_time_constraint', true));
+
+        $filter->setDateTimeConstraint($this->dateTimeConstraint);
+    }
+}

+ 101 - 0
src/Filter/DoctrineFilter/DateTimeFilter.php

@@ -0,0 +1,101 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Filter\DoctrineFilter;
+
+use App\Annotation\DateTimeConstraintAware;
+use App\Service\Utils\DateTimeConstraint;
+use App\Service\Utils\StringsUtils;
+use Doctrine\ORM\Mapping\ClassMetadata;
+use Doctrine\ORM\Query\Filter\SQLFilter;
+
+/**
+ * Classe DateTimeFilter qui définie la requête SQL devant être ajoutée aux Entités possédant l'annotation DateTimeConstraintAware
+ */
+final class DateTimeFilter extends SQLFilter
+{
+    private DateTimeConstraint $dateTimeConstraint;
+
+    /**
+     * Méthode surchargée de SQLFilter
+     * @param ClassMetadata $targetEntity
+     * @param string $targetTableAlias
+     * @return string
+     * @throws \Exception
+     */
+    public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string
+    {
+        if(!$this->hasParameter('_time_constraint')
+            || !boolval(StringsUtils::unquote($this->getParameter('_time_constraint')))
+            || !$this->hasParameter('accessId')
+        )
+            return '';
+
+        $dateTimeConstraintAware = $targetEntity->getReflectionClass()->getAttributes(DateTimeConstraintAware::class)[0] ?? null;
+        $startFieldName = $dateTimeConstraintAware?->getArguments()['startDateFieldName'] ?? null;
+        $endFieldName = $dateTimeConstraintAware?->getArguments()['endDateFieldName'] ?? null;
+        if ($startFieldName === '' || is_null($startFieldName) || $endFieldName === '' || is_null($endFieldName)) {
+            return '';
+        }
+
+        $accessId = intval(StringsUtils::unquote($this->getParameter('accessId')));
+        $constraints = $this->dateTimeConstraint->invoke($accessId);
+
+        $fields = [
+            DateTimeConstraint::START_KEY => $startFieldName,
+            DateTimeConstraint::END_KEY => $endFieldName
+        ];
+      
+        return $this->constructQuery($constraints, $targetTableAlias, $fields);
+    }
+
+    /**
+     * Fonction permettant de construire la requête SQL correspondante aux contraintes
+     * @param array $constraints
+     * @param string $targetTableAlias
+     * @param array $fields
+     * @return string
+     */
+    private function constructQuery(array $constraints, string $targetTableAlias, array $fields): string{
+        $queryConditionsAND = [];
+        foreach ($constraints as $key => $constraint) {
+            $queryConditionsOR = [];
+            foreach ($constraint as $date => $conditions){
+                foreach ($conditions as $condition){
+                    $arithmetic = $this->getArithmeticValue($condition);
+                    if(!is_null($arithmetic))
+                        $queryConditionsOR[] = sprintf("%s.%s %s '%s'", $targetTableAlias, $fields[$key], $arithmetic, $date);
+                    else
+                        $queryConditionsOR[] = sprintf("%s.%s IS NULL", $targetTableAlias, $fields[$key]);
+                }
+            }
+            if(!empty($queryConditionsOR))
+                $queryConditionsAND[] = sprintf("(%s)", join(' OR ', $queryConditionsOR));
+        }
+        return join(" AND ", $queryConditionsAND);
+    }
+
+    /**
+     * Fonction retournant la valeur arithmétique correspondant à la condition de la contrainte
+     * @param $condition
+     * @return string|null
+     */
+    private function getArithmeticValue($condition): string|null{
+        switch ($condition){
+            case DateTimeConstraint::INF : return '<';
+            case DateTimeConstraint::EQUAL :  return '=';
+            case DateTimeConstraint::SUP :  return '>';
+            case DateTimeConstraint::INF + DateTimeConstraint::EQUAL : return '<=';
+            case DateTimeConstraint::SUP + DateTimeConstraint::EQUAL  : return '>=';
+        }
+        return null;
+    }
+
+    /**
+     * Permets d'assurer l'injection de dépendance du service DateTimeConstraint
+     * @param DateTimeConstraint $dateTimeConstraint
+     */
+    public function setDateTimeConstraint(DateTimeConstraint $dateTimeConstraint){
+        $this->dateTimeConstraint = $dateTimeConstraint;
+    }
+}

+ 6 - 1
src/Filter/Person/FullNameFilter.php

@@ -20,6 +20,11 @@ class FullNameFilter extends AbstractFilter{
             ->setParameter('search', '%'.$value.'%');
     }
 
+    /**
+     * API docs
+     * @param string $resourceClass
+     * @return array[]
+     */
     public function getDescription(string $resourceClass): array
     {
         return [
@@ -28,7 +33,7 @@ class FullNameFilter extends AbstractFilter{
                 'type' => 'string',
                 'required' => false,
                 'openapi' => [
-                    'description' => 'Recherche dans parmi les champs name et givenName',
+                    'description' => 'Rechercher parmi les champs name et givenName',
                 ],
             ]
         ];

+ 7 - 1
src/Repository/Access/AccessRepository.php

@@ -95,6 +95,8 @@ class AccessRepository extends ServiceEntityRepository implements UserLoaderInte
 
     public function hasGotFunctionAtThisDate(Access $access, $function, \DateTime $date): bool
     {
+        $this->_em->getFilters()->disable('date_time_filter');
+
         $qb = $this->createQueryBuilder('access');
         $qb
             ->innerJoin('access.organizationFunction', 'organization_function')
@@ -106,6 +108,10 @@ class AccessRepository extends ServiceEntityRepository implements UserLoaderInte
         ;
         DateConditions::addDateInPeriodCondition($qb, 'organization_function', $date->format('Y-m-d'));
 
-        return count($qb->getQuery()->getResult()) > 0;
+        $result = count($qb->getQuery()->getResult()) > 0;
+
+        $this->_em->getFilters()->enable('date_time_filter');
+
+        return $result;
     }
 }

+ 29 - 0
src/Service/Organization/Utils.php

@@ -14,6 +14,9 @@ use App\Test\Service\Organization\UtilsTest;
  */
 class Utils
 {
+    const START_DATE_KEY = 'dateStart';
+    const END_DATE_KEY = 'dateEnd';
+
     /**
      * Test si l'organisation est considérée comme une structure == n'a pas un produit manager
      * @param Organization $organization
@@ -86,4 +89,30 @@ class Utils
 
         else return $year;
     }
+
+    /**
+     * Fonction permettant de récupérer les dates de début et de fin d'activité d'une structure selon une année
+     * @param Organization $organization
+     * @param int $year
+     * @return \DateTime[]
+     * @throws \Exception
+     */
+    public static function getActivityPeriodsSwitchYear(Organization $organization, int $year): array
+    {
+        $musicalDate = $organization->getParameters()->getMusicalDate();
+
+        if (empty($musicalDate)) {
+            $dateStart = new \DateTime($year . "-09-01");
+            $dateEnd = new \DateTime(($year + 1) . "-08-31");
+        } else {
+            $dateStart = new \DateTime($year . "-" . $musicalDate->format('m') . "-" . $musicalDate->format('d'));
+            $dateEnd = clone($dateStart);
+            $dateEnd->add(new \DateInterval('P1Y'))->sub(new \DateInterval('P1D'));
+        }
+
+        return [
+            self::START_DATE_KEY => $dateStart->format('Y-m-d'),
+            self::END_DATE_KEY => $dateEnd->format('Y-m-d')
+        ];
+    }
 }

+ 226 - 0
src/Service/Utils/DateTimeConstraint.php

@@ -0,0 +1,226 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Service\Utils;
+
+use App\Entity\Access\Access;
+use App\Service\Organization\Utils as organizationUtils;
+use Doctrine\ORM\EntityManagerInterface;
+
+/**
+ * Classe DateTimeConstraint qui définie les dates de débuts et fin de périodes
+ * par rapport au contraintes temporelles choisies par un utilisateur.
+ */
+class DateTimeConstraint
+{
+    const NULL = 0;
+    const INF = 1;
+    const EQUAL = 3;
+    const SUP = 5;
+    const CANCEL_OPERATION = 9;
+    const START_KEY = 'start';
+    const END_KEY = 'end';
+    const NULL_VALUE = 'NULL';
+
+    public function __construct(
+        private EntityManagerInterface $entityManager,
+    )
+    { }
+
+    /**
+     * Main méthode
+     * @param int $accessID
+     * @return array
+     * @throws \Exception
+     */
+    public function invoke(int $accessID): array
+    {
+        $access = $this->entityManager->getRepository(Access::class)->find($accessID);
+        $historical = $access->getHistorical();
+
+        $contraints = [
+            self::START_KEY => [],
+            self::END_KEY => []
+        ];
+
+        if($this->hasCustomPeriods($historical)){
+            $periods = $this->getCustomPeriods($historical['dateStart'], $historical['dateEnd']);
+            //Une période "Custom" reviens à savoir quels sont les éléments actif durant le "présent" de la période custom, donc
+            //on peut utiliser le presentConstraint avec les periods custom
+            $contraints = $this->addConstraint($contraints, $this->presentConstraint($periods));
+        }else{
+            $periods = $this->getPeriods($access);
+            if($historical['present']) $contraints = $this->addConstraint($contraints, $this->presentConstraint($periods));
+            if($historical['past']) $contraints = $this->addConstraint($contraints, $this->pastConstraint($periods));
+            if($historical['future']) $contraints = $this->addConstraint($contraints, $this->futurConstraint($periods));
+        }
+        return $this->cleanConstraints($contraints);
+    }
+
+    /**
+     * Retourne true si l'utilisateur veux une période précise
+     * @param $historical
+     * @return bool
+     */
+    private function hasCustomPeriods($historical): bool{
+        return $historical['dateStart'] && $historical['dateEnd'];
+    }
+
+    /**
+     * Retourne le tableau des périodes custom
+     * @param string $dateStart
+     * @param string $dateEnd
+     * @return string[]
+     */
+    private function getCustomPeriods(string $dateStart, string $dateEnd): array{
+        return [
+            organizationUtils::START_DATE_KEY => $dateStart,
+            organizationUtils::END_DATE_KEY => $dateEnd
+        ];
+    }
+
+    /**
+     * Fonction permettant de récupérer les périodes de début et de fin d'affichage
+     * @param Access $access
+     * @return array
+     * @throws \Exception
+     */
+    private function getPeriods(Access $access): array{
+        $organization = $access->getOrganization();
+        $activityYear = $access->getActivityYear();
+        $currentActivityYear = organizationUtils::getOrganizationCurrentActivityYear($organization);
+
+        $periods = organizationUtils::getActivityPeriodsSwitchYear($organization, $activityYear);
+        //Si l'année courante est l'année d'affichage choisie par l'utilisateur, alors la date de début est aujourd'hui
+        if($activityYear === $currentActivityYear){
+            $today = new \DateTime('now');
+            $periods[organizationUtils::START_DATE_KEY] = $today->format('Y-m-d');
+        }
+
+        return $periods;
+    }
+
+    /**
+     * Fonction permettant d'ajouter une nouvelle contrainte de date
+     * @param array $contraints
+     * @param array $newContraint
+     * @return array
+     */
+    private function addConstraint(array $contraints, array $newContraint): array{
+        $contraints = $this->mergeConstraint($contraints,$newContraint,self::START_KEY);
+        $contraints = $this->mergeConstraint($contraints,$newContraint,self::END_KEY);
+        return $contraints;
+    }
+
+    /**
+     * Construit le tableau de contraintes pour une condition (start, end)
+     * @param array $contraints
+     * @param array $newContraint
+     * @param string $key
+     * @return array
+     */
+    private function mergeConstraint(array $contraints, array $newContraint, string $key): array{
+        if(array_key_exists($key, $newContraint)){
+            foreach ($newContraint[$key] as $dateKey => $arithmeticValue){
+                //Si la date à déjà des conditions
+                if(array_key_exists($dateKey, $contraints[$key])){
+                    //Si la conditions (<, >, =, ...) n'est pas encore appliquée à la date
+                    if(!in_array($arithmeticValue, $contraints[$key][$dateKey]))
+                        $contraints[$key][$dateKey][] = $arithmeticValue;
+                }else{
+                    $contraints[$key][$dateKey] = [$arithmeticValue];
+                }
+            }
+        }
+
+        return $contraints;
+    }
+
+    /**
+     * Nettoyage des contraintes (toutes celles supérieur à la condition cancel et les valeurs null isolées)
+     * @param array $constraints
+     * @return array
+     */
+    private function cleanConstraints(array $constraints): array{
+        $constraints[self::START_KEY] = $this->filterConstraint($constraints, self::START_KEY);
+        $constraints[self::START_KEY] = $this->clearNull($constraints, self::START_KEY);
+
+        $constraints[self::END_KEY] = $this->filterConstraint($constraints, self::END_KEY);
+        $constraints[self::END_KEY] = $this->clearNull($constraints, self::END_KEY);
+        return $constraints;
+    }
+
+    /**
+     * Pour chaque contraintes appliquées à une date on vérifie qu'on ne dépasse pas la valeur cancel : c'est à dire
+     * la condition qui démontre que toutes les valeurs arithmétiques ont été choisies
+     * @param array $constraints
+     * @param $key
+     * @return array
+     */
+    private function filterConstraint(array $constraints, string $key): array{
+        return array_filter($constraints[$key], function($constraint){
+            return array_sum($constraint) < self::CANCEL_OPERATION ;
+        });
+    }
+
+    /**
+     * On ne conserve pas les contraintes sur des conditions start et end si ces dernieres ne possède qu'une valeur null :
+     * une valeur null doit obligatoirement s'appliquer avec un OR
+     * @param array $constraints
+     * @param $key
+     * @return array
+     */
+    private function clearNull(array $constraints, string $key): array{
+        if(count($constraints[$key]) === 1 && array_key_exists(self::NULL_VALUE, $constraints[$key]))
+            $constraints[$key] = [];
+        return $constraints[$key];
+    }
+
+    /**
+     * Une période est dans le présent si :
+     *  - la date de début est plus petite ou égale (<=) à la date de fin de période à afficher
+     * ET
+     *  - la date de fin est plus grande ou égale (>=) à la date de fin de période à afficher OU que la date de fin n'est pas remplie (NULL)
+     * @param $periods
+     * @return array
+     */
+    private function presentConstraint(array $periods): array{
+        return [
+          self::START_KEY => [
+              $periods[organizationUtils::END_DATE_KEY] => self::INF + self::EQUAL
+          ],
+          self::END_KEY => [
+              $periods[organizationUtils::START_DATE_KEY] => self::SUP + self::EQUAL,
+              self::NULL_VALUE => self::NULL
+          ]
+        ];
+    }
+
+    /**
+     * Une période est dans le passée si :
+     * - la date de fin est plus petite (<) que la date de début de période à afficher
+     * @param $periods
+     * @return \int[][]
+     */
+    private function pastConstraint($periods): array{
+        return [
+            self::END_KEY => [
+                $periods[organizationUtils::START_DATE_KEY] => self::INF
+            ]
+        ];
+    }
+
+    /**
+     * Une période est dans le future si :
+     * - la date de début est plus grande (>) que la date de fin de période à afficher
+     * @param $periods
+     * @return \int[][]
+     */
+    private function futurConstraint($periods): array{
+        return [
+            self::START_KEY => [
+                $periods[organizationUtils::END_DATE_KEY] => self::SUP
+            ]
+        ];
+    }
+}

+ 20 - 0
src/Service/Utils/StringsUtils.php

@@ -0,0 +1,20 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Service\Utils;
+
+/**
+ * Class StringsUtils : méthodes d'aide pour la gestion de string.
+ * @package App\Service\Utils
+ */
+class StringsUtils
+{
+    /**
+     * Supprime les quotes d'une chaine de caractères
+     * @param string $str
+     * @return string
+     */
+    public static function unquote(string $str): string {
+        return str_replace("'", "", $str);
+    }
+}