Implemented a filter constraint for entities

This commit is contained in:
Jan Böhmer 2022-08-21 01:34:17 +02:00
parent 0bc9d8cba1
commit c9151c65ba
13 changed files with 810 additions and 27 deletions

View file

@ -51,6 +51,7 @@ use App\Entity\Parts\Storelocation;
use App\Entity\Parts\Supplier;
use App\Form\Filters\PartFilterType;
use App\Services\Parts\PartsTableActionHandler;
use App\Services\Trees\NodesListBuilder;
use Doctrine\ORM\EntityManagerInterface;
use Omines\DataTablesBundle\DataTableFactory;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@ -260,7 +261,6 @@ class PartListsController extends AbstractController
return $this->render('Parts/lists/search_list.html.twig', [
'datatable' => $table,
'keyword' => $search,
'filterForm' => $filterForm->createView()
]);
}
@ -269,12 +269,12 @@ class PartListsController extends AbstractController
*
* @return JsonResponse|Response
*/
public function showAll(Request $request, DataTableFactory $dataTable)
public function showAll(Request $request, DataTableFactory $dataTable, NodesListBuilder $nodesListBuilder)
{
$formRequest = clone $request;
$formRequest->setMethod('GET');
$filter = new PartFilter();
$filter = new PartFilter($nodesListBuilder);
$filterForm = $this->createForm(PartFilterType::class, $filter, ['method' => 'GET']);
$filterForm->handleRequest($formRequest);

View file

@ -0,0 +1,180 @@
<?php
namespace App\DataTables\Filters\Constraints;
use App\Entity\Base\AbstractDBElement;
use App\Entity\Base\AbstractStructuralDBElement;
use App\Services\Trees\NodesListBuilder;
use Doctrine\ORM\QueryBuilder;
/**
* @template T
*/
class EntityConstraint extends AbstractConstraint
{
private const ALLOWED_OPERATOR_VALUES_BASE = ['=', '!='];
private const ALLOWED_OPERATOR_VALUES_STRUCTURAL = ['INCLUDING_CHILDREN', 'EXCLUDING_CHILDREN'];
/**
* @var
*/
protected $nodesListBuilder;
/**
* @var class-string<T> The class to use for the comparison
*/
protected $class;
/**
* @var string|null The operator to use
*/
protected $operator;
/**
* @var T The value to compare to
*/
protected $value;
/**
* @param NodesListBuilder $nodesListBuilder
* @param class-string<T> $class
* @param string $property
* @param string|null $identifier
* @param $value
* @param string $operator
*/
public function __construct(NodesListBuilder $nodesListBuilder, string $class, string $property, string $identifier = null, $value = null, string $operator = '')
{
$this->nodesListBuilder = $nodesListBuilder;
$this->class = $class;
parent::__construct($property, $identifier);
$this->value = $value;
$this->operator = $operator;
}
public function getClass(): string
{
return $this->class;
}
/**
* @return string|null
*/
public function getOperator(): ?string
{
return $this->operator;
}
/**
* @param string|null $operator
*/
public function setOperator(?string $operator): self
{
$this->operator = $operator;
return $this;
}
/**
* @return mixed|null
*/
public function getValue(): ?AbstractDBElement
{
return $this->value;
}
/**
* @param T|null $value
*/
public function setValue(?AbstractDBElement $value): void
{
if (!$value instanceof $this->class) {
throw new \InvalidArgumentException('The value must be an instance of ' . $this->class);
}
$this->value = $value;
}
/**
* Checks whether the constraints apply to a structural type or not
* @return bool
*/
public function isStructural(): bool
{
return is_subclass_of($this->class, AbstractStructuralDBElement::class);
}
/**
* Returns a list of operators which are allowed with the given class.
* @return string[]
*/
public function getAllowedOperatorValues(): array
{
//Base operators are allowed for everything
$tmp = self::ALLOWED_OPERATOR_VALUES_BASE;
if ($this->isStructural()) {
$tmp = array_merge($tmp, self::ALLOWED_OPERATOR_VALUES_STRUCTURAL);
}
return $tmp;
}
public function isEnabled(): bool
{
return !empty($this->operator);
}
public function apply(QueryBuilder $queryBuilder): void
{
//If no value is provided then we do not apply a filter
if (!$this->isEnabled()) {
return;
}
//Ensure we have an valid operator
if(!in_array($this->operator, $this->getAllowedOperatorValues(), true)) {
throw new \RuntimeException('Invalid operator '. $this->operator . ' provided. Valid operators are '. implode(', ', $this->getAllowedOperatorValues()));
}
//We need to handle null values differently, as they can not be compared with == or !=
if ($this->value === null) {
if($this->operator === '=' || $this->operator === 'INCLUDING_CHILDREN') {
$queryBuilder->andWhere(sprintf("%s IS NULL", $this->property));
return;
}
if ($this->operator === '!=' || $this->operator === 'EXCLUDING_CHILDREN') {
$queryBuilder->andWhere(sprintf("%s IS NOT NULL", $this->property));
return;
}
throw new \RuntimeException('Unknown operator '. $this->operator . ' provided. Valid operators are '. implode(', ', $this->getAllowedOperatorValues()));
}
if($this->operator === '=' || $this->operator === '!=') {
$this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier, $this->operator, $this->value);
return;
}
//Otherwise retrieve the children list and apply the operator to it
if($this->isStructural()) {
$list = $this->nodesListBuilder->getChildrenFlatList($this->value);
//Add the element itself to the list
$list[] = $this->value;
if ($this->operator === 'INCLUDING_CHILDREN') {
$this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier, 'IN', $list);
return;
}
if ($this->operator === 'EXCLUDING_CHILDREN') {
$this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier, 'NOT IN', $list);
return;
}
} else {
throw new \RuntimeException('Cannot apply operator '. $this->operator . ' to non-structural type');
}
}
}

