Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
- Enh #917: Rename `ColumnSchemaInterface` to `ColumnInterface` (@Tigrov)
- Enh #919: Replace `name()` with immutable `withName()` method in `ColumnInterface` interface (@Tigrov)
- Enh #921: Move `DataType` class to `Yiisoft\Db\Constant` namespace (@Tigrov)
- Enh #926: Refactor `DbArrayHelper` (@Tigrov)
- Enh #926, #954: Refactor `DbArrayHelper` (@Tigrov)
- Enh #920: Move index constants to the appropriate DBMS driver's `IndexType` and `IndexMethod` classes (@Tigrov)
- New #928: Add `ReferentialAction` class with constants of possible values of referential actions (@Tigrov)
- Enh #929: Refactor array, structured and JSON column type expressions and expression builders (@Tigrov)
Expand All @@ -75,6 +75,7 @@
- New #942: Allow PHP backed enums as values (@Tigrov)
- Enh #943: Add `getCacheKey()` and `getCacheTag()` methods to `AbstractPdoSchema` class (@Tigrov)
- Enh #925, #951: Add callback to `Query::all()` and `Query::one()` methods (@Tigrov, @vjik)
- New #954: Add `DbArrayHelper::arrange()` method (@Tigrov)

## 1.3.0 March 21, 2024

Expand Down
1 change: 1 addition & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ Each table column has its own class in the `Yiisoft\Db\Schema\Column` namespace
- `SchemaInterface::hasTable()` - returns whether the specified table exists in database;
- `SchemaInterface::hasSchema()` - returns whether the specified schema exists in database;
- `SchemaInterface::hasView()` - returns whether the specified view exists in database;
- `DbArrayHelper::arrange()` - arranges an array by specified keys;

### Remove methods

