From 01cd52aeae553ce765fdd01e47eab3fa7c82265f Mon Sep 17 00:00:00 2001 From: Sebastian Meyer Date: Mon, 5 Feb 2024 15:37:02 +0100 Subject: [PATCH] Make Collection type-sensitive --- composer.json | 8 +- src/DataStructures/Collection.php | 195 ------------ src/DataStructures/StrictCollection.php | 398 ++++++++++++++++++++++++ src/DataStructures/StrictList.php | 2 +- src/Traits/Singleton.php | 4 +- 5 files changed, 405 insertions(+), 202 deletions(-) delete mode 100644 src/DataStructures/Collection.php create mode 100644 src/DataStructures/StrictCollection.php diff --git a/composer.json b/composer.json index 281f5b1..a916f09 100644 --- a/composer.json +++ b/composer.json @@ -4,13 +4,13 @@ "type": "library", "keywords": [ "ArrayAccess", - "Collection", "Countable", "Getter", "Iterator", "IteratorAggregate", "Setter", "Singleton", + "StrictCollection", "StrictList", "StrictQueue", "StrictStack", @@ -34,14 +34,14 @@ "docs": "https://opencultureconsulting.github.io/php-basics/" }, "require": { - "php": "^8.0" + "php": "^8.1" }, "require-dev": { "phpstan/phpstan": "^1.10.56", "phpstan/phpstan-strict-rules": "^1.5", - "friendsofphp/php-cs-fixer": "^3.48", + "friendsofphp/php-cs-fixer": "^3.49", "squizlabs/php_codesniffer": "^3.8", - "vimeo/psalm": "^5.20" + "vimeo/psalm": "^5.21" }, "autoload": { "psr-4": { diff --git a/src/DataStructures/Collection.php b/src/DataStructures/Collection.php deleted file mode 100644 index 7d95fd9..0000000 --- a/src/DataStructures/Collection.php +++ /dev/null @@ -1,195 +0,0 @@ - - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -declare(strict_types=1); - -namespace OCC\Basics\DataStructures; - -use ArrayAccess; -use Countable; -use IteratorAggregate; -use OCC\Basics\InterfaceTraits\ArrayAccessTrait; -use OCC\Basics\InterfaceTraits\CountableTrait; -use OCC\Basics\InterfaceTraits\IteratorAggregateTrait; - -/** - * A generic collection of items. - * - * @author Sebastian Meyer - * @package Basics\DataStructures - * - * @api - * - * @template Item of mixed - * @implements ArrayAccess - * @implements IteratorAggregate - */ -class Collection implements ArrayAccess, Countable, IteratorAggregate -{ - /** @use ArrayAccessTrait */ - use ArrayAccessTrait; - /** @use CountableTrait */ - use CountableTrait; - /** @use IteratorAggregateTrait */ - use IteratorAggregateTrait; - - /** - * Add an item to the collection. - * - * @param Item $item The new item - * - * @return void - * - * @api - */ - public function add(mixed $item): void - { - $this->data[] = $item; - } - - /** - * Clear the collection of any items. - * - * @return void - * - * @api - */ - public function clear(): void - { - $this->data = []; - } - - /** - * Get a new collection with the same set of items. - * - * @return Collection The new collection with the same items - * - * @api - */ - public function copy(): Collection - { - return new Collection($this->data); - } - - /** - * Get the item at the specified index. - * - * @param array-key $key The item's index - * - * @return ?Item The item or NULL if key is invalid - * - * @api - */ - public function get(int|string $key): mixed - { - return $this->data[$key] ?? null; - } - - /** - * Check if collection is empty. - * - * @return bool Whether the collection contains any items - * - * @api - */ - public function isEmpty(): bool - { - return $this->count() === 0; - } - - /** - * Remove an item from the collection. - * - * @param array-key $key The item's key - * - * @return void - * - * @api - */ - public function remove(int|string $key): void - { - unset($this->data[$key]); - } - - /** - * Set the item at the specified index. - * - * @param array-key $key The new item's index - * @param Item $item The new item - * - * @return void - * - * @api - */ - public function set(int|string $key, mixed $item): void - { - $this->data[$key] = $item; - } - - /** - * Return array representation of collection. - * - * @return array Array of collection items - * - * @api - */ - public function toArray(): array - { - return $this->data; - } - - /** - * Create a collection of items. - * - * @param array $items Initial set of items - * - * @return void - */ - public function __construct(array $items = []) - { - $this->data = $items; - } - - /** - * Magic method to read collection items as properties. - * - * @param array-key $key The item's index - * - * @return ?Item The item or NULL if key is invalid - */ - public function __get(int|string $key): mixed - { - return $this->get($key); - } - - /** - * Magic method to write collection items as properties. - * - * @param array-key $key The new item's index - * @param Item $item The new item - * - * @return void - */ - public function __set(int|string $key, mixed $item): void - { - $this->set($key, $item); - } -} diff --git a/src/DataStructures/StrictCollection.php b/src/DataStructures/StrictCollection.php new file mode 100644 index 0000000..768f9cd --- /dev/null +++ b/src/DataStructures/StrictCollection.php @@ -0,0 +1,398 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace OCC\Basics\DataStructures; + +use ArrayAccess; +use Countable; +use InvalidArgumentException; +use OCC\Basics\InterfaceTraits\ArrayAccessTrait; +use OCC\Basics\InterfaceTraits\CountableTrait; +use OCC\Basics\Traits\Getter; +use Serializable; + +/** + * A type-sensitive collection. + * + * @author Sebastian Meyer + * @package Basics\DataStructures + * + * @property-read string[] $allowedTypes The allowed data types for items. + * + * @api + * + * @template AllowedType of mixed + * @implements ArrayAccess + */ +class StrictCollection implements ArrayAccess, Countable, Serializable +{ + /** @use ArrayAccessTrait */ + use ArrayAccessTrait; + /** @use CountableTrait */ + use CountableTrait; + use Getter; + + /** + * The allowed data types for collection items. + * + * @var string[] + * + * @internal + */ + protected array $allowedTypes = []; + + /** + * Add/insert a new item at the specified index. + * + * @param array-key $key The new item's index + * @param AllowedType $item The new item + * + * @return void + * + * @throws InvalidArgumentException + * + * @api + */ + public function add(int|string $key, mixed $item): void + { + if (!$this->isAllowedType($item)) { + throw new InvalidArgumentException( + sprintf( + 'Parameter 2 must be an allowed type, %s given.', + get_debug_type($item) + ) + ); + } + $this->data[$key] = $item; + } + + /** + * Clear the collection of any items. + * + * @return void + * + * @api + */ + public function clear(): void + { + $this->data = []; + } + + /** + * Get the item at the specified index. + * + * @param array-key $key The item's index + * + * @return ?AllowedType The item or NULL if key is invalid + * + * @api + */ + public function get(int|string $key): mixed + { + return $this->data[$key] ?? null; + } + + /** + * Get allowed data types for collection items. + * + * @return string[] The list of allowed data types + * + * @api + */ + public function getAllowedTypes(): array + { + return $this->allowedTypes; + } + + /** + * Check if the item's data type is allowed in the collection. + * + * @param AllowedType $item The item to check + * + * @return bool Whether the item's data type is allowed + * + * @api + */ + public function isAllowedType(mixed $item): bool + { + if (count($this->allowedTypes) === 0) { + return true; + } + foreach ($this->allowedTypes as $type) { + $function = 'is_' . $type; + if (function_exists($function) && $function($item)) { + return true; + } + /** @var class-string $fqcn */ + $fqcn = ltrim($type, '\\'); + if (is_object($item) && is_a($item, $fqcn)) { + return true; + } + } + return false; + } + + /** + * Check if collection is empty. + * + * @return bool Whether the collection contains any items + * + * @api + */ + public function isEmpty(): bool + { + return $this->count() === 0; + } + + /** + * Check if this collection can be considered a list. + * + * @return bool Whether the collection is a list + * + * @api + */ + public function isList(): bool + { + return array_is_list($this->data); + } + + /** + * Magic getter method for $this->allowedTypes. + * + * @return string[] The list of allowed data types + * + * @internal + */ + protected function magicGetAllowedTypes(): array + { + return $this->getAllowedTypes(); + } + + /** + * Set the item at the specified offset. + * + * @param ?array-key $offset The offset being set + * @param AllowedType $item The new item for the offset + * + * @return void + * + * @throws InvalidArgumentException + * + * @internal + */ + public function offsetSet(mixed $offset, mixed $item): void + { + if (is_null($offset)) { + throw new InvalidArgumentException( + 'Parameter 1 must be an integer or string, NULL given.' + ); + } + if (!$this->isAllowedType($item)) { + throw new InvalidArgumentException( + sprintf( + 'Parameter 2 must be an allowed type, %s given.', + get_debug_type($item) + ) + ); + } + $this->add($offset, $item); + } + + /** + * Remove an item from the collection. + * + * @param array-key $key The item's key + * + * @return void + * + * @api + */ + public function remove(int|string $key): void + { + unset($this->data[$key]); + } + + /** + * Get string representation of $this. + * + * @return string The string representation + * + * @internal + */ + public function serialize(): string + { + return serialize($this->__serialize()); + } + + /** + * Set allowed data types of collection items. + * + * @param string[] $allowedTypes Allowed data types of items + * + * @return void + * + * @throws InvalidArgumentException + */ + protected function setAllowedTypes(array $allowedTypes = []): void + { + if (array_sum(array_map('is_string', $allowedTypes)) !== count($allowedTypes)) { + throw new InvalidArgumentException( + 'Allowed types must be array of strings or empty array.' + ); + } + $this->allowedTypes = $allowedTypes; + } + + /** + * Return array representation of collection. + * + * @return array Array of collection items + * + * @api + */ + public function toArray(): array + { + return $this->data; + } + + /** + * Restore $this from string representation. + * + * @param string $data The string representation + * + * @return void + * + * @internal + */ + public function unserialize($data): void + { + /** @var mixed[] $dataArray */ + $dataArray = unserialize($data); + $this->__unserialize($dataArray); + } + + /** + * Create a type-sensitive collection of items. + * + * @param string[] $allowedTypes Allowed data types of items (optional) + * + * If empty, all types are allowed. + * Possible values are: + * - "array" + * - "bool" + * - "callable" + * - "countable" + * - "float" or "double" + * - "int" or "integer" or "long" + * - "iterable" + * - "null" + * - "numeric" + * - "object" or FQCN + * - "resource" + * - "scalar" + * - "string" + * + * @return void + * + * @throws InvalidArgumentException + */ + public function __construct(array $allowedTypes = []) + { + $this->setAllowedTypes($allowedTypes); + } + + /** + * Get debug information for $this. + * + * @return mixed[] The debug information + * + * @internal + */ + public function __debugInfo(): array + { + return $this->__serialize(); + } + + /** + * Get array representation of $this. + * + * @return mixed[] The array representation + * + * @internal + */ + public function __serialize(): array + { + return [ + 'StrictCollection::allowedTypes' => $this->allowedTypes, + 'StrictCollection::items' => $this->data + ]; + } + + /** + * Restore $this from array representation. + * + * @param mixed[] $data The array representation + * + * @return void + * + * @internal + * + * @psalm-suppress MethodSignatureMismatch + */ + public function __unserialize(array $data): void + { + /** @var string[] $allowedTypes */ + $allowedTypes = $data['StrictCollection::allowedTypes']; + $this->setAllowedTypes($allowedTypes); + /** @var array $items */ + $items = $data['StrictCollection::items']; + foreach ($items as $key => $item) { + $this->add($key, $item); + } + } + + /** + * Magic method to read collection items as properties. + * + * @param array-key $key The item's index + * + * @return ?AllowedType The item or NULL if key is invalid + */ + public function __get(int|string $key): mixed + { + return $this->get($key); + } + + /** + * Magic method to write collection items as properties. + * + * @param array-key $key The new item's index + * @param AllowedType $item The new item + * + * @return void + */ + public function __set(int|string $key, mixed $item): void + { + $this->add($key, $item); + } +} diff --git a/src/DataStructures/StrictList.php b/src/DataStructures/StrictList.php index 260010e..dd4df61 100644 --- a/src/DataStructures/StrictList.php +++ b/src/DataStructures/StrictList.php @@ -174,7 +174,7 @@ class StrictList extends SplDoublyLinkedList implements ArrayAccess, Countable, if (function_exists($function) && $function($value)) { return true; } - /** @var class-string */ + /** @var class-string $fqcn */ $fqcn = ltrim($type, '\\'); if (is_object($value) && is_a($value, $fqcn)) { return true; diff --git a/src/Traits/Singleton.php b/src/Traits/Singleton.php index 80b5abe..77f78d3 100644 --- a/src/Traits/Singleton.php +++ b/src/Traits/Singleton.php @@ -36,7 +36,7 @@ trait Singleton /** * Holds the singleton instance. * - * @var array + * @var array * * @internal */ @@ -77,7 +77,7 @@ trait Singleton * * @internal */ - final public function __clone(): void + final public function __clone() { throw new LogicException('Cloning a singleton is prohibited.'); }