Полноценные коллекции в PHP
Не так давно при разработке своего проекта возникла идея реализовать полноценные коллекции для хранения объектов одинакового типа, по удобству напоминающие List в C#.
Идея состоит в том, чтобы коллекции, содержащие объекты различных типов сами по себе различались, а не имели, скажем, один унифицированный тип Collection. Другими словами, коллекция объектов типа User это не то же что коллекция объектов Book. Естественно, первой мыслью было создание различных классов для коллекций (UserCollection, BookCollection, …). Но данных подход не обеспечивает нужной гибкости, плюс ко всему, нужно тратить время на объявление каждого подобного класса.
Немного поразмыслив, реализовал динамическое создание классов коллекций. Выглядит это так: пользователь создаёт коллекцию объектов типа Book, а нужный тип BookCollection создаётся (т.е. объявляется) автоматически.
Что я получил в итоге:
— Полноценный TypeHinting создаваемых типов коллекций.
— Строгая типизация коллекций.
— Возможнось обращатся к коллекции как к массиву как в C# (путём реализации интерфейса ArrayAccess)
— Полноценная итерация коллекции (возможность использования в любых циклах).
Реализация
Фабрика коллекций
- /**
- * Фабрика коллекций
- *
- * @author [x26]VOLAND
- */
- abstract class CollectionFactory
- /**
- * Создаёт коллекцию заданного типа.
- *
- * @param string $type Тип коллекции
- * @return mixed
- */
- public static function create($type)
- $ class = $type . ‘Collection’ ;
- self::__create_class($ class );
- $obj = new $ class ($type);
- return $obj;
- >
- /**
- * Создаёт класс с именем $class
- *
- * @param string $class Имя класса
- * @return void
- */
- private static function __create_class($ class )
- if ( ! class_exists($ class ))
- eval( ‘class ‘ . $ class . ‘ extends Collection < >‘ );
- >
- >
- >
Класс коллекции (описывает поведение)
- /**
- * Класс коллекции
- * Базовый универсальный тип, на основе которого будут создаваться коллекции.
- *
- * @author [x26]VOLAND
- */
- abstract class Collection implements IteratorAggregate, ArrayAccess, Countable
- /**
- * Тип элементов, хранящихся в данной коллекции.
- * @var string
- */
- private $__type;
- /**
- * Хранилище объектов
- * @var array
- */
- private $__collection = array();
- // ———————————————————————
- /**
- * Констурктор.
- * Задаёт тип элементо, которые будут хранитья в данной коллекции.
- *
- * @param string $type Тип элементов
- * @return void
- */
- public function __construct($type)
- $ this ->__type = $type;
- >
- // ———————————————————————
- /**
- * Проверяет тип объекта.
- * Препятствует добавлению в коллекцию объектов `чужого` типа.
- *
- * @param object $object Объект для проверки
- * @return void
- * @throws Exception
- */
- private function __check_type(&$ object )
- if (get_class($ object ) != $ this ->__type)
- throw new Exception( ‘Объект типа `’ . get_class($ object )
- . ‘` не может быть добавлен в коллекцию объектов типа `’ . $ this ->__type . ‘`’ );
- >
- >
- // ———————————————————————
- /**
- * Добавляет в коллекцию объекты, переданные в аргументах.
- *
- * @param object(s) Объекты
- * @return mixed Collection
- */
- public function add()
- $args = func_get_args();
- foreach ($args as $ object )
- $ this ->__check_type($ object );
- $ this ->__collection[] = $ object ;
- >
- return $ this ;
- >
- // ———————————————————————
- /**
- * Удаляет из коллекции объекты, переданные в аргументах.
- *
- * @param object(s) Объекты
- * @return mixed Collection
- */
- public function remove()
- $args = func_get_args();
- foreach ($args as $ object )
- unset($ this ->__collection[array_search($ object , $ this ->__collection)]);
- >
- return $ this ;
- >
- // ———————————————————————
- /**
- * Очищает коллекцию.
- *
- * @return mixed Collection
- */
- public function clear()
- $ this ->__collection = array();
- return $ this ;
- >
- // ———————————————————————
- /**
- * Выясняет, пуста ли коллекция.
- *
- * @return bool
- */
- public function isEmpty()
- return empty($ this ->__collection);
- >
- // ———————————————————————
- /**
- * Реализация интерфейса IteratorAggregate
- */
- /**
- * Возвращает объект итератора.
- *
- * @return CollectionIterator
- */
- public function getIterator()
- return new CollectionIterator($ this ->__collection);
- >
- // ———————————————————————
- /**
- * Реализация интерфейса ArrayAccess.
- */
- /**
- * Sets an element of collection at the offset
- *
- * @param ineter $offset Offset
- * @param mixed $offset Object
- * @return void
- */
- public function offsetSet($offset, $ object )
- $ this ->__check_type($ object );
- if ($offset === NULL)
- $offset = max(array_keys($ this ->__collection)) + 1;
- >
- $ this ->__collection[$offset] = $ object ;
- >
- // ———————————————————————
- /**
- * Выясняет существует ли элемент с данным ключом.
- *
- * @param integer $offset Ключ
- * @return bool
- */
- public function offsetExists($offset)
- return isset($ this ->__collection[$offset]);
- >
- // ———————————————————————
- /**
- * Удаляет элемент, на который ссылается ключ $offset.
- *
- * @param integer $offset Ключ
- * @return void
- */
- public function offsetUnset($offset)
- unset($ this ->__collection[$offset]);
- >
- // ———————————————————————
- /**
- * Возвращает элемент по ключу.
- *
- * @param integer $offset Ключ
- * @return mixed
- */
- public function offsetGet($offset)
- if (isset($ this ->__collection[$offset]) === FALSE)
- return NULL;
- >
- return $ this ->__collection[$offset];
- >
- // ———————————————————————
- /**
- * Реализация интерфейса Countable
- */
- /**
- * Возвращает кол-во элементов в коллекции.
- *
- * @return integer
- */
- public function count()
- return sizeof ($ this ->__collection);
- >
- >
Примеры использования
- class BookStore
- function addBooks(BookCollection $books)
- // реализация
- >
- function addMagazines(MagazineCollection $magazines)
- // реализация
- >
- function addGoods(Collection $goods)
- // Если тип коллекции не важен,
- // можно указать базовый тип Collection
- >
- >
- class Book
- var $id;
- function Book($id)
- $ this ->id = $id;
- >
- >
- class Magazine
- var $id;
- function Magazine($id)
- $ this ->id = $id;
- >
- >
- // Создаём коллекцию
- $books = CollectionFactory::create( ‘Book’ );
- echo get_class($books); // BookCollection
- // Добавим объектов в коллекцию:
- $books->add( new Book(1), new Book(2));
- $books->add( new Book(3))->add( new Book(2));
- $books[] = new Book(5);
- echo count($books); // 5
- .
- foreach ($books as $book)
- echo $book->id;
- > // 12345
- .
- $books->add( new Magazine(1)); // Ошибка (неверный тип)
- .
- $magazines = CollectionFactory::create( ‘Magazine’ );
- $magazines->add( new Magazine(1));
- .
- $bookStore = new BookStore();
- $bookStore->addBooks($books); // Всё в порядке
- $bookStore->addBooks($magazines); // Ошибка (неверный тип)
- $bookStore->addMagazines($magazines); // Всё в порядке
- $bookStore->addGoods($books); // Всё в порядке
- $bookStore->addGoods($magazines); // Всё в порядке
- ?>
Collections in PHP
For many applications, you want to create and manage groups of related objects. Unfortunately, generics will never become part of the PHP programming language. For this reason I thought again about how at least a strictly typed list with specific data types or objects can be implemented, even without generics.
I already wrote a blog post about this topic a few years ago, but this time I want to present an even simpler approach.
As goal, I have defined the following criteria.
- No dependencies (no library must be used)
- Write as less code as possible
- No fancy language features should be used
- It must be type safe
- The collection must be iterable using foreach
- It must be compatible with the PHPStorm code completion feature
- It must comply PHPStan level 9
Implementing a String Collection
For the sake of simplicity, I want to start with string collection.
The idea is to create a class that is able to add new items to an internal array of strings. For this purpose, I add a method called add .
namespace Example; class StringCollection /** @var string[] */ private array $list = []; public function add(string $string): void $this->list[] = $string; > >
The next challenge is to read the list of strings using a foreach loop.
To achieve this, we need to implement a method that returns something that can be iterated. Luckily, PHP itself provides the IteratorAggregate interface for such an use case.
The implementation is quite simple, add the IteratorAggregate to your collection class and implement the getIterator method.
namespace Example; use ArrayIterator; use IteratorAggregate; use Traversable; class StringCollection implements IteratorAggregate /** @var string[] */ private array $list = []; public function add(string $string): void $this->list[] = $string; > public function getIterator(): Traversable return new ArrayIterator($this->list); > >
This ArrayIterator allows us iterating over arrays and objects. In this case we are iterating over an array of strings.
The current implementation would already work, but PHPStan would now complain with the following error message:
Class StringCollection implements generic interface IteratorAggregate but does not specify its types: TKey, TValue You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your phpstan.neon.
To fix this, we need to tell PHPStan which type of collection we are dealing with by adding a DocBlock to that class.
/** * @implements IteratorAggregate */
This means that our collection returns a list of strings as value and an integer as key. The complete collection class, with a namespace, then looks like this:
namespace Example; use ArrayIterator; use IteratorAggregate; use Traversable; /** * @implements IteratorAggregate */ class StringCollection implements IteratorAggregate /** @var string[] */ private array $list = []; public function add(string $string): void $this->list[] = $string; > public function getIterator(): Traversable return new ArrayIterator($this->list); > >
That’s it, only 26 lines of code for the implementation of a strictly typed and iterable collection class.
The usage of the collection class is quite simple:
$strings = new StringCollection(); $strings->add('foo'); $strings->add('bar'); foreach ($strings as $value) echo $value . "\n"; >
Implementing an Object Collection
That approach works with objects too.
Let’s say you want to implement a collection for a class called User :
namespace Example; class User public string $username; public string $email; >
The Collection class can be implemented as follows:
namespace Example; use ArrayIterator; use IteratorAggregate; use Traversable; /** * @implements IteratorAggregate */ class UserCollection implements IteratorAggregate /** @var User[] */ private array $list = []; public function add(User $user): void $this->list[] = $user; > public function getIterator(): Traversable return new ArrayIterator($this->list); > >
$users = new UserCollection(); $users->add(new User()); $users->add(new User()); foreach ($users as $user) // . >
Note that this is just an pseudo example. You need to fill the User objects with real data before you add it to the collection. How you pass the data into your objects depends on your specific implementation.
Conclusion
With this simple but effective approach it is possible to fulfill the criteria mentioned above. I hope that this could inspire some people to use collection classes instead of arrays in the future.