Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
234 changes: 234 additions & 0 deletions src/Doctrine/DoctrineExpressionApplier.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
<?php

declare(strict_types=1);

namespace Zhortein\DatatableBundle\Doctrine;

use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\QueryBuilder;
use Zhortein\DatatableBundle\Definition\DatatableDefinition;
use Zhortein\DatatableBundle\Filter\Expression\AdvancedFilterExpression;
use Zhortein\DatatableBundle\Filter\Expression\ComparisonOperator;
use Zhortein\DatatableBundle\Filter\Expression\Condition;
use Zhortein\DatatableBundle\Filter\Expression\ExpressionInterface;
use Zhortein\DatatableBundle\Filter\Expression\Group;
use Zhortein\DatatableBundle\Filter\Expression\LogicOperator;

final class DoctrineExpressionApplier
{
private int $parameterIndex = 0;

public function __construct(
private readonly DoctrineFieldReferenceResolver $fieldReferenceResolver = new DoctrineFieldReferenceResolver(),
private readonly DoctrineFieldMetadataResolver $fieldMetadataResolver = new DoctrineFieldMetadataResolver(),
) {
}

/**
* @param class-string $entityClass
*/
public function apply(
QueryBuilder $queryBuilder,
AdvancedFilterExpression $expression,
DatatableDefinition $definition,
EntityManagerInterface $entityManager,
string $entityClass,
): void {
$this->parameterIndex = 0;

$doctrineExpression = $this->applyExpression(
queryBuilder: $queryBuilder,
expression: $expression->root,
definition: $definition,
entityManager: $entityManager,
entityClass: $entityClass,
);

if (null !== $doctrineExpression) {
$queryBuilder->andWhere($doctrineExpression);
}
}

/**
* @param class-string $entityClass
*/
private function applyExpression(
QueryBuilder $queryBuilder,
ExpressionInterface $expression,
DatatableDefinition $definition,
EntityManagerInterface $entityManager,
string $entityClass,
): string|object|null {
if ($expression instanceof Group) {
return $this->applyGroup($queryBuilder, $expression, $definition, $entityManager, $entityClass);
}

if ($expression instanceof Condition) {
return $this->applyCondition($queryBuilder, $expression, $definition, $entityManager, $entityClass);
}

return null;
}

/**
* @param class-string $entityClass
*/
private function applyGroup(
QueryBuilder $queryBuilder,
Group $group,
DatatableDefinition $definition,
EntityManagerInterface $entityManager,
string $entityClass,
): string|object|null {
$expressions = [];

foreach ($group->children as $child) {
$doctrineExpression = $this->applyExpression($queryBuilder, $child, $definition, $entityManager, $entityClass);

if (null !== $doctrineExpression) {
$expressions[] = $doctrineExpression;
}
}

if ([] === $expressions) {
return null;
}

if (1 === count($expressions)) {
return $expressions[0];
}

/** @var array<int, \Doctrine\ORM\Query\Expr\Andx|\Doctrine\ORM\Query\Expr\Comparison|\Doctrine\ORM\Query\Expr\Func|\Doctrine\ORM\Query\Expr\Orx|string> $expressions */
return match ($group->logic) {
LogicOperator::And => $queryBuilder->expr()->andX(...$expressions),
LogicOperator::Or => $queryBuilder->expr()->orX(...$expressions),
};
}

/**
* @param class-string $entityClass
*/
private function applyCondition(
QueryBuilder $queryBuilder,
Condition $condition,
DatatableDefinition $definition,
EntityManagerInterface $entityManager,
string $entityClass,
): string|object|null {
try {
$reference = $this->fieldReferenceResolver->normalize($condition->field, $definition);
} catch (\InvalidArgumentException) {
return null;
}

if (!$this->fieldMetadataResolver->hasField($entityManager, $entityClass, $definition, $reference)) {
return null;
}

$field = $reference->toString();
$parameterName = sprintf('advanced_filter_%d', $this->parameterIndex++);

return match ($condition->operator) {
ComparisonOperator::Equals => $this->createEqExpression($queryBuilder, $field, $parameterName, $condition->value),
ComparisonOperator::NotEquals => $this->createNeqExpression($queryBuilder, $field, $parameterName, $condition->value),
ComparisonOperator::GreaterThan => $this->createGtExpression($queryBuilder, $field, $parameterName, $condition->value),
ComparisonOperator::GreaterThanOrEquals => $this->createGteExpression($queryBuilder, $field, $parameterName, $condition->value),
ComparisonOperator::LessThan => $this->createLtExpression($queryBuilder, $field, $parameterName, $condition->value),
ComparisonOperator::LessThanOrEquals => $this->createLteExpression($queryBuilder, $field, $parameterName, $condition->value),
ComparisonOperator::In => $this->createInExpression($queryBuilder, $field, $parameterName, $condition->value),
ComparisonOperator::NotIn => $this->createNotInExpression($queryBuilder, $field, $parameterName, $condition->value),
ComparisonOperator::IsNull => $queryBuilder->expr()->isNull($field),
ComparisonOperator::IsNotNull => $queryBuilder->expr()->isNotNull($field),
ComparisonOperator::Between => $this->createBetweenExpression($queryBuilder, $field, $parameterName, $condition->value),
ComparisonOperator::StartsWith => $this->createLikeExpression($queryBuilder, $field, $parameterName, (is_scalar($condition->value) ? (string) $condition->value : '').'%'),
ComparisonOperator::EndsWith => $this->createLikeExpression($queryBuilder, $field, $parameterName, '%'.(is_scalar($condition->value) ? (string) $condition->value : '')),
ComparisonOperator::Contains => $this->createLikeExpression($queryBuilder, $field, $parameterName, '%'.(is_scalar($condition->value) ? (string) $condition->value : '').'%'),
ComparisonOperator::NotContains => $this->createNotLikeExpression($queryBuilder, $field, $parameterName, '%'.(is_scalar($condition->value) ? (string) $condition->value : '').'%'),
};
}

private function createEqExpression(QueryBuilder $queryBuilder, string $field, string $parameterName, mixed $value): object
{
$queryBuilder->setParameter($parameterName, $value);

return $queryBuilder->expr()->eq($field, ':'.$parameterName);
}

private function createNeqExpression(QueryBuilder $queryBuilder, string $field, string $parameterName, mixed $value): object
{
$queryBuilder->setParameter($parameterName, $value);

return $queryBuilder->expr()->neq($field, ':'.$parameterName);
}

private function createGtExpression(QueryBuilder $queryBuilder, string $field, string $parameterName, mixed $value): object
{
$queryBuilder->setParameter($parameterName, $value);

return $queryBuilder->expr()->gt($field, ':'.$parameterName);
}

private function createGteExpression(QueryBuilder $queryBuilder, string $field, string $parameterName, mixed $value): object
{
$queryBuilder->setParameter($parameterName, $value);

return $queryBuilder->expr()->gte($field, ':'.$parameterName);
}

private function createLtExpression(QueryBuilder $queryBuilder, string $field, string $parameterName, mixed $value): object
{
$queryBuilder->setParameter($parameterName, $value);

return $queryBuilder->expr()->lt($field, ':'.$parameterName);
}

private function createLteExpression(QueryBuilder $queryBuilder, string $field, string $parameterName, mixed $value): object
{
$queryBuilder->setParameter($parameterName, $value);

return $queryBuilder->expr()->lte($field, ':'.$parameterName);
}

private function createInExpression(QueryBuilder $queryBuilder, string $field, string $parameterName, mixed $value): object
{
$queryBuilder->setParameter($parameterName, $value);

return $queryBuilder->expr()->in($field, ':'.$parameterName);
}

private function createNotInExpression(QueryBuilder $queryBuilder, string $field, string $parameterName, mixed $value): object
{
$queryBuilder->setParameter($parameterName, $value);

return $queryBuilder->expr()->notIn($field, ':'.$parameterName);
}

private function createBetweenExpression(QueryBuilder $queryBuilder, string $field, string $parameterName, mixed $value): ?string
{
if (!is_array($value) || 2 !== count($value)) {
return null;
}

$startParam = $parameterName.'_start';
$endParam = $parameterName.'_end';

$queryBuilder->setParameter($startParam, $value[0]);
$queryBuilder->setParameter($endParam, $value[1]);

return $queryBuilder->expr()->between($field, ':'.$startParam, ':'.$endParam);
}

private function createLikeExpression(QueryBuilder $queryBuilder, string $field, string $parameterName, string $pattern): object
{
$queryBuilder->setParameter($parameterName, $pattern);

return $queryBuilder->expr()->like(sprintf('LOWER(%s)', $field), sprintf('LOWER(:%s)', $parameterName));
}

private function createNotLikeExpression(QueryBuilder $queryBuilder, string $field, string $parameterName, string $pattern): object
{
$queryBuilder->setParameter($parameterName, $pattern);

return $queryBuilder->expr()->notLike(sprintf('LOWER(%s)', $field), sprintf('LOWER(:%s)', $parameterName));
}
}
41 changes: 40 additions & 1 deletion src/Provider/DoctrineOrmDataProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Zhortein\DatatableBundle\Definition\FilterDefinition;
use Zhortein\DatatableBundle\Definition\UserFilterDefinition;
use Zhortein\DatatableBundle\Doctrine\DoctrineCountExpressionFactory;
use Zhortein\DatatableBundle\Doctrine\DoctrineExpressionApplier;
use Zhortein\DatatableBundle\Doctrine\DoctrineFieldMetadataResolver;
use Zhortein\DatatableBundle\Doctrine\DoctrineFieldReferenceResolver;
use Zhortein\DatatableBundle\Doctrine\DoctrineJoinApplier;
Expand All @@ -39,19 +40,26 @@

