src/Products/NotificationsBundle/Service/Query/NotificationsConditionQueryBuilder.php line 61

Open in your IDE?
  1. <?php
  2. namespace Products\NotificationsBundle\Service\Query;
  3. use App\Model\Query\ConditionQuery\AbstractOperatorCondition;
  4. use App\Model\Query\ConditionQuery\Condition\ContainsCondition;
  5. use App\Model\Query\ConditionQuery\Condition\EmptyCondition;
  6. use App\Model\Query\ConditionQuery\Condition\EqualCondition;
  7. use App\Model\Query\ConditionQuery\Condition\GreaterThanCondition;
  8. use App\Model\Query\ConditionQuery\Condition\GreaterThanEqualCondition;
  9. use App\Model\Query\ConditionQuery\Condition\InCondition;
  10. use App\Model\Query\ConditionQuery\Condition\LessThanCondition;
  11. use App\Model\Query\ConditionQuery\Condition\LessThanEqualCondition;
  12. use App\Model\Query\ConditionQuery\Condition\NotContainsCondition;
  13. use App\Model\Query\ConditionQuery\Condition\NotEmptyCondition;
  14. use App\Model\Query\ConditionQuery\Condition\NotEqualCondition;
  15. use App\Model\Query\ConditionQuery\Condition\NotInCondition;
  16. use App\Model\Query\ConditionQuery\Condition\NotNullCondition;
  17. use App\Model\Query\ConditionQuery\Condition\NotOverlapsCondition;
  18. use App\Model\Query\ConditionQuery\Condition\NullCondition;
  19. use App\Model\Query\ConditionQuery\Condition\OverlapsCondition;
  20. use App\Model\Query\ConditionQuery\ConditionGroupInterface;
  21. use App\Model\Query\ConditionQuery\ConditionQuery;
  22. use App\Model\Query\ConditionQuery\Type\BooleanType;
  23. use App\Model\Query\ConditionQuery\Type\DateType;
  24. use App\Model\Query\ConditionQuery\Type\FloatType;
  25. use Cms\CoreBundle\Util\Doctrine\EntityManager;
  26. use Doctrine\ORM\Query\Expr\Comparison;
  27. use Doctrine\ORM\Query\Expr\Func;
  28. use Doctrine\ORM\QueryBuilder;
  29. use Exception;
  30. use LogicException;
  31. use Products\NotificationsBundle\Util\ListBuilder\AbstractListBuilder;
  32. final class NotificationsConditionQueryBuilder
  33. {
  34.     private const JSON_LENGTH 'JSON_LENGTH';
  35.     private const JSON_FILTER_FUNCTION_MAP = [
  36.         'count' => self::JSON_LENGTH,
  37.     ];
  38.     /**
  39.      * @var EntityManager
  40.      */
  41.     protected EntityManager $em;
  42.     /**
  43.      * @param EntityManager $em
  44.      */
  45.     public function __construct(EntityManager $em)
  46.     {
  47.         $this->em $em;
  48.     }
  49.     /**
  50.      * @param ConditionQuery $query
  51.      * @param string|null $qid
  52.      * @return QueryBuilder
  53.      * @throws Exception
  54.      */
  55.     public function build(ConditionQuery $query, ?string $qid null): QueryBuilder
  56.     {
  57.         // check for config
  58.         if ( ! $query->getConditionConfig()) {
  59.             throw new Exception();
  60.         }
  61.         $class $query->getEntityClass();
  62.         $alias $query->getEntityAlias() . $qid;
  63.         $qb $this->em->createQueryBuilder()
  64.             ->select($alias)
  65.             ->from($class$alias);
  66.         // if no conditions, force some kind of condition that will always return zero results
  67.         if ( ! $query->isUsable()) {
  68.             $qb->andWhere(
  69.                 sprintf(
  70.                     '%s.id = 0',
  71.                     $alias
  72.                 )
  73.             );
  74.         } else {
  75.             [$conditions$parameters$entities] = $this->buildConditions($query$qid);
  76.             $qb->andWhere($conditions);
  77.             foreach ($parameters as $name => $value) {
  78.                 $qb->setParameter($name$value);
  79.             }
  80.             foreach ($entities as $entity) {
  81.                 if ( ! in_array($entity $qid$qb->getAllAliases())) {
  82.                     switch ($entity) {
  83.                         // if we need to add a profile, probably means that we are starting with students
  84.                         case AbstractListBuilder::ENTITIES__PROFILES:
  85.                             if ( ! in_array(
  86.                                 AbstractListBuilder::ENTITIES__PROFILE_RELATIONSHIPS $qid,
  87.                                 $qb->getAllAliases()
  88.                             )) {
  89.                                 $qb->leftJoin(
  90.                                     AbstractListBuilder::ENTITIES__STUDENTS $qid '.relationships',
  91.                                     AbstractListBuilder::ENTITIES__PROFILE_RELATIONSHIPS $qid,
  92.                                 );
  93.                             }
  94.                             $qb->leftJoin(
  95.                                 AbstractListBuilder::ENTITIES__PROFILE_RELATIONSHIPS $qid '.profile',
  96.                                 AbstractListBuilder::ENTITIES__PROFILES $qid,
  97.                             );
  98.                             break;
  99.                         // if we need to add a student, probably means that we are starting with profiles
  100.                         case AbstractListBuilder::ENTITIES__STUDENTS:
  101.                             if ( ! in_array(
  102.                                 AbstractListBuilder::ENTITIES__PROFILE_RELATIONSHIPS $qid,
  103.                                 $qb->getAllAliases()
  104.                             )) {
  105.                                 $qb->leftJoin(
  106.                                     AbstractListBuilder::ENTITIES__PROFILES $qid '.relationships',
  107.                                     AbstractListBuilder::ENTITIES__PROFILE_RELATIONSHIPS $qid,
  108.                                 );
  109.                             }
  110.                             $qb->leftJoin(
  111.                                 AbstractListBuilder::ENTITIES__PROFILE_RELATIONSHIPS $qid '.student',
  112.                                 AbstractListBuilder::ENTITIES__STUDENTS $qid,
  113.                             );
  114.                             break;
  115.                         // handle relationships
  116.                         case AbstractListBuilder::ENTITIES__PROFILE_RELATIONSHIPS:
  117.                             switch ($query->getEntity(true)) {
  118.                                 case ConditionQuery::STUDENT_ENTITY:
  119.                                     $qb->leftJoin(
  120.                                         AbstractListBuilder::ENTITIES__STUDENTS $qid '.relationships',
  121.                                         AbstractListBuilder::ENTITIES__PROFILE_RELATIONSHIPS $qid,
  122.                                     );
  123.                                     break;
  124.                                 case ConditionQuery::PROFILE_ENTITY:
  125.                                     $qb->leftJoin(
  126.                                         AbstractListBuilder::ENTITIES__PROFILES $qid '.relationships',
  127.                                         AbstractListBuilder::ENTITIES__PROFILE_RELATIONSHIPS $qid,
  128.                                     );
  129.                                     break;
  130.                                 default:
  131.                                     throw new Exception();
  132.                             }
  133.                             break;
  134.                         case AbstractListBuilder::ENTITIES__PROFILE_AUTOMATION_RECORDS:
  135.                             $qb->leftJoin(
  136.                                 AbstractListBuilder::ENTITIES__PROFILES $qid '.automationRecords',
  137.                                 AbstractListBuilder::ENTITIES__PROFILE_AUTOMATION_RECORDS $qid,
  138.                             );
  139.                             break;
  140.                         default:
  141.                             throw new Exception();
  142.                     }
  143.                 }
  144.             }
  145.         }
  146.         return $qb;
  147.     }
  148.     /**
  149.      * @param ConditionGroupInterface $query
  150.      * @param string|null $qid
  151.      * @return array
  152.      */
  153.     private function buildConditions(ConditionGroupInterface $query, ?string $qid null): array
  154.     {
  155.         $expressionBuilder $this->em->createQueryBuilder()->expr();
  156.         if ($query->getMode() === ConditionGroupInterface::MODES__OR) {
  157.             $rootExpression $expressionBuilder->orX();
  158.         } else {
  159.             $rootExpression $expressionBuilder->andX();
  160.         }
  161.         $parameters = [];
  162.         $entities = [];
  163.         foreach ($query->getConditions() as $condition) {
  164.             if ($condition instanceof ConditionGroupInterface) {
  165.                 $newConditions $this->buildConditions($condition$qid);
  166.                 $rootExpression->add($newConditions[0]);
  167.                 $parameters array_merge($parameters$newConditions[1]);
  168.                 $entities array_merge($entities$newConditions[2]);
  169.             } elseif ($condition instanceof AbstractOperatorCondition) {
  170.                 // handle special "today" value for date type here
  171.                 // use the customer timezone, as these are just strings and not like normalized date/time types in the database...
  172.                 if ($condition->getType() instanceof DateType && $condition->getValue() === 'today') {
  173.                     $condition->setValue(
  174.                         $query->getConditionContext()->getToday()->format('Y-m-d')
  175.                     );
  176.                 }
  177.                 // get the name of the entity this condition is trying to use and track it
  178.                 $entities[] = $condition->getFilterEntity();
  179.                 $paramName $this->buildParamName($condition$qid);
  180.                 $rootExpression->add($this->buildExpression($condition$paramName$qid));
  181.                 if (($param $this->buildParameter($condition)) !== null) {
  182.                     $parameters[$paramName] = $param;
  183.                 }
  184.             }
  185.         }
  186.         return [$rootExpression$parametersarray_values(array_filter(array_unique($entities)))];
  187.     }
  188.     /**
  189.      * @param AbstractOperatorCondition $operatorCondition
  190.      * @param string|null $paramName
  191.      * @param string|null $qid
  192.      * @return string
  193.      */
  194.     private function buildFieldName(
  195.         AbstractOperatorCondition $operatorCondition,
  196.         ?string $paramName,
  197.         ?string $qid null
  198.     ): string {
  199.         if (empty($operatorCondition->getFilterJsonKey())) {
  200.             return sprintf(
  201.                 '%s%s.%s',
  202.                 $operatorCondition->getFilterEntity(),
  203.                 $qid,
  204.                 $operatorCondition->getFilterEntityProperty()
  205.             );
  206.         }
  207.         $prefix $suffix '';
  208.         switch (true) {
  209.             case $operatorCondition::NAME === ContainsCondition::NAME:
  210.             case $operatorCondition::NAME === NotContainsCondition::NAME:
  211.                 $prefix sprintf('%s(''JSON_CONTAINS');
  212.                 $suffix sprintf(', :%s)'$paramName);
  213.                 break;
  214.             case $operatorCondition::NAME === OverlapsCondition::NAME:
  215.             case $operatorCondition::NAME === NotOverlapsCondition::NAME:
  216.                 $prefix sprintf('%s(''JSON_OVERLAPS');
  217.                 $suffix sprintf(', :%s)'$paramName);
  218.                 break;
  219.             case is_array($operatorCondition->getValue()) && $operatorCondition::NAME === EmptyCondition::NAME:
  220.             case is_array($operatorCondition->getValue()) && $operatorCondition::NAME === NotEmptyCondition::NAME:
  221.                 $prefix 'JSON_LENGTH(';
  222.                 $suffix sprintf(')',);
  223.                 break;
  224.             case $operatorCondition->getType() instanceof BooleanType:
  225.                 $prefix 'CAST(';
  226.                 $suffix ' AS UNSIGNED)';
  227.                 break;
  228.             case $operatorCondition->getType() instanceof FloatType:
  229.             case is_float($operatorCondition->getValue()):
  230.                 $prefix 'CAST(';
  231.                 $suffix ' AS DECIMAL(10,3))';
  232.                 break;
  233.         }
  234.         $field sprintf(
  235.             '%sJSON_EXTRACT(%s%s.%s, \'$.%s\')%s',
  236.             $prefix,
  237.             $operatorCondition->getFilterEntity(),
  238.             $qid,
  239.             $operatorCondition->getFilterEntityProperty(),
  240.             implode(
  241.                 '.',
  242.                 array_map(
  243.                     function (string $key) {
  244.                         if ($key === '*') {
  245.                             return $key;
  246.                         }
  247.                         return '"' $key '"';
  248.                     },
  249.                     explode('.'$operatorCondition->getFilterJsonKey()),
  250.                 ),
  251.             ),
  252.             $suffix,
  253.         );
  254.         $filterFunction $operatorCondition->getFilterFunction();
  255.         if ($filterFunction === null) {
  256.             return $field;
  257.         }
  258.         switch (self::JSON_FILTER_FUNCTION_MAP[$filterFunction]) {
  259.             case self::JSON_LENGTH:
  260.                 return sprintf('JSON_LENGTH(%s)'$field);
  261.             default:
  262.                 throw new LogicException('Unsupported filter function');
  263.         }
  264.     }
  265.     /**
  266.      * @param AbstractOperatorCondition $operatorCondition
  267.      * @param string|null $qid
  268.      * @return string
  269.      */
  270.     private function buildParamName(AbstractOperatorCondition $operatorCondition, ?string $qid null): ?string
  271.     {
  272.         switch ($operatorCondition::NAME) {
  273.             case NullCondition::NAME:
  274.             case NotNullCondition::NAME:
  275.                 return null;
  276.             default:
  277.                 return str_replace(
  278.                     '-',
  279.                     '_',
  280.                     sprintf(
  281.                         'p%s_%s%s%s%s',
  282.                         $qid ?: uniqid(),
  283.                         spl_object_id($operatorCondition),
  284.                         $operatorCondition->getFilterEntity(),
  285.                         $operatorCondition->getFilterEntityProperty(),
  286.                         crc32($operatorCondition->getFilterJsonKey()),
  287.                     )
  288.                 );
  289.         }
  290.     }
  291.     /**
  292.      * @param AbstractOperatorCondition $operatorCondition
  293.      * @param string|null $paramName
  294.      * @param string|null $qid
  295.      * @return Comparison|Func|string
  296.      */
  297.     private function buildExpression(
  298.         AbstractOperatorCondition $operatorCondition,
  299.         ?string $paramName,
  300.         ?string $qid null
  301.     ) {
  302.         $expressionBuilder $this->em->createQueryBuilder()->expr();
  303.         $fieldName $this->buildFieldName($operatorCondition$paramName$qid);
  304.         switch ($operatorCondition::NAME) {
  305.             case EqualCondition::NAME:
  306.             case EmptyCondition::NAME:
  307.                 return $expressionBuilder->eq($fieldName':' $paramName);
  308.             case NotEqualCondition::NAME:
  309.             case NotEmptyCondition::NAME:
  310.                 return $expressionBuilder->neq($fieldName':' $paramName);
  311.             case GreaterThanCondition::NAME:
  312.                 return $expressionBuilder->gt($fieldName':' $paramName);
  313.             case GreaterThanEqualCondition::NAME:
  314.                 return $expressionBuilder->gte($fieldName':' $paramName);
  315.             case LessThanCondition::NAME:
  316.                 return $expressionBuilder->lt($fieldName':' $paramName);
  317.             case LessThanEqualCondition::NAME:
  318.                 return $expressionBuilder->lte($fieldName':' $paramName);
  319.             case InCondition::NAME:
  320.                 return $expressionBuilder->in($fieldName':' $paramName);
  321.             case NotInCondition::NAME:
  322.                 return $expressionBuilder->notIn($fieldName':' $paramName);
  323.             case ContainsCondition::NAME:
  324.                 // when array is empty JSON_CONTAINS returns unexpected result (returns 1 but we really want a "falsey" result)
  325.                 $value $operatorCondition->getValue();
  326.                 if (is_array($value) && empty($value)) {
  327.                     return $expressionBuilder->eq($fieldName0);
  328.                 }
  329.                 return $expressionBuilder->eq($fieldName1);
  330.             case NotContainsCondition::NAME:
  331.                 // when array is empty JSON_CONTAINS returns unexpected result (returns 1 but we really want a "falsey" result)
  332.                 $value $operatorCondition->getValue();
  333.                 if (is_array($value) && empty($value)) {
  334.                     return $expressionBuilder->neq($fieldName0);
  335.                 }
  336.                 return $expressionBuilder->neq($fieldName1);
  337.             case OverlapsCondition::NAME:
  338.                 return $expressionBuilder->eq($fieldName1);
  339.             case NotOverlapsCondition::NAME:
  340.                 return $expressionBuilder->neq($fieldName1);
  341.             case NullCondition::NAME:
  342.                 return $expressionBuilder->isNull($fieldName);
  343.             case NotNullCondition::NAME:
  344.                 return $expressionBuilder->isNotNull($fieldName);
  345.             default:
  346.                 throw new LogicException("Unsupported operator");
  347.         }
  348.     }
  349.     /**
  350.      * @param AbstractOperatorCondition $operatorCondition
  351.      * @return mixed|string|null
  352.      */
  353.     private function buildParameter(AbstractOperatorCondition $operatorCondition)
  354.     {
  355.         switch ($operatorCondition::NAME) {
  356.             case EqualCondition::NAME:
  357.             case NotEqualCondition::NAME:
  358.             case GreaterThanCondition::NAME:
  359.             case GreaterThanEqualCondition::NAME:
  360.             case LessThanCondition::NAME:
  361.             case LessThanEqualCondition::NAME:
  362.             case InCondition::NAME:
  363.             case NotInCondition::NAME:
  364.                 return $operatorCondition->getValue();
  365.             case EmptyCondition::NAME:
  366.             case NotEmptyCondition::NAME:
  367.                 return is_array($operatorCondition->getValue()) ? '';
  368.             case ContainsCondition::NAME:
  369.             case NotContainsCondition::NAME:
  370.             case OverlapsCondition::NAME:
  371.             case NotOverlapsCondition::NAME:
  372.                 if (is_array($operatorCondition->getValue())) {
  373.                     return json_encode($operatorCondition->getValue());
  374.                 }
  375.                 return '"' $operatorCondition->getValue() . '"';
  376.             case NullCondition::NAME:
  377.             case NotNullCondition::NAME:
  378.                 return null;
  379.             default:
  380.                 throw new LogicException("Unsupported operator");
  381.         }
  382.     }
  383. }