Contents
- 1 Phpstan Generics: When Types Start To Feel Like Trust
- 2 Why Generics Matter In Real PHP Work
- 3 The Moment You Realize mixed Isn’t A Plan
- 4 Generics By Examples: Thinking In Types Instead Of Hopes
- 5 Returning The Same Type You Receive
- 6 Template Bounds: Trust, But Verify
- 7 Making Interfaces And Extensions Generic Without Chaos
- 8 Optional Type Arguments And Defaults
- 9 Variance: When “More Specific” And “More General” Matter
- 10 Safer Domain Code: A Realish Example
- 11 Generics And Daily Developer Life
- 12 Where To Start With Phpstan Generics
- 13 The Quiet Payoff
Phpstan Generics: When Types Start To Feel Like Trust
You know that feeling when you stare at a piece of PHP code and think: “This works… but only because nobody has pushed it hard enough yet”?
I’ve been there.
It’s late. You’re on your second coffee that shouldn’t have happened. CI is red, the deadline is not a polite suggestion anymore, and some mysterious TypeError is coming from deep inside a class named Manager or Helper or Util because Past You thought mixed was a great idea.
PHP will happily run that code — until one day it doesn’t.
That’s exactly where PHPStan generics start to feel less like theory and more like survival.
On a platform like Find PHP, where people try to hire experienced PHP developers, find better jobs, and keep up with the PHP ecosystem, the ability to say “I write type-safe, predictable code” is not marketing. It’s a skill. Generics with PHPStan sit right at the core of that.
Let’s talk about them, as if we’re sitting in front of a glowing monitor together, cleaning up a codebase we both secretly know is a little more chaotic than it admits.
Why Generics Matter In Real PHP Work
PHP doesn’t have native generics. No ArrayList<Foo> like in Java. No fancy templates like in C++.
But PHPStan, Psalm, and modern IDEs agreed on a common language: PHPDoc-based generics. Tools read your comments, infer types, and then do something magic — they tell you where your code will break, before it breaks.
According to PHPStan’s docs, generics are about making types more specific, especially in places where you used to write mixed, object, or “array of something”. Generics make those “somethings” precise, and PHPStan uses that precision to find more real bugs and fewer false positives. Documentation on Generics in PHP using PHPDocs explains that for each call of a generic function, PHPStan infers type variables from the argument types and applies that knowledge to the return type, giving you type-safe patterns that were impossible before. Generics also allow type bounds with of to constrain which types can be used as template arguments.
Translated to everyday work:
- Fewer “it worked yesterday, now it doesn’t” incidents.
- Fewer runtime type errors surprise you in production.
- More confidence when refactoring.
- Stronger signal to recruiters and clients that your code is not a lottery.
That’s why generics are a quiet but powerful selling point for both developers looking for jobs and teams looking for reliable PHP specialists.
The Moment You Realize mixed Isn’t A Plan
Picture a simple repository:
final class UserRepository
{
/** @return array */
public function all(): array
{
// ...
}
}
This works. Until you do:
$users = $userRepository->all();
echo $users[0]->getEmail();
PHPStan will probably shrug and say: “Hey, array is… something.” No idea what. That “something” might be User, might be stdClass, might be string. That’s not type safety; it’s wishful thinking.
Now add a little discipline:
final class UserRepository
{
/** @return User[] */
public function all(): array
{
// ...
}
}
Better. PHPStan now knows it’s an array of User.
But we’re still stuck in one dimension: arrays, lists, maps. Every time we build a reusable abstraction — collections, paginators, result sets — we end up repeating ourselves or sacrificing type accuracy.
This is where generics quietly walk into the room.
Generics By Examples: Thinking In Types Instead Of Hopes
PHPStan’s article Generics By Examples is full of real-world idioms: “return the same type the function accepts”, “specify template type when implementing an interface”, “make type arguments optional with defaults”. It’s not just theory, it’s patterns you can drop into living projects.
Let’s build something small and practical.
A Generic Collection
We want a collection that knows what it holds.
/**
* @template T
*/
final class Collection
{
/** @var T[] */
private array $items = [];
/**
* @param T $item
*/
public function add($item): void
{
$this->items[] = $item;
}
/**
* @return T[]
*/
public function all(): array
{
return $items;
}
}
Now, when we use it:
/** @var Collection<User> $users */
$users = new Collection();
$users->add(new User(/* ... */));
foreach ($users->all() as $user) {
$user->getEmail(); // PHPStan knows $user is User
}
PHPStan has enough information to understand that Collection<User> is a collection of User instances. The generic type T is bound to User at the call site, and then propagated through the methods. Documentation on generics explains that PHPStan infers these template type variables when analyzing calls and applies them to return types, making code like this safe to navigate.
This one little annotation @template T does something powerful: it connects parameter types, property types, and return types into a coherent, predictable flow. You don’t just say “array of objects”. You say “this is a collection of User, specifically, and PHPStan will watch that promise”.
Returning The Same Type You Receive
There’s a beautiful generic pattern described in PHPStan’s docs: a function that returns the same type that it accepts. Think of factories, transformers, or “builder” functions.
/**
* @template T
* @param T $value
* @return T
*/
function wrap($value)
{
// ...
return $value;
}
Call it with a User, you get a User. Call it with a Product, you get a Product. PHPStan’s generic inference engine looks at the parameter type at each call, infers T, and then uses that for the return type.
In practice, this is huge for code like:
hydrate($className, array $data)map(Collection<T> $items, callable(T): U): Collection<U>find(string $className, int $id): ?T
The static analysis article explains that without generics, these functions would have to return something like object or mixed, making the caller blind about which methods are safe to call. With generics, you keep those methods strongly typed while still staying abstract.
This pattern is a great thing to show on your CV or in a code sample on Find PHP. It proves you’re thinking about type flows rather than just “functions that kind of work”.
Template Bounds: Trust, But Verify
Sometimes you don’t want “anything”. You want “anything that is at least a Foo”.
PHPStan supports template bounds using the of keyword in PHPDoc. The generics documentation calls this an upper bound — a way to limit which types can be used for a template variable.
/**
* @template T of Foo
*/
final class FooCollection
{
/** @var T[] */
private array $items = [];
/**
* @param T $item
*/
public function add($item): void
{
$this->items[] = $item;
}
}
Now:
FooCollection<Foo>✅FooCollection<Bar extends Foo>✅FooCollection<stdClass>❌ — PHPStan will complain becausestdClassdoesn’t satisfy theFoobound.
This is more than a type detail. It’s a contract. It’s a way to formalize a design decision: this collection holds only objects that behave like Foo. The docs show this “type variable bound” explicitly as a way to prevent misuse of generics and keep static analysis meaningful.
When teams look for developers, this kind of constraint thinking is what separates “writes PHP” from “designs systems”.
Making Interfaces And Extensions Generic Without Chaos
Things get interesting when you start mixing generics with interfaces and inheritance.
PHPStan’s generics documentation explains two key ideas:
- When you implement a generic interface or extend a generic class, you must either:
- fix the template variable (child is not generic anymore),
- or preserve it (child stays generic by repeating the templates).
Example from the docs:
/**
* @template T
*/
interface Collection
{
/** @return T */
public function first();
}
If you want a concrete implementation:
/**
* @implements Collection<User>
*/
final class UserCollection implements Collection
{
public function first(): User
{
// ...
}
}
Here, UserCollection is not generic; we fixed T at User. PHPStan’s docs show that using @implements Collection<Foo> removes “does not specify its types” errors and fully specifies the generic parent.
If you want to keep it generic:
/**
* @template T
* @implements Collection<T>
*/
final class GenericCollection implements Collection
{
public function first()
{
// ...
}
}
Now both Collection and GenericCollection are generic over T. Anything that uses GenericCollection<Product> will get strong typing based on that Product.
There’s a similar story with @extends when inheriting from a generic parent class. The documentation makes it clear: @implements, @extends, and @use match the PHP keywords and control how template types flow into child classes.
This pattern shows up when building:
- generic repositories,
- abstract base controllers,
- domain services that work over multiple entity types.
It’s the kind of architectural clarity hiring teams love to see in code reviews.
Optional Type Arguments And Defaults
Real code isn’t always black-and-white. Sometimes you want “a pair of things”, and if nobody says otherwise, it’s string and int.
PHPStan supports defaults for template type variables using =. The generics examples demonstrate this with simple pairs.
/**
* @template T = string
* @template U = int
*/
final class Pair
{
/** @var T */
public $first;
/** @var U */
public $second;
}
Now you get:
$defaultPair = new Pair(); // Pair<string, int>
$customPair = new Pair(); // but with annotations: Pair<User, Money>
You can even mix defaults with bounds: @template T of object = stdClass. PHPStan’s docs explicitly mention this combination as a way to keep types flexible but safe.
From a practical perspective, defaults are a good way to keep PHPDocs less verbose while still giving PHPStan useful information. They make generic APIs friendlier, especially in libraries or shared internal tools.
And yes: when someone scans your code on a platform like Find PHP, seeing well-thought-out defaults tells them you care about usability, not just theory.
Variance: When “More Specific” And “More General” Matter
Once you get comfortable with @template, the next layer is variance: how generic types behave when you pass them around.
PHPStan supports:
@template-covariant@template-contravariant- and even call-site variance and star projections (
Collection<*>) as documented in the PHPDocs basics.
In simple terms:
- Covariant: you can use a more specific type where a more general one is expected.
- Contravariant: you can use a more general type where a more specific one is expected.
Example from the docs:
/**
* @template-contravariant T
*/
interface Comparator
{
/**
* @param T $value
*/
public function compare($value): int;
}
With contravariance, a Comparator<Animal> can be passed where Comparator<Cat> is expected, because comparing Animal is “more general” than just Cat. PHPStan’s docs explain that contravariant template variables cannot appear in return positions, only in input.
Is this something you’ll use every day? Probably not.
But when you design more advanced libraries, or complex business logic, variance becomes the difference between “I hope this works” and “the type system proves this works”.
It’s the kind of subtle skill that, once you understand it, quietly reflects in how you structure everything else.
Safer Domain Code: A Realish Example
Let’s leave the abstract world for a moment and talk about something closer to how we actually write PHP for living.
Imagine an ecommerce system. You have entities: Product, Customer, Order. You have relations: addresses loaded or not, reviews fetched or not. A blog post describing how to constrain classes with PHPStan explores a similar idea: using generics to represent “customer with loaded addresses but without orders”, and having PHPStan check that type statically.
Even if we don’t reproduce the entire solution, the intent is clear: use generics to represent state, not just “structure”.
Example:
/**
* @template TAddresses
*/
final class Customer
{
/** @var TAddresses */
private $addresses;
/**
* @param TAddresses $addresses
*/
public function __construct($addresses)
{
$this->addresses = $addresses;
}
/**
* @return TAddresses
*/
public function getAddresses()
{
return $this->addresses;
}
}
Now you can have:
Customer<NotLoaded>— addresses not loaded.Customer<Address[]>— addresses fully loaded.
Your services can then declare:
/**
* @param Customer<Address[]> $customer
*/
function shipOrder($customer): void
{
// PHPStan knows: addresses are present and loaded
}
Generics become a way to encode expectations about object state. At runtime nothing changes — generics are erased — but PHPStan stands guard in CI, saying: “You promised me loaded addresses here. I’ll make sure you don’t accidentally pass a customer without them.”
From the outside, this is invisible. But from the perspective of someone reviewing your code, it’s a strong signal: you think in terms of invariants, not just methods.
Generics And Daily Developer Life
Let’s be honest. Most of us don’t wake up excited about @template-covariant. We wake up thinking about:
- “Will this refactor blow up staging?”
- “Will a junior on the team accidentally break this API?”
- “Will this bug reach production before someone notices?”
Generics with PHPStan quietly help with all three.
When your collection, repository, or service is generic and type-safe:
- Your IDE helps you earlier.
- PHPStan complains louder and sooner.
- Refactors feel less like surgery and more like mechanical changes.
The PHPDocs basics mention things like @phpstan-param, @phpstan-return, and even type-narrowing tags for assertions — advanced tools on top of generics. Together they form a language in which you describe your intentions, and PHPStan enforces them.
This is exactly the sort of thing that turns a PHP developer into a PHP engineer in the eyes of hiring managers on places like Find PHP. It’s not about buzzwords. It’s about code that holds together.
Where To Start With Phpstan Generics
If generics still feel abstract, here’s a simple progression you can follow in your own codebase:
- Replace
arrayin PHPDocs with specific element types:User[],Product[]. - Extract those patterns into generic containers:
Collection<T>,PaginatedResult<T>. - Use
@template Tand propagate it into properties, params, and return types as shown in PHPStan’s generics docs. - Use
@implementsand@extendsto ensure your child classes and interfaces specify or preserve template types. - Add bounds with
ofwhen you want to constrain template types:@template T of Foo. - Explore defaults and variance for more advanced abstractions, using PHPStan’s documentation as a guide.
Every time you do this, you’re not just “improving types”. You’re doing something more quiet and more meaningful: you are making your future self’s life easier.
And when you share that code — in a portfolio, in a pull request, in a resume — it shows.
The Quiet Payoff
I still remember the first big “aha” moment with PHPStan generics.
A CI pipeline that had been politely green for months suddenly lit up with a few new errors after I added generic types to a shared collection class. They were real bugs. Hidden in edge-case code paths, only triggered when a certain feature flag was on, in combination with a particular data shape.
Production had never hit them. Yet.
PHPStan did, the moment the types became honest.
There’s a particular kind of calm that follows that experience.
It’s not dramatic. It’s just the quiet realization that your code is a little more trustworthy now, and that you’ve learned a tool you can carry into every future project, job, or collaboration.
Generics won’t make PHP beautiful overnight. They won’t replace common sense or good design. But they give you a language to describe your intentions clearly — and a tool that keeps you honest.
And maybe that’s what we’re all trying to build, sitting alone in front of glowing monitors and stubborn codebases: systems where our intentions survive time, changes, and other people’s edits, and where the types whisper back that everything still fits together.