Allow to filter parameters by their (numeric) value

This commit is contained in:
Jan Böhmer 2022-09-08 00:04:53 +02:00
parent b56a970d5b
commit dd400ae70c
8 changed files with 288 additions and 23 deletions

View file

@ -22,6 +22,9 @@ class ParameterConstraint extends AbstractConstraint
/** @var TextConstraint */
protected $value_text;
/** @var ParameterValueConstraint */
protected $value;
/** @var string The alias to use for the subquery */
protected $alias;
@ -33,6 +36,7 @@ class ParameterConstraint extends AbstractConstraint
$this->alias = uniqid('param_', false);
$this->value_text = new TextConstraint($this->alias . '.value_text');
$this->value = new ParameterValueConstraint($this->alias );
}
public function isEnabled(): bool
@ -71,6 +75,7 @@ class ParameterConstraint extends AbstractConstraint
//Apply all subfilters
$this->value_text->apply($subqb);
$this->value->apply($subqb);
//Copy all parameters from the subquery to the main query
//We can not use setParameters here, as this would override the exiting paramaters in queryBuilder
@ -144,14 +149,12 @@ class ParameterConstraint extends AbstractConstraint
}
/**
* DO NOT USE THIS SETTER!
* This is just a workaround for collectionType behavior
* @param $value
* @return $this
* @return ParameterValueConstraint
*/
/*public function setValueText($value): self
public function getValue(): ParameterValueConstraint
{
//Do not really set the value here, as we dont want to override the constraint created in the constructor
return $this;
}*/
return $this->value;
}
}

View file

@ -0,0 +1,130 @@
<?php
namespace App\DataTables\Filters\Constraints\Part;
use App\DataTables\Filters\Constraints\NumberConstraint;
use Doctrine\ORM\QueryBuilder;
class ParameterValueConstraint extends NumberConstraint
{
protected $alias;
public const ALLOWED_OPERATOR_VALUES = ['=', '!=', '<', '>', '<=', '>=', 'BETWEEN',
//Additional operators
'IN_RANGE', 'NOT_IN_RANGE', 'GREATER_THAN_RANGE', 'GREATER_EQUAL_RANGE', 'LESS_THAN_RANGE', 'LESS_EQUAL_RANGE', 'RANGE_IN_RANGE', 'RANGE_INTERSECT_RANGE'];
/**
* @param string $alias The alias which is used in the sub query of ParameterConstraint
*/
public function __construct(string $alias) {
$this->alias = $alias;
parent::__construct($alias . '.value_typical');
}
public function apply(QueryBuilder $queryBuilder): void
{
//Skip if not enabled
if(!$this->isEnabled()) {
return;
}
$paramName1 = $this->generateParameterIdentifier('value1');
$paramName2 = $this->generateParameterIdentifier('value2');
if ($this->operator === 'IN_RANGE') {
$queryBuilder->andWhere(
"({$this->alias}.value_min <= :{$paramName1} AND {$this->alias}.value_max >= :{$paramName1}) OR
({$this->alias}.value_typical = :{$paramName1})"
);
$queryBuilder->setParameter($paramName1, $this->value1);
return;
}
if ($this->operator === 'NOT_IN_RANGE') {
$queryBuilder->andWhere(
"({$this->alias}.value_min > :{$paramName1} OR {$this->alias}.value_max < :{$paramName1}) AND
({$this->alias}.value_typical IS NULL OR {$this->alias}.value_typical != :{$paramName1})"
);
$queryBuilder->setParameter($paramName1, $this->value1);
return;
}
if ($this->operator === 'GREATER_THAN_RANGE') {
$queryBuilder->andWhere(
"{$this->alias}.value_max < :{$paramName1} OR {$this->alias}.value_typical < :{$paramName1}"
);
$queryBuilder->setParameter($paramName1, $this->value1);
return;
}
if ($this->operator === 'GREATER_EQUAL_RANGE') {
$queryBuilder->andWhere(
"{$this->alias}.value_max <= :{$paramName1} OR {$this->alias}.value_typical <= :{$paramName1}"
);
$queryBuilder->setParameter($paramName1, $this->value1);
return;
}
if ($this->operator === 'LESS_THAN_RANGE') {
$queryBuilder->andWhere(
"{$this->alias}.value_min > :{$paramName1} OR {$this->alias}.value_typical > :{$paramName1}"
);
$queryBuilder->setParameter($paramName1, $this->value1);
return;
}
if ($this->operator === 'LESS_EQUAL_RANGE') {
$queryBuilder->andWhere(
"{$this->alias}.value_min >= :{$paramName1} OR {$this->alias}.value_typical >= :{$paramName1}"
);
$queryBuilder->setParameter($paramName1, $this->value1);
return;
}
// This operator means the constraint range must lie completely within the parameter value range
if ($this->operator === 'RANGE_IN_RANGE') {
$queryBuilder->andWhere(
"({$this->alias}.value_min <= :{$paramName1} AND {$this->alias}.value_max >= :{$paramName2}) OR
({$this->alias}.value_typical >= :{$paramName1} AND {$this->alias}.value_typical <= :{$paramName2})"
);
$queryBuilder->setParameter($paramName1, $this->value1);
$queryBuilder->setParameter($paramName2, $this->value2);
return;
}
if ($this->operator === 'RANGE_INTERSECT_RANGE') {
$queryBuilder->andWhere(
//The ORs are important here!!
"({$this->alias}.value_min <= :{$paramName1} OR {$this->alias}.value_min <= :{$paramName2}) OR
({$this->alias}.value_max >= :{$paramName1} OR {$this->alias}.value_max >= :{$paramName2}) OR
({$this->alias}.value_typical >= :{$paramName1} AND {$this->alias}.value_typical <= :{$paramName2})"
);
$queryBuilder->setParameter($paramName1, $this->value1);
$queryBuilder->setParameter($paramName2, $this->value2);
return;
}
//For all other cases use the default implementation
parent::apply($queryBuilder);
}
}

