diff --git a/Makefile b/Makefile index 708d42a..68e280d 100644 --- a/Makefile +++ b/Makefile @@ -27,7 +27,7 @@ install: ## fix php code style ecs: - vendor/bin/ecs --fix + vendor/bin/ecs --fix --clear-cache ## check code with phpstan phpstan: diff --git a/composer.json b/composer.json index 6850d55..4874aca 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,8 @@ "araise/core-bundle": "^1.0", "araise/search-bundle": "^3.0", "phpoffice/phpspreadsheet": "^1.22", - "symfony/stimulus-bundle": "^2.10" + "symfony/stimulus-bundle": "^2.10", + "coduo/php-to-string": "^3.2" }, "require-dev": { "symfony/phpunit-bridge": "^v6.0", diff --git a/src/Extension/FilterExtension.php b/src/Extension/FilterExtension.php index 6dcef45..8fbd2a3 100644 --- a/src/Extension/FilterExtension.php +++ b/src/Extension/FilterExtension.php @@ -34,8 +34,10 @@ use araise\TableBundle\Entity\Filter as FilterEntity; use araise\TableBundle\Exception\InvalidFilterAcronymException; use araise\TableBundle\Filter\FilterGuesser; +use araise\TableBundle\Filter\Type\FilterType; use araise\TableBundle\Filter\Type\FilterTypeInterface; use araise\TableBundle\Helper\RouterHelper; +use araise\TableBundle\Manager\FilterTypeManager; use araise\TableBundle\Table\Filter; use araise\TableBundle\Table\Table; use Doctrine\Common\Annotations\AnnotationException; @@ -69,6 +71,7 @@ class FilterExtension extends AbstractExtension public function __construct( protected EntityManagerInterface $entityManager, + protected FilterTypeManager $filterTypeManager, protected RequestStack $requestStack, protected FilterGuesser $filterGuesser, protected LoggerInterface $logger @@ -95,6 +98,7 @@ public function configureOptions(OptionsResolver $resolver): void /** * @return $this + * @interal use addFilterType instead */ public function addFilter(string $acronym, string $label, FilterTypeInterface $type) { @@ -103,6 +107,19 @@ public function addFilter(string $acronym, string $label, FilterTypeInterface $t return $this; } + /** + * @return $this + */ + public function addFilterType(string $acronym, string $label, string $typeClass, array $options = []) + { + if (!array_key_exists(FilterType::OPT_COLUMN, $options)) { + $options[FilterType::OPT_COLUMN] = $acronym; + } + $type = $this->filterTypeManager->getFilterType($typeClass)->setOptions($options); + $this->filters[$acronym] = new Filter($acronym, $label, $type); + return $this; + } + /** * @return $this */ diff --git a/src/Filter/FilterGuesser.php b/src/Filter/FilterGuesser.php index f53255c..ccd2f00 100644 --- a/src/Filter/FilterGuesser.php +++ b/src/Filter/FilterGuesser.php @@ -35,9 +35,11 @@ use araise\TableBundle\Filter\Type\BooleanFilterType; use araise\TableBundle\Filter\Type\DateFilterType; use araise\TableBundle\Filter\Type\DatetimeFilterType; +use araise\TableBundle\Filter\Type\FilterType; use araise\TableBundle\Filter\Type\FilterTypeInterface; use araise\TableBundle\Filter\Type\NumberFilterType; use araise\TableBundle\Filter\Type\TextFilterType; +use araise\TableBundle\Manager\FilterTypeManager; use Doctrine\Common\Annotations\AnnotationReader; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\Column; @@ -65,7 +67,8 @@ class FilterGuesser ]; public function __construct( - protected EntityManagerInterface $entityManager + protected EntityManagerInterface $entityManager, + protected FilterTypeManager $filterTypeManager, ) { } @@ -102,7 +105,9 @@ private function newColumnFilter(string $type, string $accessor): ?FilterTypeInt return null; } - return new (self::SCALAR_TYPES[$type])($accessor); + return $this->filterTypeManager->getFilterType(self::SCALAR_TYPES[$type])->setOptions([ + FilterType::OPT_COLUMN => $accessor, + ]); } private function newManyToManyFilter(string $class, string $acronym, string $targetEntity, callable $jsonSearchCallable, array $joins): ?FilterTypeInterface @@ -111,13 +116,12 @@ private function newManyToManyFilter(string $class, string $acronym, string $tar return null; } - return new (self::RELATION_TYPE[$class])( - $acronym, - $targetEntity, - $this->entityManager, - $jsonSearchCallable($targetEntity), - $joins - ); + return $this->filterTypeManager->getFilterType(self::RELATION_TYPE[$class])->setOptions([ + FilterType::OPT_COLUMN => $acronym, + FilterType::OPT_JOINS => $joins, + AjaxOneToManyFilterType::OPT_TARGET_CLASS => $targetEntity, + AjaxOneToManyFilterType::OPT_JSON_SEARCH_URL => $jsonSearchCallable($targetEntity), + ]); } private function newRelationFilter(string $class, string $acronym, string $targetEntity, string $namespace, callable $jsonSearchCallable, array $joins): ?FilterTypeInterface @@ -130,16 +134,15 @@ private function newRelationFilter(string $class, string $acronym, string $targe $targetEntity = $namespace.'\\'.$targetEntity; } - return new (self::RELATION_TYPE[$class])( - $acronym, - $targetEntity, - $this->entityManager, - $jsonSearchCallable($targetEntity), - $joins - ); + return $this->filterTypeManager->getFilterType(self::RELATION_TYPE[$class])->setOptions([ + FilterType::OPT_COLUMN => $acronym, + FilterType::OPT_JOINS => $joins, + AjaxRelationFilterType::OPT_TARGET_CLASS => $targetEntity, + AjaxRelationFilterType::OPT_JSON_SEARCH_URL => $jsonSearchCallable($targetEntity), + ]); } - private function getAnnotationsAndAttributes(\ReflectionProperty $property): ?array + private function getAnnotationsAndAttributes(\ReflectionProperty $property): array { $annotations = (new AnnotationReader())->getPropertyAnnotations($property); $attributes = $property->getAttributes(); diff --git a/src/Filter/Type/AjaxManyToManyFilterType.php b/src/Filter/Type/AjaxManyToManyFilterType.php index e5fe31b..e6b3310 100644 --- a/src/Filter/Type/AjaxManyToManyFilterType.php +++ b/src/Filter/Type/AjaxManyToManyFilterType.php @@ -35,19 +35,17 @@ class AjaxManyToManyFilterType extends AjaxOneToManyFilterType { public function toDql(string $operator, string $value, string $parameterName, QueryBuilder $queryBuilder) { + $targetClass = $this->getOption(static::OPT_TARGET_CLASS); $targetParameter = 'target_'.hash('crc32', random_bytes(10)); $queryBuilder->setParameter( $targetParameter, - $this->entityManager->getRepository($this->targetClass)->find($value) + $this->entityManager->getRepository($targetClass)->find($value) ); - - switch ($operator) { - case static::CRITERIA_EQUAL: - return $queryBuilder->expr()->isMemberOf(':'.$targetParameter, $this->column); - case static::CRITERIA_NOT_EQUAL: - return $queryBuilder->expr()->not($queryBuilder->expr()->isMemberOf($targetParameter, $this->column)); - } - - return null; + $column = $this->getOption(static::OPT_COLUMN); + return match ($operator) { + static::CRITERIA_EQUAL => $queryBuilder->expr()->isMemberOf(':'.$targetParameter, $column), + static::CRITERIA_NOT_EQUAL => $queryBuilder->expr()->not($queryBuilder->expr()->isMemberOf($targetParameter, $column)), + default => null, + }; } } diff --git a/src/Filter/Type/AjaxOneToManyFilterType.php b/src/Filter/Type/AjaxOneToManyFilterType.php index f7b6fde..00c705d 100644 --- a/src/Filter/Type/AjaxOneToManyFilterType.php +++ b/src/Filter/Type/AjaxOneToManyFilterType.php @@ -29,8 +29,10 @@ namespace araise\TableBundle\Filter\Type; +use Coduo\ToString\StringConverter; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\QueryBuilder; +use Symfony\Component\OptionsResolver\OptionsResolver; class AjaxOneToManyFilterType extends FilterType { @@ -38,14 +40,14 @@ class AjaxOneToManyFilterType extends FilterType public const CRITERIA_NOT_EQUAL = 'not_equal'; + public const OPT_TARGET_CLASS = 'target_class'; + + public const OPT_JSON_SEARCH_URL = 'json_search_url'; + public function __construct( - string $column, - protected string $targetClass, protected EntityManagerInterface $entityManager, - protected string $jsonSearchUrl, - array $joins = [] ) { - parent::__construct($column, $joins); + parent::__construct(); } public function getOperators(): array @@ -58,14 +60,15 @@ public function getOperators(): array public function getValueField(?string $value = '0'): string { + $targetClass = $this->getOption(static::OPT_TARGET_CLASS); $field = sprintf( ''; @@ -75,19 +78,29 @@ public function getValueField(?string $value = '0'): string public function toDql(string $operator, string $value, string $parameterName, QueryBuilder $queryBuilder) { + $targetClass = $this->getOption(static::OPT_TARGET_CLASS); $targetParameter = 'target_'.hash('crc32', random_bytes(10)); $queryBuilder->setParameter( $targetParameter, - $this->entityManager->getRepository($this->targetClass)->find($value) + $this->entityManager->getRepository($targetClass)->find($value) ); - switch ($operator) { - case static::CRITERIA_EQUAL: - return $queryBuilder->expr()->in($this->column, ':'.$targetParameter); - case static::CRITERIA_NOT_EQUAL: - return $queryBuilder->expr()->notIn($this->column, ':'.$targetParameter); - } + $column = $this->getOption(static::OPT_COLUMN); + return match ($operator) { + static::CRITERIA_EQUAL => $queryBuilder->expr()->in($column, ':'.$targetParameter), + static::CRITERIA_NOT_EQUAL => $queryBuilder->expr()->notIn($column, ':'.$targetParameter), + default => null, + }; + } - return null; + protected function configureOptions(OptionsResolver $resolver): void + { + parent::configureOptions($resolver); + $resolver->setDefaults([ + static::OPT_TARGET_CLASS => null, + static::OPT_JSON_SEARCH_URL => null, + ]); + $resolver->setAllowedTypes(static::OPT_TARGET_CLASS, ['string']); + $resolver->setAllowedTypes(static::OPT_JSON_SEARCH_URL, ['string']); } } diff --git a/src/Filter/Type/AjaxRelationFilterType.php b/src/Filter/Type/AjaxRelationFilterType.php index 29ca4eb..5acfc01 100644 --- a/src/Filter/Type/AjaxRelationFilterType.php +++ b/src/Filter/Type/AjaxRelationFilterType.php @@ -4,8 +4,10 @@ namespace araise\TableBundle\Filter\Type; +use Coduo\ToString\StringConverter; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\QueryBuilder; +use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\PropertyAccess\PropertyAccessor; class AjaxRelationFilterType extends FilterType @@ -14,16 +16,16 @@ class AjaxRelationFilterType extends FilterType public const CRITERIA_NOT_EQUAL = 'not_equal'; + public const OPT_TARGET_CLASS = 'target_class'; + + public const OPT_JSON_SEARCH_URL = 'json_search_url'; + protected static PropertyAccessor $propertyAccessor; public function __construct( - string $column, - protected string $targetClass, protected EntityManagerInterface $entityManager, - protected string $jsonSearchUrl, - array $joins = [] ) { - parent::__construct($column, $joins); + parent::__construct(); } public function getOperators(): array @@ -36,14 +38,15 @@ public function getOperators(): array public function getValueField(?string $value = '0'): string { + $jsonSearchUrl = $this->getOption(static::OPT_JSON_SEARCH_URL); $field = sprintf( ''; @@ -53,22 +56,34 @@ public function getValueField(?string $value = '0'): string public function toDql(string $operator, string $value, string $parameterName, QueryBuilder $queryBuilder) { + $column = $this->getOption(static::OPT_COLUMN); switch ($operator) { case static::CRITERIA_EQUAL: if ((int) $value) { - return $queryBuilder->expr()->eq($this->getColumn().'.id', (int) $value); + return $queryBuilder->expr()->eq($column.'.id', (int) $value); } - return $queryBuilder->expr()->isNull($this->getColumn().'.id'); + return $queryBuilder->expr()->isNull($column.'.id'); case static::CRITERIA_NOT_EQUAL: if ((int) $value) { - return $queryBuilder->expr()->neq($this->getColumn().'.id', (int) $value); + return $queryBuilder->expr()->neq($column.'.id', (int) $value); } - return $queryBuilder->expr()->isNotNull($this->getColumn().'.id'); + return $queryBuilder->expr()->isNotNull($column.'.id'); } return null; } + + protected function configureOptions(OptionsResolver $resolver): void + { + parent::configureOptions($resolver); + $resolver->setDefaults([ + static::OPT_TARGET_CLASS => null, + static::OPT_JSON_SEARCH_URL => null, + ]); + $resolver->setAllowedTypes(static::OPT_TARGET_CLASS, ['string']); + $resolver->setAllowedTypes(static::OPT_JSON_SEARCH_URL, ['string']); + } } diff --git a/src/Filter/Type/BooleanFilterType.php b/src/Filter/Type/BooleanFilterType.php index 675784a..c8cc6f3 100644 --- a/src/Filter/Type/BooleanFilterType.php +++ b/src/Filter/Type/BooleanFilterType.php @@ -32,14 +32,11 @@ public function getValueField(?string $value = '1'): string public function toDql(string $operator, string $value, string $parameterName, QueryBuilder $queryBuilder) { $value = $value === '1' ? 'true' : 'false'; - - switch ($operator) { - case static::CRITERIA_EQUAL: - return $queryBuilder->expr()->eq($this->getColumn(), $value); - case static::CRITERIA_NOT_EQUAL: - return $queryBuilder->expr()->neq($this->getColumn(), $value); - } - - return false; + $column = $this->getOption(static::OPT_COLUMN); + return match ($operator) { + static::CRITERIA_EQUAL => $queryBuilder->expr()->eq($column, $value), + static::CRITERIA_NOT_EQUAL => $queryBuilder->expr()->neq($column, $value), + default => false, + }; } } diff --git a/src/Filter/Type/ChoiceFilterType.php b/src/Filter/Type/ChoiceFilterType.php index 347f886..74eb4f4 100644 --- a/src/Filter/Type/ChoiceFilterType.php +++ b/src/Filter/Type/ChoiceFilterType.php @@ -5,6 +5,7 @@ namespace araise\TableBundle\Filter\Type; use Doctrine\ORM\QueryBuilder; +use Symfony\Component\OptionsResolver\OptionsResolver; class ChoiceFilterType extends FilterType { @@ -12,13 +13,7 @@ class ChoiceFilterType extends FilterType public const CRITERIA_NOT_EQUAL = 'not_equal'; - protected array $choices = []; - - public function __construct(string $column, array $choices, array $joins = []) - { - parent::__construct($column, $joins); - $this->choices = $choices; - } + public const OPT_CHOICES = 'choices'; public function getOperators(): array { @@ -32,7 +27,7 @@ public function getValueField(?string $value = null): string { $result = '