Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Array source #134

Merged
merged 2 commits into from
Oct 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions docs/src/docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,76 @@ Now, in the template, render the data table using the `data_table` function:
```

By default, the data table will look somewhat _ugly_, because we haven't configured the theme yet - see [theming](features/theming.md) documentation section.

## Using array as data source

:::warning In most cases, using array as data source is used only for fast prototyping!
Remember that paginating an array is not memory efficient, as every item is already loaded into memory.
If your data comes from a database, pass an instance of Doctrine ORM query builder instead.
:::

In some cases, you might want to use an array as a data source. This can be achieved by simply passing an array as data to the data table factory method:

```php
use App\Entity\Product;
use App\DataTable\Type\ProductDataTableType;
use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

class ProductController extends AbstractController
{
use DataTableFactoryAwareTrait;

public function index()
{
$products = [
new Product(id: 1, name: 'Product 1'),
new Product(id: 2, name: 'Product 2'),
new Product(id: 3, name: 'Product 3'),
];

$dataTable = $this->createDataTable(ProductDataTableType::class, $products);
}
}
```

Alternatively, you can manually create an instance of `ArrayProxyQuery` to provide total item count different from given array count.
This can be useful in cases where you're already retrieving paginated data and still want the data table to properly display the pagination controls:

```php
use App\Entity\Product;
use App\DataTable\Type\ProductDataTableType;
use Kreyu\Bundle\DataTableBundle\DataTableFactoryAwareTrait;
use Kreyu\Bundle\DataTableBundle\Pagination\PaginationData;
use Kreyu\Bundle\DataTableBundle\Query\ArrayProxyQuery;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

