Skip to content

Commit

Permalink
feat(QueryBuilderAdapter): add indexBy parameter (#96)
Browse files Browse the repository at this point in the history
* feat(`QueryBuilderAdapter`): add `indexBy` parameter

* remove flex
  • Loading branch information
priyadi authored Jun 22, 2024
1 parent 10f3491 commit 2d2c614
Show file tree
Hide file tree
Showing 11 changed files with 268 additions and 8 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

* fix(`PagerItem`): `withPageNumber` should return static
* build: update php-cs-fixer
* feat(`QueryBuilderAdapter`): add `indexBy` parameter

# 0.11.2

Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
"symfony/debug-bundle": "^6.4 || ^7.0",
"symfony/doctrine-bridge": "^6.4 || ^7.0",
"symfony/dotenv": "^6.4 || ^7.0",
"symfony/error-handler": "^6.4 || ^7.0",
"symfony/form": "^6.4 || ^7.0",
"symfony/framework-bundle": "^6.4 || ^7.0",
"symfony/http-client": "^6.4 || ^7.0",
Expand Down
1 change: 1 addition & 0 deletions packages/rekapager-doctrine-orm-adapter/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"require": {
"php": "^8.2",
"doctrine/orm": "^2.14 || ^3.0",
"doctrine/collections": "^2.2",
"rekalogika/rekapager-keyset-pagination": "^0.11.2"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

/*
* This file is part of rekalogika/rekapager package.
*
* (c) Priyadi Iman Nurcahyo <https://rekalogika.dev>
*
* For the full copyright and license information, please view the LICENSE file
* that was distributed with this source code.
*/

namespace Rekalogika\Rekapager\Doctrine\ORM\Exception;

use Rekalogika\Contracts\Rekapager\Exception\LogicException;

class CannotResolveIndexException extends LogicException
{
public function __construct(mixed $row, string $indexBy, \Throwable $previous)
{
parent::__construct(sprintf(
'Unable to resolve the index "%s" from the result row of type "%s".',
$indexBy,
get_debug_type($row),
), 0, $previous);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

/*
* This file is part of rekalogika/rekapager package.
*
* (c) Priyadi Iman Nurcahyo <https://rekalogika.dev>
*
* For the full copyright and license information, please view the LICENSE file
* that was distributed with this source code.
*/

namespace Rekalogika\Rekapager\Doctrine\ORM\Exception;

use Rekalogika\Contracts\Rekapager\Exception\UnexpectedValueException;

class IncompatibleKeyTypeException extends UnexpectedValueException
{
public function __construct(mixed $row, string $indexBy, mixed $key)
{
parent::__construct(sprintf(
'Trying to get the index "%s" from the result row of type "%s", but the resulting index has the type of "%s". The resulting index must be an integer, string, or a "Stringable" object.',
$indexBy,
get_debug_type($row),
get_debug_type($key),
));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

/*
* This file is part of rekalogika/rekapager package.
*
* (c) Priyadi Iman Nurcahyo <https://rekalogika.dev>
*
* For the full copyright and license information, please view the LICENSE file
* that was distributed with this source code.
*/

namespace Rekalogika\Rekapager\Doctrine\ORM\Exception;

use Rekalogika\Contracts\Rekapager\Exception\UnexpectedValueException;

class RowNotCompatibleWithIndexByException extends UnexpectedValueException
{
public function __construct(mixed $row, string $indexBy)
{
parent::__construct(sprintf('Your query returns rows of type "%s", but it is not compatible with the index by "%s". The row must be an array or an object with a property named "%s".', get_debug_type($row), $indexBy, $indexBy));
}
}
53 changes: 53 additions & 0 deletions packages/rekapager-doctrine-orm-adapter/src/Internal/Utils.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

declare(strict_types=1);

/*
* This file is part of rekalogika/rekapager package.
*
* (c) Priyadi Iman Nurcahyo <https://rekalogika.dev>
*
* For the full copyright and license information, please view the LICENSE file
* that was distributed with this source code.
*/

namespace Rekalogika\Rekapager\Doctrine\ORM\Internal;

use Doctrine\Common\Collections\Expr\ClosureExpressionVisitor;
use Rekalogika\Rekapager\Doctrine\ORM\Exception\CannotResolveIndexException;
use Rekalogika\Rekapager\Doctrine\ORM\Exception\IncompatibleKeyTypeException;
use Rekalogika\Rekapager\Doctrine\ORM\Exception\RowNotCompatibleWithIndexByException;

/**
* @internal
*/
final class Utils
{
private function __construct()
{
}

public static function resolveIndex(mixed $row, string $indexBy): int|string
{
if (!\is_array($row) && !\is_object($row)) {
throw new RowNotCompatibleWithIndexByException($row, $indexBy);
}

try {
/** @var mixed */
$key = ClosureExpressionVisitor::getObjectFieldValue($row, $indexBy);
} catch (\Throwable $e) {
throw new CannotResolveIndexException($row, $indexBy, $e);
}

if (!\is_string($key) && !\is_int($key) && !$key instanceof \Stringable) {
throw new IncompatibleKeyTypeException($row, $indexBy, $key);
}

if ($key instanceof \Stringable) {
$key = (string) $key;
}

return $key;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
use Rekalogika\Rekapager\Doctrine\ORM\Exception\UnsupportedQueryBuilderException;
use Rekalogika\Rekapager\Doctrine\ORM\Internal\QueryBuilderKeysetItem;
use Rekalogika\Rekapager\Doctrine\ORM\Internal\QueryCounter;
use Rekalogika\Rekapager\Doctrine\ORM\Internal\Utils;
use Rekalogika\Rekapager\Keyset\Contracts\BoundaryType;
use Rekalogika\Rekapager\Keyset\KeysetPaginationAdapterInterface;
use Symfony\Bridge\Doctrine\Types\UlidType;
Expand All @@ -44,8 +45,9 @@ final class QueryBuilderAdapter implements KeysetPaginationAdapterInterface
*/
public function __construct(
private readonly QueryBuilder $queryBuilder,
private array $typeMapping = [],
private bool|null $useOutputWalkers = null,
private readonly array $typeMapping = [],
private readonly bool|null $useOutputWalkers = null,
private readonly string|null $indexBy = null,
) {
if ($queryBuilder->getFirstResult() !== 0 || $queryBuilder->getMaxResults() !== null) {
throw new UnsupportedQueryBuilderException();
Expand Down Expand Up @@ -264,9 +266,7 @@ public function getKeysetItems(
$boundaryFieldNames = $this->getBoundaryFieldNames();
$results = [];

$i = 0;

foreach ($result as $row) {
foreach ($result as $key => $row) {
/** @var array<string,mixed> */
$boundaryValues = [];
foreach (array_reverse($boundaryFieldNames) as $field) {
Expand All @@ -281,9 +281,11 @@ public function getKeysetItems(
$row = array_pop($row);
}

$results[] = new QueryBuilderKeysetItem($i, $row, $boundaryValues);
if ($this->indexBy !== null) {
$key = Utils::resolveIndex($row, $this->indexBy);
}

$i++;
$results[] = new QueryBuilderKeysetItem($key, $row, $boundaryValues);
}

/**
Expand Down
1 change: 1 addition & 0 deletions tests/config/packages/framework.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ framework:
http_method_override: false
handle_all_throwables: true
php_errors:
throw: true
log: true
uid:
default_uuid_version: 7
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ public function generatePageable(
queryBuilder: $queryBuilder,
typeMapping: [
'p.date' => Types::DATE_MUTABLE
]
],
indexBy: 'id'
);

$pageable = new KeysetPageable(
Expand Down
119 changes: 119 additions & 0 deletions tests/src/IntegrationTests/Index/QueryBuilderAdapterIndexByTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<?php

declare(strict_types=1);

/*
* This file is part of rekalogika/rekapager package.
*
* (c) Priyadi Iman Nurcahyo <https://rekalogika.dev>
*
* For the full copyright and license information, please view the LICENSE file
* that was distributed with this source code.
*/

namespace Rekalogika\Rekapager\Tests\IntegrationTests\Index;

use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\QueryBuilder;
use Rekalogika\Rekapager\Doctrine\ORM\Exception\IncompatibleKeyTypeException;
use Rekalogika\Rekapager\Doctrine\ORM\Exception\RowNotCompatibleWithIndexByException;
use Rekalogika\Rekapager\Doctrine\ORM\QueryBuilderAdapter;
use Rekalogika\Rekapager\Keyset\KeysetPageable;
use Rekalogika\Rekapager\Tests\App\Entity\Post;
use Rekalogika\Rekapager\Tests\App\Repository\PostRepository;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;

class QueryBuilderAdapterIndexByTest extends KernelTestCase
{
protected function getQueryBuilder(): QueryBuilder
{
$postRepository = self::getContainer()->get(PostRepository::class);

return $postRepository
->createQueryBuilder('p')
->where('p.setName = :setName')
->setParameter('setName', 'large')
->addOrderBy('p.date', 'DESC')
->addOrderBy('p.title', 'ASC')
->addOrderBy('p.id', 'ASC');
}

public function testIndexBy(): void
{
$queryBuilder = $this->getQueryBuilder();

$adapter = new QueryBuilderAdapter(
queryBuilder: $queryBuilder,
typeMapping: [
'p.date' => Types::DATE_MUTABLE
],
indexBy: 'id'
);

$pageable = new KeysetPageable(
adapter: $adapter,
itemsPerPage: 1000,
);

/** @var Post $post */
foreach ($pageable->getFirstPage() as $key => $post) {
static::assertInstanceOf(Post::class, $post);
static::assertEquals($key, $post->getId());
}
}

public function testInvalidIndexBy(): void
{
$queryBuilder = $this->getQueryBuilder();

$adapter = new QueryBuilderAdapter(
queryBuilder: $queryBuilder,
typeMapping: [
'p.date' => Types::DATE_MUTABLE
],
indexBy: 'foo'
);

$pageable = new KeysetPageable(
adapter: $adapter,
itemsPerPage: 1000,
);

$this->expectException(IncompatibleKeyTypeException::class);

iterator_to_array($pageable->getFirstPage());
}

public function testIncompatibleRow(): void
{
$entityManager = self::getContainer()->get(EntityManagerInterface::class);

$queryBuilder = $entityManager
->createQueryBuilder()
->from(Post::class, 'p')
->select('p.id')
->where('p.setName = :setName')
->setParameter('setName', 'large')
->addOrderBy('p.date', 'DESC')
->addOrderBy('p.title', 'ASC')
->addOrderBy('p.id', 'ASC');

$adapter = new QueryBuilderAdapter(
queryBuilder: $queryBuilder,
typeMapping: [
'p.date' => Types::DATE_MUTABLE
],
indexBy: 'foo'
);

$pageable = new KeysetPageable(
adapter: $adapter,
itemsPerPage: 1000,
);

$this->expectException(RowNotCompatibleWithIndexByException::class);

iterator_to_array($pageable->getFirstPage());
}
}

0 comments on commit 2d2c614

Please sign in to comment.