Expand Down
186 changes: 123 additions & 63 deletions src/Helper/DbArrayHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,12 @@
* Array manipulation methods.
*
* @psalm-import-type IndexBy from QueryInterface
* @psalm-import-type ResultCallback from QueryInterface
*/
final class DbArrayHelper
{
/**
* Indexes and/or groups the array according to a specified key.
*
* The input should be either a multidimensional array or an array of objects.
*
* The $key can be either a key name of the sub-array, a property name of an object, or an anonymous function that
* must return the value that will be used as a key.
*
* $groups is an array of keys, that will be used to group the input array into one or more sub-arrays based on keys
* specified.
*
* If the `$key` is specified as `null` or a value of an element corresponding to the key is `null` in addition
* to `$groups` not specified then the element is discarded.
* Arranges the array of rows according to specified keys.
*
* For example:
*
Expand All @@ -49,27 +39,10 @@ final class DbArrayHelper
* ['id' => '345', 'data' => 'def', 'device' => 'tablet'],
* ['id' => '345', 'data' => 'hgi', 'device' => 'smartphone'],
* ];
* $result = DbArrayHelper::index($array, 'id');
* $result = DbArrayHelper::arrange($rows, ['id']);
* ```
*
* The result will be an associative array, where the key is the value of `id` attribute
*
* ```php
* [
* '123' => ['id' => '123', 'data' => 'abc', 'device' => 'laptop'],
* '345' => ['id' => '345', 'data' => 'hgi', 'device' => 'smartphone']
* // The second element of an original array is overwritten by the last element because of the same id
* ]
* ```
*
* Passing `id` as a third argument will group `$array` by `id`:
*
* ```php
* $result = DbArrayHelper::index($array, null, 'id');
* ```
*
* The result will be a multidimensional array grouped by `id` on the first level, by `device` on the second level
* and indexed by `data` on the third level:
* The result will be a multidimensional array arranged by `id`:
*
* ```php
* [
Expand All @@ -83,8 +56,14 @@ final class DbArrayHelper
* ]
* ```
*
* The result will be a multidimensional array grouped by `id` on the first level, by the `device` on the second one
* and indexed by the `data` on the third level:
* Another example:
*
* ```php
* $result = DbArrayHelper::arrange($rows, ['id', 'device'], 'data');
* ```
*
* The result will be a multidimensional array arranged by `id` on the first level, by `device` on the second level
* and indexed by `data` on the third level:
*
* ```php
* [
Expand All @@ -104,35 +83,37 @@ final class DbArrayHelper
* ]
* ```
*
* @param array[] $array The array that needs to be indexed or arranged.
* @param Closure|string|null $indexBy The column name or anonymous function which result will be used to index the
* array. If the array does not have the key, the ordinal indexes will be used if `$arrangeBy` is not specified or
* a warning will be triggered if `$arrangeBy` is specified.
* @param array[] $rows The array of rows that needs to be arranged.
* @param string[] $arrangeBy The array of keys that will be used to arrange the input array by one or more keys.
* @param Closure|string|null $indexBy The column name or anonymous function which result will be used to index the
* array.
* @param Closure|null $resultCallback The callback function that will be called with the result array. This can be
* used to modify the result before returning it.
*
* @return array[] The indexed and/or arranged array.
* @return (array|object)[] The arranged array.
*
* @psalm-param list<array> $rows
* @psalm-param IndexBy|null $indexBy
* @psalm-suppress MixedArrayAssignment
* @psalm-param ResultCallback|null $resultCallback
*/
public static function index(array $array, Closure|string|null $indexBy = null, array $arrangeBy = []): array
{
if (empty($array) || $indexBy === null && empty($arrangeBy)) {
return $array;
public static function arrange(
array $rows,
array $arrangeBy = [],
Closure|string|null $indexBy = null,
Closure|null $resultCallback = null,
): array {
if (empty($rows)) {
return [];
}

if (empty($arrangeBy)) {
if (is_string($indexBy)) {
return array_column($array, null, $indexBy);
}

return array_combine(array_map($indexBy, $array), $array);
return self::index($rows, $indexBy, $resultCallback);
}

$result = [];
$arranged = [];

foreach ($array as $element) {
$lastArray = &$result;
foreach ($rows as $element) {
$lastArray = &$arranged;

foreach ($arrangeBy as $group) {
$value = (string) $element[$group];
Expand All @@ -144,23 +125,79 @@ public static function index(array $array, Closure|string|null $indexBy = null,
$lastArray = &$lastArray[$value];
}

if ($indexBy === null) {
$lastArray[] = $element;
} else {
if (is_string($indexBy)) {
$value = $element[$indexBy];
} else {
$value = $indexBy($element);
}
/** @psalm-suppress MixedArrayAssignment */
$lastArray[] = $element;

$lastArray[(string) $value] = $element;
unset($lastArray);
}

/** @var array[] $arranged */
if ($indexBy !== null || $resultCallback !== null) {
self::indexArranged($arranged, $indexBy, $resultCallback, count($arrangeBy));
}

return $arranged;
}

/**
* Indexes the array of rows according to a specified key.
*
* For example:
*
* ```php
* $array = [
* ['id' => '123', 'data' => 'abc', 'device' => 'laptop'],
* ['id' => '345', 'data' => 'def', 'device' => 'tablet'],
* ['id' => '345', 'data' => 'hgi', 'device' => 'smartphone'],
* ];
* $result = DbArrayHelper::index($array, 'id');
* ```
*
* The result will be an associative array, where the key is the value of `id` attribute
*
* ```php
* [
* '123' => ['id' => '123', 'data' => 'abc', 'device' => 'laptop'],
* '345' => ['id' => '345', 'data' => 'hgi', 'device' => 'smartphone']
* // The second element of an original array is overwritten by the last element because of the same id
* ]
* ```
*
* @param array[] $rows The array of rows that needs to be indexed.
* @param Closure|string|null $indexBy The column name or anonymous function which result will be used to index the
* array.
* @param Closure|null $resultCallback The callback function that will be called with the result array. This can be
* used to modify the result before returning it.
*
* @return (array|object)[] The indexed array.
*
* @psalm-param list<array> $rows
* @psalm-param IndexBy|null $indexBy
* @psalm-param ResultCallback|null $resultCallback
*/
public static function index(
array $rows,
Closure|string|null $indexBy = null,
Closure|null $resultCallback = null,
): array {
if (empty($rows)) {
return [];
}

if ($indexBy !== null) {
if (is_string($indexBy)) {
$indexes = array_column($rows, $indexBy);
} else {
$indexes = array_map($indexBy, $rows);
}
}

unset($lastArray);
if ($resultCallback !== null) {
$rows = ($resultCallback)($rows);
}

/** @var array[] $result */
return $result;
/** @psalm-suppress MixedArgument */
return !empty($indexes) ? array_combine($indexes, $rows) : $rows;
}

/**
Expand Down Expand Up @@ -249,4 +286,27 @@ public static function toArray(array|object $object): array

return get_object_vars($object);
}

/**
* Recursively indexes the arranged array.
*
* @psalm-assert (array|object)[] $arranged
* @psalm-param IndexBy|null $indexBy
* @psalm-param ResultCallback|null $resultCallback
*/
private static function indexArranged(
array &$arranged,
Closure|string|null $indexBy,
Closure|null $resultCallback,
int $depth,
): void {
/** @var list<array> $rows */
foreach ($arranged as &$rows) {
if ($depth > 1) {
self::indexArranged($rows, $indexBy, $resultCallback, $depth - 1);
} else {
$rows = self::index($rows, $indexBy, $resultCallback);
}
}
}
}
21 changes: 3 additions & 18 deletions src/Query/Query.php
Original file line number Diff line number Diff line change
Expand Up @@ -238,24 +238,7 @@ public function all(): array

$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;
return DbArrayHelper::index($rows, $this->indexBy, $this->resultCallback);
}

public function average(string $sql): int|float|null|string
Expand All @@ -268,6 +251,7 @@ public function average(string $sql): int|float|null|string

public function batch(int $batchSize = 100): BatchQueryResultInterface
{
/** @psalm-suppress InvalidArgument, ArgumentTypeCoercion */
return $this->db
->createBatchQueryResult($this)
->batchSize($batchSize)
Expand Down Expand Up @@ -345,6 +329,7 @@ public function distinct(bool|null $value = true): static

public function each(int $batchSize = 100): BatchQueryResultInterface
{
/** @psalm-suppress InvalidArgument, ArgumentTypeCoercion */
return $this->db
->createBatchQueryResult($this, true)
->batchSize($batchSize)
Expand Down
Loading