ArrayShapes in PHPStan
06. Juni 2025
06. Juni 2025
Once you get to levels 6,7,8 PHPStan wants you to define the shape of arrays more and more. This can be tedious to manage through all of the codebase, especially if arrays are passed around as parameters and returned as results. Short of refactoring them into ValueObjects (which might be still a good idea, depending on the case), this might help you a bit:
// note: options can be optional
$myArray = [
'name' => 'the name',
'id' => 15,
'options' => [
[
'optionId' => 25,
'optionValue' => 'the value'
],
[
'optionId' => 'e3aa11cf-0bdc-4964-a19e-0057a796f26b',
'optionValue' => 'this time from a uuid'
]
],
];
/**
* @var array{name: string, id: int, options?: array<int,array{optionId: int|string, optionValue: string}>}
*/
Now, this ArrayShape would have to be carried around throughout the code, depending where the array is used:
// myServiceInterface.php
interface myServiceInterface {
/**
* @param array{name: string, id: int, options?: array<int,array{optionId: int|string, optionValue: string}>} $myArray
* @return array{name: string, id: int, options?: array<int,array{optionId: int|string, optionValue: string}>}
*/
public function handle(array $myArray):array;
}
// myService.php
class myService implements myServiceInterface {
/**
* @param array{name: string, id: int, options?: array<int,array{optionId: int|string, optionValue: string}>} $myArray
*/
public function __construct(protected array $myArray) {}
/**
* @return array{name: string, id: int, options?: array<int,array{optionId: int|string, optionValue: string}>}
*/
public function getMyArray():array
{
return $this->myArray;
}
/**
* @param array{name: string, id: int, options?: array<int,array{optionId: int|string, optionValue: string}>} $myArray
* @return array{name: string, id: int, options?: array<int,array{optionId: int|string, optionValue: string}>}
*/
public function handle(array $myArray):array
{
$this->myArray = array_merge($this-myArray,$myArray);
return $this->myArray;
}
}
as you can see, this gets rather hard to handle, to read and to maintain. Just imagine we have to add another key to $myArray.
To keep this manageable you can do something like this:
// myServiceInterface.php
/**
* @phpstan-type MyArray array{name: string, id: int, options?: array<int,array{optionId: int|string, optionValue: string}>}
*/
interface myServiceInterface {
/**
* @param MyArray $myArray
* @return MyArray
*/
public function handle(array $myArray):array;
}
// myService.php
/**
* @phpstan-import-type MyArray from myServiceInterface
*/
class myService implements myServiceInterface {
/**
* @param MyArray $myArray
*/
public function __construct(protected array $myArray) {}
/**
* @return MyArray
*/
public function getMyArray():array
{
return $this->myArray;
}
/**
* @param MyArray $myArray
* @return MyArray
*/
public function handle(array $myArray):array
{
$this->myArray = array_merge($this-myArray,$myArray);
return $this->myArray;
}
}
This makes it much more manageable and extensible on a single point. Personally I would recommend doing this in Interfaces, but that is up to you. As a bonus, IDE like PHPStorm do understand these annotations and auto-complete them, and you can navigate through them like you are used with methods and classes.
You can do this as well in the phpstan.neon file, but in this case I think it is more something code-related and should be near the code rather than phpstan related in general.
More Informations: