chapter hgroup + calming decor
This commit is contained in:
parent
0545b131de
commit
94d14d70c1
370 changed files with 9583 additions and 1566 deletions
551
kirby/src/Cms/LazyCollection.php
Normal file
551
kirby/src/Cms/LazyCollection.php
Normal file
|
|
@ -0,0 +1,551 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Closure;
|
||||
use Iterator;
|
||||
use Kirby\Exception\LogicException;
|
||||
|
||||
/**
|
||||
* The LazyCollection class is a variant of the CMS
|
||||
* Collection that is only initialized with keys for
|
||||
* each collection element or without any data.
|
||||
* Collection elements and their values (= objects)
|
||||
* are loaded and initialized lazily when they are
|
||||
* first used.
|
||||
*
|
||||
* You can use LazyCollection in two ways:
|
||||
* 1. Initialize with keys only (values are `null`),
|
||||
* define `hydrateElement` method that initializes
|
||||
* an element dynamically.
|
||||
* 2. Option 1, but also don't initialize any keys,
|
||||
* set `$initialized` prop to `false` and define
|
||||
* `initialize` method that defines which keys
|
||||
* are available.
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Lukas Bestle <lukas@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://getkirby.com/license
|
||||
*
|
||||
* @template TValue
|
||||
* @extends \Kirby\Cms\Collection<TValue>
|
||||
*/
|
||||
abstract class LazyCollection extends Collection
|
||||
{
|
||||
/**
|
||||
* Flag that tells whether hydration has been
|
||||
* completed for all collection elements;
|
||||
* this is used to increase performance
|
||||
*/
|
||||
protected bool $hydrated = false;
|
||||
|
||||
/**
|
||||
* Flag that tells whether all possible collection
|
||||
* items have been loaded (only relevant in lazy
|
||||
* initialization mode)
|
||||
*/
|
||||
protected bool $initialized = true;
|
||||
|
||||
/**
|
||||
* Temporary auto-hydration whenever a collection
|
||||
* method is called; some methods may not need raw
|
||||
* access to all collection data, so performance
|
||||
* will be improved if methods call initialization
|
||||
* or hydration themselves only if they need it
|
||||
* @deprecated
|
||||
* @todo Remove this in v6
|
||||
*/
|
||||
public function __call(string $key, $arguments)
|
||||
{
|
||||
$this->hydrate();
|
||||
return parent::__call($key, $arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Low-level getter for elements
|
||||
*
|
||||
* @return TValue|null
|
||||
*/
|
||||
public function __get(string $key)
|
||||
{
|
||||
$element = parent::__get($key);
|
||||
|
||||
// `$element === null` could mean "element does not exist"
|
||||
// or "element found but not hydrated"
|
||||
if (
|
||||
$element === null &&
|
||||
(array_key_exists($key, $this->data) || $this->initialized === false)
|
||||
) {
|
||||
return $this->hydrateElement($key);
|
||||
}
|
||||
|
||||
return $element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Low-level element remover
|
||||
*/
|
||||
public function __unset(string $key)
|
||||
{
|
||||
// first initialize, otherwise a later initialization
|
||||
// might bring back the element that was unset
|
||||
$this->initialize();
|
||||
|
||||
return parent::__unset($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates chunks of the same size.
|
||||
* The last chunk may be smaller
|
||||
*
|
||||
* @param int $size Number of elements per chunk
|
||||
* @return static A new collection with an element for each chunk and
|
||||
* a sub collection in each chunk
|
||||
*/
|
||||
public function chunk(int $size): static
|
||||
{
|
||||
// chunking at least requires the collection structure
|
||||
$this->initialize();
|
||||
|
||||
return parent::chunk($size);
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts all elements
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
$this->initialize();
|
||||
|
||||
return parent::count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current element
|
||||
* @deprecated
|
||||
* @todo Remove in v6
|
||||
*
|
||||
* @return TValue
|
||||
*/
|
||||
public function current(): mixed
|
||||
{
|
||||
$current = parent::current();
|
||||
|
||||
// `$current === null` could mean "empty collection"
|
||||
// or "element found but not hydrated"
|
||||
if ($current === null && $key = $this->key()) {
|
||||
return $this->hydrateElement($key);
|
||||
}
|
||||
|
||||
return $current;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone and remove all elements from the collection
|
||||
*/
|
||||
public function empty(): static
|
||||
{
|
||||
$empty = parent::empty();
|
||||
|
||||
// prevent new collection from initializing its
|
||||
// elements into the now empty collection
|
||||
// (relevant when emptying a collection that
|
||||
// has not been (fully) initialized yet)
|
||||
$empty->initialized = true;
|
||||
|
||||
return $empty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find one or multiple elements by id
|
||||
*
|
||||
* @param string ...$keys
|
||||
* @return TValue|static
|
||||
*/
|
||||
public function find(...$keys)
|
||||
{
|
||||
$result = parent::find(...$keys);
|
||||
|
||||
// when the result is a cloned collection (multiple keys),
|
||||
// mark it as initialized to prevent it from initializing
|
||||
// all of its elements again after we filtered it above
|
||||
// (relevant when finding elements in a collection that
|
||||
// has not been (fully) initialized yet)
|
||||
if ($result instanceof static && $result !== $this) {
|
||||
$result->initialized = true;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the elements in reverse order
|
||||
*/
|
||||
public function flip(): static
|
||||
{
|
||||
// flipping at least requires the collection structure
|
||||
$this->initialize();
|
||||
|
||||
return parent::flip();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters elements by one of the
|
||||
* predefined filter methods, by a
|
||||
* custom filter function or an array of filters
|
||||
*/
|
||||
public function filter(string|array|Closure $field, ...$args): static
|
||||
{
|
||||
// to filter through values, we need all values present
|
||||
$this->hydrate();
|
||||
|
||||
return parent::filter($field, ...$args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first element
|
||||
*
|
||||
* @return TValue
|
||||
*/
|
||||
public function first()
|
||||
{
|
||||
// returning a specific offset requires the collection structure
|
||||
$this->initialize();
|
||||
|
||||
$first = parent::first();
|
||||
|
||||
// `$first === null` could mean "empty collection"
|
||||
// or "element found but not hydrated"
|
||||
if ($first === null && $key = array_key_first($this->data)) {
|
||||
return $this->hydrateElement($key);
|
||||
}
|
||||
|
||||
return $first;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an iterator for the elements
|
||||
* @return \Iterator<TKey, TValue>
|
||||
*/
|
||||
public function getIterator(): Iterator
|
||||
{
|
||||
// ensure we are looping over all possible elements
|
||||
$this->initialize();
|
||||
|
||||
foreach ($this->data as $key => $value) {
|
||||
if ($value === null) {
|
||||
$value = $this->hydrateElement($key);
|
||||
}
|
||||
|
||||
yield $key => $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks by key if an element is included
|
||||
* @param TKey $key
|
||||
*/
|
||||
public function has(mixed $key): bool
|
||||
{
|
||||
$this->initialize();
|
||||
|
||||
return parent::has($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that all collection elements are loaded,
|
||||
* essentially converting the lazy collection into a
|
||||
* normal collection
|
||||
*/
|
||||
public function hydrate(): void
|
||||
{
|
||||
// first ensure all keys are initialized
|
||||
$this->initialize();
|
||||
|
||||
// skip another hydration loop if no longer needed
|
||||
if ($this->hydrated === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->data as $key => $value) {
|
||||
if ($value === null) {
|
||||
$this->hydrateElement($key);
|
||||
}
|
||||
}
|
||||
|
||||
$this->hydrated = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a collection element, sets it in `$this->data[$key]`
|
||||
* and returns the hydrated object value (or `null` if the
|
||||
* element does not exist in the collection); to be
|
||||
* implemented in each specific collection
|
||||
*/
|
||||
abstract protected function hydrateElement(string $key): object|null;
|
||||
|
||||
/**
|
||||
* Ensures that the keys for all valid collection elements
|
||||
* are loaded in the `$data` array and sets `$initialized`
|
||||
* to `true` afterwards; to be implemented in each collection
|
||||
* that wants to use lazy initialization; be sure to keep
|
||||
* existing `$data` values and not overwrite the entire array
|
||||
*/
|
||||
public function initialize(): void
|
||||
{
|
||||
if ($this->initialized === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new LogicException(static::class . ' class does not implement `initialize()` method that is required for lazy initialization'); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of all keys
|
||||
*/
|
||||
public function keys(): array
|
||||
{
|
||||
// ensure we are returning all possible keys
|
||||
$this->initialize();
|
||||
|
||||
return parent::keys();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to find the key for the given element
|
||||
*
|
||||
* @param TValue $needle the element to search for
|
||||
* @return int|string|false the name of the key or false
|
||||
*/
|
||||
public function keyOf(mixed $needle): int|string|false
|
||||
{
|
||||
// quick lookup without having to hydrate the collection
|
||||
// (keys in CMS collections are the object IDs)
|
||||
if (
|
||||
is_object($needle) === true &&
|
||||
method_exists($needle, 'id') === true
|
||||
) {
|
||||
return $needle->id();
|
||||
}
|
||||
|
||||
$this->hydrate();
|
||||
return parent::keyOf($needle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last element
|
||||
*
|
||||
* @return TValue
|
||||
*/
|
||||
public function last()
|
||||
{
|
||||
// returning a specific offset requires the collection structure
|
||||
$this->initialize();
|
||||
|
||||
$last = parent::last();
|
||||
|
||||
// `$last === null` could mean "empty collection"
|
||||
// or "element found but not hydrated"
|
||||
if ($last === null && $key = array_key_last($this->data)) {
|
||||
return $this->hydrateElement($key);
|
||||
}
|
||||
|
||||
return $last;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a function to each element
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function map(callable $callback): static
|
||||
{
|
||||
// to map a function, we need all values present
|
||||
$this->hydrate();
|
||||
|
||||
return parent::map($callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the cursor to the next element
|
||||
* and returns it
|
||||
* @deprecated
|
||||
* @todo Remove in v6
|
||||
*
|
||||
* @return TValue
|
||||
*/
|
||||
public function next(): mixed
|
||||
{
|
||||
$this->initialize();
|
||||
|
||||
$next = parent::next();
|
||||
|
||||
// `$next === null` could mean "empty collection"
|
||||
// or "element found but not hydrated"
|
||||
if ($next === null && $key = $this->key()) {
|
||||
return $this->hydrateElement($key);
|
||||
}
|
||||
|
||||
return $next;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the nth element from the collection
|
||||
*
|
||||
* @return TValue|null
|
||||
*/
|
||||
public function nth(int $n)
|
||||
{
|
||||
// returning a specific offset requires the collection structure
|
||||
$this->initialize();
|
||||
|
||||
$nth = parent::nth($n);
|
||||
|
||||
// `$nth === null` could mean "empty collection"
|
||||
// or "element found but not hydrated"
|
||||
if ($nth === null) {
|
||||
$key = array_keys($this->data)[$n] ?? null;
|
||||
|
||||
if (is_string($key) === true) {
|
||||
return $this->hydrateElement($key);
|
||||
}
|
||||
}
|
||||
|
||||
return $nth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepends an element to the data array
|
||||
*
|
||||
* ```php
|
||||
* $collection->prepend('key', $value);
|
||||
* $collection->prepend($value);
|
||||
* ```
|
||||
*
|
||||
* @param string|TValue ...$args
|
||||
* @return $this
|
||||
*/
|
||||
public function prepend(...$args): static
|
||||
{
|
||||
// prepending to an uninitialized collection would
|
||||
// destroy the order on later initialization
|
||||
$this->initialize();
|
||||
|
||||
return parent::prepend(...$args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the cursor to the previous element
|
||||
* and returns it
|
||||
* @deprecated
|
||||
* @todo Remove in v6
|
||||
*
|
||||
* @return TValue
|
||||
*/
|
||||
public function prev(): mixed
|
||||
{
|
||||
$this->initialize();
|
||||
|
||||
$prev = parent::prev();
|
||||
|
||||
// `$prev === null` could mean "empty collection"
|
||||
// or "element found but not hydrated"
|
||||
if ($prev === null && $key = $this->key()) {
|
||||
return $this->hydrateElement($key);
|
||||
}
|
||||
|
||||
return $prev;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new collection consisting of random elements,
|
||||
* from the original collection, shuffled or ordered
|
||||
*/
|
||||
public function random(int $count = 1, bool $shuffle = false): static
|
||||
{
|
||||
// picking random elements at least requires the collection structure
|
||||
$this->initialize();
|
||||
|
||||
return parent::random($count, $shuffle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuffle all elements
|
||||
*/
|
||||
public function shuffle(): static
|
||||
{
|
||||
// shuffling at least requires the collection structure
|
||||
$this->initialize();
|
||||
|
||||
return parent::shuffle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a slice of the object
|
||||
*
|
||||
* @param int $offset The optional index to start the slice from
|
||||
* @param int|null $limit The optional number of elements to return
|
||||
* @return $this|static
|
||||
* @psalm-return ($offset is 0 && $limit is null ? $this : static)
|
||||
*/
|
||||
public function slice(
|
||||
int $offset = 0,
|
||||
int|null $limit = null
|
||||
): static {
|
||||
// returning a specific subset requires the collection structure
|
||||
$this->initialize();
|
||||
|
||||
return parent::slice($offset, $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts the elements by any number of fields
|
||||
*
|
||||
* ```php
|
||||
* $collection->sort('fieldName');
|
||||
* $collection->sort('fieldName', 'desc');
|
||||
* $collection->sort('fieldName', 'asc', SORT_REGULAR);
|
||||
* $collection->sort(fn ($a) => ...);
|
||||
* ```
|
||||
*
|
||||
* @param string|callable $field Field name or value callback to sort by
|
||||
* @param string|null $direction asc or desc
|
||||
* @param int|null $method The sort flag, SORT_REGULAR, SORT_NUMERIC etc.
|
||||
* @return $this|static
|
||||
*/
|
||||
public function sort(...$args): static
|
||||
{
|
||||
// to sort through values, we need all values present
|
||||
$this->hydrate();
|
||||
|
||||
return parent::sort(...$args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts all objects in the collection
|
||||
* to an array. This can also take a callback
|
||||
* function to further modify the array result.
|
||||
*/
|
||||
public function toArray(Closure|null $map = null): array
|
||||
{
|
||||
// to export an array, we need all values present
|
||||
$this->hydrate();
|
||||
|
||||
return parent::toArray($map);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a non-associative array
|
||||
* with all values. If a mapping Closure is passed,
|
||||
* all values are processed by the Closure.
|
||||
*/
|
||||
public function values(Closure|null $map = null): array
|
||||
{
|
||||
// to export an array, we need all values present
|
||||
$this->hydrate();
|
||||
|
||||
return parent::values($map);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue