# Should I use PHPStan generics for type-safe collections or hand-write a class per type?

> PHP has no runtime generics; PHPStan generics are exactly for this. For most cases list<Order> docblocks + strict PHPStan is enough. Write a single generic Collection<T> only when you need behavior.

- Asked: 2026-07-26
- Answered: 2026-07-31
- Asked by: Ozan
- Tags: php, generics, static-analysis
- Source: https://www.muhammetsafak.com.tr/en/just-ask/should-i-use-phpstan-generics-for-type-safe-collections-or-hand/
- Language: en-US
- Author: Muhammet Şafak

---
**Question:** My domain layer passes untyped entity arrays everywhere — I pass `Order[]` through signatures like `function process(array $orders)`, but PHPStan tells me nothing when a wrong type slips in.

I want compile-time (really, analysis-time) safety without runtime generics support. Is the right move to hand-write a class per entity type — `OrderCollection`, `InvoiceCollection` — or should I solve it with PHPStan generics? Where's the line between boilerplate and type safety?


Short answer: for most cases use PHPStan generics (generic docblocks and array shapes); move to a hand-written concrete collection class only when you need behavior or a runtime guarantee. Writing a class per type purely for typing is wasted boilerplate.

1. **PHP has no runtime generics — PHPStan generics are purely static.** `@param list<Order> $orders` gives you analysis-time type safety at zero runtime cost. That's exactly what you asked for; the interpreter checks nothing, PHPStan catches it in CI.

2. **Start with typed arrays.** To just pass collections around, use `list<Order>` or `array<int, Order>` on your signatures. No new class needed; a call passing the wrong type goes red on the PHPStan side immediately. That's the cheapest, fastest win.

3. **Write a single generic Collection when you want behavior.** If you want `->map()`, `->filter()`, or invariants like "non-empty" or "unique", write ONE `@template`-annotated `Collection<T>` class — not one per type. Because the methods carry the type through, the IDE and PHPStan preserve it along the chain.

4. **Introduce a concrete subclass only with a real reason.** Write `OrderCollection extends Collection` either for domain readability, or to add a runtime `instanceof` check in `add()` when the data comes from outside your type-checked boundary (JSON, DB, user input). It costs runtime and boilerplate; make the justification explicit.

5. **Functions can be generic too, not just classes.** If you need a type-carrying helper (`first`, `map`), you can make the function generic with `@template` without writing a class at all — a function taking `@param list<T> $items` and returning `@return T` preserves the type at the call site. That covers most needs around a collection without introducing a class.

6. **Generics only help if PHPStan actually runs strict.** Run level 8/max, enable `checkGenericClassInNonGenericObjectType`, and make PHPStan mandatory in CI. Otherwise the annotations rot and give you false confidence.

The single generic collection skeleton looks like this:

```php
/**
 * @template T of object
 */
final class TypedCollection
{
    /** @var list<T> */
    private array $items = [];

    /** @param T $item */
    public function add(object $item): void
    {
        $this->items[] = $item;
    }

    /** @return list<T> */
    public function all(): array
    {
        return $this->items;
    }
}

/** @var TypedCollection<Order> $orders */
```

**Bottom line:** personally I'd default to `list<Order>` docblocks + strict PHPStan; add a single generic `Collection<T>` when I need behavior like `map/filter`; and hand-write concrete subclasses only at a trust boundary, where I need to validate incoming data at runtime. That keeps boilerplate minimal and gets you most of the safety almost for free. Just remember: this safety exists only as long as PHPStan actually runs — if you don't make it mandatory in CI, no annotation protects you.
