Added an basic TextConstraint for part filtering.

This commit is contained in:
Jan Böhmer 2022-08-18 00:00:54 +02:00
parent f6239dfd50
commit f8562f9622
9 changed files with 234 additions and 7 deletions

View file

@ -5,7 +5,7 @@ namespace App\DataTables\Filters\Constraints;
use App\DataTables\Filters\FilterInterface;
use Doctrine\ORM\QueryBuilder;
abstract class AbstractSimpleConstraint implements FilterInterface
abstract class AbstractConstraint implements FilterInterface
{
use FilterTrait;
@ -20,6 +20,12 @@ abstract class AbstractSimpleConstraint implements FilterInterface
protected $identifier;
/**
* Determines whether this constraint is active or not. This should be decided accordingly to the value of the constraint
* @return bool True if the constraint is active, false otherwise
*/
abstract public function isEnabled(): bool;
public function __construct(string $property, string $identifier = null)
{
$this->property = $property;

View file

@ -5,7 +5,7 @@ namespace App\DataTables\Filters\Constraints;
use App\DataTables\Filters\FilterInterface;
use Doctrine\ORM\QueryBuilder;
class BooleanConstraint extends AbstractSimpleConstraint
class BooleanConstraint extends AbstractConstraint
{
/** @var bool|null The value of our constraint */
protected $value;
@ -34,12 +34,16 @@ class BooleanConstraint extends AbstractSimpleConstraint
$this->value = $value;
}
public function isEnabled(): bool
{
return $this->value !== null;
}
public function apply(QueryBuilder $queryBuilder): void
{
//Do not apply a filter if value is null (filter is set to ignore)
if($this->value === null) {
if(!$this->isEnabled()) {
return;
}

View file

@ -5,7 +5,7 @@ namespace App\DataTables\Filters\Constraints;
use Doctrine\ORM\QueryBuilder;
use \RuntimeException;
class NumberConstraint extends AbstractSimpleConstraint
class NumberConstraint extends AbstractConstraint
{
public const ALLOWED_OPERATOR_VALUES = ['=', '!=', '<', '>', '<=', '>=', 'BETWEEN'];
@ -62,7 +62,7 @@ class NumberConstraint extends AbstractSimpleConstraint
/**
* @return mixed|string
*/
public function getOperator()
public function getOperator(): string
{
return $this->operator;
}
@ -70,7 +70,7 @@ class NumberConstraint extends AbstractSimpleConstraint
/**
* @param mixed|string $operator
*/
public function setOperator($operator): void
public function setOperator(?string $operator): void
{
$this->operator = $operator;
}
@ -84,6 +84,12 @@ class NumberConstraint extends AbstractSimpleConstraint
$this->operator = $operator;
}
public function isEnabled(): bool
{
return $this->value1 !== null
&& !empty($this->operator);
}
public function apply(QueryBuilder $queryBuilder): void
{
//If no value is provided then we do not apply a filter
@ -93,7 +99,7 @@ class NumberConstraint extends AbstractSimpleConstraint
//Ensure we have an valid operator
if(!in_array($this->operator, self::ALLOWED_OPERATOR_VALUES, true)) {
throw new \InvalidArgumentException('Invalid operator '. $this->operator . ' provided. Valid operators are '. implode(', ', self::ALLOWED_OPERATOR_VALUES));
throw new \RuntimeException('Invalid operator '. $this->operator . ' provided. Valid operators are '. implode(', ', self::ALLOWED_OPERATOR_VALUES));
}
if ($this->operator !== 'BETWEEN') {

View file

@ -0,0 +1,109 @@
<?php
namespace App\DataTables\Filters\Constraints;
use Doctrine\ORM\QueryBuilder;
class TextConstraint extends AbstractConstraint
{
public const ALLOWED_OPERATOR_VALUES = ['=', '!=', 'STARTS', 'ENDS', 'CONTAINS', 'LIKE', 'REGEX'];
/**
* @var string|null The operator to use
*/
protected $operator;
/**
* @var string The value to compare to
*/
protected $value;
public function __construct(string $property, string $identifier = null, $value = null, $operator = '')
{
parent::__construct($property, $identifier);
$this->value = $value;
$this->operator = $operator;
}
/**
* @return string
*/
public function getOperator(): ?string
{
return $this->operator;
}
/**
* @param string $operator
*/
public function setOperator(?string $operator): self
{
$this->operator = $operator;
return $this;
}
/**
* @return string
*/
public function getValue(): string
{
return $this->value;
}
/**
* @param string $value
*/
public function setValue(string $value): self
{
$this->value = $value;
return $this;
}
public function isEnabled(): bool
{
return $this->value !== null
&& !empty($this->operator);
}
public function apply(QueryBuilder $queryBuilder): void
{
if(!$this->isEnabled()) {
return;
}
if(!in_array($this->operator, self::ALLOWED_OPERATOR_VALUES, true)) {
throw new \RuntimeException('Invalid operator '. $this->operator . ' provided. Valid operators are '. implode(', ', self::ALLOWED_OPERATOR_VALUES));
}
//Equal and not equal can be handled easily
if($this->operator === '=' || $this->operator === '!=') {
$this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier, $this->operator, $this->value);
return;
}
//The CONTAINS, LIKE, STARTS and ENDS operators use the LIKE operator but we have to build the value string differently
$like_value = null;
if ($this->operator === 'LIKE') {
$like_value = $this->value;
} else if ($this->operator === 'STARTS') {
$like_value = $this->value . '%';
} else if ($this->operator === 'ENDS') {
$like_value = '%' . $this->value;
} else if ($this->operator === 'CONTAINS') {
$like_value = '%' . $this->value . '%';
}
if ($like_value !== null) {
$this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier, 'LIKE', $like_value);
return;
}
//Regex is only supported on MySQL and needs a special function
if ($this->operator === 'REGEX') {
$queryBuilder->andWhere(sprintf('REGEXP(%s, :%s) = 1', $this->property, $this->identifier));
$queryBuilder->setParameter($this->identifier, $this->value);
}
}
}

View file

@ -4,10 +4,17 @@ namespace App\DataTables\Filters;
use App\DataTables\Filters\Constraints\BooleanConstraint;
use App\DataTables\Filters\Constraints\NumberConstraint;
use App\DataTables\Filters\Constraints\TextConstraint;
use Doctrine\ORM\QueryBuilder;
class PartFilter implements FilterInterface
{
/** @var TextConstraint */
protected $name;
/** @var TextConstraint */
protected $description;
/** @var BooleanConstraint */
protected $favorite;
@ -38,11 +45,23 @@ class PartFilter implements FilterInterface
return $this->mass;
}
public function getName(): TextConstraint
{
return $this->name;
}
public function getDescription(): TextConstraint
{
return $this->description;
}
public function __construct()
{
$this->favorite = new BooleanConstraint('part.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');
}
public function apply(QueryBuilder $queryBuilder): void
@ -50,5 +69,7 @@ class PartFilter implements FilterInterface
$this->favorite->apply($queryBuilder);
$this->needsReview->apply($queryBuilder);
$this->mass->apply($queryBuilder);
$this->name->apply($queryBuilder);
$this->description->apply($queryBuilder);
}
}

View file

@ -32,6 +32,7 @@ class NumberConstraintType extends AbstractType
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$choices = [
'' => '',
'=' => '=',
'!=' => '!=',
'<' => '<',
@ -68,6 +69,7 @@ class NumberConstraintType extends AbstractType
$builder->add('operator', ChoiceType::class, [
'label' => 'filter.number_constraint.operator',
'choices' => $choices,
'required' => false,
]);
}

View file

@ -0,0 +1,60 @@
<?php
namespace App\Form\Filters\Constraints;
use App\DataTables\Filters\Constraints\TextConstraint;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\Extension\Core\Type\SearchType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
class TextConstraintType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'compound' => true,
'data_class' => TextConstraint::class,
'text_suffix' => '', // An suffix which is attached as text-append to the input group. This can for example be used for units
]);
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$choices = [
'' => '',
'=' => '=',
'!=' => '!=',
'STARTS' => 'STARTS',
'ENDS' => 'ENDS',
'CONTAINS' => 'CONTAINS',
'LIKE' => 'LIKE',
'REGEX' => 'REGEX',
];
$builder->add('value', SearchType::class, [
'attr' => [
'placeholder' => 'filter.text_constraint.value',
],
'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

@ -5,6 +5,7 @@ namespace App\Form\Filters;
use App\DataTables\Filters\PartFilter;
use App\Form\Filters\Constraints\BooleanConstraintType;
use App\Form\Filters\Constraints\NumberConstraintType;
use App\Form\Filters\Constraints\TextConstraintType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
@ -38,6 +39,14 @@ class PartFilterType extends AbstractType
'min' => 0,
]);
$builder->add('name', TextConstraintType::class, [
'label' => 'part.edit.name',
]);
$builder->add('description', TextConstraintType::class, [
'label' => 'part.edit.description',
]);
$builder->add('submit', SubmitType::class, [
'label' => 'Update',
]);

View file

@ -8,4 +8,14 @@
<span class="input-group-text">{{ form.vars["text_suffix"] }}</span>
{% endif %}
</div>
{% endblock %}
{% block text_constraint_widget %}
<div class="input-group">
{{ form_widget(form.operator, {"attr": {"class": "form-select"}}) }}
{{ form_widget(form.value) }}
{% if form.vars["text_suffix"] %}
<span class="input-group-text">{{ form.vars["text_suffix"] }}</span>
{% endif %}
</div>
{% endblock %}