diff --git a/composer.json b/composer.json index 6ddd735..2427703 100644 --- a/composer.json +++ b/composer.json @@ -26,8 +26,10 @@ "symfony/expression-language": "^6.4|^7.0", "symfony/framework-bundle": "^6.4|^7.0", "symfony/phpunit-bridge": "^6.3|^7.0", + "symfony/security-bundle": "^6.4|^7.0", "symfony/var-dumper": "^6.4|^7.0", - "zenstruck/foundry": "^1.33" + "zenstruck/foundry": "^1.33", + "zenstruck/uri": "^2.3" }, "suggest": { "doctrine/orm": "To use ORM implementation and batch utilities (>=2.10).", diff --git a/config/symfony/doctrine.php b/config/symfony/doctrine.php new file mode 100644 index 0000000..3de43ac --- /dev/null +++ b/config/symfony/doctrine.php @@ -0,0 +1,20 @@ +services() + ->set('.zenstruck_collection.doctrine.orm.object_repo_factory', EntityRepositoryFactory::class) + ->args([service('doctrine')]) + + ->set('.zenstruck_collection.doctrine.chain_object_repo_factory', ChainObjectRepositoryFactory::class) + ->args([service('.zenstruck_collection.doctrine.orm.object_repo_factory')]) + ->tag('kernel.reset', ['method' => 'reset']) + + ->alias(ObjectRepositoryFactory::class, '.zenstruck_collection.doctrine.chain_object_repo_factory') + ; +}; diff --git a/config/symfony/grid.php b/config/symfony/grid.php new file mode 100644 index 0000000..81c502a --- /dev/null +++ b/config/symfony/grid.php @@ -0,0 +1,16 @@ +services() + ->set('.zenstruck_collection.grid_factory', GridFactory::class) + ->args([ + tagged_locator('zenstruck_collection.grid_definition', indexAttribute: 'key'), + ]) + + ->alias(GridFactory::class, '.zenstruck_collection.grid_factory') + ; +}; diff --git a/src/Collection/Doctrine/Grid/ObjectGridDefinition.php b/src/Collection/Doctrine/Grid/ObjectGridDefinition.php new file mode 100644 index 0000000..40c6092 --- /dev/null +++ b/src/Collection/Doctrine/Grid/ObjectGridDefinition.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Collection\Doctrine\Grid; + +use Zenstruck\Collection\Doctrine\ObjectRepositoryFactory; +use Zenstruck\Collection\Grid\GridBuilder; +use Zenstruck\Collection\Grid\GridDefinition; + +/** + * @author Kevin Bond + * + * @implements GridDefinition + */ +final class ObjectGridDefinition implements GridDefinition +{ + /** + * @param class-string $class + * @param GridDefinition $inner + */ + public function __construct( + private string $class, + private ObjectRepositoryFactory $repositoryFactory, + private GridDefinition $inner, + ) { + } + + public function configure(GridBuilder $builder): void + { + $this->inner->configure($builder); + + if ($builder->source) { + return; + } + + $builder->source = $this->repositoryFactory->create($this->class); + } +} diff --git a/src/Collection/Grid.php b/src/Collection/Grid.php new file mode 100644 index 0000000..68b4880 --- /dev/null +++ b/src/Collection/Grid.php @@ -0,0 +1,107 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Collection; + +use Zenstruck\Collection\Grid\Column; +use Zenstruck\Collection\Grid\Columns; +use Zenstruck\Collection\Grid\Filter; +use Zenstruck\Collection\Grid\Filters; +use Zenstruck\Collection\Grid\Input; +use Zenstruck\Collection\Grid\PerPage; +use Zenstruck\Collection\Grid\PerPage\FixedPerPage; +use Zenstruck\Collection\Specification\Filter\Contains; +use Zenstruck\Collection\Specification\Logic\AndX; +use Zenstruck\Collection\Specification\Logic\OrX; + +/** + * @author Kevin Bond + * + * @template T of array|object + * + * @implements \IteratorAggregate + */ +final class Grid implements \IteratorAggregate +{ + public readonly PerPage $perPage; + + /** @var Page */ + private Page $page; + + /** + * @internal + * + * @param Matchable $source + */ + public function __construct( + public readonly Input $input, + public readonly Matchable $source, + public readonly Columns $columns, + public readonly Filters $filters, + ?PerPage $perPage = null, + private ?object $defaultSpecification = null, + ) { + $this->perPage = $perPage ?? new FixedPerPage(); + } + + public function getIterator(): \Traversable + { + return $this->page(); + } + + /** + * @return Page + */ + public function page(): Page + { + if (isset($this->page)) { + return $this->page; + } + + $specification = new AndX(...\array_filter([ + $this->defaultSpecification, + $this->columns->sort(), + $this->searchSpecification(), + new AndX(...\array_filter($this->filterSpecification())), + ])); + + return $this->page = $this->source->filter($specification) + ->paginate($this->input->page(), $this->perPage->value($this->input->perPage()))->strict() + ; + } + + private function searchSpecification(): OrX + { + if (!$query = $this->input->query()) { + return new OrX(); + } + + return new OrX(...$this->columns + ->searchable() + ->all() + ->map(fn(Column $column) => new Contains($column->name(), $query)) + ->values() + ->all() + ); + } + + /** + * @return list + */ + private function filterSpecification(): array + { + return $this->filters->all() + ->map(fn(Filter $filter, string $name) => $filter->apply($this->input->filter($name))) + ->values() + ->all() + ; + } +} diff --git a/src/Collection/Grid/Column.php b/src/Collection/Grid/Column.php new file mode 100644 index 0000000..648554b --- /dev/null +++ b/src/Collection/Grid/Column.php @@ -0,0 +1,73 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Collection\Grid; + +use Zenstruck\Collection\Grid\Definition\ColumnDefinition; +use Zenstruck\Collection\Specification\OrderBy; + +/** + * @author Kevin Bond + */ +final class Column +{ + /** + * @internal + */ + public function __construct( + private ColumnDefinition $definition, + private Input $input, + ) { + } + + public function name(): string + { + return $this->definition->name; + } + + public function isSearchable(): bool + { + return $this->definition->searchable; + } + + public function isSortable(): bool + { + return $this->definition->sortable; + } + + public function sort(): ?OrderBy + { + if (!($sort = $this->input->sort()) || $this->name() !== $sort->field) { + return null; + } + + return $sort; + } + + public function applyAscSort(): Input + { + return $this->input->applySort(OrderBy::asc($this->name())); + } + + public function applyDescSort(): Input + { + return $this->input->applySort(OrderBy::desc($this->name())); + } + + public function applyOppositeSort(): Input + { + if (!$sort = $this->sort()) { + return $this->applyAscSort(); + } + + return $this->input->applySort($sort->opposite()); + } +} diff --git a/src/Collection/Grid/Columns.php b/src/Collection/Grid/Columns.php new file mode 100644 index 0000000..a6e1a92 --- /dev/null +++ b/src/Collection/Grid/Columns.php @@ -0,0 +1,82 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Collection\Grid; + +use Zenstruck\Collection\ArrayCollection; +use Zenstruck\Collection\Specification\OrderBy; + +/** + * @author Kevin Bond + * + * @implements \IteratorAggregate + */ +final class Columns implements \IteratorAggregate, \Countable +{ + private self $searchable; + private self $sortable; + + /** + * @internal + * + * @param ArrayCollection $columns + */ + public function __construct(private ArrayCollection $columns, private Input $input, private ?OrderBy $defaultSort) + { + } + + public function get(string $name): ?Column + { + return $this->columns->get($name); + } + + public function has(string $name): bool + { + return $this->columns->has($name); + } + + public function searchable(): self + { + return $this->searchable ??= new self($this->columns->filter(fn(Column $c) => $c->isSearchable()), $this->input, $this->defaultSort); + } + + public function sortable(): self + { + return $this->sortable ??= new self($this->columns->filter(fn(Column $c) => $c->isSortable()), $this->input, $this->defaultSort); + } + + public function sort(): ?OrderBy + { + if (!$sort = $this->input->sort()) { + return $this->defaultSort; + } + + return $this->sortable()->has($sort->field) ? $sort : $this->defaultSort; + } + + /** + * @return ArrayCollection + */ + public function all(): ArrayCollection + { + return $this->columns; + } + + public function getIterator(): \Traversable + { + return $this->columns; + } + + public function count(): int + { + return $this->columns->count(); + } +} diff --git a/src/Collection/Grid/Definition/ColumnDefinition.php b/src/Collection/Grid/Definition/ColumnDefinition.php new file mode 100644 index 0000000..ea4cfa1 --- /dev/null +++ b/src/Collection/Grid/Definition/ColumnDefinition.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Collection\Grid\Definition; + +/** + * @author Kevin Bond + */ +final class ColumnDefinition +{ + public function __construct( + public string $name, + public bool $searchable = false, + public bool $sortable = false, + ) { + } +} diff --git a/src/Collection/Grid/Filter.php b/src/Collection/Grid/Filter.php new file mode 100644 index 0000000..4b46b5b --- /dev/null +++ b/src/Collection/Grid/Filter.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Collection\Grid; + +/** + * @author Kevin Bond + */ +interface Filter +{ + public function apply(mixed $value): ?object; +} diff --git a/src/Collection/Grid/Filter/AutoFilter.php b/src/Collection/Grid/Filter/AutoFilter.php new file mode 100644 index 0000000..9df6bfa --- /dev/null +++ b/src/Collection/Grid/Filter/AutoFilter.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Collection\Grid\Filter; + +use Zenstruck\Collection\Grid\Filter; +use Zenstruck\Collection\Specification\Filter\Between; +use Zenstruck\Collection\Specification\Filter\Contains; +use Zenstruck\Collection\Specification\Filter\EndsWith; +use Zenstruck\Collection\Specification\Filter\EqualTo; +use Zenstruck\Collection\Specification\Filter\GreaterThan; +use Zenstruck\Collection\Specification\Filter\GreaterThanOrEqualTo; +use Zenstruck\Collection\Specification\Filter\In; +use Zenstruck\Collection\Specification\Filter\IsNull; +use Zenstruck\Collection\Specification\Filter\LessThan; +use Zenstruck\Collection\Specification\Filter\LessThanOrEqualTo; +use Zenstruck\Collection\Specification\Filter\StartsWith; +use Zenstruck\Collection\Specification\Logic\Not; + +/** + * @author Kevin Bond + */ +final class AutoFilter implements Filter +{ + public function __construct(private string $field) + { + } + + public function apply(mixed $value): ?object + { + if (!\is_string($value) || !$value) { + return null; + } + + if (2 === \count($parts = \explode('...', $value))) { + [$begin, $end] = $parts; + $type = \str_starts_with($begin, '(') ? '(' : '['; + $type .= \str_ends_with($end, ')') ? ')' : ']'; + + return new Between($this->field, \trim($begin, '[('), \trim($end, ')]'), $type); + } + + return match (true) { + '~' === $value => new IsNull($this->field), + \str_starts_with($value, '*') && \str_ends_with($value, '*') => new Contains($this->field, \mb_substr($value, 1, -1)), + \str_starts_with($value, '*') => new StartsWith($this->field, \mb_substr($value, 1)), + \str_ends_with($value, '*') => new EndsWith($this->field, \mb_substr($value, 0, -1)), + \str_starts_with($value, '!') => new Not($this->apply(\mb_substr($value, 1))), + \str_starts_with($value, '<=') => new LessThanOrEqualTo($this->field, \mb_substr($value, 2)), + \str_starts_with($value, '>=') => new GreaterThanOrEqualTo($this->field, \mb_substr($value, 2)), + \str_starts_with($value, '>') => new GreaterThan($this->field, \mb_substr($value, 1)), + \str_starts_with($value, '<') => new LessThan($this->field, \mb_substr($value, 1)), + \count($values = \explode(',', $value)) > 1 => new In($this->field, $values), + default => new EqualTo($this->field, $value), + }; + } +} diff --git a/src/Collection/Grid/Filter/Choice.php b/src/Collection/Grid/Filter/Choice.php new file mode 100644 index 0000000..321c630 --- /dev/null +++ b/src/Collection/Grid/Filter/Choice.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Collection\Grid\Filter; + +/** + * @author Kevin Bond + */ +final class Choice implements \Stringable +{ + public function __construct( + public readonly ?string $value, + public readonly ?object $specification = null, + public readonly ?string $label = null, + ) { + } + + public function __toString(): string + { + return $this->label ?? $this->value ?? ''; + } +} diff --git a/src/Collection/Grid/Filter/ChoiceFilter.php b/src/Collection/Grid/Filter/ChoiceFilter.php new file mode 100644 index 0000000..58ceef4 --- /dev/null +++ b/src/Collection/Grid/Filter/ChoiceFilter.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Collection\Grid\Filter; + +use Zenstruck\Collection\ArrayCollection; +use Zenstruck\Collection\Grid\Filter; + +/** + * @author Kevin Bond + * + * @implements \IteratorAggregate + */ +final class ChoiceFilter implements Filter, \IteratorAggregate, \Countable +{ + /** @var ArrayCollection */ + private ArrayCollection $choices; + + public function __construct(Choice ...$choices) + { + $this->choices = ArrayCollection::for($choices)->keyBy(fn(Choice $choice) => (string) $choice->value); + } + + public function apply(mixed $value): ?object + { + if (!\is_string($value)) { + return null; + } + + return $this->choices->get($value)?->specification; + } + + public function getIterator(): \Traversable + { + return $this->choices; + } + + public function count(): int + { + return $this->choices->count(); + } +} diff --git a/src/Collection/Grid/Filters.php b/src/Collection/Grid/Filters.php new file mode 100644 index 0000000..0439c62 --- /dev/null +++ b/src/Collection/Grid/Filters.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Collection\Grid; + +use Zenstruck\Collection\ArrayCollection; + +/** + * @author Kevin Bond + * + * @implements \IteratorAggregate + */ +final class Filters implements \IteratorAggregate, \Countable +{ + /** @var ArrayCollection */ + private ArrayCollection $filters; + + /** + * @param array $filters + */ + public function __construct(array $filters) + { + $this->filters = new ArrayCollection($filters); + } + + public function get(string $name): ?Filter + { + return $this->filters->get($name); + } + + public function has(string $name): bool + { + return $this->filters->has($name); + } + + /** + * @return ArrayCollection + */ + public function all(): ArrayCollection + { + return $this->filters; + } + + public function getIterator(): \Traversable + { + return $this->filters; + } + + public function count(): int + { + return $this->filters->count(); + } +} diff --git a/src/Collection/Grid/GridBuilder.php b/src/Collection/Grid/GridBuilder.php new file mode 100644 index 0000000..7cb27af --- /dev/null +++ b/src/Collection/Grid/GridBuilder.php @@ -0,0 +1,131 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Collection\Grid; + +use Zenstruck\Collection\Grid; +use Zenstruck\Collection\Grid\Definition\ColumnDefinition; +use Zenstruck\Collection\Grid\Filter\AutoFilter; +use Zenstruck\Collection\Matchable; +use Zenstruck\Collection\Specification\OrderBy; + +use function Zenstruck\collect; + +/** + * @author Kevin Bond + * + * @template T of array|object + */ +final class GridBuilder +{ + /** @var Matchable|null */ + public ?Matchable $source = null; + public ?OrderBy $defaultSort = null; + public ?PerPage $perPage = null; + public ?object $defaultSpecification = null; + + /** @var array */ + private array $columns = []; + + /** @var array */ + private array $filters = []; + + /** + * @return Grid + */ + public function build(Input $input): Grid + { + $columns = collect($this->columns) + ->map(fn(ColumnDefinition $column) => new Column( + definition: $column, + input: $input, + )) + ; + + return new Grid( + input: $input, + source: $this->source ?? throw new \LogicException('No source defined.'), + columns: new Columns($columns, $input, $this->defaultSort), + filters: new Filters($this->filters), + perPage: $this->perPage, + defaultSpecification: $this->defaultSpecification, + ); + } + + /** + * @param OrderBy::*|null $defaultSort + * + * @return $this + */ + public function addColumn( + string $name, + bool $searchable = false, + bool $sortable = false, + bool $autofilter = false, + ?string $defaultSort = null, + ): self { + $this->columns[$name] = new ColumnDefinition( + name: $name, + searchable: $searchable, + sortable: $sortable, + ); + + if ($defaultSort) { + $this->defaultSort = new OrderBy($name, $defaultSort); + } + + if ($autofilter) { + $this->addFilter($name, new AutoFilter($name)); + } + + return $this; + } + + public function getColumn(string $name): ColumnDefinition + { + return $this->columns[$name] ?? throw new \InvalidArgumentException(\sprintf('Column "%s" does not exist.', $name)); + } + + /** + * @return $this + */ + public function removeColumn(string $name): self + { + unset($this->columns[$name]); + + return $this; + } + + /** + * @return $this + */ + public function addFilter(string $name, Filter $filter): self + { + $this->filters[$name] = $filter; + + return $this; + } + + public function getFilter(string $name): Filter + { + return $this->filters[$name] ?? throw new \InvalidArgumentException(\sprintf('Filter "%s" does not exist.', $name)); + } + + /** + * @return $this + */ + public function removeFilter(string $name): self + { + unset($this->filters[$name]); + + return $this; + } +} diff --git a/src/Collection/Grid/GridDefinition.php b/src/Collection/Grid/GridDefinition.php new file mode 100644 index 0000000..9a7d414 --- /dev/null +++ b/src/Collection/Grid/GridDefinition.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Collection\Grid; + +/** + * @author Kevin Bond + * + * @template T of array|object + */ +interface GridDefinition +{ + /** + * @param GridBuilder $builder + */ + public function configure(GridBuilder $builder): void; +} diff --git a/src/Collection/Grid/Input.php b/src/Collection/Grid/Input.php new file mode 100644 index 0000000..f70d31e --- /dev/null +++ b/src/Collection/Grid/Input.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Collection\Grid; + +use Zenstruck\Collection\ArrayCollection; +use Zenstruck\Collection\Specification\OrderBy; + +/** + * @author Kevin Bond + * + * @immutable + */ +interface Input +{ + public function page(): int; + + /** + * @param positive-int $value + */ + public function applyPage(int $value): static; + + public function query(): ?string; + + public function applyQuery(?string $value): static; + + public function perPage(): ?int; + + /** + * @param positive-int $value + */ + public function applyPerPage(int $value): static; + + public function sort(): ?OrderBy; + + public function applySort(OrderBy $orderBy): static; + + public function filter(string $name): mixed; + + public function applyFilter(string $name, mixed $value): static; + + public function reset(): static; + + /** + * @return ArrayCollection + */ + public function values(): ArrayCollection; +} diff --git a/src/Collection/Grid/Input/UriInput.php b/src/Collection/Grid/Input/UriInput.php new file mode 100644 index 0000000..31cd83c --- /dev/null +++ b/src/Collection/Grid/Input/UriInput.php @@ -0,0 +1,179 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Collection\Grid\Input; + +use Symfony\Component\HttpFoundation\Request; +use Zenstruck\Collection\ArrayCollection; +use Zenstruck\Collection\Grid\Input; +use Zenstruck\Collection\Specification\OrderBy; +use Zenstruck\Uri; +use Zenstruck\Uri\ParsedUri; + +use function Zenstruck\collect; + +/** + * @author Kevin Bond + */ +final class UriInput implements Input, \Stringable +{ + private const PAGE = 'page'; + private const PER_PAGE = 'perPage'; + private const SORT = 'sort'; + private const QUERY = 'q'; + private const FILTERS = 'filters'; + + private ParsedUri $uri; + + /** @var mixed[] */ + private array $query; + + public function __construct(string|Request|Uri $uri, private ?string $key = null) + { + if (!\class_exists(ParsedUri::class)) { + throw new \LogicException('The "zenstruck/uri" package is required to use UriInput. Run "composer require zenstruck/uri".'); + } + + $this->uri = ParsedUri::wrap($uri); + $query = $key ? $this->uri->query()->get($key, []) : $this->uri->query()->all(); + $this->query = \is_array($query) ? $query : []; + } + + public function __toString(): string + { + if (!$this->key) { + return $this->uri->withQuery($this->query)->toString(); + } + + return $this->uri->withQueryParam($this->key, $this->query)->toString(); + } + + public function page(): int + { + if (\is_numeric($page = $this->query[self::PAGE] ?? 1)) { + return (int) $page; + } + + return 1; + } + + public function applyPage(int $value): static + { + $clone = clone $this; + $clone->query[self::PAGE] = $value; + + return $clone; + } + + public function query(): ?string + { + $query = $this->query[self::QUERY] ?? null; + + return \is_scalar($query) ? (string) $query : null; + } + + public function applyQuery(?string $value): static + { + $clone = clone $this; + $clone->query[self::QUERY] = $value; + + return $clone; + } + + public function perPage(): ?int + { + if (\is_numeric($page = $this->query[self::PER_PAGE] ?? null)) { + return (int) $page; + } + + return null; + } + + public function applyPerPage(int $value): static + { + $clone = clone $this; + $clone->query[self::PER_PAGE] = $value; + + return $clone; + } + + public function sort(): ?OrderBy + { + $sort = $this->query[self::SORT] ?? null; + + return match (true) { + \is_string($sort) && \str_starts_with($sort, '-') => OrderBy::desc(\mb_substr($sort, 1)), + \is_string($sort) => OrderBy::asc($sort), + default => null, + }; + } + + public function applySort(OrderBy $orderBy): static + { + $clone = clone $this; + $clone->query[self::SORT] = \sprintf('%s%s', $orderBy->isDesc() ? '-' : '', $orderBy->field); + + return $clone; + } + + public function filter(string $name): mixed + { + return $this->filters()[$name] ?? null; + } + + public function applyFilter(string $name, mixed $value): static + { + $filters = $this->filters(); + $filters[$name] = $value; + + $clone = clone $this; + $clone->query[self::FILTERS] = $filters; + + return $clone; + } + + public function reset(): static + { + $clone = clone $this; + unset( + $clone->query[self::PAGE], + $clone->query[self::PER_PAGE], + $clone->query[self::SORT], + $clone->query[self::QUERY], + $clone->query[self::FILTERS] + ); + + return $clone; + } + + public function values(): ArrayCollection + { + return collect([ + self::PAGE => $this->query[self::PAGE] ?? null, + self::PER_PAGE => $this->query[self::PER_PAGE] ?? null, + self::SORT => $this->query[self::SORT] ?? null, + self::QUERY => $this->query[self::QUERY] ?? null, + self::FILTERS => $this->filters(), + ])->filter(); + } + + /** + * @return mixed[] + */ + private function filters(): array + { + if (\is_array($filters = $this->query[self::FILTERS] ?? [])) { + return $filters; + } + + return []; + } +} diff --git a/src/Collection/Grid/PerPage.php b/src/Collection/Grid/PerPage.php new file mode 100644 index 0000000..0662191 --- /dev/null +++ b/src/Collection/Grid/PerPage.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Collection\Grid; + +/** + * @author Kevin Bond + */ +interface PerPage +{ + public function value(?int $input): int; +} diff --git a/src/Collection/Grid/PerPage/FixedPerPage.php b/src/Collection/Grid/PerPage/FixedPerPage.php new file mode 100644 index 0000000..7061465 --- /dev/null +++ b/src/Collection/Grid/PerPage/FixedPerPage.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Collection\Grid\PerPage; + +use Zenstruck\Collection\Grid\PerPage; + +/** + * @author Kevin Bond + */ +final class FixedPerPage implements PerPage +{ + /** + * @param positive-int $value + */ + public function __construct(private int $value = 20) + { + } + + public function value(?int $input): int + { + return $this->value; + } +} diff --git a/src/Collection/Grid/PerPage/RangePerPage.php b/src/Collection/Grid/PerPage/RangePerPage.php new file mode 100644 index 0000000..8507e55 --- /dev/null +++ b/src/Collection/Grid/PerPage/RangePerPage.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Collection\Grid\PerPage; + +use Zenstruck\Collection\Grid\PerPage; + +/** + * @author Kevin Bond + */ +final class RangePerPage implements PerPage +{ + /** + * @param positive-int $min + * @param positive-int $max + * @param positive-int $default + */ + public function __construct( + public readonly int $min = 1, + public readonly int $max = 100, + public readonly int $default = 20, + ) { + } + + public function value(?int $input): int + { + return \max($this->min, \min($this->max, $input ?? $this->default)); + } +} diff --git a/src/Collection/Grid/PerPage/SetPerPage.php b/src/Collection/Grid/PerPage/SetPerPage.php new file mode 100644 index 0000000..463b16b --- /dev/null +++ b/src/Collection/Grid/PerPage/SetPerPage.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Collection\Grid\PerPage; + +use Zenstruck\Collection\Grid\PerPage; + +/** + * @author Kevin Bond + */ +final class SetPerPage implements PerPage +{ + /** + * @param list $values + * @param positive-int $default + */ + public function __construct( + public readonly array $values = [20, 50, 100], + public readonly int $default = 20, + ) { + } + + public function value(?int $input): int + { + return \in_array($input, $this->values, true) ? $input : $this->default; + } +} diff --git a/src/Collection/Specification/OrderBy.php b/src/Collection/Specification/OrderBy.php index c5c3464..60be8f7 100644 --- a/src/Collection/Specification/OrderBy.php +++ b/src/Collection/Specification/OrderBy.php @@ -22,7 +22,10 @@ final class OrderBy extends Field /** @var self::* */ public readonly string $direction; - private function __construct(string $field, string $direction) + /** + * @param self::* $direction + */ + public function __construct(string $field, string $direction) { parent::__construct($field); @@ -34,11 +37,26 @@ private function __construct(string $field, string $direction) public static function asc(string $field): self { - return new self($field, 'ASC'); + return new self($field, self::ASC); } public static function desc(string $field): self { - return new self($field, 'DESC'); + return new self($field, self::DESC); + } + + public function opposite(): self + { + return new self($this->field, self::ASC === $this->direction ? self::DESC : self::ASC); + } + + public function isAsc(): bool + { + return self::ASC === $this->direction; + } + + public function isDesc(): bool + { + return self::DESC === $this->direction; } } diff --git a/src/Collection/Symfony/Attributes/AsGrid.php b/src/Collection/Symfony/Attributes/AsGrid.php new file mode 100644 index 0000000..67f163e --- /dev/null +++ b/src/Collection/Symfony/Attributes/AsGrid.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Collection\Symfony\Attributes; + +/** + * @author Kevin Bond + */ +#[\Attribute(\Attribute::TARGET_CLASS)] +final class AsGrid +{ + public function __construct(public readonly string $name) + { + } +} diff --git a/src/Collection/Symfony/Attributes/ForDefinition.php b/src/Collection/Symfony/Attributes/ForDefinition.php new file mode 100644 index 0000000..03db1d7 --- /dev/null +++ b/src/Collection/Symfony/Attributes/ForDefinition.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Collection\Symfony\Attributes; + +use Symfony\Component\DependencyInjection\Attribute\Autowire; + +/** + * @author Kevin Bond + */ +#[\Attribute(\Attribute::TARGET_PARAMETER)] +final class ForDefinition extends Autowire +{ + public function __construct(string $name, ?string $key = null) + { + parent::__construct( + expression: \sprintf( + 'service(".zenstruck_collection.grid_factory").createFor("%s", service("request_stack").getCurrentRequest(), "%s")', + \addslashes($name), + $key, + ) + ); + } +} diff --git a/src/Collection/Symfony/Doctrine/ForObject.php b/src/Collection/Symfony/Attributes/ForObject.php similarity index 71% rename from src/Collection/Symfony/Doctrine/ForObject.php rename to src/Collection/Symfony/Attributes/ForObject.php index 85d0a7d..2896858 100644 --- a/src/Collection/Symfony/Doctrine/ForObject.php +++ b/src/Collection/Symfony/Attributes/ForObject.php @@ -9,22 +9,21 @@ * file that was distributed with this source code. */ -namespace Zenstruck\Collection\Symfony\Doctrine; +namespace Zenstruck\Collection\Symfony\Attributes; use Symfony\Component\DependencyInjection\Attribute\Autowire; /** * @author Kevin Bond */ -#[\Attribute(\Attribute::TARGET_PARAMETER)] +#[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::TARGET_CLASS)] final class ForObject extends Autowire { - public function __construct( - /** - * @var class-string $class - */ - public readonly string $class, - ) { + /** + * @param class-string $class + */ + public function __construct(public readonly string $class) + { parent::__construct( expression: \sprintf('service(".zenstruck_collection.doctrine.chain_object_repo_factory").create("%s")', \addslashes($this->class)), ); diff --git a/src/Collection/Symfony/Grid/GridFactory.php b/src/Collection/Symfony/Grid/GridFactory.php new file mode 100644 index 0000000..1af2448 --- /dev/null +++ b/src/Collection/Symfony/Grid/GridFactory.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Collection\Symfony\Grid; + +use Psr\Container\ContainerInterface; +use Symfony\Component\HttpFoundation\Request; +use Zenstruck\Collection\Grid; +use Zenstruck\Collection\Grid\GridBuilder; +use Zenstruck\Collection\Grid\GridDefinition; +use Zenstruck\Collection\Grid\Input\UriInput; + +/** + * @author Kevin Bond + */ +final class GridFactory +{ + public function __construct(private ContainerInterface $definitions) + { + } + + /** + * @return Grid|object> + */ + public function createFor(string $definition, string|Request $input, ?string $key = null): Grid + { + $definitionObject = $this->definitions->get($definition); + + if (!$definitionObject instanceof GridDefinition) { + throw new \LogicException(\sprintf('Definition "%s" must be an instance of "%s".', $definition, GridDefinition::class)); + } + + $definitionObject->configure($builder = new GridBuilder()); + + return $builder->build(new UriInput($input, $key)); + } +} diff --git a/src/Collection/Symfony/ZenstruckCollectionBundle.php b/src/Collection/Symfony/ZenstruckCollectionBundle.php index ea3b0ff..ac14b08 100644 --- a/src/Collection/Symfony/ZenstruckCollectionBundle.php +++ b/src/Collection/Symfony/ZenstruckCollectionBundle.php @@ -11,39 +11,88 @@ namespace Zenstruck\Collection\Symfony; +use Symfony\Component\Config\FileLocator; +use Symfony\Component\DependencyInjection\ChildDefinition; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; +use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpKernel\Bundle\AbstractBundle; -use Zenstruck\Collection\Doctrine\ObjectRepositoryFactory; -use Zenstruck\Collection\Doctrine\ORM\EntityRepositoryFactory; -use Zenstruck\Collection\Symfony\Doctrine\ChainObjectRepositoryFactory; +use Zenstruck\Collection\Doctrine\Grid\ObjectGridDefinition; +use Zenstruck\Collection\Symfony\Attributes\AsGrid; +use Zenstruck\Collection\Symfony\Attributes\ForObject; + +use function Zenstruck\collect; /** * @author Kevin Bond * * @codeCoverageIgnore */ -final class ZenstruckCollectionBundle extends AbstractBundle +final class ZenstruckCollectionBundle extends AbstractBundle implements CompilerPassInterface { + public function build(ContainerBuilder $container): void + { + $container->addCompilerPass($this); + } + + public function getPath(): string + { + return __DIR__.'/../../../'; + } + /** * @param mixed[] $config */ public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void { - if (!isset($builder->getParameter('kernel.bundles')['DoctrineBundle'])) { + $loader = new PhpFileLoader($builder, new FileLocator(__DIR__.'/../../../config/symfony')); + + $loader->load('grid.php'); + + $builder->registerAttributeForAutoconfiguration(AsGrid::class, function(ChildDefinition $definition, AsGrid $attribute) { + $definition->addTag('zenstruck_collection.grid_definition', ['key' => $attribute->name]); + }); + + if (isset($builder->getParameter('kernel.bundles')['DoctrineBundle'])) { + $loader->load('doctrine.php'); + + $builder->registerAttributeForAutoconfiguration(ForObject::class, function(ChildDefinition $definition, ForObject $attribute) { + $definition->addTag('zenstruck_collection.grid_definition', ['key' => $attribute->class, 'as_object' => true]); + }); + } + } + + public function process(ContainerBuilder $container): void + { + if (!isset($container->getParameter('kernel.bundles')['DoctrineBundle'])) { return; } - $builder->register('.zenstruck_collection.doctrine.orm.object_repo_factory', EntityRepositoryFactory::class) - ->addArgument(new Reference('doctrine')) - ; + foreach ($container->findTaggedServiceIds('zenstruck_collection.grid_definition') as $id => $tags) { + foreach ($tags as $tag) { + if (!($tag['as_object'] ?? false)) { + continue; + } - $builder->register('.zenstruck_collection.doctrine.chain_object_repo_factory', ChainObjectRepositoryFactory::class) - ->addArgument(new Reference('.zenstruck_collection.doctrine.orm.object_repo_factory')) - ->addTag('kernel.reset', ['method' => 'reset']) - ; + $container->register($id.'.object', ObjectGridDefinition::class) + ->setDecoratedService($id) + ->setArguments([ + $tag['key'], + new Reference('.zenstruck_collection.doctrine.chain_object_repo_factory'), + new Reference($id.'.object.inner'), + ]) + ; - $builder->setAlias(ObjectRepositoryFactory::class, '.zenstruck_collection.doctrine.chain_object_repo_factory'); + if ($gridTag = collect($tags)->find(fn(array $t) => false === ($t['as_object'] ?? false))) { + // service was also tagged using AsGrid - use it as the "alias" + $container->getDefinition($id) + ->clearTag('zenstruck_collection.grid_definition') + ->addTag('zenstruck_collection.grid_definition', ['key' => $gridTag['key']]) + ; + } + } + } } } diff --git a/tests/Grid/Filter/AutoFilterTest.php b/tests/Grid/Filter/AutoFilterTest.php new file mode 100644 index 0000000..c0f73ab --- /dev/null +++ b/tests/Grid/Filter/AutoFilterTest.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Collection\Tests\Grid\Filter; + +use PHPUnit\Framework\TestCase; +use Zenstruck\Collection\Grid\Filter\AutoFilter; +use Zenstruck\Collection\Specification\Filter\Between; +use Zenstruck\Collection\Specification\Filter\Contains; +use Zenstruck\Collection\Specification\Filter\EndsWith; +use Zenstruck\Collection\Specification\Filter\EqualTo; +use Zenstruck\Collection\Specification\Filter\GreaterThan; +use Zenstruck\Collection\Specification\Filter\GreaterThanOrEqualTo; +use Zenstruck\Collection\Specification\Filter\In; +use Zenstruck\Collection\Specification\Filter\IsNull; +use Zenstruck\Collection\Specification\Filter\LessThan; +use Zenstruck\Collection\Specification\Filter\LessThanOrEqualTo; +use Zenstruck\Collection\Specification\Filter\StartsWith; +use Zenstruck\Collection\Specification\Logic\Not; + +/** + * @author Kevin Bond + */ +final class AutoFilterTest extends TestCase +{ + /** + * @test + * @dataProvider applyProvider + */ + public function apply(mixed $value, ?object $expected): void + { + $this->assertEquals($expected, (new AutoFilter('foo'))->apply($value)); + } + + public static function applyProvider(): iterable + { + yield ['bar', new EqualTo('foo', 'bar')]; + yield ['*ba*r', new StartsWith('foo', 'ba*r')]; + yield ['ba*r*', new EndsWith('foo', 'ba*r')]; + yield ['*ba*r*', new Contains('foo', 'ba*r')]; + yield ['<=bar', new LessThanOrEqualTo('foo', 'bar')]; + yield ['=bar', new GreaterThanOrEqualTo('foo', 'bar')]; + yield ['>bar', new GreaterThan('foo', 'bar')]; + yield ['!bar', new Not(new EqualTo('foo', 'bar'))]; + yield ['bar,baz,qux', new In('foo', ['bar', 'baz', 'qux'])]; + yield ['!bar,baz,qux', new Not(new In('foo', ['bar', 'baz', 'qux']))]; + yield ['bar...baz', new Between('foo', 'bar', 'baz')]; + yield ['(bar...baz)', new Between('foo', 'bar', 'baz', Between::EXCLUSIVE)]; + yield ['[bar...baz', new Between('foo', 'bar', 'baz', Between::INCLUSIVE)]; + yield ['bar...baz]', new Between('foo', 'bar', 'baz', Between::INCLUSIVE)]; + yield ['(bar...baz', new Between('foo', 'bar', 'baz', Between::EXCLUSIVE_BEGIN)]; + yield ['bar...baz)', new Between('foo', 'bar', 'baz', Between::EXCLUSIVE_END)]; + yield ['(bar...baz]', new Between('foo', 'bar', 'baz', Between::EXCLUSIVE_BEGIN)]; + yield ['[bar...baz)', new Between('foo', 'bar', 'baz', Between::EXCLUSIVE_END)]; + yield ['~', new IsNull('foo')]; + yield ['!~', new Not(new IsNull('foo'))]; + yield ['', null]; + yield [null, null]; + yield [['array'], null]; + } +} diff --git a/tests/Grid/PerPage/FixedPerPageTest.php b/tests/Grid/PerPage/FixedPerPageTest.php new file mode 100644 index 0000000..9a08ad3 --- /dev/null +++ b/tests/Grid/PerPage/FixedPerPageTest.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Collection\Tests\Grid\PerPage; + +use PHPUnit\Framework\TestCase; +use Zenstruck\Collection\Grid\PerPage\FixedPerPage; + +/** + * @author Kevin Bond + */ +final class FixedPerPageTest extends TestCase +{ + /** + * @test + */ + public function value(): void + { + $this->assertSame(20, (new FixedPerPage(20))->value(null)); + $this->assertSame(20, (new FixedPerPage(20))->value(30)); + } +} diff --git a/tests/Grid/PerPage/RangePerPageTest.php b/tests/Grid/PerPage/RangePerPageTest.php new file mode 100644 index 0000000..5e25ee3 --- /dev/null +++ b/tests/Grid/PerPage/RangePerPageTest.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Collection\Tests\Grid\PerPage; + +use PHPUnit\Framework\TestCase; +use Zenstruck\Collection\Grid\PerPage\RangePerPage; + +/** + * @author Kevin Bond + */ +final class RangePerPageTest extends TestCase +{ + /** + * @test + */ + public function value(): void + { + $this->assertSame(20, (new RangePerPage())->value(null)); + $this->assertSame(30, (new RangePerPage())->value(30)); + $this->assertSame(100, (new RangePerPage())->value(200)); + $this->assertSame(90, (new RangePerPage())->value(90)); + $this->assertSame(1, (new RangePerPage())->value(-10)); + } +} diff --git a/tests/Grid/PerPage/SetPerPageTest.php b/tests/Grid/PerPage/SetPerPageTest.php new file mode 100644 index 0000000..09149fc --- /dev/null +++ b/tests/Grid/PerPage/SetPerPageTest.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Collection\Tests\Grid\PerPage; + +use PHPUnit\Framework\TestCase; +use Zenstruck\Collection\Grid\PerPage\SetPerPage; + +/** + * @author Kevin Bond + */ +final class SetPerPageTest extends TestCase +{ + /** + * @test + */ + public function value(): void + { + $this->assertSame(20, (new SetPerPage())->value(null)); + $this->assertSame(20, (new SetPerPage())->value(20)); + $this->assertSame(20, (new SetPerPage())->value(21)); + $this->assertSame(50, (new SetPerPage())->value(50)); + $this->assertSame(100, (new SetPerPage())->value(100)); + $this->assertSame(20, (new SetPerPage())->value(1000)); + } +} diff --git a/tests/Symfony/Fixture/Grid/Grid1Definition.php b/tests/Symfony/Fixture/Grid/Grid1Definition.php new file mode 100644 index 0000000..82b9788 --- /dev/null +++ b/tests/Symfony/Fixture/Grid/Grid1Definition.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Collection\Tests\Symfony\Fixture\Grid; + +use Zenstruck\Collection\Grid\GridBuilder; +use Zenstruck\Collection\Grid\GridDefinition; +use Zenstruck\Collection\Symfony\Attributes\AsGrid; +use Zenstruck\Collection\Tests\Symfony\Fixture\Repository\PostRepository; + +/** + * @author Kevin Bond + */ +#[AsGrid('grid1')] +final class Grid1Definition implements GridDefinition +{ + public function __construct(private PostRepository $repository) + { + } + + public function configure(GridBuilder $builder): void + { + $builder->source = $this->repository; + $builder->addColumn('id'); + } +} diff --git a/tests/Symfony/Fixture/Grid/Grid2Definition.php b/tests/Symfony/Fixture/Grid/Grid2Definition.php new file mode 100644 index 0000000..1d343a1 --- /dev/null +++ b/tests/Symfony/Fixture/Grid/Grid2Definition.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Collection\Tests\Symfony\Fixture\Grid; + +use Zenstruck\Collection\Grid\GridBuilder; +use Zenstruck\Collection\Grid\GridDefinition; +use Zenstruck\Collection\Specification\Filter\GreaterThan; +use Zenstruck\Collection\Symfony\Attributes\ForObject; +use Zenstruck\Collection\Tests\Symfony\Fixture\Entity\Post; + +/** + * @author Kevin Bond + */ +#[ForObject(Post::class)] +final class Grid2Definition implements GridDefinition +{ + public function configure(GridBuilder $builder): void + { + $builder->addColumn('id'); + $builder->defaultSpecification = new GreaterThan('id', 1); + } +} diff --git a/tests/Symfony/Fixture/Grid/Grid3Definition.php b/tests/Symfony/Fixture/Grid/Grid3Definition.php new file mode 100644 index 0000000..833d320 --- /dev/null +++ b/tests/Symfony/Fixture/Grid/Grid3Definition.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Collection\Tests\Symfony\Fixture\Grid; + +use Zenstruck\Collection\Grid\GridBuilder; +use Zenstruck\Collection\Grid\GridDefinition; +use Zenstruck\Collection\Specification\Filter\GreaterThan; +use Zenstruck\Collection\Symfony\Attributes\AsGrid; +use Zenstruck\Collection\Symfony\Attributes\ForObject; +use Zenstruck\Collection\Tests\Symfony\Fixture\Entity\Post; + +/** + * @author Kevin Bond + */ +#[ForObject(Post::class)] +#[AsGrid('grid3')] +final class Grid3Definition implements GridDefinition +{ + public function configure(GridBuilder $builder): void + { + $builder->addColumn('id'); + $builder->defaultSpecification = new GreaterThan('id', 2); + } +} diff --git a/tests/Symfony/Fixture/Service2.php b/tests/Symfony/Fixture/Service2.php index f638553..1543928 100644 --- a/tests/Symfony/Fixture/Service2.php +++ b/tests/Symfony/Fixture/Service2.php @@ -12,7 +12,7 @@ namespace Zenstruck\Collection\Tests\Symfony\Fixture; use Zenstruck\Collection\Doctrine\ObjectRepository; -use Zenstruck\Collection\Symfony\Doctrine\ForObject; +use Zenstruck\Collection\Symfony\Attributes\ForObject; use Zenstruck\Collection\Tests\Symfony\Fixture\Entity\Category; /** diff --git a/tests/Symfony/Fixture/Service3.php b/tests/Symfony/Fixture/Service3.php new file mode 100644 index 0000000..8398c00 --- /dev/null +++ b/tests/Symfony/Fixture/Service3.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Collection\Tests\Symfony\Fixture; + +use Zenstruck\Collection\Grid; +use Zenstruck\Collection\Symfony\Attributes\ForDefinition; +use Zenstruck\Collection\Symfony\Grid\GridFactory; +use Zenstruck\Collection\Tests\Symfony\Fixture\Entity\Post; + +/** + * @author Kevin Bond + */ +final class Service3 +{ + public function __construct( + GridFactory $factory, + + #[ForDefinition('grid1')] + public Grid $grid1, + + #[ForDefinition(Post::class)] + public Grid $grid2, + + #[ForDefinition('grid3')] + public Grid $grid3, + ) { + } +} diff --git a/tests/Symfony/Fixture/TestKernel.php b/tests/Symfony/Fixture/TestKernel.php index c95233e..6c8421c 100644 --- a/tests/Symfony/Fixture/TestKernel.php +++ b/tests/Symfony/Fixture/TestKernel.php @@ -11,7 +11,6 @@ namespace Zenstruck\Collection\Tests\Symfony\Fixture; -use Composer\InstalledVersions; use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; use Psr\Log\NullLogger; use Symfony\Bundle\FrameworkBundle\FrameworkBundle; @@ -21,6 +20,9 @@ use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; use Zenstruck\Collection\Symfony\ZenstruckCollectionBundle; +use Zenstruck\Collection\Tests\Symfony\Fixture\Grid\Grid1Definition; +use Zenstruck\Collection\Tests\Symfony\Fixture\Grid\Grid2Definition; +use Zenstruck\Collection\Tests\Symfony\Fixture\Grid\Grid3Definition; use Zenstruck\Collection\Tests\Symfony\Fixture\Repository\PostRepository; use Zenstruck\Foundry\ZenstruckFoundryBundle; @@ -79,14 +81,28 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load ->setAutowired(true) ->setAutoconfigured(true) ; - - if (\version_compare(InstalledVersions::getVersion('symfony/dependency-injection'), '6.3.0', '>=')) { - $c->register(Service2::class) - ->setPublic(true) - ->setAutowired(true) - ->setAutoconfigured(true) - ; - } + $c->register(Service2::class) + ->setPublic(true) + ->setAutowired(true) + ->setAutoconfigured(true) + ; + $c->register(Grid1Definition::class) + ->setAutowired(true) + ->setAutoconfigured(true) + ; + $c->register(Grid2Definition::class) + ->setAutowired(true) + ->setAutoconfigured(true) + ; + $c->register(Grid3Definition::class) + ->setAutowired(true) + ->setAutoconfigured(true) + ; + $c->register(Service3::class) + ->setPublic(true) + ->setAutowired(true) + ->setAutoconfigured(true) + ; } protected function configureRoutes(RoutingConfigurator $routes): void diff --git a/tests/Symfony/ZenstruckCollectionBundleTest.php b/tests/Symfony/ZenstruckCollectionBundleTest.php index 91160b4..669d6b1 100644 --- a/tests/Symfony/ZenstruckCollectionBundleTest.php +++ b/tests/Symfony/ZenstruckCollectionBundleTest.php @@ -12,10 +12,12 @@ namespace Zenstruck\Collection\Tests\Symfony; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Symfony\Component\HttpFoundation\Request; use Zenstruck\Collection\Tests\Symfony\Fixture\Entity\Category; use Zenstruck\Collection\Tests\Symfony\Fixture\Entity\Post; use Zenstruck\Collection\Tests\Symfony\Fixture\Service1; use Zenstruck\Collection\Tests\Symfony\Fixture\Service2; +use Zenstruck\Collection\Tests\Symfony\Fixture\Service3; use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Test\ResetDatabase; @@ -31,7 +33,7 @@ final class ZenstruckCollectionBundleTest extends KernelTestCase /** * @test */ - public function autowiring(): void + public function doctrine_autowiring(): void { create(Post::class, ['id' => 1]); create(Category::class, ['id' => 2]); @@ -48,7 +50,7 @@ public function autowiring(): void /** * @test */ - public function autowiring_for_object(): void + public function doctrine_autowiring_for_object(): void { create(Category::class, ['id' => 2]); @@ -61,4 +63,23 @@ public function autowiring_for_object(): void $this->assertInstanceOf(Category::class, $service2->categoryRepo->find(2)); $this->assertSame($service2->categoryRepo, $service1->factory->create(Category::class)); } + + /** + * @test + */ + public function grid_autowiring(): void + { + create(Post::class, ['id' => 1]); + create(Post::class, ['id' => 2]); + create(Post::class, ['id' => 3]); + + self::getContainer()->get('request_stack')->push(Request::create('/foo')); + + /** @var Service3 $service3 */ + $service3 = self::getContainer()->get(Service3::class); + + $this->assertCount(3, $service3->grid1->page()); + $this->assertCount(2, $service3->grid2->page()); + $this->assertCount(1, $service3->grid3->page()); + } }