class ProductController extends AbstractController
{
use DataTableFactoryAwareTrait;

public function index()
{
$products = new ArrayProxyQuery(
data: [
new Product(id: 1, name: 'Product 1'),
new Product(id: 2, name: 'Product 2'),
new Product(id: 3, name: 'Product 3'),
],
totalItemCount: 25,
);

$dataTable = $this->createDataTable(ProductDataTableType::class, $products);

// For example, in this case, paginating with 3 items per page will result in 9 pages,
// because the proxy query now assumes there's 25 items in total, and the data array
// only represents results of a currently displayed page.
$dataTable->paginate(new PaginationData(page: 1, perPage: 3));
}
}
```

Sorting will perform `usort` on the given array, while paginating will simply slice the array.
However, **there are no built-in filters** for this proxy query, but you can implement
your own filter types - see [creating filter types](components/filters#creating-filter-types).

71 changes: 71 additions & 0 deletions src/Query/ArrayProxyQuery.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

declare(strict_types=1);

namespace Kreyu\Bundle\DataTableBundle\Query;

use Kreyu\Bundle\DataTableBundle\Pagination\PaginationData;
use Kreyu\Bundle\DataTableBundle\Sorting\SortingData;
use Symfony\Component\PropertyAccess\PropertyAccess;

class ArrayProxyQuery implements ProxyQueryInterface
{
private ?array $originalData = null;
private ?array $sortedData = null;

public function __construct(
private array $data,
private ?int $totalItemCount = null,
) {
$this->originalData = $this->data;
$this->sortedData = $this->data;
$this->totalItemCount ??= count($this->data);
}

public function sort(SortingData $sortingData): void
{
$propertyAccessor = PropertyAccess::createPropertyAccessor();

$this->originalData ??= $this->data;

$this->data = $this->originalData;

usort($this->data, function ($a, $b) use ($sortingData, $propertyAccessor) {
foreach ($sortingData->getColumns() as $sortingColumnData) {
$propertyPath = $sortingColumnData->getPropertyPath();
$direction = $sortingColumnData->getDirection();

$valueA = $propertyAccessor->getValue($a, $propertyPath);
$valueB = $propertyAccessor->getValue($b, $propertyPath);

if ($valueA < $valueB) {
return 'asc' === $direction ? -1 : 1;
} elseif ($valueA > $valueB) {
return 'asc' === $direction ? 1 : -1;
}
}

return 0;
});

$this->sortedData = $this->data;
}

public function paginate(PaginationData $paginationData): void
{
$this->data = array_slice(
$this->sortedData ?? $this->originalData,
$paginationData->getOffset(),
$paginationData->getPerPage(),
);
}

public function getResult(): ResultSetInterface
{
return new ResultSet(
iterator: new \ArrayIterator($this->data),
currentPageItemCount: count($this->data),
totalItemCount: $this->totalItemCount,
);
}
}
18 changes: 18 additions & 0 deletions src/Query/ArrayProxyQueryFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Kreyu\Bundle\DataTableBundle\Query;

class ArrayProxyQueryFactory implements ProxyQueryFactoryInterface
{
public function create(mixed $data): ProxyQueryInterface
{
return new ArrayProxyQuery($data);
}

public function supports(mixed $data): bool
{
return is_array($data);
}
}
6 changes: 6 additions & 0 deletions src/Resources/config/core.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Kreyu\Bundle\DataTableBundle\Persistence\PersistenceClearerInterface;
use Kreyu\Bundle\DataTableBundle\Persistence\StaticPersistenceSubjectProvider;
use Kreyu\Bundle\DataTableBundle\Persistence\TokenStoragePersistenceSubjectProvider;
use Kreyu\Bundle\DataTableBundle\Query\ArrayProxyQueryFactory;
use Kreyu\Bundle\DataTableBundle\Request\HttpFoundationRequestHandler;
use Kreyu\Bundle\DataTableBundle\Type\DataTableType;
use Kreyu\Bundle\DataTableBundle\Type\ResolvedDataTableTypeFactory;
Expand Down Expand Up @@ -68,6 +69,11 @@
->set('kreyu_data_table.request_handler.http_foundation', HttpFoundationRequestHandler::class)
;

$services
->set('kreyu_data_table.proxy_query.factory.array', ArrayProxyQueryFactory::class)
->tag('kreyu_data_table.proxy_query.factory')
;

$services
->set('kreyu_data_table.proxy_query.factory.doctrine_orm', DoctrineOrmProxyQueryFactory::class)
->tag('kreyu_data_table.proxy_query.factory')
Expand Down
41 changes: 41 additions & 0 deletions tests/Unit/Query/ArrayProxyQueryFactoryTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace Kreyu\Bundle\DataTableBundle\Tests\Unit\Query;

use Kreyu\Bundle\DataTableBundle\Query\ArrayProxyQuery;
use Kreyu\Bundle\DataTableBundle\Query\ArrayProxyQueryFactory;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

class ArrayProxyQueryFactoryTest extends TestCase
{
private ArrayProxyQueryFactory $factory;

protected function setUp(): void
{
$this->factory = new ArrayProxyQueryFactory();
}

public function testCreate()
{
$this->assertInstanceOf(ArrayProxyQuery::class, $this->factory->create([]));
}

#[DataProvider('provideSupportsCases')]
public function testSupports(mixed $data, bool $expected)
{
$this->assertEquals($expected, $this->factory->supports($data));
}

public static function provideSupportsCases(): iterable
{
yield 'array' => [[], true];
yield 'string' => ['', false];
yield 'integer' => [123, false];
yield 'bool' => [true, false];
yield 'null' => [null, false];
yield 'object' => [new \stdClass(), false];
}
}
103 changes: 103 additions & 0 deletions tests/Unit/Query/ArrayProxyQueryTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php

declare(strict_types=1);

namespace Kreyu\Bundle\DataTableBundle\Tests\Unit\Query;

use Kreyu\Bundle\DataTableBundle\Pagination\PaginationData;
use Kreyu\Bundle\DataTableBundle\Query\ArrayProxyQuery;
use Kreyu\Bundle\DataTableBundle\Sorting\SortingColumnData;
use Kreyu\Bundle\DataTableBundle\Sorting\SortingData;
use PHPUnit\Framework\TestCase;

class ArrayProxyQueryTest extends TestCase
{
public function testGetResult()
{
$query = new ArrayProxyQuery(
data: [['foo' => 'bar'], ['bar' => 'baz']],
);

$result = $query->getResult();

$this->assertEquals(
[['foo' => 'bar'], ['bar' => 'baz']],
iterator_to_array($result->getIterator()),
);

$this->assertEquals(2, $result->count());
$this->assertEquals(2, $result->getTotalItemCount());
$this->assertEquals(2, $result->getCurrentPageItemCount());
}

public function testGetResultWithTotalItemCountSet()
{
$query = new ArrayProxyQuery(
data: [['foo' => 'bar'], ['bar' => 'baz']],
totalItemCount: 25,
);

$result = $query->getResult();

$this->assertEquals(
[['foo' => 'bar'], ['bar' => 'baz']],
iterator_to_array($result->getIterator()),
);

$this->assertEquals(2, $result->count());
$this->assertEquals(25, $result->getTotalItemCount());
$this->assertEquals(2, $result->getCurrentPageItemCount());
}

public function testSort()
{
$query = new ArrayProxyQuery(
data: [['id' => 1], ['id' => 2], ['id' => 3]],
);

$query->sort(new SortingData([
new SortingColumnData('id', 'desc', '[id]'),
]));

$result = $query->getResult();

$this->assertEquals(
[['id' => 3], ['id' => 2], ['id' => 1]],
iterator_to_array($result->getIterator()),
);
}

public function testPaginate()
{
$query = new ArrayProxyQuery(
data: [['id' => 1], ['id' => 2], ['id' => 3]],
);

$query->paginate(new PaginationData(page: 2, perPage: 1));

$result = $query->getResult();

$this->assertEquals([['id' => 2]], iterator_to_array($result->getIterator()));
$this->assertEquals(1, $result->getCurrentPageItemCount());
$this->assertEquals(3, $result->getTotalItemCount());
}

public function testSortAndPaginate()
{
$query = new ArrayProxyQuery(
data: [['id' => 1], ['id' => 2], ['id' => 3]],
);

$query->sort(new SortingData([
new SortingColumnData('id', 'desc', '[id]'),
]));

$query->paginate(new PaginationData(page: 3, perPage: 1));

$result = $query->getResult();

$this->assertEquals([['id' => 1]], iterator_to_array($result->getIterator()));
$this->assertEquals(1, $result->getCurrentPageItemCount());
$this->assertEquals(3, $result->getTotalItemCount());
}
}
Loading