📜 ⬆️ ⬇️

About generics in PHP and why we need them


In this article we will look at some common problems associated with arrays in PHP. All problems can be solved with the help of RFC, which adds generics to PHP. We will not go deep into what generics are, but by the end of the article you should understand why they are useful and why many people are waiting for their appearance in PHP.


Suppose you have a set of blog posts downloaded from some data source.


$posts = $blogModel->find(); 

You need to cycle through all the posts and do something with their data. For example, with id .


 foreach ($posts as $post) { $id = $post->getId(); // -  } 

This is a common scenario, on the basis of which we will discuss the role of generics and why the community needs them so much. Consider the problems that arise in this scenario.


Data Integrity


In PHP, an array is a collection of ... elements.


 $posts = [ 'foo', null, self::BAR, new Post('Lorem'), ]; 

If we cycle through our set of posts, as a result, we get a critical error.


 PHP Fatal error: Uncaught Error: Call to a member function getId() on string 

Call ->getId() with reference to the string 'foo' . Not a ride. When cycling through an array, we want to be sure that all values ​​belong to a particular type. You can do this:


 foreach ($posts as $post) { if (!$post instanceof Post) { continue; } $id = $post->getId(); // -  } 

This will work, but if you have already written the PHP code for production, then you know that such checks sometimes grow quickly and pollute the code base. In our example, you can check the type of each entry in the method ->find() in $blogModel . But this only transfers the problem from one place to another. Although the situation has improved slightly.


There is another difficulty with the integrity of the data structure. Suppose you have a method that needs an array of blog posts:


 function handlePosts(array $posts) { foreach ($posts as $post) { // ... } } 

Again we can add additional checks to the loop, but this does not guarantee that $posts contains only the Posts collection of Posts.


Starting in PHP 7.0, you can use the ... operator to solve this problem:


 function handlePosts(Post ...$posts) { foreach ($posts as $post) { // ... } } 

But this approach has a reverse side: you have to call the function as applied to the unpacked array.


 handlePosts(...$posts); 

Performance


It can be assumed that it is better to know in advance whether the array contains only elements of a certain type than to manually check the types in each cycle each time.


We can not drive away on the generics of the benchmark, because they are not there yet, so we can only guess how they will affect the performance. But it is not insane to assume that the optimized behavior of PHP written in C is the best way to solve the problem compared to creating a heap of code for the user space.


Auto completion (Code completion)


I do not know about you, but when writing PHP code, I resort to the IDE. Autocompletion greatly increases productivity, so I would like to use it here too. When looping through posts, we need the IDE to treat each $post a Post instance. Let's look at a simple PHP implementation:


 # BlogModel public function find() : array { //  ... } 

Starting with PHP 7.0, return types appeared, and in PHP 7.1 they were improved using void and null-type types. But we can not tell the IDE that is contained in the array. Therefore, we return to PHPDoc.


 /** * @return Post[] */ public function find() : array { //  ... } 

When using the generic implementation, for example, the model class (model class), the prompting method ->find() not always possible. So in our code, we’ll have to limit the $ posts variable.


 /** @var Blog[] $posts */ $posts = $blogModel->find(); 

The lack of confidence in the contents of the array and the effect of code scatter on performance and maintainability, as well as the inconvenience of writing additional checks, forced me to look for a better solution for a long time.


* * *


In my opinion, this solution is generics . I will not describe in detail what they are doing, you can read about it in the RFC. But I will give an example of how generics can help in solving the problems described above, always ensuring that the collection contains correct data.


Important note : generics are not yet in PHP. The RFC is for PHP 7.1, there is no further information about its future. The code below is based on the Iterator and ArrayAccess interfaces that exist with PHP 5.0. In the end, we will examine the generic example, which is a dummy code.


First, create a Collection class that works in PHP 5.0+. This class implements Iterator so that it can cycle through its elements, as well as ArrayAccess , so that you can use an “array-like” syntax to add and access elements of the collection.


 class Collection implements Iterator, ArrayAccess { private $position; private $array = []; public function __construct() { $this->position = 0; } public function current() { return $this->array[$this->position]; } public function next() { ++$this->position; } public function key() { return $this->position; } public function valid() { return isset($this->array[$this->position]); } public function rewind() { $this->position = 0; } public function offsetExists($offset) { return isset($this->array[$offset]); } public function offsetGet($offset) { return isset($this->array[$offset]) ? $this->array[$offset] : null; } public function offsetSet($offset, $value) { if (is_null($offset)) { $this->array[] = $value; } else { $this->array[$offset] = $value; } } public function offsetUnset($offset) { unset($this->array[$offset]); } } 

Now we can use this class:


 $collection = new Collection(); $collection[] = new Post(1); foreach ($collection as $item) { echo "{$item->getId()}\n"; } 

Note: there is no guarantee that $collection contains only Posts . If we add, for example, a string value, it will work, but our cycle will break.


 $collection[] = 'abc'; foreach ($collection as $item) { // This fails echo "{$item->getId()}\n"; } 

At the current level of PHP development, we can solve this problem by creating a PostCollection class. Note: return types that allow null are available only with PHP 7.1.


 class PostCollection extends Collection { public function current() : ?Post { return parent::current(); } public function offsetGet($offset) : ?Post { return parent::offsetGet($offset); } public function offsetSet($offset, $value) { if (!$value instanceof Post) { throw new InvalidArgumentException("value must be instance of Post."); } parent::offsetSet($offset, $value); } } 

Now only Posts can be added to our collection.


 $collection = new PostCollection(); $collection[] = new Post(1); // This would throw the InvalidArgumentException. $collection[] = 'abc'; foreach ($collection as $item) { echo "{$item->getId()}\n"; } 

Works! Even without generics! There is only one problem: the solution is not scalable. You need separate implementations for each type of collection, even if the classes differ only in type.


It is likely that subclasses can be created with greater convenience by “abusing” late static binding and the reflective PHP API. But in any case, you will need to create classes for each available type.


Gorgeous generics


Considering all this, let's consider the code that we could write if the generics were implemented in PHP. This can be a single class used for all types. For the sake of convenience, I will cite only changes from the previous Collection class, keep this in mind.


 class GenericCollection<T> implements Iterator, ArrayAccess { public function current() : ?T { return $this->array[$this->position]; } public function offsetGet($offset) : ?T { return isset($this->array[$offset]) ? $this->array[$offset] : null; } public function offsetSet($offset, $value) { if (!$value instanceof T) { throw new InvalidArgumentException("value must be instance of {T}."); } if (is_null($offset)) { $this->array[] = $value; } else { $this->array[$offset] = $value; } } // public function __construct() ... // public function next() ... // public function key() ... // public function valid() ... // public function rewind() ... // public function offsetExists($offset) ... } $collection = new GenericCollection<Post>(); $collection[] = new Post(1); // This would throw the InvalidArgumentException. $collection[] = 'abc'; foreach ($collection as $item) { echo "{$item->getId()}\n"; } 

And that's it! We use <T> as a dynamic type that can be checked before runtime. And again, the GenericCollection class could be taken for any type.


If you are as impressed with generics as I am (and this is just the tip of the iceberg), conduct community education and share the RFC: https://wiki.php.net/rfc/generics


')

Source: https://habr.com/ru/post/329494/


All Articles