From 9f7495cbed3c274f02f452717b395e19646aa6ff Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com> Date: Fri, 11 Nov 2022 22:55:44 +0100 Subject: [PATCH 1/3] Add new methods to `Arr` --- formwork/src/Utils/Arr.php | 181 +++++++++++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) diff --git a/formwork/src/Utils/Arr.php b/formwork/src/Utils/Arr.php index 5169282e7..e2c761ea6 100644 --- a/formwork/src/Utils/Arr.php +++ b/formwork/src/Utils/Arr.php @@ -2,6 +2,10 @@ namespace Formwork\Utils; +use Formwork\Data\Contracts\Arrayable; +use Traversable; +use UnexpectedValueException; + class Arr { /** @@ -80,6 +84,45 @@ public static function remove(array &$array, string $key): void unset($array[$key]); } + /** + * Remove from an array all the occurrences of the given value + */ + public static function pull(array &$array, $value): void + { + foreach ($array as $key => $v) { + if ($v === $value) { + unset($array[$key]); + } + } + } + + /** + * Get the array value at the given index, + * negative indices are allowed and start from the end + */ + public static function at(array $array, int $index) + { + return array_values($array)[$index >= 0 ? $index : count($array) + $index]; + } + + /** + * Get the index of the given value or null if not found + */ + public static function indexOf(array $array, $value): ?int + { + $index = array_search($value, array_values($array), true); + return $index !== false ? $index : null; + } + + /** + * Get the key of the given value or null if not found + */ + public static function keyOf(array $array, $value): int|string|null + { + $key = array_search($value, $array, true); + return $key !== false ? $key : null; + } + /** * Recursively append elements from the second array that are missing in the first */ @@ -129,4 +172,142 @@ public static function isAssociative(array $array): bool { return $array !== [] && array_keys($array) !== range(0, count($array) - 1); } + + /** + * Apply a callback to the given array and return the result + * + * The key of each element is passed to the callback as second argument + */ + public static function map(array $array, callable $callback): array + { + return array_map($callback, $array, array_keys($array)); + } + + /** + * Filter an array keeping only the values for which the callback returns `true` + * + * The key of each element is passed to the callback as second argument + */ + public static function filter(array $array, callable $callback): array + { + return array_filter($array, $callback, ARRAY_FILTER_USE_BOTH); + } + + /** + * Reject values from an array keeping only the values for which the callback returns `false` + * + * The key of each element is passed to the callback as second argument + */ + public static function reject(array $array, callable $callback): array + { + return static::filter($array, fn ($value, $key) => !$callback($value, $key)); + } + + /** + * Return whether every element of the array passes a test callback + * + * The key of each element is passed to the callback as second argument + */ + public static function every(array $array, callable $callback): bool + { + foreach ($array as $key => $value) { + if (!$callback($value, $key)) { + return false; + } + } + return true; + } + + /** + * Return whether some element of the array passes a test callback + * + * The key of each element is passed to the callback as second argument + */ + public static function some(array $array, callable $callback): bool + { + foreach ($array as $key => $value) { + if ($callback($value, $key)) { + return true; + } + } + return false; + } + + /** + * Sort an array with the given options + * + * @param $direction Direction of sorting. Possible values are `SORT_ASC` and `SORT_DESC`. + * @param $type Type of sorting. Possible values are `SORT_REGULAR`, `SORT_NUMERIC`, `SORT_STRING` and `SORT_NATURAL`. + * @param $caseSensitive Whether to perform a case-sensitive sorting + * @param $sortBy A callback or second array of values used to sort the first + * @param $preserveKeys Whether to preserve array keys after sorting + */ + public static function sort( + array $array, + int $direction = SORT_ASC, + int $type = SORT_NATURAL, + array|callable $sortBy = null, + bool $caseSensitive = false, + bool $preserveKeys = true + ): array { + if (!in_array($direction, [SORT_ASC, SORT_DESC], true)) { + throw new UnexpectedValueException(sprintf('%s() only accepts SORT_ASC and SORT_DESC as "direction" option', __METHOD__)); + } + + if (!in_array($type, [SORT_REGULAR, SORT_NUMERIC, SORT_STRING, SORT_NATURAL], true)) { + throw new UnexpectedValueException(sprintf('%s() only accepts SORT_REGULAR, SORT_NUMERIC, SORT_STRING and SORT_NATURAL as "type" option', __METHOD__)); + } + + $flags = $type; + + if ($caseSensitive === false) { + $flags |= SORT_FLAG_CASE; + } + + if (is_callable($sortBy)) { + $function = $preserveKeys ? 'uasort' : 'usort'; + $function($array, $sortBy); + } else { + $keys = $preserveKeys ? array_keys($array) : []; + + $arguments = []; + + if ($sortBy === null) { + $arguments = [&$array, $direction, $flags]; + } elseif (is_array($sortBy)) { + $arguments = [&$sortBy, $direction, $flags, &$array]; + } + + if ($preserveKeys) { + $arguments[] = &$keys; + } + + array_multisort(...$arguments); + + if ($preserveKeys) { + $array = array_combine($keys, $array); + } + } + + return $array; + } + + /** + * Try to convert the given object to array + */ + public static function from($object): array + { + switch (true) { + case is_array($object): + return $object; + + case $object instanceof Arrayable: + return $object->toArray(); + + case $object instanceof Traversable: + return iterator_to_array($object); + } + + throw new UnexpectedValueException(sprintf('Cannot convert to array an object of type %s', get_debug_type($object))); + } } From c2f4a75457cf446102518de654a1739bc35ce501 Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com> Date: Fri, 11 Nov 2022 23:00:57 +0100 Subject: [PATCH 2/3] Rewrite `Collection` --- formwork/src/Data/AbstractCollection.php | 478 ++++++++++++++++++++ formwork/src/Data/AssociativeCollection.php | 10 - formwork/src/Data/Collection.php | 88 ++-- formwork/src/Data/CollectionDataProxy.php | 42 ++ 4 files changed, 581 insertions(+), 37 deletions(-) create mode 100644 formwork/src/Data/AbstractCollection.php delete mode 100644 formwork/src/Data/AssociativeCollection.php create mode 100644 formwork/src/Data/CollectionDataProxy.php diff --git a/formwork/src/Data/AbstractCollection.php b/formwork/src/Data/AbstractCollection.php new file mode 100644 index 000000000..b2863f31f --- /dev/null +++ b/formwork/src/Data/AbstractCollection.php @@ -0,0 +1,478 @@ +isAssociative()) !== ($dataAssoc = Arr::isAssociative($data))) { + throw new LogicException(sprintf( + '%s collections cannot be created from %s data', + $selfAssoc ? 'Associative' : 'Non-associative', + $dataAssoc ? 'associative' : 'non-associative' + )); + } + + if ($this->isTyped() && !Arr::every($data, fn ($value) => Constraint::isOfType($value, $this->dataType()))) { + throw new LogicException('Typed collections cannot be created from data of different types'); + } + + $this->data = $data; + } + + /** + * Return whether the collection is associative + */ + public function isAssociative(): bool + { + return $this->associative; + } + + /** + * Return whether the collection is mutable + */ + public function isMutable(): bool + { + return $this->mutable; + } + + /** + * Return whether the collection is typed + */ + public function isTyped(): bool + { + return $this->dataType !== null; + } + + /** + * Get the data type + */ + public function dataType(): ?string + { + return $this->dataType; + } + + /** + * Return whether collection is empty + */ + public function isEmpty(): bool + { + return $this->data === []; + } + + /** + * Return the collection item at the specified index + */ + public function nth(int $index) + { + return $this->data[$index] ?? null; + } + + /** + * Return the collection item at the specified index + * + * A negative index starts from the last item + */ + public function at(int $index) + { + return Arr::at($this->data, $index); + } + + /** + * Return first collection item + */ + public function first() + { + return $this->at(0); + } + + /** + * Return last collection item + */ + public function last() + { + return $this->at(-1); + } + + /** + * Return a random item or a given default value if the collection is empty + */ + public function random($default = null) + { + return Arr::random($this->data, $default); + } + + /** + * Get the index of the given value + * + * Return `null` if the item is not present + */ + public function indexOf($value): ?int + { + return Arr::indexOf($this->data, $value); + } + + /** + * Get the key of the given value + * + * Return `null` if the item is not present + */ + public function keyOf($value): int|string|null + { + if (!$this->isAssociative()) { + throw new LogicException('Only associative collections support keys'); + } + + return Arr::keyOf($this->data, $value); + } + + /** + * Return whether the collection contains the given value + */ + public function contains($value): bool + { + return $this->indexOf($value) !== null; + } + + /** + * Return whether the given callback returns `true` for every item in the collection + */ + public function every(callable $callback): bool + { + return Arr::every($this->data, $callback); + } + + /** + * Return whether the given callback returns `true` for some item in the collection + */ + public function some(callable $callback): bool + { + return Arr::some($this->data, $callback); + } + + /** + * Clone the collection instance + */ + public function clone(): static + { + return clone $this; + } + + /** + * Return a copy of the collection with its items reversed + */ + public function reverse(): static + { + $collection = $this->clone(); + $collection->data = array_reverse($collection->data); + return $collection; + } + + /** + * Return a copy of the collection with its items shuffled + */ + public function shuffle(): static + { + $collection = $this->clone(); + $collection->data = Arr::shuffle($collection->data); + return $collection; + } + + /** + * Return a copy of the collection with duplicates removed + */ + public function unique(): static + { + $collection = $this->clone(); + $collection->data = array_unique($this->data); + return $collection; + } + + /** + * Return a copy of the collection with duplicates only + */ + public function duplicates(): static + { + $collection = $this->clone(); + $collection->data = array_diff_key($this->data, array_unique($this->data)); + return $collection; + } + + /** + * Return a copy of the collection with its items from a given index + */ + public function slice(int $index, int $length): static + { + $collection = $this->clone(); + $collection->data = array_slice($collection->data, $index, $length); + return $collection; + } + + /** + * Return a copy of the collection with only the given number of items + * counting from the beginning + */ + public function limit(int $length): static + { + return $this->slice(0, $length); + } + + /** + * Apply a callback to the collection items + */ + public function map(callable $callback): static + { + $collection = $this->clone(); + $collection->data = array_map($callback, $collection->data); + return $collection; + } + + /** + * Filter the collection items using a callback + * + * Only the elements on which the callback returns `true` are retained + */ + public function filter(callable $callback): static + { + $collection = $this->clone(); + $collection->data = Arr::filter($collection->data, $callback); + return $collection; + } + + /** + * Reject the collection items using a callback + * + * Only the elements on which the callback returns `false` are retained + * + * This is the opposite of `Collection::filter()` + */ + public function reject(callable $callback): static + { + $collection = $this->clone(); + return $collection->data = Arr::reject($collection->data, $callback); + return $collection; + } + + /** + * Return a copy of the collection with its items sorted with the given options + * + * Keys are preserved by default in associative collections + */ + public function sort( + int $direction = SORT_ASC, + int $type = SORT_NATURAL, + array|callable $sortBy = null, + bool $caseSensitive = false, + bool $preserveKeys = null + ): static { + $collection = $this->clone(); + $collection->data = Arr::sort($collection->data, $direction, $type, $sortBy, $caseSensitive, $preserveKeys ?? $this->isAssociative()); + return $collection; + } + + /** + * Return a copy of the collection with the given values + + * If a value is already in the collection, it will not be added + */ + public function with(...$values): static + { + $collection = $this->clone(); + + foreach ($values as $value) { + if (!$collection->contains($value)) { + $collection->data[] = $value; + } + } + + return $collection; + } + + /** + * Return a copy of the collection without the given values + */ + public function without(...$values): static + { + $collection = $this->clone(); + + foreach ($values as $value) { + Arr::pull($collection->data, $value); + } + + return $collection; + } + + /** + * Return a special object on which property accesses and method calls + * are redirected to every item of the collection and the results + * are collected again + */ + public function everyItem(): CollectionDataProxy + { + return new CollectionDataProxy($this); + } + + /** + * Add the given value to the collection + */ + public function add($value) + { + if (!$this->isMutable() || $this->isAssociative()) { + throw new LogicException('Values can be added only to mutable and non-associative collections'); + } + + if ($this->isTyped() && !Constraint::isOfType($value, $this->dataType())) { + throw new LogicException(sprintf('Value must be of type %s to be added, %s given', $this->dataType(), get_debug_type($value))); + } + + $this->data[] = $value; + } + + /** + * Add multiple values to the collection + */ + public function addMultiple(array $values) + { + foreach ($values as $value) { + $this->add($value); + } + } + + /** + * Remove all occurrences of the given value from the collection + */ + public function pull($value) + { + if (!$this->isMutable() || $this->isAssociative()) { + throw new LogicException('Values can be pulled only from mutable and non-associative collections'); + } + + Arr::pull($this->data, $value); + } + + /** + * Remove all occurrences of the given values from the collection + */ + public function pullMultiple(array $values) + { + foreach ($values as $value) { + $this->pull($value); + } + } + + /** + * Return whether the collection has an item with the given key + */ + public function has(string $key): bool + { + if (!$this->isAssociative()) { + throw new LogicException('Value presence can be checked only in associative collections'); + } + return $this->baseHas($key); + } + + /** Get a collection item by the given key + * + * A default value is returned if the item is not present + */ + public function get(string $key, $default = null) + { + if (!$this->isAssociative()) { + throw new LogicException('Values can be get only from associative collections'); + } + return $this->baseGet($key, $default); + } + + /** + * Set a collection item + */ + public function set(string $key, $value) + { + if (!$this->isAssociative() || !$this->isMutable()) { + throw new LogicException('Values can be set only to associative and mutable collections'); + } + + if ($this->isTyped() && !Constraint::isOfType($value, $this->dataType())) { + throw new LogicException(sprintf('Value must be of type %s, %s given', $this->dataType(), get_debug_type($value))); + } + + if ($this->dataType() !== 'array') { + // Avoid dot notation traversal by setting the key before + $this->data[$key] = null; + } + + $this->baseSet($key, $value); + } + + /** + * Remove a collection item by key + */ + public function remove(string $key) + { + if (!$this->isAssociative() || !$this->isMutable()) { + throw new LogicException('Values can be removed only from associative and mutable collections'); + } + $this->baseRemove($key); + } + + /** + * Merge another collection into the current + */ + public function merge(self $collection): void + { + if (!$this->isMutable()) { + throw new LogicException('Values can be merged only into mutable collections'); + } + + if ($collection->isAssociative() !== $this->isAssociative()) { + throw new LogicException('Collections cannot be merged if their associativeness is different'); + } + + if ($collection->dataType() !== $this->dataType()) { + throw new LogicException('Collections with data of different types cannot be merged'); + } + + $this->data = array_merge($this->data, $collection->data); + } +} diff --git a/formwork/src/Data/AssociativeCollection.php b/formwork/src/Data/AssociativeCollection.php deleted file mode 100644 index 8dd061d2d..000000000 --- a/formwork/src/Data/AssociativeCollection.php +++ /dev/null @@ -1,10 +0,0 @@ -data = $data; + if ($this->isMutable()) { + throw new LogicException('Cannot convert an already mutable collection to mutable'); + } + $collection = $this->clone(); + $collection->mutable = true; + return $collection; } /** - * Return first collection item + * Convert a collection to immutable * - * @return mixed|null + * @throws LogicException If collection is already immutable */ - public function first() + public function toImmutable(): static { - return $this->data[0] ?? null; + if (!$this->isMutable()) { + throw new LogicException('Cannot convert an already immmutable collection to immmutable'); + } + $collection = $this->clone(); + $collection->mutable = false; + return $collection; } /** - * Return last collection item - * - * @return mixed|null + * Create a collection with the given options */ - public function last() + public static function create(array $data = [], string $dataType = null, bool $associative = false, bool $mutable = false): static { - return $this->data[$this->count() - 1] ?? null; + $collection = new static(); + + $collection->associative = $associative; + $collection->dataType = $dataType; + $collection->mutable = $mutable; + + $collection->__construct($data); + + return $collection; } /** - * Return a random item or a given default value if the collection is empty + * Create a collection of the given type */ - public function random($default = null) + public static function of(string $dataType, array $data = [], bool $associative = false, bool $mutable = false): static { - return Arr::random($this->data, $default); + return static::create($data, $dataType, $associative, $mutable); } /** - * Return whether collection is empty + * Convert an arrayable object to a collection trying to guess its data type */ - public function isEmpty(): bool + public static function from($object, bool $typed = null, bool $mutable = false): static { - return empty($this->data); + $data = Arr::from($object); + + if ($typed !== false) { + $dataType = null; + + foreach ($data as $value) { + $type = get_debug_type($value); + + // A type was guessed but a different one is found + if ($dataType !== null && $type !== $dataType) { + // Cannot enforce a typed collection when values have different types + if ($typed === true) { + throw new LogicException('Cannot create a typed collection with data of different types'); + } + + $dataType = null; + break; + } + + $dataType = $type; + } + } + + return static::create($data, $dataType, Arr::isAssociative($data), $mutable); } } diff --git a/formwork/src/Data/CollectionDataProxy.php b/formwork/src/Data/CollectionDataProxy.php new file mode 100644 index 000000000..bb8adc53e --- /dev/null +++ b/formwork/src/Data/CollectionDataProxy.php @@ -0,0 +1,42 @@ +collection = $collection; + } + + public function __get(string $name) + { + $result = []; + + foreach ($this->collection as $key => $item) { + $result[$key] = $item->{$name}; + } + + return Collection::from($result, mutable: $this->collection->isMutable()); + } + + public function __set(string $name, $value): void + { + foreach ($this->collection as $item) { + $item->{$name} = $value; + } + } + + public function __call(string $name, array $arguments) + { + $result = []; + + foreach ($this->collection as $key => $item) { + $result[$key] = $item->{$name}(...$arguments); + } + + return Collection::from($result, mutable: $this->collection->isMutable()); + } +} From 4017187776258d47149ea1fcc7d54b03ceac05a3 Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com> Date: Fri, 11 Nov 2022 23:11:39 +0100 Subject: [PATCH 3/3] Update collections --- .../Admin/Controllers/DashboardController.php | 4 +- .../src/Admin/Controllers/PagesController.php | 4 +- formwork/src/Admin/Users/Users.php | 8 +- formwork/src/Fields/Fields.php | 6 +- formwork/src/Files/Files.php | 8 +- formwork/src/Metadata/Metadata.php | 24 ++-- formwork/src/Page.php | 2 +- formwork/src/PageCollection.php | 115 +++++------------- formwork/src/Router/RouteCollection.php | 15 ++- formwork/src/Router/RouteFilterCollection.php | 15 ++- formwork/src/Schemes/Schemes.php | 3 +- formwork/src/Translations/Translations.php | 3 +- site/templates/controllers/blog.php | 4 +- site/templates/partials/menu.php | 2 +- 14 files changed, 88 insertions(+), 125 deletions(-) diff --git a/formwork/src/Admin/Controllers/DashboardController.php b/formwork/src/Admin/Controllers/DashboardController.php index e60de23ce..a16d82c33 100644 --- a/formwork/src/Admin/Controllers/DashboardController.php +++ b/formwork/src/Admin/Controllers/DashboardController.php @@ -19,7 +19,7 @@ public function index(): Response $this->modal('newPage', [ 'templates' => $this->site()->templates(), - 'pages' => $this->site()->descendants()->sort('path') + 'pages' => $this->site()->descendants()->sortBy('path') ]); $this->modal('deletePage'); @@ -27,7 +27,7 @@ public function index(): Response return new Response($this->view('dashboard.index', [ 'title' => $this->admin()->translate('admin.dashboard.dashboard'), 'lastModifiedPages' => $this->view('pages.list', [ - 'pages' => $this->site()->descendants()->sort('lastModifiedTime', SORT_DESC)->slice(0, 5), + 'pages' => $this->site()->descendants()->sortBy('lastModifiedTime', direction: SORT_DESC)->slice(0, 5), 'subpages' => false, 'class' => 'pages-list-top', 'parent' => null, diff --git a/formwork/src/Admin/Controllers/PagesController.php b/formwork/src/Admin/Controllers/PagesController.php index 1911f1b35..379cbec4f 100644 --- a/formwork/src/Admin/Controllers/PagesController.php +++ b/formwork/src/Admin/Controllers/PagesController.php @@ -47,7 +47,7 @@ public function index(): Response $this->modal('newPage', [ 'templates' => $this->site()->templates(), - 'pages' => $this->site()->descendants()->sort('path') + 'pages' => $this->site()->descendants()->sortBy('path') ]); $this->modal('deletePage'); @@ -190,7 +190,7 @@ public function edit(RouteParams $params): Response 'page' => $page, 'fields' => $fields, 'templates' => $this->site()->templates(), - 'parents' => $this->site()->descendants()->sort('path'), + 'parents' => $this->site()->descendants()->sortBy('path'), 'currentLanguage' => $params->get('language', $page->language()), 'availableLanguages' => $this->availableSiteLanguages() ], true)); diff --git a/formwork/src/Admin/Users/Users.php b/formwork/src/Admin/Users/Users.php index 387104221..627617268 100644 --- a/formwork/src/Admin/Users/Users.php +++ b/formwork/src/Admin/Users/Users.php @@ -2,13 +2,17 @@ namespace Formwork\Admin\Users; -use Formwork\Data\AssociativeCollection; +use Formwork\Data\AbstractCollection; use Formwork\Formwork; use Formwork\Parsers\YAML; use Formwork\Utils\FileSystem; -class Users extends AssociativeCollection +class Users extends AbstractCollection { + protected bool $associative = true; + + protected ?string $dataType = User::class; + /** * All available roles */ diff --git a/formwork/src/Fields/Fields.php b/formwork/src/Fields/Fields.php index ad9e5de66..168dff8f5 100644 --- a/formwork/src/Fields/Fields.php +++ b/formwork/src/Fields/Fields.php @@ -2,11 +2,13 @@ namespace Formwork\Fields; -use Formwork\Data\AssociativeCollection; +use Formwork\Data\AbstractCollection; use Formwork\Data\DataGetter; -class Fields extends AssociativeCollection +class Fields extends AbstractCollection { + protected ?string $dataType = Field::class; + /** * Create a new Fields instance * diff --git a/formwork/src/Files/Files.php b/formwork/src/Files/Files.php index d9019688e..b07a38b68 100644 --- a/formwork/src/Files/Files.php +++ b/formwork/src/Files/Files.php @@ -2,11 +2,15 @@ namespace Formwork\Files; -use Formwork\Data\AssociativeCollection; +use Formwork\Data\AbstractCollection; use Formwork\Utils\FileSystem; -class Files extends AssociativeCollection +class Files extends AbstractCollection { + protected bool $associative = true; + + protected ?string $dataType = File::class; + /** * Filter files by a given type */ diff --git a/formwork/src/Metadata/Metadata.php b/formwork/src/Metadata/Metadata.php index 93dac2f6b..15f4dc47c 100644 --- a/formwork/src/Metadata/Metadata.php +++ b/formwork/src/Metadata/Metadata.php @@ -2,10 +2,16 @@ namespace Formwork\Metadata; -use Formwork\Data\AssociativeCollection; +use Formwork\Data\AbstractCollection; -class Metadata extends AssociativeCollection +class Metadata extends AbstractCollection { + protected bool $associative = true; + + protected ?string $dataType = Metadatum::class; + + protected bool $mutable = true; + /** * Create a new Metadata instance */ @@ -18,18 +24,8 @@ public function __construct(array $data) /** * Set a metadatum */ - public function set(string $name, string $content): void - { - $this->data[$name] = new Metadatum($name, $content); - } - - /** - * Set multiple metadata - */ - public function setMultiple(array $data): void + public function set(string $key, $value) { - foreach ($data as $name => $content) { - $this->set($name, $content); - } + $this->data[$key] = new Metadatum($key, $value); } } diff --git a/formwork/src/Page.php b/formwork/src/Page.php index c8d7b97e2..ff60973d0 100644 --- a/formwork/src/Page.php +++ b/formwork/src/Page.php @@ -284,7 +284,7 @@ public function siblings(): PageCollection return $this->siblings; } $parentPath = dirname($this->path) . DS; - return $this->siblings = PageCollection::fromPath($parentPath)->remove($this); + return $this->siblings = PageCollection::fromPath($parentPath)->without($this); } /** diff --git a/formwork/src/PageCollection.php b/formwork/src/PageCollection.php index 4b2daaf3d..5ac33617a 100644 --- a/formwork/src/PageCollection.php +++ b/formwork/src/PageCollection.php @@ -2,19 +2,20 @@ namespace Formwork; +use Formwork\Data\AbstractCollection; use Formwork\Data\Collection; -use Formwork\Utils\Arr; use Formwork\Utils\FileSystem; use Formwork\Utils\Str; -use InvalidArgumentException; -class PageCollection extends Collection +class PageCollection extends AbstractCollection { /** * Default property used to sort pages */ protected const DEFAULT_SORT_PROPERTY = 'relativePath'; + protected ?string $dataType = AbstractPage::class; + /** * Pagination related to the collection */ @@ -29,53 +30,30 @@ public function pagination(): Pagination } /** - * Reverse the order of collection items - */ - public function reverse(): self - { - $pageCollection = clone $this; - $pageCollection->data = array_reverse($pageCollection->data); - return $pageCollection; - } - - /** - * Extract a slice from the collection containing a given number of items - * and starting from a given offset + * Paginate the collection * - * @param int $length + * @param int $length Number of items in the pagination */ - public function slice(int $offset, int $length = null): self + public function paginate(int $length): static { - $pageCollection = clone $this; - $pageCollection->data = array_slice($pageCollection->data, $offset, $length); + $pagination = new Pagination($this->count(), $length); + $pageCollection = $this->slice($pagination->offset(), $pagination->length()); + $pageCollection->pagination = $pagination; return $pageCollection; } /** - * Remove a given element from the collection + * Return an array containing the specified property of each collection item */ - public function remove(Page $element): self + public function pluck(string $property): array { - $pageCollection = clone $this; - foreach ($pageCollection->data as $key => $item) { - if ($item->path() === $element->path()) { - unset($pageCollection->data[$key]); - } + $result = []; + + foreach ($this->data as $page) { + $result[] = $page->get($property); } - return $pageCollection; - } - /** - * Paginate the collection - * - * @param int $length Number of items in the pagination - */ - public function paginate(int $length): self - { - $pagination = new Pagination($this->count(), $length); - $pageCollection = $this->slice($pagination->offset(), $pagination->length()); - $pageCollection->pagination = $pagination; - return $pageCollection; + return $result; } /** @@ -85,11 +63,9 @@ public function paginate(int $length): self * @param $value Value to check in filtered items (default: true) * @param callable $process Callable to process items before filtering */ - public function filter(string $property, $value = true, callable $process = null): self + public function filterBy(string $property, $value = true, callable $process = null): static { - $pageCollection = clone $this; - - $pageCollection->data = array_filter($pageCollection->data, static function (Page $item) use ($property, $value, $process): bool { + return $this->filter(static function (Page $item) use ($property, $value, $process): bool { if ($item->has($property)) { $propertyValue = $item->get($property); @@ -106,44 +82,19 @@ public function filter(string $property, $value = true, callable $process = null return false; }); - - return $pageCollection; } /** * Sort collection items - * - * @param int $direction Sorting direction (SORT_ASC or 1 for ascending order, SORT_DESC or -1 for descending) - */ - public function sort(string $property = self::DEFAULT_SORT_PROPERTY, int $direction = SORT_ASC): self - { - $pageCollection = clone $this; - - if ($pageCollection->count() <= 1) { - return $pageCollection; - } - - if ($direction === SORT_ASC || $direction === 1) { - $direction = 1; - } elseif ($direction === SORT_DESC || $direction === -1) { - $direction = -1; - } else { - throw new InvalidArgumentException('Invalid sorting direction. Use SORT_ASC or 1 for ascending order, SORT_DESC or -1 for descending'); - } - - usort($pageCollection->data, static fn (Page $item1, Page $item2): int => $direction * strnatcasecmp($item1->get($property), $item2->get($property))); - - return $pageCollection; - } - - /** - * Shuffle collection items */ - public function shuffle(): self - { - $pageCollection = clone $this; - $pageCollection->data = Arr::shuffle($pageCollection->data); - return $pageCollection; + public function sortBy( + string $property = self::DEFAULT_SORT_PROPERTY, + int $direction = SORT_ASC, + int $type = SORT_NATURAL, + bool $caseSensitive = false, + bool $preserveKeys = true + ): static { + return parent::sort($direction, $type, $this->pluck($property), $caseSensitive, $preserveKeys); } /** @@ -152,7 +103,7 @@ public function shuffle(): self * @param string $query Query to search for * @param int $min Minimum query length (default: 4) */ - public function search(string $query, int $min = 4): self + public function search(string $query, int $min = 4): static { $query = trim(preg_replace('/\s+/u', ' ', $query)); if (strlen($query) < $min) { @@ -192,7 +143,7 @@ public function search(string $query, int $min = 4): self } } - return $pageCollection->filter('score')->sort('score', SORT_DESC); + return $pageCollection->filterBy('score')->sortBy('score', direction: SORT_DESC); } /** @@ -221,13 +172,7 @@ public static function fromPath(string $path, bool $recursive = false): self } $pages = new static($pages); - return $pages->sort(); - } - public function __debugInfo(): array - { - return [ - 'items' => $this->data - ]; + return $pages->sortBy('path'); } } diff --git a/formwork/src/Router/RouteCollection.php b/formwork/src/Router/RouteCollection.php index 7a7f67b81..1e1ebca16 100644 --- a/formwork/src/Router/RouteCollection.php +++ b/formwork/src/Router/RouteCollection.php @@ -2,15 +2,22 @@ namespace Formwork\Router; -use Formwork\Data\AssociativeCollection; +use Formwork\Data\AbstractCollection; -class RouteCollection extends AssociativeCollection +class RouteCollection extends AbstractCollection { + protected bool $associative = true; + + protected ?string $dataType = Route::class; + + protected bool $mutable = true; + /** * Add route to the collection */ - public function add(Route $route): Route + public function add($route): Route { - return $this->data[$route->getName()] = $route; + $this->set($route->getName(), $route); + return $route; } } diff --git a/formwork/src/Router/RouteFilterCollection.php b/formwork/src/Router/RouteFilterCollection.php index 67f9e36bb..fcd111834 100644 --- a/formwork/src/Router/RouteFilterCollection.php +++ b/formwork/src/Router/RouteFilterCollection.php @@ -2,15 +2,22 @@ namespace Formwork\Router; -use Formwork\Data\AssociativeCollection; +use Formwork\Data\AbstractCollection; -class RouteFilterCollection extends AssociativeCollection +class RouteFilterCollection extends AbstractCollection { + protected bool $associative = true; + + protected ?string $dataType = RouteFilter::class; + + protected bool $mutable = true; + /** * Add filter to collection */ - public function add(RouteFilter $filter): RouteFilter + public function add($filter): RouteFilter { - return $this->data[$filter->getName()] = $filter; + $this->set($filter->getName(), $filter); + return $filter; } } diff --git a/formwork/src/Schemes/Schemes.php b/formwork/src/Schemes/Schemes.php index 4af9b57c0..34c26f993 100644 --- a/formwork/src/Schemes/Schemes.php +++ b/formwork/src/Schemes/Schemes.php @@ -2,11 +2,10 @@ namespace Formwork\Schemes; -use Formwork\Data\Collection; use Formwork\Utils\FileSystem; use InvalidArgumentException; -class Schemes extends Collection +class Schemes { /** * Scheme objects storage diff --git a/formwork/src/Translations/Translations.php b/formwork/src/Translations/Translations.php index f75d3792d..c0afc5071 100644 --- a/formwork/src/Translations/Translations.php +++ b/formwork/src/Translations/Translations.php @@ -2,13 +2,12 @@ namespace Formwork\Translations; -use Formwork\Data\Collection; use Formwork\Formwork; use Formwork\Parsers\YAML; use Formwork\Utils\FileSystem; use InvalidArgumentException; -class Translations extends Collection +class Translations { /** * Translation objects storage diff --git a/site/templates/controllers/blog.php b/site/templates/controllers/blog.php index 2e5281e49..de71a582b 100644 --- a/site/templates/controllers/blog.php +++ b/site/templates/controllers/blog.php @@ -1,9 +1,9 @@ children()->filter('published'); + $posts = $page->children()->filterBy('published'); if ($params->has('tagName')) { - $posts = $posts->filter('tags', $params->get('tagName'), 'Formwork\Utils\Str::slug'); + $posts = $posts->filterBy('tags', $params->get('tagName'), 'Formwork\Utils\Str::slug'); } $posts = $posts->reverse()->paginate($page->get('posts-per-page', 5)); diff --git a/site/templates/partials/menu.php b/site/templates/partials/menu.php index c2598dbb7..0a20ce005 100644 --- a/site/templates/partials/menu.php +++ b/site/templates/partials/menu.php @@ -3,7 +3,7 @@ title() ?>