403 lines
8.9 KiB
PHP
403 lines
8.9 KiB
PHP
<?php
|
|
|
|
namespace Kirby\Cms;
|
|
|
|
use Closure;
|
|
use Kirby\Exception\InvalidArgumentException;
|
|
use Kirby\Toolkit\Collection as BaseCollection;
|
|
use Kirby\Toolkit\Str;
|
|
use Kirby\Uuid\Uuid;
|
|
|
|
/**
|
|
* The Collection class serves as foundation
|
|
* for the Pages, Files, Users and Structure
|
|
* classes. It handles object validation and sets
|
|
* the parent collection property for each object.
|
|
* The `getAttribute` method is also adjusted to
|
|
* handle values from Field objects correctly, so
|
|
* those can be used in filters as well.
|
|
*
|
|
* @package Kirby Cms
|
|
* @author Bastian Allgeier <bastian@getkirby.com>
|
|
* @link https://getkirby.com
|
|
* @copyright Bastian Allgeier
|
|
* @license https://getkirby.com/license
|
|
*
|
|
* @template TValue
|
|
* @extends \Kirby\Toolkit\Collection<TValue>
|
|
*/
|
|
class Collection extends BaseCollection
|
|
{
|
|
use HasMethods;
|
|
|
|
/**
|
|
* @var \Kirby\Cms\Pagination|null
|
|
*/
|
|
protected $pagination;
|
|
|
|
/**
|
|
* Creates a new Collection with the given objects
|
|
*
|
|
* @param iterable<TValue> $objects
|
|
* @param object|null $parent Stores the parent object,
|
|
* which is needed in some collections
|
|
* to get the finder methods right
|
|
*/
|
|
public function __construct(
|
|
iterable $objects = [],
|
|
protected object|null $parent = null
|
|
) {
|
|
foreach ($objects as $object) {
|
|
$this->add($object);
|
|
}
|
|
}
|
|
|
|
public function __call(string $key, $arguments)
|
|
{
|
|
// collection methods
|
|
if ($this->hasMethod($key) === true) {
|
|
return $this->callMethod($key, $arguments);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Internal setter for each object in the Collection;
|
|
* override from the Toolkit Collection is needed to
|
|
* make the CMS collections case-sensitive;
|
|
* child classes can override it again to add validation
|
|
* and custom behavior depending on the object type
|
|
*
|
|
* @param TValue $object
|
|
*/
|
|
public function __set(string $id, $object): void
|
|
{
|
|
$this->data[$id] = $object;
|
|
}
|
|
|
|
/**
|
|
* Internal remover for each object in the Collection;
|
|
* override from the Toolkit Collection is needed to
|
|
* make the CMS collections case-sensitive
|
|
*/
|
|
public function __unset(string $id)
|
|
{
|
|
unset($this->data[$id]);
|
|
}
|
|
|
|
/**
|
|
* Adds a single object or
|
|
* an entire second collection to the
|
|
* current collection
|
|
*
|
|
* @param \Kirby\Cms\Collection<TValue>|array<TValue>|TValue $object
|
|
* @return $this
|
|
*/
|
|
public function add($object): static
|
|
{
|
|
if ($object instanceof self) {
|
|
$this->data = [...$this->data, ...$object->data];
|
|
} elseif (
|
|
is_object($object) === true &&
|
|
method_exists($object, 'id') === true
|
|
) {
|
|
$this->__set($object->id(), $object);
|
|
} else {
|
|
$this->append($object);
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Appends an element to the data array
|
|
*
|
|
* ```php
|
|
* $collection->append($object);
|
|
* $collection->append('key', $object);
|
|
* ```
|
|
*
|
|
* @param string|TValue ...$args
|
|
* @return $this
|
|
*/
|
|
public function append(...$args): static
|
|
{
|
|
if (count($args) === 1) {
|
|
// try to determine the key from the provided item
|
|
if (
|
|
is_object($args[0]) === true &&
|
|
is_callable([$args[0], 'id']) === true
|
|
) {
|
|
return parent::append($args[0]->id(), $args[0]);
|
|
}
|
|
|
|
return parent::append($args[0]);
|
|
}
|
|
|
|
return parent::append(...$args);
|
|
}
|
|
|
|
/**
|
|
* Find a single element by an attribute and its value
|
|
*
|
|
* @return TValue|null
|
|
*/
|
|
public function findBy(string $attribute, $value)
|
|
{
|
|
// $value: cast UUID object to string to allow uses
|
|
// like `$pages->findBy('related', $page->uuid())`
|
|
if ($value instanceof Uuid) {
|
|
$value = $value->toString();
|
|
}
|
|
|
|
return parent::findBy($attribute, $value);
|
|
}
|
|
|
|
/**
|
|
* Groups the items by a given field or callback. Returns a collection
|
|
* with an item for each group and a collection for each group.
|
|
*
|
|
* @param string|\Closure $field
|
|
* @param bool $caseInsensitive Ignore upper/lowercase for group names
|
|
* @throws \Kirby\Exception\Exception
|
|
*/
|
|
public function group(
|
|
$field,
|
|
bool $caseInsensitive = true
|
|
): self {
|
|
$groups = new self(parent: $this->parent());
|
|
|
|
if (is_string($field) === true) {
|
|
foreach ($this->data as $key => $item) {
|
|
$value = $this->getAttribute($item, $field);
|
|
|
|
// make sure that there's always a proper value to group by
|
|
if (!$value) {
|
|
throw new InvalidArgumentException(
|
|
message: 'Invalid grouping value for key: ' . $key
|
|
);
|
|
}
|
|
|
|
$value = (string)$value;
|
|
|
|
// ignore upper/lowercase for group names
|
|
if ($caseInsensitive) {
|
|
$value = Str::lower($value);
|
|
}
|
|
|
|
if (isset($groups->data[$value]) === false) {
|
|
// create a new entry for the group if it does not exist yet
|
|
$groups->data[$value] = new static([$key => $item]);
|
|
} else {
|
|
// add the item to an existing group
|
|
$groups->data[$value]->set($key, $item);
|
|
}
|
|
}
|
|
|
|
return $groups;
|
|
}
|
|
|
|
// use the parent method but unwrap the Toolkit collection
|
|
// and rewrap it as a Cms\Collection instance
|
|
$groups->data = parent::group($field, $caseInsensitive)->data;
|
|
return $groups;
|
|
}
|
|
|
|
/**
|
|
* Checks if the given object or id
|
|
* is in the collection
|
|
*
|
|
* @param string|TValue $key
|
|
*/
|
|
public function has($key): bool
|
|
{
|
|
if (is_object($key) === true) {
|
|
$key = $key->id();
|
|
}
|
|
|
|
return parent::has($key);
|
|
}
|
|
|
|
/**
|
|
* Correct position detection for objects.
|
|
* The method will automatically detect objects
|
|
* or ids and then search accordingly.
|
|
*
|
|
* @param string|TValue $needle
|
|
*/
|
|
public function indexOf($needle): int|false
|
|
{
|
|
if (is_string($needle) === true) {
|
|
return array_search($needle, $this->keys());
|
|
}
|
|
|
|
return array_search($needle->id(), $this->keys());
|
|
}
|
|
|
|
/**
|
|
* Returns a Collection without the given element(s)
|
|
*
|
|
* @param string|array|object ...$keys any number of keys,
|
|
* passed as individual arguments
|
|
*/
|
|
public function not(string|array|object ...$keys): static
|
|
{
|
|
$collection = $this->clone();
|
|
|
|
foreach ($keys as $key) {
|
|
if (is_array($key) === true) {
|
|
return $this->not(...$key);
|
|
}
|
|
|
|
if ($key instanceof BaseCollection) {
|
|
$collection = $collection->not(...$key->keys());
|
|
} elseif (is_object($key) === true) {
|
|
$key = $key->id();
|
|
}
|
|
|
|
unset($collection->{$key});
|
|
}
|
|
|
|
return $collection;
|
|
}
|
|
|
|
/**
|
|
* Add pagination and return a sliced set of data.
|
|
*
|
|
* @return $this|static
|
|
*/
|
|
public function paginate(...$arguments): static
|
|
{
|
|
$this->pagination = Pagination::for($this, ...$arguments);
|
|
|
|
// slice and clone the collection according to the pagination
|
|
return $this->slice(
|
|
$this->pagination->offset(),
|
|
$this->pagination->limit()
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get the previously added pagination object
|
|
*/
|
|
public function pagination(): Pagination|null
|
|
{
|
|
return $this->pagination;
|
|
}
|
|
|
|
/**
|
|
* Returns the parent model
|
|
*/
|
|
public function parent(): object|null
|
|
{
|
|
return $this->parent;
|
|
}
|
|
|
|
/**
|
|
* Prepends an element to the data array
|
|
*
|
|
* ```php
|
|
* $collection->prepend($object);
|
|
* $collection->prepend('key', $object);
|
|
* ```
|
|
*
|
|
* @param string|TValue ...$args
|
|
* @return $this
|
|
*/
|
|
public function prepend(...$args): static
|
|
{
|
|
if (count($args) === 1) {
|
|
// try to determine the key from the provided item
|
|
if (
|
|
is_object($args[0]) === true &&
|
|
is_callable([$args[0], 'id']) === true
|
|
) {
|
|
return parent::prepend($args[0]->id(), $args[0]);
|
|
}
|
|
|
|
return parent::prepend($args[0]);
|
|
}
|
|
|
|
return parent::prepend(...$args);
|
|
}
|
|
|
|
/**
|
|
* Runs a combination of filter, sort, not,
|
|
* offset, limit, search and paginate on the collection.
|
|
* Any part of the query is optional.
|
|
*/
|
|
public function query(array $arguments = []): static
|
|
{
|
|
$paginate = $arguments['paginate'] ?? null;
|
|
$search = $arguments['search'] ?? null;
|
|
|
|
unset($arguments['paginate']);
|
|
|
|
$result = parent::query($arguments);
|
|
|
|
if (empty($search) === false) {
|
|
$result = match (true) {
|
|
is_array($search) => $result->search(
|
|
$search['query'] ?? null,
|
|
$search['options'] ?? []
|
|
),
|
|
default => $result->search($search)
|
|
};
|
|
}
|
|
|
|
if (empty($paginate) === false) {
|
|
$result = $result->paginate($paginate);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Removes an object
|
|
*
|
|
* @param string|TValue $key the name of the key
|
|
*/
|
|
public function remove(string|object $key): static
|
|
{
|
|
if (is_object($key) === true) {
|
|
$key = $key->id();
|
|
}
|
|
|
|
return parent::remove($key);
|
|
}
|
|
|
|
/**
|
|
* Searches the collection
|
|
*/
|
|
public function search(
|
|
string|null $query = null,
|
|
string|array $params = []
|
|
): static {
|
|
return Search::collection($this, $query, $params);
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
{
|
|
return parent::toArray(
|
|
$map ?? fn ($object) => $object->toArray()
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Updates an object in the collection
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function update(string|object $key, $object = null): static
|
|
{
|
|
if (is_object($key) === true) {
|
|
return $this->update($key->id(), $key);
|
|
}
|
|
|
|
return $this->set($key, $object);
|
|
}
|
|
}
|