Generics in PHP
When most people think of PHP, strong typing is not the first thing that comes to mind. After many great updates to the language, and some great community tooling. PHP is well on its way to the programming bliss of strong typing like its peers Golang and Typescript.
At some point in your strong typing journey you'll come across a need for type flexibility while maintaining type safety.
In this post I'll give you a peek at how to accomplish this using generics.
PHPStan
PHPStan is a well known static analyzer for PHP. We'll be using it to get the most out of our generics and to be more strict with our typing.
Install it via composer
composer require --dev phpstan/phpstanPHPStan uses a level system (1 through 10) to determine its strictness for checking our code. Level 6 seems to be the sweet spot for the code we'll be working with.
You can run PHPStan using this command
vendor/bin/phpstan analyse src --level 6A class worthy of a generic
We'll be creating a class called Stack that takes a generic type and adds it to an array. This will let us add some functionality to arrays.
/**
* @template T
*/
class Stack {
/**
* @var array<int, T>
*/
private array $items = [];
/**
* @param T $item
*/
public function push($item): void {
$this->items[] = $item;
}
/**
* @return T|null
*/
public function pop() {
if (empty($this->items)) {
return null;
}
return array_pop($this->items);
}
}I'll be the first to admit generics are not the most intuitive thing to accomplish in PHP. The language itself has no concept of what generics are, its static analyzers and IDE intellisense that make generics "work" in PHP.
Because of this all our generic handling happens in docblocks.
/**
* @template T
*/
class Stack {This docblock tells our IDE that the class Stack takes a generic. Will see T used again and it'll be a reference to this definition of T at the top of the class.
Let' s create a stack and see how we pass T.
/**
* @return Stack
*/
function createStack(): Stack
{
return new Stack();
}
$stack = createStack();What does PHPStan think of our code so far?
--------------------------------------------------------------------------------------------------
Function crateStack()
return type with generic class App\Stack does not specify its types: T
--------------------------------------------------------------------------------------------------PHPStan understands that Stack requires a generic and that we have not specified it in our function return.
/**
* @return Stack<string>
*/
function createStack(): Stack
{
return new Stack();
}
$stack = createStack();All we need to do is pass <string> to our return definition and PHPStan is happy.
Whats interesting is that our return type on createStack is just Stack.
PHP can understand and enforce this return type at runtime. PHPStan takes it to another level by ensuring stack is also supplied its generic type.
Let's see how can pass T as a value.
/**
* @param T $item
*/
public function push($item): void {
$this->items[] = $item;
}Here we're using the generic as a param to our push method. What happens if we pass something other than the expected type?
$stack = createStack();
$stack->push(5);----------------------------------------------------------------------------------
Parameter #1 $item of method App\Stack<string>::push()
expects string, int given.
----------------------------------------------------------------------------------Solely based off our return type of Stack<string> Stan is able to know that this instance of a stack is not allowed to have int or anything besides a string passed to it.
Let's look at returning T from the our stack.
/**
* @return T|null
*/
public function pop() {
if (empty($this->items)) {
return null;
}
return array_pop($this->items);
}@return T|null we're returning our generic type or null here.
$stack = createStack();
$stack->push('foo');
$item = $stack->pop();
if($item !== null) {
exit();
}
if(is_string($item)) {
echo "Its a string!";
}Since we possibly return null we need to check that first and narrow our type down to just the generic. I added this is_string() check to show you a neat thing Stan can do for us.
----------------------------------------------------------------------------------
Call to function is_string()
with non-falsy-string will always evaluate to true.
---------------------------------------------------------------------------------Here Stan is letting you know there is no reason to check for is_string() because we already know $item is not null and since T is a string then our value must be a string!
Constraints on Generics
There will be times when we expect a generic type but we know it should be constrained to something like an array, class or object. We can even go a step further and expect certain properties or keys on these parameters.
/**
* @template T of array{numbers: array<int>}
* @param T $input
* @return array{numbers: T, sum: int}
*/
function sumNumbers(array $input): array
{
return [
'numbers' => $input,
'sum' => count($input)
];
}We do this with a line like @template T of array which expects T to be an array. In our example above we're being even more specific array{numbers: array<int>} the expectation here is an array with a key named numbers that is an array of integers.
The passed array can have more keys than numbers but it must at least have that key.
What happens when we pass an array of strings
sumNumbers([
'numbers' => ['A', 'B', 'C']
])-------------------------------------------------------------------------------------------------------------------------
Parameter #1 $input of function sumNumbers
expects array{numbers: array<int>}, array{numbers: array{'A', 'B', 'C'}} given.
Offset 'numbers' (array<int>) does not accept type array<int, string>.
--------------------------------------------------------------------------------------------------------------------------Neat right? We can narrow down what our functions expect. Another positive is what our IDE sees as the return type when we pass a valid array of int's.
$result = sumNumbers([
'numbers' => [1,2,3]
])This is what our IDE sees $result as...
@var array{numbers: array{numbers: int[]}, sum: int} $resultHow cool is that? We're constraining the param but also utilizing our generic to get better types from our IDE.
Final Notes
PHPStan was the key to unlocking extra safety in our PHP codebase. I highly suggest trying it out in your projects.
There is certainly more depth to generics in PHP but I hope you got a nice primer on how you can utilize them.
Thanks for reading!