Skip to content

Commit

Permalink
Merge pull request #14105 from craftcms/feature/defer-instantiation-v5
Browse files Browse the repository at this point in the history
Defer instantiation for memoizable array items (Craft 5)
  • Loading branch information
brandonkelly authored Jan 4, 2024
2 parents a1e9e58 + 856c3d6 commit e81ba23
Show file tree
Hide file tree
Showing 11 changed files with 155 additions and 142 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@

- Added the `showFirstAndLastNameFields` config setting. ([#14097](https://github.com/craftcms/cms/pull/14097))
- `queue/get-job-info` action requests no longer create a mutex lock.
- `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.
- Fixed a PHP error that occurred when viewing a user’s addresses.
- Fixed a bug where all field layouts were getting instantiated before the Debug Toolbar had a chance to register its `*` wildcard event

## 5.0.0-alpha.4 - 2024-01-02

Expand Down
60 changes: 54 additions & 6 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 @@ -120,7 +167,8 @@ public function firstWhere(string $key, mixed $value = true, bool $strict = fals

// 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
59 changes: 24 additions & 35 deletions src/services/Entries.php
Original file line number Diff line number Diff line change
Expand Up @@ -209,34 +209,35 @@ public function getEditableSectionIds(): array
private function _sections(): MemoizableArray
{
if (!isset($this->_sections)) {
$sections = [];
$results = $this->_createSectionQuery()->all();
$siteSettingsBySection = [];

foreach ($this->_createSectionQuery()->all() as $result) {
if (!empty($results) && Craft::$app->getRequest()->getIsCpRequest()) {
// Eager load the site settings
$sectionIds = array_map(fn(array $result) => $result['id'], $results);
$siteSettingsBySection = ArrayHelper::index(
$this->_createSectionSiteSettingsQuery()->where(['sections_sites.sectionId' => $sectionIds])->all(),
null,
['sectionId'],
);
}

$this->_sections = new MemoizableArray($results, function(array $result) use (&$siteSettingsBySection) {
if (!empty($result['previewTargets'])) {
$result['previewTargets'] = Json::decode($result['previewTargets']);
} else {
$result['previewTargets'] = [];
}
$sections[$result['id']] = new Section($result);
}

$this->_sections = new MemoizableArray(array_values($sections));

if (!empty($sections) && Craft::$app->getRequest()->getIsCpRequest()) {
// Eager load the site settings
$allSiteSettings = $this->_createSectionSiteSettingsQuery()
->where(['sections_sites.sectionId' => array_keys($sections)])
->all();

$siteSettingsBySection = [];
foreach ($allSiteSettings as $siteSettings) {
$siteSettingsBySection[$siteSettings['sectionId']][] = new Section_SiteSettings($siteSettings);
$section = new Section($result);
/** @phpstan-ignore-next-line */
$siteSettings = ArrayHelper::remove($siteSettingsBySection, $section->id);
if ($siteSettings !== null) {
$section->setSiteSettings(
array_map(fn(array $config) => new Section_SiteSettings($config), $siteSettings),
);
}

foreach ($siteSettingsBySection as $sectionId => $sectionSiteSettings) {
$sections[$sectionId]->setSiteSettings($sectionSiteSettings);
}
}
return $section;
});
}

return $this->_sections;
Expand Down Expand Up @@ -1191,22 +1192,10 @@ public function getEntryTypesBySectionId(int $sectionId): array
private function _entryTypes(): MemoizableArray
{
if (!isset($this->_entryTypes)) {
$entryTypes = array_map(
fn(array $result) => new EntryType($result),
$this->_entryTypes = new MemoizableArray(
$this->_createEntryTypeQuery()->all(),
fn(array $result) => new EntryType($result),
);
$this->_entryTypes = new MemoizableArray($entryTypes);

if (!empty($entryTypes) && Craft::$app->getRequest()->getIsCpRequest()) {
// Eager load the field layouts
/** @var EntryType[] $entryTypesByLayoutId */
$entryTypesByLayoutId = ArrayHelper::index($entryTypes, 'fieldLayoutId');
$allLayouts = Craft::$app->getFields()->getLayoutsByIds(array_filter(array_keys($entryTypesByLayoutId)));

foreach ($allLayouts as $layout) {
$entryTypesByLayoutId[$layout->id]->setFieldLayout($layout);
}
}
}

return $this->_entryTypes;
Expand Down
Loading

0 comments on commit e81ba23

Please sign in to comment.