Skip to content

Commit

Permalink
Refactor DbArrayHelper (#926)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tigrov authored Jan 31, 2025
1 parent 8c77a84 commit 17cd981
Show file tree
Hide file tree
Showing 8 changed files with 131 additions and 281 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,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)

## 1.3.0 March 21, 2024

Expand Down
30 changes: 17 additions & 13 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,19 +124,22 @@ Each table column has its own class in the `Yiisoft\Db\Schema\Column` namespace

### Remove methods

- `AbstractQueryBuilder::getColumnType()`
- `AbstractDMLQueryBuilder::getTypecastValue()`
- `TableSchemaInterface::compositeForeignKey()`
- `SchemaInterface::createColumn()`
- `SchemaInterface::isReadQuery()`
- `SchemaInterface::getRawTableName()`
- `AbstractSchema::isReadQuery()`
- `AbstractSchema::getRawTableName()`
- `AbstractSchema::normalizeRowKeyCase()`
- `Quoter::unquoteParts()`
- `AbstractPdoCommand::logQuery()`
- `ColumnSchemaInterface::phpType()`
- `ConnectionInterface::getServerVersion()`
- `AbstractQueryBuilder::getColumnType()` - use `AbstractQueryBuilder::buildColumnDefinition()` instead;
- `AbstractDMLQueryBuilder::getTypecastValue()`;
- `TableSchemaInterface::compositeForeignKey()`;
- `SchemaInterface::createColumn()` - use `ColumnBuilder` instead;
- `SchemaInterface::isReadQuery()` - use `DbStringHelper::isReadQuery()` instead;
- `SchemaInterface::getRawTableName()` - use `QuoterInterface::getRawTableName()` instead;
- `AbstractSchema::isReadQuery()` - use `DbStringHelper::isReadQuery()` instead;
- `AbstractSchema::getRawTableName()` - use `QuoterInterface::getRawTableName()` instead;
- `AbstractSchema::normalizeRowKeyCase()` - use `array_change_key_case()` instead;
- `Quoter::unquoteParts()`;
- `AbstractPdoCommand::logQuery()`;
- `ColumnSchemaInterface::phpType()`;
- `ConnectionInterface::getServerVersion()` - use `ConnectionInterface::getServerInfo()` instead;
- `DbArrayHelper::getColumn()` - use `array_column()` instead;
- `DbArrayHelper::getValueByPath()`;
- `DbArrayHelper::populate()` - use `DbArrayHelper::index()` instead;

### Remove deprecated parameters

Expand Down Expand Up @@ -164,3 +167,4 @@ Each table column has its own class in the `Yiisoft\Db\Schema\Column` namespace
- Allow `QueryInterface::all()` to return array of objects;
- Change `Quoter::quoteValue()` parameter type and return type from `mixed` to `string`;
- Move `DataType` class to `Yiisoft\Db\Constant` namespace;
- Change `DbArrayHelper::index()` parameter names and allow to accept `Closure` for `$indexBy` parameter;
222 changes: 46 additions & 176 deletions src/Helper/DbArrayHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,139 +5,24 @@
namespace Yiisoft\Db\Helper;

use Closure;
use Exception;

use function array_key_exists;
use Yiisoft\Db\Query\QueryInterface;

use function array_column;
use function array_combine;
use function array_map;
use function array_multisort;
use function count;
use function is_array;
use function is_object;
use function property_exists;
use function is_string;
use function range;
use function strrpos;
use function substr;

