If you are a web applications builder with Laravel and happens to use PHPStan for static code analysis, you will start seeing new errors when you upgrade to Laravel 11.x.
In a fresh Laravel install with PHPStan, the first time running ./vendor/bin/phpstan
the following error get thrown:
------ -----------------------------------------------------------------------------------
Line app\Models\User.php
------ -----------------------------------------------------------------------------------
13 Class App\Models\User uses generic trait
Illuminate\Database\Eloquent\Factories\HasFactory but does not specify its types:
TFactory
------ -----------------------------------------------------------------------------------
So what was changed? In Laravel 11, the HasFactory
trait now has a PHPDoc with the @template
tag which is one of the reserved generics tags. As you may already have guessed, generics are being used in many parts of the framework.
/**
* @template TFactory of \Illuminate\Database\Eloquent\Factories\Factory
*/
trait HasFactory
{
...
}
Although it is not recommended, this category of errors can be ignored by simply adding these lines of code to your phpstan.neon
file:
parameters:
ignoreErrors:
-
identifier: missingType.generics
But, generics are not that hard to understand so let’s get started!
What are Generics?
Generics in programming refer to a feature that allows you to write code that can work with multiple data types. Instead of writing separate code for each data type, you can write a single, generic piece of code that can operate on various types while maintaining type safety, unlike using general types like mixed
or object
.
Take the Illuminate\Database\Concerns\BuildsQueries::first
method from Laravel 10, it can return an instance of Model
, a general object
, an instance of the class using it like Illuminate\Database\Eloquent\Builder
or null
.
/**
* Execute the query and get the first result.
*
* @param array|string $columns
* @return \Illuminate\Database\Eloquent\Model|object|static|null
*/
public function first($columns = ['*'])
{
return $this->take(1)->get($columns)->first();
}
Generics Syntax
Generics are not supported in PHP as a first-class citizen, to have them we use the PHPDocs tags @template
, @template-covariant
, @template-contravariant
, @extends
, @implements
, and @use
.
The rules of the generic types are defined using type parameters. In PHPDocs we annotate them with the @template
tag. The type parameter name can be anything, as long as you don’t use an existing class name. You can also limit which types can be used in place of the type parameter with an upper bound using the of
keyword. This is called bounded type parameter.
<?php
namespace Illuminate\Database\Eloquent;
/**
* @template TModel of \Illuminate\Database\Eloquent\Model
*
*/
class Builder implements BuilderContract
{
}
Types of PHP Generics
Generic Function
A generic function is exactly like a normal function, however, it has type parameters. This allows the generic method to be used in a more general way.
Take the Illuminate\Support\ValidatedInput::enum
method as an example:
It defines a type parameter
TEnum
.The
$enumClass
parameter is of the pseudo typeclass-string
and bounded to the same type parameterTEnum
.The return type also can either be of
TEnum
ornull
.
/**
* @template TEnum
*
* @param string $key
* @param class-string<TEnum> $enumClass
* @return TEnum|null
*/
public function enum($key, $enumClass)
{
if ($this->isNotFilled($key) ||
! enum_exists($enumClass) ||
! method_exists($enumClass, 'tryFrom')) {
return null;
}
return $enumClass::tryFrom($this->input($key));
}
If you then call $request→validated()→enum(‘status‘, OrderStatus::class)
, PHPStan will know that you’re getting an OrderStatus
object or null!
Generic Class
Generic classes allows for creating classes that can operate on any data type while ensuring type safety. They enable a class to be defined with a placeholder for a specific type, which can later be substituted when the class is instantiated.
A good example from Laravel source code would be the Illuminate\Database\Eloquent\Builder
class:
<?php
namespace Illuminate\Database\Eloquent;
/**
* @template TModel of \Illuminate\Database\Eloquent\Model
*/
class Builder implements BuilderContract
{
/**
* @param array $attributes
* @return TModel
*/
public function make(array $attributes = [])
{
return $this->newModelInstance($attributes);
}
}
A type parameter TModel
is defined and bounded to any sub-class of Illuminate\Database\Eloquent\Model
. The same type parameter is used as a return type of the method make
.
Another example would be if we have an Order
model, which has a local scope to filter orders based on their status. The scope method should specify the TModel
type
/**
* @param Builder<Order> $query
*/
public function scopeWithStatus(Builder $query, OrderStatus $status): void
{
$query->whereStatus($status->value);
}
ℹ️ info: All the Eloquent relations classes in namespace
Illuminate\Database\Eloquent\Relations
likeBelongsTo
andHasOne
are now generic.
Generic Interface
Generic interfaces are not so different. The Illuminate\Contracts\Support\Arrayable
is an example of a generic interface
/**
* @template TKey of array-key
* @template TValue
*/
interface Arrayable
{
/**
* Get the instance as an array.
*
* @return array<TKey, TValue>
*/
public function toArray();
}
The interface defines two type parameters: TKey
of type array-key
(it can be int
or string
) and TValue
. Theses two parameters are used to define the return type of the toArray
function. Here is an example:
/**
* @implements Arrayable<int, string>
*/
class User implements Arrayable
{
public int $id;
public string $name;
/**
* @return array<int, string>
*/
public function toArray(): array
{
return [
$this->id => $this->name,
];
}
}
The user class implements the Arrayable
interface and specify the Tkey
type as an int
and the TValue
as a string
.
Generic Trait
We came across the Illuminate\Database\Eloquent\Factories\HasFactory
trait in the error at the beginning of this post. Let’s have a closer look:
<?php
namespace Illuminate\Database\Eloquent\Factories;
/**
* @template TFactory of \Illuminate\Database\Eloquent\Factories\Factory
*/
trait HasFactory
{
/**
* Create a new factory instance for the model.
*
* @return TFactory|null
*/
protected static function newFactory()
{
if (isset(static::$factory)) {
return static::$factory::new();
}
return null;
}
}
HasFactory
defines a type parameter TFactory
bounded to the sub-classes of Illuminate\Database\Eloquent\Factories\Factory
. So how can that error be fixed?
The TFactory
type must be specified when the trait is being used. So, the use
statement of the HasFactory
trait needs to be annotated with the PHPDocs @use
:
<?php
namespace App\Models;
use Database\Factories\UserFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
/** @use HasFactory<UserFactory> */
use HasFactory;
}
Preserving Genericness
When extending a class, implementing an interface, or using a trait it is possible to maintain the genericness in the sub-class.
Preserving the genericness is implemented by defining the same type parameters above the child class and passing it to @extends
, @implements
and @use
tags.
We will use the Illuminate\Database\Concerns\BuildsQueries
generic trait as an example,
it defines a type parameter TValue
:
/**
* @template TValue
*
*/
trait BuildsQueries
{
...
}
The Illuminate\Database\Eloquent\Builder
class uses this trait but keeps its genericness by passing the TModel
parameter type to it. It is now left to the client code to specify the type of TModel
and consequently TValue
in the BuildsQueries
trait.
/**
* @template TModel of \Illuminate\Database\Eloquent\Model
*/
class Builder implements BuilderContract
{
/** @use \Illuminate\Database\Concerns\BuildsQueries<TModel> */
use BuildsQueries, ForwardsCalls, QueriesRelationships {
BuildsQueries::sole as baseSole;
}
}
Final Thoughts
In conclusion, while PHP does not natively support generics in the same way as some other programming languages, the introduction of advanced type hints and tools like PHPStan allows developers to implement generics-like functionality in their code. By leveraging PHPDocs, parameterized classes, and interfaces, you can create more flexible and type-safe applications that promote code reusability and maintainability. As PHP continues to evolve, the community's growing focus on type safety and static analysis will likely lead to more robust solutions for implementing generics. Embracing these practices not only enhances your coding skills but also contributes to the development of high-quality software that stands the test of time.
Top comments (1)
I think it's important to understand how @template can help you: chatgpt.com/share/671d97a7-c5d4-80...