View file

@ -27,7 +27,7 @@ trait FilterTrait
*/
protected function addSimpleAndConstraint(QueryBuilder $queryBuilder, string $property, string $parameterIdentifier, string $comparison_operator, $value): void
{
$queryBuilder->andWhere(sprintf("%s %s :%s", $property, $comparison_operator, $parameterIdentifier));
$queryBuilder->andWhere(sprintf("%s %s (:%s)", $property, $comparison_operator, $parameterIdentifier));
$queryBuilder->setParameter($parameterIdentifier, $value);
}
}

View file

@ -76,7 +76,7 @@ class NumberConstraint extends AbstractConstraint
}
public function __construct(string $property, string $identifier = null, $value1 = null, $operator = null, $value2 = null)
public function __construct(string $property, string $identifier = null, $value1 = null, string $operator = null, $value2 = null)
{
parent::__construct($property, $identifier);
$this->value1 = $value1;
@ -93,7 +93,7 @@ class NumberConstraint extends AbstractConstraint
public function apply(QueryBuilder $queryBuilder): void
{
//If no value is provided then we do not apply a filter
if ($this->value1 === null) {
if (!$this->isEnabled()) {
return;
}

View file

@ -19,7 +19,7 @@ class TextConstraint extends AbstractConstraint
*/
protected $value;
public function __construct(string $property, string $identifier = null, $value = null, $operator = '')
public function __construct(string $property, string $identifier = null, $value = null, string $operator = '')
{
parent::__construct($property, $identifier);
$this->value = $value;

View file

@ -4,8 +4,11 @@ namespace App\DataTables\Filters;
use App\DataTables\Filters\Constraints\BooleanConstraint;
use App\DataTables\Filters\Constraints\DateTimeConstraint;
use App\DataTables\Filters\Constraints\EntityConstraint;
use App\DataTables\Filters\Constraints\NumberConstraint;
use App\DataTables\Filters\Constraints\TextConstraint;
use App\Entity\Parts\Category;
use App\Services\Trees\NodesListBuilder;
use Doctrine\ORM\QueryBuilder;
class PartFilter implements FilterInterface
@ -34,6 +37,28 @@ class PartFilter implements FilterInterface
/** @var DateTimeConstraint */
protected $addedDate;
/** @var EntityConstraint */
protected $category;
public function __construct(NodesListBuilder $nodesListBuilder)
{
$this->favorite =
$this->needsReview = new BooleanConstraint('part.needs_review');
$this->mass = new NumberConstraint('part.mass');
$this->name = new TextConstraint('part.name');
$this->description = new TextConstraint('part.description');
$this->addedDate = new DateTimeConstraint('part.addedDate');
$this->lastModified = new DateTimeConstraint('part.lastModified');
$this->category = new EntityConstraint($nodesListBuilder, Category::class, 'part.category');
}
public function apply(QueryBuilder $queryBuilder): void
{
$this->applyAllChildFilters($queryBuilder);
}
/**
* @return BooleanConstraint|false
*/
@ -81,21 +106,8 @@ class PartFilter implements FilterInterface
return $this->addedDate;
}
public function __construct()
public function getCategory(): EntityConstraint
{
$this->favorite =
$this->needsReview = new BooleanConstraint('part.needs_review');
$this->mass = new NumberConstraint('part.mass');
$this->name = new TextConstraint('part.name');
$this->description = new TextConstraint('part.description');
$this->addedDate = new DateTimeConstraint('part.addedDate');
$this->lastModified = new DateTimeConstraint('part.lastModified');
}
public function apply(QueryBuilder $queryBuilder): void
{
$this->applyAllChildFilters($queryBuilder);
return $this->category;
}
}

View file

@ -0,0 +1,55 @@
<?php
namespace App\Form\Filters\Constraints;
use App\DataTables\Filters\Constraints\EntityConstraint;
use App\Form\Type\StructuralEntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
class StructuralEntityConstraintType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'compound' => true,
'data_class' => EntityConstraint::class,
'text_suffix' => '', // An suffix which is attached as text-append to the input group. This can for example be used for units
]);
$resolver->setRequired('entity_class');
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$choices = [
'' => '',
'filter.entity_constraint.operator.EQ' => '=',
'filter.entity_constraint.operator.NEQ' => '!=',
'filter.entity_constraint.operator.INCLUDING_CHILDREN' => 'INCLUDING_CHILDREN',
'filter.entity_constraint.operator.EXCLUDING_CHILDREN' => 'EXCLUDING_CHILDREN',
];
$builder->add('value', StructuralEntityType::class, [
'class' => $options['entity_class'],
'required' => false,
]);
$builder->add('operator', ChoiceType::class, [
'label' => 'filter.text_constraint.operator',
'choices' => $choices,
'required' => false,
]);
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
parent::buildView($view, $form, $options);
$view->vars['text_suffix'] = $options['text_suffix'];
}
}

