Skip to content

Commit

Permalink
feat: data grid (#43)
Browse files Browse the repository at this point in the history
  • Loading branch information
kbond authored Mar 7, 2024
1 parent 4e9aa29 commit cfad537
Show file tree
Hide file tree
Showing 38 changed files with 1,654 additions and 37 deletions.
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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).",
Expand Down
20 changes: 20 additions & 0 deletions config/symfony/doctrine.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use Zenstruck\Collection\Doctrine\ObjectRepositoryFactory;
use Zenstruck\Collection\Doctrine\ORM\EntityRepositoryFactory;
use Zenstruck\Collection\Symfony\Doctrine\ChainObjectRepositoryFactory;

return static function(ContainerConfigurator $configurator) {
$configurator->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')
;
};
16 changes: 16 additions & 0 deletions config/symfony/grid.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use Zenstruck\Collection\Symfony\Grid\GridFactory;

return static function(ContainerConfigurator $configurator) {
$configurator->services()
->set('.zenstruck_collection.grid_factory', GridFactory::class)
->args([
tagged_locator('zenstruck_collection.grid_definition', indexAttribute: 'key'),
])

->alias(GridFactory::class, '.zenstruck_collection.grid_factory')
;
};
46 changes: 46 additions & 0 deletions src/Collection/Doctrine/Grid/ObjectGridDefinition.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

/*
* This file is part of the zenstruck/collection package.
*
* (c) Kevin Bond <[email protected]>
*
* 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 <[email protected]>
*
* @implements GridDefinition<object>
*/
final class ObjectGridDefinition implements GridDefinition
{
/**
* @param class-string<object> $class
* @param GridDefinition<object> $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);
}
}
107 changes: 107 additions & 0 deletions src/Collection/Grid.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?php

/*
* This file is part of the zenstruck/collection package.
*
* (c) Kevin Bond <[email protected]>
*
* 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 <[email protected]>
*
* @template T of array<string,mixed>|object
*
* @implements \IteratorAggregate<T>
*/
final class Grid implements \IteratorAggregate
{
public readonly PerPage $perPage;

/** @var Page<int,T> */
private Page $page;

/**
* @internal
*
* @param Matchable<mixed,T> $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<int,T>
*/
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<object|null>
*/
private function filterSpecification(): array
{
return $this->filters->all()
->map(fn(Filter $filter, string $name) => $filter->apply($this->input->filter($name)))
->values()
->all()
;
}
}
73 changes: 73 additions & 0 deletions src/Collection/Grid/Column.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

/*
* This file is part of the zenstruck/collection package.
*
* (c) Kevin Bond <[email protected]>
*
* 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 <[email protected]>
*/
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());
}
}
82 changes: 82 additions & 0 deletions src/Collection/Grid/Columns.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

/*
* This file is part of the zenstruck/collection package.
*
* (c) Kevin Bond <[email protected]>
*
* 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 <[email protected]>
*
* @implements \IteratorAggregate<string,Column>
*/
final class Columns implements \IteratorAggregate, \Countable
{
private self $searchable;
private self $sortable;

/**
* @internal
*
* @param ArrayCollection<string,Column> $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<string,Column>
*/
public function all(): ArrayCollection
{
return $this->columns;
}

public function getIterator(): \Traversable
{
return $this->columns;
}

public function count(): int
{
return $this->columns->count();
}
}
25 changes: 25 additions & 0 deletions src/Collection/Grid/Definition/ColumnDefinition.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

/*
* This file is part of the zenstruck/collection package.
*
* (c) Kevin Bond <[email protected]>
*
* 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 <[email protected]>
*/
final class ColumnDefinition
{
public function __construct(
public string $name,
public bool $searchable = false,
public bool $sortable = false,
) {
}
}
Loading

0 comments on commit cfad537

Please sign in to comment.