View file

@ -13,6 +13,17 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
class NumberConstraintType extends AbstractType
{
protected const CHOICES = [
'' => '',
'=' => '=',
'!=' => '!=',
'<' => '<',
'>' => '>',
'<=' => '<=',
'>=' => '>=',
'filter.number_constraint.value.operator.BETWEEN' => 'BETWEEN',
];
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
@ -31,17 +42,6 @@ class NumberConstraintType extends AbstractType
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$choices = [
'' => '',
'=' => '=',
'!=' => '!=',
'<' => '<',
'>' => '>',
'<=' => '<=',
'>=' => '>=',
'filter.number_constraint.value.operator.BETWEEN' => 'BETWEEN',
];
$builder->add('value1', NumberType::class, array_merge_recursive([
'label' => 'filter.number_constraint.value1',
'attr' => [
@ -68,7 +68,7 @@ class NumberConstraintType extends AbstractType
$builder->add('operator', ChoiceType::class, [
'label' => 'filter.number_constraint.operator',
'choices' => $choices,
'choices' => static::CHOICES,
'required' => false,
]);
}

View file

@ -41,6 +41,9 @@ class ParameterConstraintType extends AbstractType
//'required' => false,
] );
$builder->add('value', ParameterValueConstraintType::class, [
]);
/*
* I am not quite sure why this is needed, but somehow symfony tries to create a new instance of TextConstraint
* instead of using the existing one for the prototype (or the one from empty data). This fails as the constructor of TextConstraint requires

View file

@ -0,0 +1,33 @@
<?php
namespace App\Form\Filters\Constraints;
class ParameterValueConstraintType extends NumberConstraintType
{
protected const CHOICES = [
'' => '',
'filter.parameter_value_constraint.operator.=' => '=',
'filter.parameter_value_constraint.operator.!=' => '!=',
'filter.parameter_value_constraint.operator.<' => '<',
'filter.parameter_value_constraint.operator.>' => '>',
'filter.parameter_value_constraint.operator.<=' => '<=',
'filter.parameter_value_constraint.operator.>=' => '>=',
'filter.parameter_value_constraint.operator.BETWEEN' => 'BETWEEN',
//Extensions by ParameterValueConstraint
'filter.parameter_value_constraint.operator.IN_RANGE' => 'IN_RANGE',
'filter.parameter_value_constraint.operator.NOT_IN_RANGE' => 'NOT_IN_RANGE',
'filter.parameter_value_constraint.operator.GREATER_THAN_RANGE' => 'GREATER_THAN_RANGE',
'filter.parameter_value_constraint.operator.GREATER_EQUAL_RANGE' => 'GREATER_EQUAL_RANGE',
'filter.parameter_value_constraint.operator.LESS_THAN_RANGE' => 'LESS_THAN_RANGE',
'filter.parameter_value_constraint.operator.LESS_EQUAL_RANGE' => 'LESS_EQUAL_RANGE',
'filter.parameter_value_constraint.operator.RANGE_IN_RANGE' => 'RANGE_IN_RANGE',
'filter.parameter_value_constraint.operator.RANGE_INTERSECT_RANGE' => 'RANGE_INTERSECT_RANGE'
];
public function getParent(): string
{
return NumberConstraintType::class;
}
}