/**
* Array manipulation methods.
*
* @psalm-import-type IndexBy from QueryInterface
*/
final class DbArrayHelper
{
/**
* Returns the values of a specified column in an array.
*
* The input array should be multidimensional or an array of objects.
*
* For example,
*
* ```php
* $array = [
* ['id' => '123', 'data' => 'abc'],
* ['id' => '345', 'data' => 'def'],
* ];
* $result = DbArrayHelper::getColumn($array, 'id');
* // the result is: ['123', '345']
*
* // using anonymous function
* $result = DbArrayHelper::getColumn($array, function ($element) {
* return $element['id'];
* });
* ```
*
* @param array $array Array to extract values from.
* @param string $name The column name.
*
* @return array The list of column values.
*/
public static function getColumn(array $array, string $name): array
{
return array_map(
static fn (array|object $element): mixed => self::getValueByPath($element, $name),
$array
);
}

/**
* Retrieves the value of an array element or object property with the given key or property name.
*
* If the key doesn't exist in the array, the default value will be returned instead.
*
* Not used when getting value from an object.
*
* The key may be specified in a dot format to retrieve the value of a sub-array or the property of an embedded
* object.
*
* In particular, if the key is `x.y.z`, then the returned value would be `$array['x']['y']['z']` or
* `$array->x->y->z` (if `$array` is an object).
*
* If `$array['x']` or `$array->x` is neither an array nor an object, the default value will be returned.
*
* Note that if the array already has an element `x.y.z`, then its value will be returned instead of going through
* the sub-arrays.
*
* So it's better to be done specifying an array of key names like `['x', 'y', 'z']`.
*
* Below are some usage examples.
*
* ```php
* // working with array
* $username = DbArrayHelper::getValueByPath($_POST, 'username');
* // working with object
* $username = DbArrayHelper::getValueByPath($user, 'username');
* // working with anonymous function
* $fullName = DbArrayHelper::getValueByPath($user, function ($user, $defaultValue) {
* return $user->firstName . ' ' . $user->lastName;
* });
* // using dot format to retrieve the property of embedded object
* $street = \yii\helpers\DbArrayHelper::getValue($users, 'address.street');
* // using an array of keys to retrieve the value
* $value = \yii\helpers\DbArrayHelper::getValue($versions, ['1.0', 'date']);
* ```
*
* @param array|object $array Array or object to extract value from.
* @param Closure|string $key Key name of the array element, an array of keys or property name of the object, or an
* anonymous function returning the value. The anonymous function signature should be:
* `function($array, $defaultValue)`.
* @param mixed|null $default The default value to be returned if the specified array key doesn't exist. Not used
* when getting value from an object.
*
* @return mixed The value of the element if found, default value otherwise
*/
public static function getValueByPath(object|array $array, Closure|string $key, mixed $default = null): mixed
{
if ($key instanceof Closure) {
return $key($array, $default);
}

if (is_object($array) && property_exists($array, $key)) {
return $array->$key;
}

if (is_array($array) && array_key_exists($key, $array)) {
return $array[$key];
}

if ($key && ($pos = strrpos($key, '.')) !== false) {
/** @psalm-var array<string, mixed>|object $array */
$array = self::getValueByPath($array, substr($key, 0, $pos), $default);
$key = substr($key, $pos + 1);
}

if (is_object($array)) {
/**
* This is expected to fail if the property doesn't exist, or __get() isn't implemented it isn't reliably
* possible to check whether a property is accessible beforehand
*/
return $array->$key;
}

if (array_key_exists($key, $array)) {
return $array[$key];
}

return $default;
}

/**
* Indexes and/or groups the array according to a specified key.
*
Expand Down Expand Up @@ -215,55 +100,62 @@ public static function getValueByPath(object|array $array, Closure|string $key,
* ]
* ```
*
* @param array $array The array that needs to be indexed or grouped.
* @param string|null $key The column name or anonymous function which result will be used to index the array.
* @param array $groups The array of keys that will be used to group the input array by one or more keys. If the
* $key attribute or its value for the particular element is null and $groups aren't defined.
* The array element will be discarded.
* Otherwise, if $groups are specified, an array element will be added to the result array without any key.
* @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 string[] $arrangeBy The array of keys that will be used to arrange the input array by one or more keys.
*
* @throws Exception
*
* @return array The indexed and/or grouped array.
*
* @psalm-param array[] $array The array that needs to be indexed or grouped.
* @psalm-param string[] $groups The array of keys that will be used to group the input array by one or more keys.
* @return array[] The indexed and/or arranged array.
*
* @psalm-param IndexBy|null $indexBy
* @psalm-suppress MixedArrayAssignment
*/
public static function index(array $array, string|null $key = null, array $groups = []): array
public static function index(array $array, Closure|string|null $indexBy = null, array $arrangeBy = []): array
{
if (empty($array) || $indexBy === null && empty($arrangeBy)) {
return $array;
}

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

return array_combine(array_map($indexBy, $array), $array);
}

$result = [];

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

foreach ($groups as $group) {
/** @psalm-var string $value */
$value = self::getValueByPath($element, $group);
if (!array_key_exists($value, $lastArray)) {
foreach ($arrangeBy as $group) {
$value = (string) $element[$group];

if (!isset($lastArray[$value])) {
$lastArray[$value] = [];
}

$lastArray = &$lastArray[$value];
}

if ($key === null) {
if (!empty($groups)) {
$lastArray[] = $element;
}
if ($indexBy === null) {
$lastArray[] = $element;
} else {
/** @psalm-var mixed $value */
$value = self::getValueByPath($element, $key);

if ($value !== null) {
$lastArray[(string) $value] = $element;
if (is_string($indexBy)) {
$value = $element[$indexBy];
} else {
$value = $indexBy($element);
}

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

unset($lastArray);
}

/** @psalm-var array $result */
/** @var array[] $result */
return $result;
}

Expand Down Expand Up @@ -298,6 +190,10 @@ public static function isAssociative(array $array): bool
*
* @param array $array The array to be sorted. The array will be modified after calling this method.
* @param string $key The key(s) to be sorted by.
*
* @psalm-template T
* @psalm-param array<T> $array
* @psalm-param-out array<T> $array
*/
public static function multisort(
array &$array,
Expand All @@ -307,7 +203,7 @@ public static function multisort(
return;
}

$column = self::getColumn($array, $key);
$column = array_column($array, $key);

array_multisort(
$column,
Expand All @@ -324,30 +220,4 @@ public static function multisort(
$array
);
}

/**
* Returns the value of an array element or object property with the given path.
*
* This method is internally used to convert the data fetched from a database into the format as required by this
* query.
*
* @param array[] $rows The raw query result from a database.
*
* @return array[]
*/
public static function populate(array $rows, Closure|string|null $indexBy = null): array
{
if ($indexBy === null) {
return $rows;
}

$result = [];

foreach ($rows as $row) {
/** @psalm-suppress MixedArrayOffset */
$result[self::getValueByPath($row, $indexBy)] = $row;
}

return $result;
}
}
3 changes: 2 additions & 1 deletion src/Query/BatchQueryResultInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
*
* @extends Iterator<int|string, mixed>
*
* @psalm-type PopulateClosure=Closure(array[],Closure|string|null): mixed
* @psalm-import-type IndexBy from QueryInterface
* @psalm-type PopulateClosure = Closure(array[],IndexBy|null): array[]
*/
interface BatchQueryResultInterface extends Iterator
{
Expand Down
9 changes: 5 additions & 4 deletions src/Query/Query.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
* Query internally uses the {@see \Yiisoft\Db\QueryBuilder\AbstractQueryBuilder} class to generate the SQL statement.
*
* @psalm-import-type SelectValue from QueryPartsInterface
* @psalm-import-type IndexBy from QueryInterface
*/
class Query implements QueryInterface
{
Expand All @@ -87,7 +88,7 @@ class Query implements QueryInterface
protected array $params = [];
protected array $union = [];
protected array $withQueries = [];
/** @psalm-var Closure(array):array-key|string|null $indexBy */
/** @psalm-var IndexBy|null $indexBy */
protected Closure|string|null $indexBy = null;
protected ExpressionInterface|int|null $limit = null;
protected ExpressionInterface|int|null $offset = null;
Expand Down Expand Up @@ -230,7 +231,7 @@ public function all(): array
return [];
}

return DbArrayHelper::populate($this->createCommand()->queryAll(), $this->indexBy);
return DbArrayHelper::index($this->createCommand()->queryAll(), $this->indexBy);
}

public function average(string $sql): int|float|null|string
Expand All @@ -246,7 +247,7 @@ public function batch(int $batchSize = 100): BatchQueryResultInterface
return $this->db
->createBatchQueryResult($this)
->batchSize($batchSize)
->setPopulatedMethod(fn (array $rows, Closure|string|null $indexBy = null): array => DbArrayHelper::populate($rows, $indexBy))
->setPopulatedMethod(fn (array $rows, Closure|string|null $indexBy = null): array => DbArrayHelper::index($rows, $indexBy))
;
}

Expand Down Expand Up @@ -323,7 +324,7 @@ public function each(int $batchSize = 100): BatchQueryResultInterface
return $this->db
->createBatchQueryResult($this, true)
->batchSize($batchSize)
->setPopulatedMethod(fn (array $rows, Closure|string|null $indexBy = null): array => DbArrayHelper::populate($rows, $indexBy))
->setPopulatedMethod(fn (array $rows, Closure|string|null $indexBy = null): array => DbArrayHelper::index($rows, $indexBy))
;
}

Expand Down
Loading

0 comments on commit 17cd981

Please sign in to comment.