diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f5a04842..cc58c5c17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,7 @@ - New #939: Add `caseSensitive` option to like condition (@vjik) - New #942: Allow PHP backed enums as values (@Tigrov) - Enh #943: Add `getCacheKey()` and `getCacheTag()` methods to `AbstractPdoSchema` class (@Tigrov) +- Enh #925: Add callback to `Query::all()` and `Query::one()` methods (@Tigrov) ## 1.3.0 March 21, 2024 diff --git a/UPGRADE.md b/UPGRADE.md index ab3639b34..ac451e5fd 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -117,6 +117,9 @@ Each table column has its own class in the `Yiisoft\Db\Schema\Column` namespace ### New methods - `QuoterInterface::getRawTableName()` - returns the raw table name without quotes; +- `QueryInterface::resultCallback()` - allows to use a callback, to be called on all rows of the query result; +- `QueryInterface::getResultCallback()` - returns the callback to be called on all rows of the query result or + `null` if the callback is not set; - `ConnectionInterface::getColumnFactory()` - returns the column factory object for concrete DBMS; - `ConnectionInterface::getServerInfo()` - returns `ServerInfoInterface` instance which provides server information; - `QueryBuilderInterface::buildColumnDefinition()` - builds column definition for `CREATE TABLE` statement; diff --git a/src/Query/Query.php b/src/Query/Query.php index 29438927a..3e37d40b1 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -74,6 +74,7 @@ * * @psalm-import-type SelectValue from QueryPartsInterface * @psalm-import-type IndexBy from QueryInterface + * @psalm-import-type ResultCallback from QueryInterface */ class Query implements QueryInterface { @@ -87,6 +88,8 @@ class Query implements QueryInterface protected array $join = []; protected array $orderBy = []; protected array $params = []; + /** @psalm-var ResultCallback|null $resultCallback */ + protected Closure|null $resultCallback = null; protected array $union = []; protected array $withQueries = []; /** @psalm-var IndexBy|null $indexBy */ @@ -233,7 +236,26 @@ public function all(): array return []; } - return DbArrayHelper::index($this->createCommand()->queryAll(), $this->indexBy); + $rows = $this->createCommand()->queryAll(); + + if (empty($rows)) { + return []; + } + + if ($this->indexBy !== null) { + if (is_string($this->indexBy)) { + $indexes = array_column($rows, $this->indexBy); + } else { + $indexes = array_map($this->indexBy, $rows); + } + } + + if ($this->resultCallback !== null) { + $rows = ($this->resultCallback)($rows); + } + + /** @psalm-suppress MixedArgument */ + return isset($indexes) ? array_combine($indexes, $rows) : $rows; } public function average(string $sql): int|float|null|string @@ -437,6 +459,11 @@ public function getParams(): array return $this->params; } + public function getResultCallback(): Closure|null + { + return $this->resultCallback; + } + public function getSelect(): array { return $this->select; @@ -539,10 +566,17 @@ public function offset(ExpressionInterface|int|null $offset): static public function one(): array|object|null { - return match ($this->emulateExecution) { - true => null, - false => $this->createCommand()->queryOne(), - }; + if ($this->emulateExecution) { + return null; + } + + $row = $this->createCommand()->queryOne(); + + if ($this->resultCallback === null || $row === null) { + return $row; + } + + return ($this->resultCallback)([$row])[0]; } public function orderBy(array|string|ExpressionInterface $columns): static @@ -610,6 +644,12 @@ public function prepare(QueryBuilderInterface $builder): QueryInterface return $this; } + public function resultCallback(Closure|null $resultCallback): static + { + $this->resultCallback = $resultCallback; + return $this; + } + public function rightJoin(array|string $table, array|string $on = '', array $params = []): static { $this->join[] = ['RIGHT JOIN', $table, $on]; diff --git a/src/Query/QueryInterface.php b/src/Query/QueryInterface.php index 6995afc2f..8c2dacb3b 100644 --- a/src/Query/QueryInterface.php +++ b/src/Query/QueryInterface.php @@ -30,6 +30,7 @@ * @psalm-type IndexBy = Closure(array):array-key|string * @psalm-import-type ParamsType from ConnectionInterface * @psalm-import-type SelectValue from QueryPartsInterface + * @psalm-type ResultCallback = Closure(non-empty-array):non-empty-array */ interface QueryInterface extends ExpressionInterface, QueryPartsInterface, QueryFunctionsInterface { @@ -210,6 +211,14 @@ public function getOrderBy(): array; */ public function getParams(): array; + /** + * Returns the callback to be called on all rows of the query result. + * `null` will be returned if the callback is not set. + * + * @psalm-return ResultCallback|null + */ + public function getResultCallback(): Closure|null; + /** * @return array The "select" value. * @psalm-return SelectValue @@ -288,6 +297,27 @@ public function params(array $params): static; */ public function prepare(QueryBuilderInterface $builder): self; + /** + * Sets the callback, to be called on all rows of the query result before returning them. + * + * For example: + * + * ```php + * $users = (new Query($db)) + * ->from('user') + * ->resultCallback(function (array $rows): array { + * foreach ($rows as &$row) { + * $row['name'] = strtoupper($row['name']); + * } + * return $rows; + * }) + * ->all(); + * ``` + * + * @psalm-param ResultCallback|null $resultCallback + */ + public function resultCallback(Closure|null $resultCallback): static; + /** * Returns the query results as a scalar value. * The value returned will be the first column in the first row of the query results. diff --git a/tests/AbstractQueryTest.php b/tests/AbstractQueryTest.php index a068e8715..dd96344e8 100644 --- a/tests/AbstractQueryTest.php +++ b/tests/AbstractQueryTest.php @@ -4,6 +4,7 @@ namespace Yiisoft\Db\Tests; +use Closure; use PHPUnit\Framework\TestCase; use Throwable; use Yiisoft\Db\Exception\Exception; @@ -824,4 +825,21 @@ public function testCountGreaterThanPhpIntMax(): void $this->assertSame('12345678901234567890', $query->count()); } + + public function testResultCallback(): void + { + $db = $this->getConnection(); + + $query = (new Query($db)); + + $this->assertNull($query->getResultCallback()); + + $query->resultCallback(fn (array $rows) => array_map(fn (array $row) => (object) $row, $rows)); + + $this->assertInstanceOf(Closure::class, $query->getResultCallback()); + + $query->resultCallback(null); + + $this->assertNull($query->getResultCallback()); + } } diff --git a/tests/Common/CommonQueryTest.php b/tests/Common/CommonQueryTest.php index 45f38bbe0..09e60600a 100644 --- a/tests/Common/CommonQueryTest.php +++ b/tests/Common/CommonQueryTest.php @@ -9,8 +9,38 @@ use Yiisoft\Db\Query\Query; use Yiisoft\Db\Tests\AbstractQueryTest; +use function array_keys; + abstract class CommonQueryTest extends AbstractQueryTest { + public function testAllEmpty(): void + { + $db = $this->getConnection(true); + + $query = (new Query($db))->from('customer')->where(['id' => 0]); + + $this->assertSame([], $query->all()); + } + + public function testAllWithIndexBy(): void + { + $db = $this->getConnection(true); + + $query = (new Query($db)) + ->from('customer') + ->indexBy('name'); + + $this->assertSame(['user1', 'user2', 'user3'], array_keys($query->all())); + + $query = (new Query($db)) + ->from('customer') + ->indexBy(fn (array $row) => $row['id'] * 2); + + $this->assertSame([2, 4, 6], array_keys($query->all())); + + $db->close(); + } + public function testColumnIndexByWithClosure() { $db = $this->getConnection(true); @@ -83,6 +113,35 @@ public function testSelectWithoutFrom() $db->close(); } + public function testCallbackAll(): void + { + $db = $this->getConnection(true); + + $query = (new Query($db)) + ->from('customer') + ->resultCallback(fn (array $rows) => array_map(fn (array $row) => (object) $row, $rows)); + + foreach ($query->all() as $row) { + $this->assertIsObject($row); + } + + $db->close(); + } + + public function testCallbackOne(): void + { + $db = $this->getConnection(true); + + $query = (new Query($db)) + ->from('customer') + ->where(['id' => 2]) + ->resultCallback(fn (array $rows) => [(object) $rows[0]]); + + $this->assertIsObject($query->one()); + + $db->close(); + } + public function testLikeDefaultCaseSensitive(): void { $db = $this->getConnection(true);