⬆️ ⬇️

PHP generics today (well, almost)

If you ask PHP developers what opportunity they want to see in PHP, most would call generics.



Generic language support would be the best solution. But it is difficult to implement them. We hope that one day the native support will become part of the language, but it is likely that this will have to wait several years.



This article will show how, using existing tools, in some cases with minimal modifications, we can get the power of generics in PHP right now.



From the translator: I deliberately use tracing from English "generics", because I have never heard anyone communicate that he called it "generalized programming."


Content:





What are generics?



This section covers a brief introduction to generics .



Links to read:





Simplest example



Since it is currently not possible to define generics at the language level, we will have to use another great opportunity - to define them in docblocks.



We already use this option in a variety of projects. Take a look at this example:



/** * @param string[] $names * @return User[] */ function createUsers(iterable $names): array { ... } 


In the code above, we do what is possible at the language level. We have defined the $names parameter as something that can be listed. We also indicated that the function will return an array. PHP will throw a TypeError if the parameter types and return value do not match.



Dokblok improves understanding of the code. $names must be strings, and the function must return an array of User objects. PHP itself does not make such checks. But IDE, such as PhpStorm, understand this notation and warn the developer that the additional contract is not respected. In addition to this, static analysis tools such as Psalm, PHPStan and Phan can validate the correctness of the transferred data to and from the function.



Generics for defining keys and values ​​of enumerated types



The above is the simplest example of a generic. More sophisticated methods include the ability to specify the type of its keys, along with the type of values. Below is one of the ways of this description:



 /** * @return array<string, User> */ function getUsers(): array { ... } 


It says that the array returned by the getUsers function has string keys and values ​​of type User .



Static analyzers such as Psalm, PHPStan and Phan understand this annotation and will take it into account when checking.



Consider the following code:



 /** * @return array<string, User> */ function getUsers(): array { ... } function showAge(int $age): void { ... } foreach(getUsers() as $name => $user) { showAge($name); } 


Static analyzers will throw a warning on the showAge call with an error like this: Argument 1 of showAge expects int, string provided .



Unfortunately, at the time of writing, PhpStorm does not know how.



More complicated generics



We continue to delve into the topic of generics. Consider an object representing a stack :



 class Stack { public function push($item): void { ... } public function pop() { ... } } 


The stack can accept any type of object. But what if we want to limit the stack to only User objects?



Psalm and Phan support the following annotations:



 /** * @template T */ class Stack { /** * @param T $item */ public function push($item): void; /** * @return T */ public function pop(); } 


A dockblock is used to transfer additional information about types, for example:



 /** @var Stack<User> $userStack */ $stack = new Stack(); Means that $userStack must only contain Users. 


Psalm, when analyzing the following code:



 $userStack->push(new User()); $userStack->push("hello"); 


Will complain about line 2 with the error Argument 1 of Stack::push expects User, string(hello) provided.



Currently PhpStorm does not support this annotation.



In fact, we have covered only some of the information on generics, but for now, that’s sufficient.



How to implement generics without language support



You must perform the following steps:





Standardization



At the moment, the PHP community has unofficially adopted this generic format (they are supported by most tools and their meaning is clear to most):



 /** * @return User[] */ function getUsers(): array { ... } 


However, we have problems with simple examples, like this:



 /** * @return array<string, User> */ function getUsers(): array { ... } 


Psalm understands it, and knows what type the key has and the value of the returned array.



At the time of writing, PhpStorm does not understand this. Using this entry, I miss the power of real-time static analysis offered by PhpStorm.



Consider the code below. PhpStorm does not understand that $user is of type User , and $name is string:



 foreach(getUsers() as $name => $user) { ... } 


If I chose Psalm as a static analysis tool, I could write the following:



 /** * @return User[] * @psalm-return array<string, User> */ function getUsers(): array { ... } 


Psalm understands all this.



PhpStorm knows that the $user variable is of type User . But, he still does not understand that the array key refers to the string. Phan and PHPStan do not understand the psalm specific annotations. The maximum that they understand in this code is the same as in PhpStorm: the type of $user



You can argue that PhpStorm just needs to accept the agreement array<keyType, valueType> . I do not agree with you, because I believe that this dictation of standards is the task of the language and the community, and the tools only have to follow them.



I assume that the agreement described above will be warmly welcomed by most of the PHP community. The one that is interested in generics. However, things get much more complicated when it comes to patterns. Currently neither PHPStan nor PhpStorm support templates. Unlike Psalm and Phan. Their purpose is similar, but if you dig deeper, you will realize that the implementations are a little different.



Each of the options presented is a kind of compromise.



Simply put, there is a need for an agreement on the format of the generic drugs:





Tool support



Psalm has all the necessary functionality to test generics. Phan is kind of like, too.



I’m sure PhpStorm will implement generics as soon as the single format agreement appears in the community.



Support for third-party code



The final part of the generics puzzle is the addition of support for third-party libraries.



Hopefully, as soon as the standard for the definition of generics appears, most libraries will implement it. However, this will not happen immediately. Some libraries are used, but have no active support. When using static analyzers for type validation in generics, it is important that all functions that accept or return these generics are defined.



What happens if your project relies on the work of third-party libraries that do not have generic support?



Fortunately, this problem has already been solved, and the solutions are stub functions. Psalm, Phan and PhpStorm support stubs.



Stubs are ordinary files that contain the signatures of functions and methods, but do not implement them. By adding docblocks to the stubs, the static analysis tools get the additional information they need. For example, if you have a stack class without taypkhintov and generics, like this.



 class Stack { public function push($item) { /* some implementation */ } public function pop() { /* some implementation */ } } 


You can create a stub file that has identical methods, but with the addition of docblocks and without implementing functions.



 /** * @template T */ class Stack { /** * @param T $item * @return void */ public function push($item); /** * @return T */ public function pop(); } 


When a static analyzer sees a stack class, it takes type information from the stub, rather than from the actual code.



The ability to simply share the stub code (for example, through the composer) would be extremely useful, since would allow to share the work done.



Further steps



The community needs to move away from agreements and define standards.



Maybe the best option would be a PSR about generics?



Or, perhaps, the creators of the basic static analyzers, PhpStorm, other IDEs, and some of the people involved in the development of PHP (for control) could develop a standard that everyone would use.



As soon as the standard appears, everyone will be able to help with adding generics to existing libraries and projects, creating pull requests. And where this is not possible, developers can write and share stubs.



When everything is done, we will be able to use tools like PhpStorm to test generics in real time while we are writing code. We can use static analysis tools as part of our CI as a security guarantee.



In addition, generics can be implemented in PHP (well, almost).



Restrictions



There are a number of restrictions. PHP is a dynamic language that allows you to do many "magical" things, such as these . If you use too much PHP magic, it may happen that static analyzers cannot accurately extract all types in the system. If any types are unknown, then the tools will not be able to correctly use generics in all cases.



However, the main use of this analysis is to test your business logic. If you are writing clean code, then you should not use too much magic.



Why don't you just add generics to the language?



That would be the best option. PHP has open source code, and no one bothers you to clone sources and implement generics!



What if I don't need generics?



Just ignore the above. One of the main advantages of PHP is that it is flexible in choosing the right level of implementation complexity depending on what you create. With a one-time code, you don’t need to think about things like tyinghinting. But in large projects it is worth using such opportunities.



Thanks to all who read to this place. I will be glad to your comments on drugs.


')

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



All Articles