View file

@ -3,9 +3,11 @@
namespace App\Form\Filters;
use App\DataTables\Filters\PartFilter;
use App\Entity\Parts\Category;
use App\Form\Filters\Constraints\BooleanConstraintType;
use App\Form\Filters\Constraints\DateTimeConstraintType;
use App\Form\Filters\Constraints\NumberConstraintType;
use App\Form\Filters\Constraints\StructuralEntityConstraintType;
use App\Form\Filters\Constraints\TextConstraintType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
@ -27,6 +29,11 @@ class PartFilterType extends AbstractType
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('category', StructuralEntityConstraintType::class, [
'label' => 'part.edit.category',
'entity_class' => Category::class
]);
$builder->add('favorite', BooleanConstraintType::class, [
'label' => 'part.edit.is_favorite'
]);

View file

@ -84,10 +84,20 @@ class NodesListBuilder
return $this->cache->get($key, function (ItemInterface $item) use ($class_name, $parent, $secure_class_name) {
// Invalidate when groups, a element with the class or the user changes
$item->tag(['groups', 'tree_list', $this->keyGenerator->generateKey(), $secure_class_name]);
/** @var StructuralDBElementRepository */
$repo = $this->em->getRepository($class_name);
return $repo->toNodesList($parent);
return $this->em->getRepository($class_name)->toNodesList($parent);
});
}
/**
* Returns a flattened list of all (recursive) children elements of the given AbstractStructuralDBElement.
* The value is cached for performance reasons.
*
* @template T
* @param T $element
* @return T[]
*/
public function getChildrenFlatList(AbstractStructuralDBElement $element): array
{
return $this->typeToNodesList(get_class($element), $element);
}
}