private DoctrineCountExpressionFactory $countExpressionFactory;

private DoctrineExpressionApplier $expressionApplier;

public function __construct(
private ManagerRegistry $managerRegistry,
?DoctrineFieldReferenceResolver $fieldReferenceResolver = null,
?DoctrineFieldMetadataResolver $fieldMetadataResolver = null,
?DoctrineJoinApplier $joinApplier = null,
?DoctrinePaginationApplier $paginationApplier = null,
?DoctrineCountExpressionFactory $countExpressionFactory = null,
?DoctrineExpressionApplier $expressionApplier = null,
) {
$this->fieldReferenceResolver = $fieldReferenceResolver ?? new DoctrineFieldReferenceResolver();
$this->fieldMetadataResolver = $fieldMetadataResolver ?? new DoctrineFieldMetadataResolver();
$this->joinApplier = $joinApplier ?? new DoctrineJoinApplier();
$this->paginationApplier = $paginationApplier ?? new DoctrinePaginationApplier();
$this->countExpressionFactory = $countExpressionFactory ?? new DoctrineCountExpressionFactory();
$this->expressionApplier = $expressionApplier ?? new DoctrineExpressionApplier(
fieldReferenceResolver: $this->fieldReferenceResolver,
fieldMetadataResolver: $this->fieldMetadataResolver,
);
}

