Skip to content

Commit

Permalink
feat: array source
Browse files Browse the repository at this point in the history
  • Loading branch information
Kreyu committed Oct 5, 2024
1 parent 2d859b6 commit 4e1153e
Show file tree
Hide file tree
Showing 6 changed files with 313 additions and 0 deletions.
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 $direction === 'asc' ? -1 : 1;
} elseif ($valueA > $valueB) {
return $direction === 'asc' ? 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);
}
}
7 changes: 7 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,12 @@
->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());
}
}

0 comments on commit 4e1153e

Please sign in to comment.