Skip to content

Commit

Permalink
Merge pull request #14104 from craftcms/feature/defer-instantiation-v4
Browse files Browse the repository at this point in the history
Defer instantiation for memoizable array items (Craft 4)
  • Loading branch information
brandonkelly authored Jan 4, 2024
2 parents b5a6c0b + d372f0a commit ca6efc8
Show file tree
Hide file tree
Showing 11 changed files with 185 additions and 250 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG-WIP.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,6 @@
- Added `craft\services\Elements::setElementUri()`.
- Added `craft\services\Elements::EVENT_SET_ELEMENT_URI`. ([#13930](https://github.com/craftcms/cms/discussions/13930))
- Added `craft\services\Search::createDbQuery()`.
- `craft\base\MemoizableArray` now supports passing a normalizer method to the constructor, which will be lazily applied to each array item once, only if returned by `all()` or `firstWhere()`. ([#14104](https://github.com/craftcms/cms/pull/14104))
- `craft\helpers\ArrayHelper::firstWhere()` now has a `$valueKey` argument, which can be passed a variable by reference that should be set to the resulting value’s key in the array.
- Admin tables now have `footerActions`, `moveToPageAction`, `onCellClicked`, `onCellDoubleClicked`, `onRowClicked`, `onRowDoubleClicked`, and `paginatedReorderAction` settings. ([#14051](https://github.com/craftcms/cms/pull/14051))
62 changes: 55 additions & 7 deletions src/base/MemoizableArray.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,58 @@ class MemoizableArray implements IteratorAggregate, Countable
*/
private array $_elements;

/**
* @var callable|null Normalizer method
*/
private $_normalizer;

/**
* @var array Normalized elements
*/
private array $_normalized = [];

/**
* @var array Memoized array elements
*/
private array $_memoized = [];

/**
* Constructor
*
* @param array $elements The items to be memoized
* @param callable|null $normalizer A method that the items should be normalized with when first returned by
* [[all()]] or [[firstWhere()]].
*/
public function __construct(array $elements)
public function __construct(array $elements, ?callable $normalizer = null)
{
$this->_elements = $elements;
$this->_normalizer = $normalizer;
}

private function normalize(array $elements): array
{
if (!isset($this->_normalizer)) {
return $elements;
}

return array_values(array_map(fn($key) => $this->normalizeByKey($key), array_keys($elements)));
}

private function normalizeByKey(int|string|null $key): mixed
{
if ($key === null) {
return null;
}

if (!isset($this->_normalizer)) {
return $this->_elements[$key];
}

if (!isset($this->_normalized[$key])) {
$this->_normalized[$key] = call_user_func($this->_normalizer, $this->_elements[$key], $key);
}

return $this->_normalized[$key];
}

/**
Expand All @@ -60,7 +101,7 @@ public function __construct(array $elements)
*/
public function all(): array
{
return $this->_elements;
return $this->normalize($this->_elements);
}

/**
Expand All @@ -78,7 +119,10 @@ public function where(string $key, mixed $value = true, bool $strict = false): s
$memKey = $this->_memKey(__METHOD__, $key, $value, $strict);

if (!isset($this->_memoized[$memKey])) {
$this->_memoized[$memKey] = new MemoizableArray(ArrayHelper::where($this, $key, $value, $strict, false));
$this->_memoized[$memKey] = new MemoizableArray(
ArrayHelper::where($this->_elements, $key, $value, $strict),
isset($this->_normalizer) ? fn($element, $key) => $this->normalizeByKey($key) : null,
);
}

return $this->_memoized[$memKey];
Expand All @@ -100,7 +144,10 @@ public function whereIn(string $key, array $values, bool $strict = false): self
$memKey = $this->_memKey(__METHOD__, $key, $values, $strict);

if (!isset($this->_memoized[$memKey])) {
$this->_memoized[$memKey] = new MemoizableArray(ArrayHelper::whereIn($this, $key, $values, $strict, false));
$this->_memoized[$memKey] = new MemoizableArray(
ArrayHelper::whereIn($this->_elements, $key, $values, $strict),
isset($this->_normalizer) ? fn($element, $key) => $this->normalizeByKey($key) : null,
);
}

return $this->_memoized[$memKey];
Expand All @@ -112,15 +159,16 @@ public function whereIn(string $key, array $values, bool $strict = false): self
* @param string $key the column name whose result will be used to index the array
* @param mixed $value the value that `$key` should be compared with
* @param bool $strict whether a strict type comparison should be used when checking array element values against `$value`
* @return T the first matching value, or `null` if no match is found
* @return T|null the first matching value, or `null` if no match is found
*/
public function firstWhere(string $key, mixed $value = true, bool $strict = false)
{
$memKey = $this->_memKey(__METHOD__, $key, $value, $strict);

// Use array_key_exists() because it could be null
if (!array_key_exists($memKey, $this->_memoized)) {
$this->_memoized[$memKey] = ArrayHelper::firstWhere($this, $key, $value, $strict);
ArrayHelper::firstWhere($this->_elements, $key, $value, $strict, valueKey: $valueKey);
$this->_memoized[$memKey] = $this->normalizeByKey($valueKey);
}

return $this->_memoized[$memKey];
Expand Down Expand Up @@ -148,7 +196,7 @@ private function _memKey(string $method, string $key, mixed $value, bool $strict
*/
public function getIterator(): ArrayIterator
{
return new ArrayIterator($this->_elements);
return new ArrayIterator($this->normalize($this->_elements));
}

/**
Expand Down
13 changes: 10 additions & 3 deletions src/helpers/ArrayHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -237,19 +237,26 @@ public static function whereMultiple(iterable $array, array $conditions, bool $s
* @param callable|string $key the column name or anonymous function which must be set to $value
* @param mixed $value the value that $key should be compared with
* @param bool $strict whether a strict type comparison should be used when checking array element values against $value
* @param int|string|null $valueKey The key of the resulting value, or null if it can't be found
* @return mixed the value, or null if it can't be found
* @since 3.1.0
*/
public static function firstWhere(iterable $array, callable|string $key, mixed $value = true, bool $strict = false): mixed
{
foreach ($array as $element) {
public static function firstWhere(
iterable $array,
callable|string $key,
mixed $value = true,
bool $strict = false,
int|string|null &$valueKey = null,
): mixed {
foreach ($array as $valueKey => $element) {
$elementValue = static::getValue($element, $key);
/** @noinspection TypeUnsafeComparisonInspection */
if (($strict && $elementValue === $value) || (!$strict && $elementValue == $value)) {
return $element;
}
}

$valueKey = null;
return null;
}

Expand Down
12 changes: 4 additions & 8 deletions src/services/Categories.php
Original file line number Diff line number Diff line change
Expand Up @@ -116,19 +116,15 @@ public function getEditableGroupIds(): array
private function _groups(): MemoizableArray
{
if (!isset($this->_groups)) {
$groups = [];

/** @var CategoryGroupRecord[] $groupRecords */
$groupRecords = CategoryGroupRecord::find()
->orderBy(['name' => SORT_ASC])
->with('structure')
->all();

foreach ($groupRecords as $groupRecord) {
$groups[] = $this->_createCategoryGroupFromRecord($groupRecord);
}

$this->_groups = new MemoizableArray($groups);
$this->_groups = new MemoizableArray(
$groupRecords,
fn(CategoryGroupRecord $record) => $this->_createCategoryGroupFromRecord($record),
);
}

return $this->_groups;
Expand Down
Loading

0 comments on commit ca6efc8

Please sign in to comment.