public function supports(DatatableDefinition $definition): bool
Expand All @@ -72,7 +80,7 @@ public function getData(DatatableDefinition $definition, DatatableRequest $reque

$rows = $this->loadRows($entityManager, $entityClass, $selectedColumns, $definition, $request);
$totalItems = $this->countRows($entityManager, $entityClass, $definition);
$filteredItems = $request->hasSearchQuery() || $request->hasFilters()
$filteredItems = $request->hasSearchQuery() || $request->hasFilters() || $request->hasAdvancedFilters()
? $this->countRows($entityManager, $entityClass, $definition, $request)
: $totalItems;

Expand Down Expand Up @@ -163,6 +171,7 @@ private function loadRows(

$this->applyPermanentFilters($queryBuilder, $definition);
$this->applyUserFilters($queryBuilder, $entityManager, $entityClass, $definition, $request);
$this->applyAdvancedFilters($queryBuilder, $entityManager, $entityClass, $definition, $request);
$this->applySearch($queryBuilder, $entityManager, $entityClass, $definition, $request);
$this->applySorting($queryBuilder, $entityManager, $entityClass, $definition, $request);

Expand Down Expand Up @@ -199,6 +208,7 @@ private function countRows(

if (null !== $request) {
$this->applyUserFilters($queryBuilder, $entityManager, $entityClass, $definition, $request);
$this->applyAdvancedFilters($queryBuilder, $entityManager, $entityClass, $definition, $request);
$this->applySearch($queryBuilder, $entityManager, $entityClass, $definition, $request);
}

Expand Down Expand Up @@ -318,6 +328,35 @@ private function applyUserFilters(
}
}

/**
* @param class-string $entityClass
*/
private function applyAdvancedFilters(
QueryBuilder $queryBuilder,
EntityManagerInterface $entityManager,
string $entityClass,
DatatableDefinition $definition,
DatatableRequest $request,
): void {
if (!$request->hasAdvancedFilters()) {
return;
}

$expression = $request->getAdvancedFilterExpression();

if (null === $expression) {
return;
}

$this->expressionApplier->apply(
queryBuilder: $queryBuilder,
expression: $expression,
definition: $definition,
entityManager: $entityManager,
entityClass: $entityClass,
);
}

/**
* @param class-string $entityClass
*/
Expand Down
Loading
Loading