From f8562f9622b2b9e3f3abb8cfeb5bb1be45b31049 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Thu, 18 Aug 2022 00:00:54 +0200 Subject: [PATCH] Added an basic TextConstraint for part filtering. --- ...eConstraint.php => AbstractConstraint.php} | 8 +- .../Filters/Constraints/BooleanConstraint.php | 8 +- .../Filters/Constraints/NumberConstraint.php | 14 ++- .../Filters/Constraints/TextConstraint.php | 109 ++++++++++++++++++ src/DataTables/Filters/PartFilter.php | 21 ++++ .../Constraints/NumberConstraintType.php | 2 + .../Constraints/TextConstraintType.php | 60 ++++++++++ src/Form/Filters/PartFilterType.php | 9 ++ templates/Form/FilterTypesLayout.html.twig | 10 ++ 9 files changed, 234 insertions(+), 7 deletions(-) rename src/DataTables/Filters/Constraints/{AbstractSimpleConstraint.php => AbstractConstraint.php} (63%) create mode 100644 src/DataTables/Filters/Constraints/TextConstraint.php create mode 100644 src/Form/Filters/Constraints/TextConstraintType.php diff --git a/src/DataTables/Filters/Constraints/AbstractSimpleConstraint.php b/src/DataTables/Filters/Constraints/AbstractConstraint.php similarity index 63% rename from src/DataTables/Filters/Constraints/AbstractSimpleConstraint.php rename to src/DataTables/Filters/Constraints/AbstractConstraint.php index e45f1909..0af33ce5 100644 --- a/src/DataTables/Filters/Constraints/AbstractSimpleConstraint.php +++ b/src/DataTables/Filters/Constraints/AbstractConstraint.php @@ -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; diff --git a/src/DataTables/Filters/Constraints/BooleanConstraint.php b/src/DataTables/Filters/Constraints/BooleanConstraint.php index 4051ad49..dd2870ed 100644 --- a/src/DataTables/Filters/Constraints/BooleanConstraint.php +++ b/src/DataTables/Filters/Constraints/BooleanConstraint.php @@ -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; } diff --git a/src/DataTables/Filters/Constraints/NumberConstraint.php b/src/DataTables/Filters/Constraints/NumberConstraint.php index 410b6f63..ef758e23 100644 --- a/src/DataTables/Filters/Constraints/NumberConstraint.php +++ b/src/DataTables/Filters/Constraints/NumberConstraint.php @@ -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') { diff --git a/src/DataTables/Filters/Constraints/TextConstraint.php b/src/DataTables/Filters/Constraints/TextConstraint.php new file mode 100644 index 00000000..f4cf9b40 --- /dev/null +++ b/src/DataTables/Filters/Constraints/TextConstraint.php @@ -0,0 +1,109 @@ +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); + } + } +} \ No newline at end of file diff --git a/src/DataTables/Filters/PartFilter.php b/src/DataTables/Filters/PartFilter.php index 49fc8b9b..be8a7e7f 100644 --- a/src/DataTables/Filters/PartFilter.php +++ b/src/DataTables/Filters/PartFilter.php @@ -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); } } diff --git a/src/Form/Filters/Constraints/NumberConstraintType.php b/src/Form/Filters/Constraints/NumberConstraintType.php index 2dfde4de..022cfdbe 100644 --- a/src/Form/Filters/Constraints/NumberConstraintType.php +++ b/src/Form/Filters/Constraints/NumberConstraintType.php @@ -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, ]); } diff --git a/src/Form/Filters/Constraints/TextConstraintType.php b/src/Form/Filters/Constraints/TextConstraintType.php new file mode 100644 index 00000000..be54a3ee --- /dev/null +++ b/src/Form/Filters/Constraints/TextConstraintType.php @@ -0,0 +1,60 @@ +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']; + } +} \ No newline at end of file diff --git a/src/Form/Filters/PartFilterType.php b/src/Form/Filters/PartFilterType.php index 3a609b95..dcd5ce78 100644 --- a/src/Form/Filters/PartFilterType.php +++ b/src/Form/Filters/PartFilterType.php @@ -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', ]); diff --git a/templates/Form/FilterTypesLayout.html.twig b/templates/Form/FilterTypesLayout.html.twig index b49a79d0..9344cdc7 100644 --- a/templates/Form/FilterTypesLayout.html.twig +++ b/templates/Form/FilterTypesLayout.html.twig @@ -8,4 +8,14 @@ {{ form.vars["text_suffix"] }} {% endif %} +{% endblock %} + +{% block text_constraint_widget %} +
+ {{ form_widget(form.operator, {"attr": {"class": "form-select"}}) }} + {{ form_widget(form.value) }} + {% if form.vars["text_suffix"] %} + {{ form.vars["text_suffix"] }} + {% endif %} +
{% endblock %} \ No newline at end of file