diff --git a/docs/components/pager/index.md b/docs/components/pager/index.md index c23393aa..c9a23af6 100644 --- a/docs/components/pager/index.md +++ b/docs/components/pager/index.md @@ -66,7 +66,14 @@ if ($pager->hasNextPage()) { } ``` -## Adapters +## Custom Adapters + +Creating Custom Adapters is easy. You can take a look at the available adapters +to see how easy it is. + +Please see the [Pager Contract](../../contracts/pager/index.md) to learn more. + +## Available Adapters ### ArrayAdapter @@ -97,10 +104,9 @@ $adapter = new CallableAdapter( ); ``` -### ArrayCollectionAdapter - +### ArrayCollectionAdapter (doctrine/collections) -!!! warning +!!! warning "Requires `sonsofphp/pager-doctrine-collections`" ```shell composer require sonsofphp/pager-doctrine-collections ``` @@ -115,3 +121,41 @@ $collection = new ArrayCollection(); $adapter = new ArrayCollectionAdapter($collection); ``` + +### QueryBuilderAdapter (doctrine/dbal) + +!!! warning "Requires `sonsofphp/pager-doctrine-dbal`" + ```shell + composer require sonsofphp/pager-doctrine-dbal + ``` + +```php +select('COUNT(e.id) as total'); +}); +``` + +### QueryBuilderAdapter (doctrine/orm) + +!!! warning "Requires `sonsofphp/pager-doctrine-orm`" + ```shell + composer require sonsofphp/pager-doctrine-orm + ``` + +```php +createQueryBuilder('e'); + +$adapter = new QueryBuilderAdapter($builder); +``` diff --git a/docs/contracts/pager/index.md b/docs/contracts/pager/index.md index 550b6de1..d2333a30 100644 --- a/docs/contracts/pager/index.md +++ b/docs/contracts/pager/index.md @@ -10,3 +10,37 @@ components. ```shell composer require sonsofphp/pager-contract ``` + +## AdapterInterface + +```php +assertInstanceOf(AdapterInterface::class, $adapter); + } + + /** + * @covers ::count + */ + public function testCount(): void + { + $adapter = new ArrayCollectionAdapter(new ArrayCollection(range(0, 9))); + + $this->assertCount(10, $adapter); + } + + /** + * @covers ::getSlice + */ + public function testGetSlice(): void + { + $adapter = new ArrayCollectionAdapter(new ArrayCollection(range(0, 9))); + + $this->assertSame([0], $adapter->getSlice(0, 1)); + } +} diff --git a/src/SonsOfPHP/Bridge/Doctrine/DBAL/Pager/QueryBuilderAdapter.php b/src/SonsOfPHP/Bridge/Doctrine/DBAL/Pager/QueryBuilderAdapter.php index 66ecfd23..7f7eefbb 100644 --- a/src/SonsOfPHP/Bridge/Doctrine/DBAL/Pager/QueryBuilderAdapter.php +++ b/src/SonsOfPHP/Bridge/Doctrine/DBAL/Pager/QueryBuilderAdapter.php @@ -8,20 +8,36 @@ use SonsOfPHP\Contract\Pager\AdapterInterface; /** + * Usage: + * $adapter = new QueryBuilderAdapter($queryBuilder, function (QueryBuilder $builder): void { + * $builder->select('COUNT(DISTINCT e.id) AS cnt'); + * }); + * * @author Joshua Estes */ class QueryBuilderAdapter implements AdapterInterface { + private $countQuery; + public function __construct( - private QueryBuilder $builder, - ) {} + private readonly QueryBuilder $builder, + callable $countQuery, + ) { + $this->countQuery = $countQuery; + } /** * {@inheritdoc} */ public function count(): int { - throw new \Exception('@todo'); + $builder = clone $this->builder; + $callable = $this->countQuery; + + $callable($builder); + $builder->setMaxResults(1); + + return (int) $builder->executeQuery()->fetchOne(); } /** @@ -29,6 +45,13 @@ public function count(): int */ public function getSlice(int $offset, ?int $length): iterable { - throw new \Exception('@todo'); + $builder = clone $this->builder; + + return $builder + ->setFirstResult($offset) + ->setMaxResults($length) + ->executeQuery() + ->fetchAllAssociative() + ; } } diff --git a/src/SonsOfPHP/Bridge/Doctrine/DBAL/Pager/Tests/QueryBuilderAdapterTest.php b/src/SonsOfPHP/Bridge/Doctrine/DBAL/Pager/Tests/QueryBuilderAdapterTest.php new file mode 100644 index 00000000..fb2050d0 --- /dev/null +++ b/src/SonsOfPHP/Bridge/Doctrine/DBAL/Pager/Tests/QueryBuilderAdapterTest.php @@ -0,0 +1,78 @@ +builder = $this->createMock(QueryBuilder::class); + $this->result = $this->createMock(Result::class); + } + + /** + * @covers ::__construct + */ + public function testItHasTheRightInterface(): void + { + $adapter = new QueryBuilderAdapter($this->builder, function (QueryBuilder $builder): void {}); + + $this->assertInstanceOf(AdapterInterface::class, $adapter); + } + + /** + * @covers ::count + */ + public function testCount(): void + { + $this->builder->method('executeQuery')->willReturn($this->result); + $this->builder->expects($this->once())->method('select'); + + $this->result->method('fetchOne')->willReturn(123); + + $adapter = new QueryBuilderAdapter($this->builder, function (QueryBuilder $builder): void { + $builder->select('COUNT()'); + }); + + $this->assertCount(123, $adapter); + } + + /** + * @covers ::getSlice + */ + public function testSlice(): void + { + $this->builder + ->expects($this->once()) + ->method('setFirstResult') + ->with($this->identicalTo(0)) + ->willReturn($this->builder) + ; + $this->builder + ->expects($this->once()) + ->method('setMaxResults') + ->with($this->identicalTo(100)) + ->willReturn($this->builder) + ; + + $adapter = new QueryBuilderAdapter($this->builder, function (QueryBuilder $builder): void {}); + + $adapter->getSlice(0, 100); + } +} diff --git a/src/SonsOfPHP/Bridge/Doctrine/ORM/Pager/QueryBuilderAdapter.php b/src/SonsOfPHP/Bridge/Doctrine/ORM/Pager/QueryBuilderAdapter.php new file mode 100644 index 00000000..2f7cb6e6 --- /dev/null +++ b/src/SonsOfPHP/Bridge/Doctrine/ORM/Pager/QueryBuilderAdapter.php @@ -0,0 +1,45 @@ + + */ +class QueryBuilderAdapter implements AdapterInterface +{ + private readonly Paginator $paginator; + + public function __construct( + QueryBuilder $builder, + ) { + $this->paginator = new Paginator($builder, fetchJoinCollection: true); + } + + /** + * {@inheritdoc} + */ + public function count(): int + { + return count($this->paginator); + } + + /** + * {@inheritdoc} + */ + public function getSlice(int $offset, ?int $length): iterable + { + $query = $this->paginator->getQuery(); + $query + ->setFirstResult($offset) + ->setMaxResults($length) + ; + + return $this->paginator->getIterator(); + } +} diff --git a/src/SonsOfPHP/Bridge/Doctrine/ORM/Pager/Tests/QueryBuilderAdapterTest.php b/src/SonsOfPHP/Bridge/Doctrine/ORM/Pager/Tests/QueryBuilderAdapterTest.php new file mode 100644 index 00000000..8536ffec --- /dev/null +++ b/src/SonsOfPHP/Bridge/Doctrine/ORM/Pager/Tests/QueryBuilderAdapterTest.php @@ -0,0 +1,35 @@ +builder = $this->createMock(QueryBuilder::class); + } + + /** + * @covers ::__construct + */ + public function testItHasTheRightInterface(): void + { + $adapter = new QueryBuilderAdapter($this->builder); + + $this->assertInstanceOf(AdapterInterface::class, $adapter); + } +} diff --git a/src/SonsOfPHP/Contract/Pager/AdapterInterface.php b/src/SonsOfPHP/Contract/Pager/AdapterInterface.php index 518c8187..a157d13e 100644 --- a/src/SonsOfPHP/Contract/Pager/AdapterInterface.php +++ b/src/SonsOfPHP/Contract/Pager/AdapterInterface.php @@ -20,7 +20,7 @@ interface AdapterInterface extends \Countable * Length is how many results to return. For example, if length is 10, a * MAXIMUM of 10 results will be returned * - * Length should always be a positive number that is 1 or greater + * Length MUST always be a positive number that is 1 or greater OR be null * * If null is passed in as length, this should return ALL the results. *