diff --git a/docs/src/docs/usage.md b/docs/src/docs/usage.md index 8c9739e8..58554970 100644 --- a/docs/src/docs/usage.md +++ b/docs/src/docs/usage.md @@ -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). + diff --git a/src/Query/ArrayProxyQuery.php b/src/Query/ArrayProxyQuery.php new file mode 100644 index 00000000..8c4444ff --- /dev/null +++ b/src/Query/ArrayProxyQuery.php @@ -0,0 +1,71 @@ +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, + ); + } +} \ No newline at end of file diff --git a/src/Query/ArrayProxyQueryFactory.php b/src/Query/ArrayProxyQueryFactory.php new file mode 100644 index 00000000..43363559 --- /dev/null +++ b/src/Query/ArrayProxyQueryFactory.php @@ -0,0 +1,18 @@ +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') diff --git a/tests/Unit/Query/ArrayProxyQueryFactoryTest.php b/tests/Unit/Query/ArrayProxyQueryFactoryTest.php new file mode 100644 index 00000000..ec3f3723 --- /dev/null +++ b/tests/Unit/Query/ArrayProxyQueryFactoryTest.php @@ -0,0 +1,41 @@ +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]; + } +} diff --git a/tests/Unit/Query/ArrayProxyQueryTest.php b/tests/Unit/Query/ArrayProxyQueryTest.php new file mode 100644 index 00000000..43ebdcda --- /dev/null +++ b/tests/Unit/Query/ArrayProxyQueryTest.php @@ -0,0 +1,103 @@ + '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()); + } +} \ No newline at end of file