Back to blog

Core PHP

Generics in PHP: Current Workarounds and What the Future Holds

Explores PHP's current lack of runtime generics: PHPDoc template annotations, Psalm and PHPStan generics, and the current RFC landscape.

  • PHP
  • Generics
  • PHPStan
  • Psalm
  • Static Analysis
  • Core PHP

SEO Metadata

SEO Title Options

  1. Generics in PHP: Current Workarounds and What the Future
  2. Generics in PHP: Current Workarounds and: Practical 2026
  3. Core PHP Playbook: Generics in PHP: Current Workarounds

Meta Description Options

  1. Learn Generics in PHP: Current Workarounds and What the Future Holds with a practical Core PHP framework, expert mistakes, implementation steps, examples.
  2. Explores PHP's current lack of runtime generics: PHPDoc template annotations, Psalm and PHPStan generics, and the current RFC landscape.

URL Slug

generics-in-php-current-workarounds-what-future-holds

Focus Keyword

Generics in PHP: Current Workarounds and What the Future Holds

Additional LSI Keywords

  • Core PHP
  • PHP
  • Generics
  • PHPStan
  • Psalm
  • Static Analysis
  • Generics in PHP: Current Workarounds and What the Future Holds
  • production checklist
  • implementation guide
  • best practices
  • architecture decisions
  • testing strategy

Table of Contents

Article overview

Generics in PHP: Current Workarounds and What the Future Holds is the kind of topic that looks simple until it reaches production. Teams usually discover the real cost late: unclear boundaries, weak defaults, hidden maintenance work, and decisions that seemed harmless when the codebase was small.

The problem gets worse when the article, tutorial, or implementation guide only explains the happy path. This guide closes that gap with a practical framework, a comparison table, common mistakes, and a deep technical section you can use while planning real work.

Keep reading for the non-obvious part: the safest implementation is rarely the most impressive-looking one. It is the one your team can debug, test, document, and evolve without turning every future change into archaeology.

Key Takeaways

  • Generics in PHP: Current Workarounds and What the Future Holds should be evaluated as a production decision, not only as a syntax or tooling choice.
  • The best implementation keeps responsibilities visible, with clear ownership, tests, documentation, and rollback paths.
  • Search visibility improves when practical depth, structured answers, and expert examples live on the same page.

[IMAGE: A mobile-first technical article layout showing the main concept, decision table, implementation checklist, and FAQ blocks. Alt: Generics in PHP: Current Workarounds and What the Future Holds expert guide for Core PHP]

What Generics in PHP: Current Workarounds and What the Future Holds means

Generics in PHP: Current Workarounds and What the Future Holds means applying core php knowledge to a concrete engineering decision, then turning that decision into reliable code, documentation, and operational behavior. In practice, it combines the topic's core concepts with trade-off analysis, implementation boundaries, testing strategy, and maintenance discipline.

This is the definition worth optimizing for featured snippets because it avoids hype. It tells the reader what the topic does and what a professional implementation must include.

Why it matters now

The technical web is more crowded than it was a few years ago. Thin tutorials can still get indexed, but they rarely earn trust from senior developers, buyers, AI answer systems, or teams that need production guidance.

For core php topics, the strongest content now has three layers:

  • a clear answer for fast scanning
  • a practical framework for implementation
  • expert context that explains what breaks later

That same structure helps search engines understand the page. It also helps readers decide whether the advice fits their project.

Implementation framework

Use this framework before adopting the approach described in this article.

  1. Define the user problem and the production risk.
  2. Identify the smallest reliable implementation boundary.
  3. Keep configuration, secrets, and environment-specific behavior outside the article's core logic.
  4. Add tests for the behavior that would hurt if it regressed.
  5. Document the trade-off, not only the final code.
  6. Measure the result with logs, metrics, or user-facing outcomes.
  7. Revisit the decision after real usage exposes edge cases.

The sequence is deliberately conservative. It keeps the work grounded in outcomes instead of novelty.

[IMAGE: A seven-step implementation framework with discovery, boundary design, configuration, tests, documentation, measurement, and iteration. Alt: Generics in PHP: Current Workarounds and What the Future Holds implementation framework]

Practical comparison

Decision areaStrong approachWeak approachWhy it matters
ScopeSolve one clear problemMix unrelated concernsFocus improves testing and search intent
ArchitecturePut logic in explicit classes or documented boundariesHide behavior in templates or incidental callbacksFuture changes stay easier to review
Data flowPass prepared data into the view or endpointQuery or compute in presentation codeReduces regressions and performance surprises
TestingCover the risky behavior directlyTest only the happy pathCatches production failures earlier
DocumentationExplain trade-offs and limitsRepeat generic definitionsBuilds E-E-A-T and reader trust
OperationsTrack logs, metrics, and rollback stepsShip without measurementMakes the decision reversible

This table is intentionally practical. It gives a reviewer something to check before the implementation becomes expensive to change.

Expert workflow

Expert tip: "Treat Generics in PHP: Current Workarounds and What the Future Holds as a system boundary. If the next developer cannot find where the decision lives, how it is tested, and when it should be avoided, the implementation is not finished."

A useful workflow is simple:

  • Start with the smallest working example.
  • Add the constraints that exist in your real project.
  • Remove anything that only demonstrates cleverness.
  • Write down the failure modes.
  • Add links to related decisions so future readers can navigate the topic cluster.

That last point matters for both humans and search systems. A single article can answer a question; a cluster proves authority.

Common mistakes

Mistake 1: Copying a pattern without its context

A pattern that works in a small demo can fail in a real application. The missing context is usually data volume, team experience, deployment process, security requirements, or observability.

Before copying the pattern, ask what assumption made it safe in the original example.

Mistake 2: Putting business logic in the wrong layer

This is the fastest way to make future debugging expensive. In Laravel, PHP, and server-rendered websites, presentation should receive prepared data, not discover rules on its own.

Keep decision logic in models, actions, services, policies, requests, jobs, or documented helpers where it can be tested directly.

Mistake 3: Optimizing for novelty instead of maintainability

Newer tools and language features can be valuable. They can also hide simple behavior behind unfamiliar syntax.

Use the option that makes the next production incident easier to understand.

Mistake 4: Publishing without a measurement plan

If the article describes a performance, SEO, security, or architecture improvement, define how success will be checked. Logs, tests, crawl diagnostics, analytics, and user behavior are all stronger than assumptions.

[IMAGE: A common-mistakes board with context loss, wrong layer, novelty bias, and missing measurement highlighted. Alt: Generics in PHP: Current Workarounds and What the Future Holds common mistakes]

Image placeholders

  • [IMAGE: A concept diagram for Generics in PHP: Current Workarounds and What the Future Holds with input, decision boundary, implementation, tests, and production feedback. Alt: Generics in PHP: Current Workarounds and What the Future Holds concept diagram]
  • [IMAGE: A mobile screenshot-style checklist for Generics in PHP: Current Workarounds and What the Future Holds. Alt: Generics in PHP: Current Workarounds and What the Future Holds mobile checklist]
  • [IMAGE: A comparison table visualization for strong versus weak implementation choices. Alt: Generics in PHP: Current Workarounds and What the Future Holds comparison table]

Video placeholder

[VIDEO: Insert a 5-8 minute YouTube walkthrough that demonstrates the main decision, the implementation boundary, the test strategy, and the production caveats for Generics in PHP: Current Workarounds and What the Future Holds.]

Internal linking opportunities

Original Technical Deep Dive

The short version

PHP does not have native runtime generics. You cannot write this in production PHP:

final class Collection<T>
{
    /** @var list<T> */
    private array $items = [];
}

You also cannot enforce this at runtime with a normal PHP type declaration:

/** Not valid PHP syntax. */
function sendAll(Collection<EmailMessage> $messages): void
{
}

What you can use today:

NeedCurrent PHP answerRuntime-enforced?
A list of stringslist<string> in PHPDocNo
A map of IDs to usersarray<int, User> in PHPDocNo
A reusable typed collection@template T plus PHPStan or PsalmNo
A factory returning the requested classclass-string<T> plus @return TPartly, if you validate class names
A DTO with known fieldsNative class properties or array shapesProperties yes, array shape no
A public API contractNative parameter and return types first, PHPDoc generics secondNative parts only
Full native generic classesNot available in PHP as of May 7, 2026No

The practical rule is simple: use native PHP types everywhere PHP can enforce them, then use PHPDoc generics where the native type system cannot express the relationship.

Generics are most useful when a type flows through code. A Collection<User> should produce User. A Repository<Invoice> should return Invoice. A class-string<Order> should create Order. Without generics, analyzers often see mixed, object, or a broad interface, and useful type information disappears.

Current language state

PHP's native type declarations cover parameters, return values, properties, and class constants. PHP supports scalar types, class/interface types, array, iterable, object, nullable types, union types, intersection types, DNF types, mixed, never, void, static, self, and related special types.

It does not support type parameters.

This is valid PHP:

<?php

declare(strict_types=1);

final readonly class UserId
{
    public function __construct(public int $value)
    {
    }
}

function findUser(UserId $id): User
{
    // ...
}

This is not valid PHP:

<?php

declare(strict_types=1);

final class Repository<T>
{
    public function find(int $id): T
    {
        // ...
    }
}

PHPDoc generics fill that gap for static analysis. They are not checked by PHP itself.

Why generics matter

Without generics, reusable abstractions lose information.

<?php

declare(strict_types=1);

interface Repository
{
    public function find(int $id): object;
}

final readonly class InvoiceController
{
    public function __construct(
        private Repository $invoices,
    ) {
    }

    public function show(int $id): string
    {
        $invoice = $this->invoices->find($id);

        return $invoice->number; // Analyzer sees object, not Invoice.
    }
}

The controller knows it is using an invoice repository. PHP does not.

You can narrow with concrete interfaces:

<?php

declare(strict_types=1);

interface InvoiceRepository
{
    public function find(int $id): Invoice;
}

That is often the best answer. Do not reach for generics when a concrete interface is clearer.

Generics help when the abstraction is genuinely reusable:

  • Collections.
  • Repositories with a shared base class.
  • Hydrators.
  • Serializers.
  • Event maps.
  • Service locators.
  • Middleware stacks.
  • Message buses.
  • Factories.
  • Result wrappers.
  • Option or Maybe types.

Native types first, PHPDoc second

Do not use PHPDoc as a replacement for native types.

[IMAGE: Supporting visual 1 for Generics in PHP: Current Workarounds and What the Future Holds, showing Generics in PHP: Current Workarounds and What the Future Holds decisions, examples, and PHP, Generics, PHPStan. Alt: Generics in PHP: Current Workarounds and What the Future Holds generics-in-php-current-workarounds-what-future-holds visual 1]

[IMAGE: Supporting visual 1 for Generics in PHP: Current Workarounds and What the Future Holds, showing Generics in PHP: Current Workarounds and What the Future Holds decisions, examples, and PHP, Generics, PHPStan. Alt: Generics in PHP: Current Workarounds and What the Future Holds generics-in-php-current-workarounds-what-future-holds visual 1]

Bad:

<?php

/**
 * @param User $user
 * @return Invoice
 */
function createInvoice($user)
{
    // ...
}

Better:

<?php

declare(strict_types=1);

function createInvoice(User $user): Invoice
{
    // ...
}

Use PHPDoc where the native signature cannot express the full contract.

<?php

declare(strict_types=1);

/**
 * @param list<InvoiceLine> $lines
 */
function createInvoice(User $user, array $lines): Invoice
{
    // ...
}

PHP enforces array. PHPStan or Psalm verifies list<InvoiceLine>.

That split matters. Runtime still needs validation at trust boundaries. Static generics are a development-time guarantee, not a substitute for validating JSON, forms, files, queues, or webhook payloads.

Use array shapes for records

When the structure is fixed, use an array shape instead of pretending the array is a generic collection.

<?php

declare(strict_types=1);

/**
 * @return array{
 *     id: int,
 *     email: non-empty-string,
 *     roles: list<non-empty-string>,
 *     last_login_at?: non-empty-string
 * }
 */
function userPayload(User $user): array
{
    return [
        'id' => $user->id,
        'email' => $user->email,
        'roles' => $user->roles,
        'last_login_at' => $user->lastLoginAt?->format(DATE_ATOM),
    ];
}

Use array shapes for payloads. Use generics for reusable containers and type relationships.

/**
 * Good generic shape: list of the same reusable type.
 *
 * @return list<User>
 */
function activeUsers(): array
{
    // ...
}

Generic functions with PHPDoc

The smallest useful generic is "return the same type I receive."

<?php

declare(strict_types=1);

/**
 * @template T
 * @param T $value
 * @return T
 */
function tapValue(mixed $value, callable $callback): mixed
{
    $callback($value);

    return $value;
}

The native signature must use mixed because PHP has no T type. The analyzer sees the template relationship.

$user = tapValue(new User('a@example.com'), function (User $user): void {
    audit('created_user', $user->email);
});

// Static analyzer knows $user is User.

Add bounds when the generic value must be a subtype.

<?php

declare(strict_types=1);

/**
 * @template T of DomainEvent
 * @param T $event
 * @return T
 */
function recordEvent(DomainEvent $event): DomainEvent
{
    EventLog::append($event);

    return $event;
}

The native declaration still says DomainEvent. The analyzer preserves the concrete subtype.

class-string<T> is the workhorse

Factories and containers often receive a class name and return an object of that class.

Without generics:

<?php

declare(strict_types=1);

final class Container
{
    public function get(string $class): object
    {
        return new $class();
    }
}

$mailer = $container->get(Mailer::class);

// Analyzer sees object, not Mailer.

With class-string<T>:

<?php

declare(strict_types=1);

final class Container
{
    /**
     * @template T of object
     * @param class-string<T> $class
     * @return T
     */
    public function get(string $class): object
    {
        if (! class_exists($class)) {
            throw new InvalidArgumentException("Class does not exist: {$class}");
        }

        return new $class();
    }
}

Usage:

$mailer = $container->get(Mailer::class);

$mailer->send($message);

The analyzer can infer Mailer.

Runtime still needs checks. class-string<T> is a static-analysis type. PHP will still accept any string unless your code validates it.

Generic collections

A simple mutable collection:

<?php

declare(strict_types=1);

use IteratorAggregate;
use Traversable;

/**
 * @template T
 * @implements IteratorAggregate<int, T>
 */
final class Collection implements IteratorAggregate
{
    /**
     * @param list<T> $items
     */
    public function __construct(
        private array $items = [],
    ) {
    }

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

    /**
     * @return T|null
     */
    public function first(): mixed
    {
        return $this->items[0] ?? null;
    }

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

    /**
     * @return Traversable<int, T>
     */
    public function getIterator(): Traversable
    {
        yield from $this->items;
    }
}

Usage:

/** @var Collection<User> $users */
$users = new Collection();

$users->add(new User('a@example.com'));

$first = $users->first();

if ($first !== null) {
    echo $first->email;
}

This gives analyzers useful information, but there is no runtime enforcement. This would run unless you add your own guard:

/** @var Collection<User> $users */
$users = new Collection();

$users->add(new Invoice('INV-1')); // Static analyzer error, runtime accepts it.

If runtime safety matters, make the collection concrete.

<?php

declare(strict_types=1);

final class UserCollection
{
    /** @var list<User> */
    private array $users = [];

    public function add(User $user): void
    {
        $this->users[] = $user;
    }

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

Concrete collections are boring, but they are runtime-enforced and easy to read.

Generic immutable collections

Variance becomes easier when the collection is read-only.

<?php

declare(strict_types=1);

use IteratorAggregate;
use Traversable;

/**
 * @template-covariant T
 * @implements IteratorAggregate<int, T>
 */
final readonly class ReadonlyCollection implements IteratorAggregate
{
    /**
     * @param list<T> $items
     */
    public function __construct(
        private array $items,
    ) {
    }

    /**
     * @return T|null
     */
    public function first(): mixed
    {
        return $this->items[0] ?? null;
    }

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

    /**
     * @return Traversable<int, T>
     */
    public function getIterator(): Traversable
    {
        yield from $this->items;
    }
}

Covariance is safe here because the collection only produces values. It does not accept new T values.

If you add this method, the design stops being safely covariant:

/**
 * @param T $item
 */
public function add(mixed $item): void
{
}

A ReadonlyCollection<Dog> can be used where a ReadonlyCollection<Animal> is expected. A mutable Collection<Dog> generally cannot be used as Collection<Animal>, because code accepting animals could add a Cat to a dog collection.

[IMAGE: Supporting visual 2 for Generics in PHP: Current Workarounds and What the Future Holds, showing Generics in PHP: Current Workarounds and What the Future Holds decisions, examples, and PHP, Generics, PHPStan. Alt: Generics in PHP: Current Workarounds and What the Future Holds generics-in-php-current-workarounds-what-future-holds visual 2]

That is not a PHPStan or Psalm quirk. It is type safety.

Generic repositories

Generic repositories are useful for infrastructure base classes, but domain code usually reads better with concrete repository interfaces.

Base class:

<?php

declare(strict_types=1);

/**
 * @template TEntity of object
 */
abstract class DatabaseRepository
{
    /**
     * @param class-string<TEntity> $entityClass
     */
    public function __construct(
        private string $entityClass,
    ) {
    }

    /**
     * @return TEntity|null
     */
    public function find(int $id): ?object
    {
        $row = $this->fetchRow($id);

        if ($row === null) {
            return null;
        }

        return $this->hydrate($this->entityClass, $row);
    }

    /**
     * @param class-string<TEntity> $class
     * @param array<string, mixed> $row
     * @return TEntity
     */
    abstract protected function hydrate(string $class, array $row): object;

    /**
     * @return array<string, mixed>|null
     */
    abstract protected function fetchRow(int $id): ?array;
}

Concrete repository:

<?php

declare(strict_types=1);

/** @extends DatabaseRepository<Invoice> */
final class InvoiceDatabaseRepository extends DatabaseRepository implements InvoiceRepository
{
    public function __construct()
    {
        parent::__construct(Invoice::class);
    }

    public function get(int $id): Invoice
    {
        return $this->find($id)
            ?? throw InvoiceNotFound::forId($id);
    }
}

Domain-facing interface:

<?php

declare(strict_types=1);

interface InvoiceRepository
{
    public function get(int $id): Invoice;
}

The generic base class removes infrastructure duplication. The domain interface stays specific.

[IMAGE: Supporting visual 2 for Generics in PHP: Current Workarounds and What the Future Holds, showing Generics in PHP: Current Workarounds and What the Future Holds decisions, examples, and PHP, Generics, PHPStan. Alt: Generics in PHP: Current Workarounds and What the Future Holds generics-in-php-current-workarounds-what-future-holds visual 2]

Generic event maps

Generics can couple an event to its handler.

<?php

declare(strict_types=1);

interface Event
{
}

/**
 * @template TEvent of Event
 */
interface EventHandler
{
    /**
     * @param TEvent $event
     */
    public function handle(Event $event): void;
}

Concrete handler:

<?php

declare(strict_types=1);

/** @implements EventHandler<OrderPlaced> */
final class SendOrderConfirmation implements EventHandler
{
    public function handle(Event $event): void
    {
        if (! $event instanceof OrderPlaced) {
            throw new InvalidArgumentException('Expected OrderPlaced.');
        }

        // Send email.
    }
}

You may dislike the runtime guard, but it is honest. PHP cannot enforce TEvent at runtime, so the handler still receives the broad native interface. Static analysis catches wrong registrations; runtime validation protects execution.

If you want native enforcement, skip the generic interface and use concrete invokable handlers:

<?php

declare(strict_types=1);

final class SendOrderConfirmation
{
    public function __invoke(OrderPlaced $event): void
    {
        // Send email.
    }
}

Then the dispatcher must validate callable signatures when registering handlers.

Generic result wrappers

Result wrappers are a good generic use case because the value type travels through the wrapper.

<?php

declare(strict_types=1);

/**
 * @template TValue
 */
final readonly class Result
{
    /**
     * @param TValue|null $value
     */
    private function __construct(
        private mixed $value,
        private ?string $error,
    ) {
    }

    /**
     * @template T
     * @param T $value
     * @return self<T>
     */
    public static function ok(mixed $value): self
    {
        return new self($value, null);
    }

    /**
     * @return self<never>
     */
    public static function fail(string $error): self
    {
        return new self(null, $error);
    }

    public function isOk(): bool
    {
        return $this->error === null;
    }

    /**
     * @return TValue
     */
    public function value(): mixed
    {
        if ($this->error !== null) {
            throw new LogicException($this->error);
        }

        return $this->value;
    }
}

Usage:

/**
 * @return Result<Invoice>
 */
function createInvoice(CreateInvoiceCommand $command): Result
{
    if ($command->lines === []) {
        return Result::fail('Invoice needs at least one line.');
    }

    return Result::ok(new Invoice(/* ... */));
}

This is useful if your team already uses static analysis. Without PHPStan or Psalm, the docblocks are just documentation.

Prefer precise collection aliases

Repeated generic array types get noisy.

PHPStan and Psalm both support local type aliases. Use them for complex shapes.

<?php

declare(strict_types=1);

/**
 * @phpstan-type AddressPayload array{
 *     line1: non-empty-string,
 *     line2?: non-empty-string,
 *     city: non-empty-string,
 *     country: non-empty-string,
 *     postal_code: non-empty-string
 * }
 *
 * @phpstan-type UserPayload array{
 *     id: positive-int,
 *     email: non-empty-string,
 *     addresses: list<AddressPayload>
 * }
 */
final class UserPayloadMapper
{
    /**
     * @return UserPayload
     */
    public function map(User $user): array
    {
        // ...
    }
}

Use aliases for stable contracts. Do not use them to hide badly shaped arrays that should become DTOs.

Laravel collection annotations

Laravel's Collection is heavily dynamic. Static analyzers can still understand common generic annotations.

<?php

declare(strict_types=1);

use Illuminate\Support\Collection;

/**
 * @return Collection<int, Invoice>
 */
function paidInvoices(Customer $customer): Collection
{
    return $customer->invoices()
        ->where('status', 'paid')
        ->get();
}

For Eloquent query builders, Larastan and Psalm plugins can infer many model types, but explicit annotations still help at boundaries:

<?php

declare(strict_types=1);

use Illuminate\Database\Eloquent\Builder;

/**
 * @param Builder<Invoice> $query
 * @return Builder<Invoice>
 */
function onlyOverdue(Builder $query): Builder
{
    return $query
        ->where('status', 'open')
        ->whereDate('due_at', '<', now());
}

Keep framework magic behind typed methods. Do not let every service receive Collection<mixed> and hope the IDE works it out.

Runtime enforcement options

PHPDoc generics do not run in production. If the input crosses a trust boundary, enforce it.

For arrays:

<?php

declare(strict_types=1);

/**
 * @param list<mixed> $values
 * @return list<EmailAddress>
 */
function emailList(array $values): array
{
    $emails = [];

    foreach ($values as $value) {
        if (! is_string($value)) {
            throw new InvalidArgumentException('Email must be a string.');
        }

        $emails[] = new EmailAddress($value);
    }

    return $emails;
}

For collections:

<?php

declare(strict_types=1);

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

    /**
     * @param class-string<T> $type
     */
    public function __construct(
        private readonly string $type,
    ) {
    }

    /**
     * @param T $item
     */
    public function add(object $item): void
    {
        if (! $item instanceof $this->type) {
            throw new InvalidArgumentException("Expected {$this->type}.");
        }

        $this->items[] = $item;
    }

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

Usage:

/** @var CheckedCollection<User> $users */
$users = new CheckedCollection(User::class);

$users->add(new User('a@example.com'));

This pattern gives you both static and runtime safety, but it has a cost:

  • More boilerplate.
  • Runtime checks.
  • A class-string stored on each collection.
  • Still no native Collection<User> type in method signatures.

[IMAGE: Supporting visual 3 for Generics in PHP: Current Workarounds and What the Future Holds, showing Generics in PHP: Current Workarounds and What the Future Holds decisions, examples, and PHP, Generics, PHPStan. Alt: Generics in PHP: Current Workarounds and What the Future Holds generics-in-php-current-workarounds-what-future-holds visual 3]

Use it where runtime safety is worth the friction.

Do not fake native generics with attributes

Attributes can store metadata, but they do not change PHP's type system.

<?php

#[Generic(User::class)]
final class UserCollection
{
}

That may help a framework or a custom validator, but PHP will not enforce it. Static analyzers will not automatically treat arbitrary attributes as type parameters unless you build tooling for that.

Use attributes for runtime metadata. Use PHPDoc generics for analyzer-readable types.

What to avoid

Avoid these patterns:

PatternProblem
array everywhere with no PHPDocType information disappears immediately
Collection<mixed>Usually just a typed way to say "unknown"
Generic repositories in domain interfacesConcrete repository interfaces are clearer
Mutable covariant collectionsType unsafe if values can be added
Runtime reflection hacks pretending to be genericsComplex, slow, and still not native
Copying Psalm-only annotations into PHPStan-only projects without checking supportTool-specific syntax can drift
Docblocks that contradict native signaturesThe analyzer and runtime tell different stories

[IMAGE: Supporting visual 3 for Generics in PHP: Current Workarounds and What the Future Holds, showing Generics in PHP: Current Workarounds and What the Future Holds decisions, examples, and PHP, Generics, PHPStan. Alt: Generics in PHP: Current Workarounds and What the Future Holds generics-in-php-current-workarounds-what-future-holds visual 3]

The most common mistake is adding @template because it looks sophisticated. If the type variable is not used in at least two meaningful places, it probably is not modeling a useful relationship.

Bad:

/**
 * @template T
 */
final class Logger
{
    public function info(string $message): void
    {
    }
}

The template is unused. Remove it.

Good:

/**
 * @template TMessage of Message
 */
interface MessageHandler
{
    /**
     * @param TMessage $message
     */
    public function handle(Message $message): void;
}

The template ties the handler to a message subtype.

PHPStan and Psalm differences

PHPStan and Psalm share the broad PHPDoc generics model:

  • @template T
  • @template T of Foo
  • @param T $value
  • @return T
  • @extends Parent<T>
  • @implements Interface<T>
  • @use Trait<T>
  • class-string<T>
  • array<TKey, TValue>
  • list<T>

They are not identical. Each tool has extra features and slightly different preferred annotations.

Practical guidance:

  • Pick one primary analyzer.
  • Use generic syntax supported by that analyzer.
  • Keep vendor-specific annotations only where needed.
  • Add analyzer tests or CI examples for tricky templates.
  • Do not assume an annotation works because another tool accepts something similar.

If you run both, prefer the common subset unless a file has a clear reason to use @phpstan-* or @psalm-* tags.

[IMAGE: Supporting visual 4 for Generics in PHP: Current Workarounds and What the Future Holds, showing Generics in PHP: Current Workarounds and What the Future Holds decisions, examples, and PHP, Generics, PHPStan. Alt: Generics in PHP: Current Workarounds and What the Future Holds generics-in-php-current-workarounds-what-future-holds visual 4]

The RFC landscape in 2026

As of May 7, 2026, PHP has no accepted native generics RFC and no generics RFC listed in the official RFC overview's active voting or discussion sections.

There are older draft RFCs:

  • Generic Types and Functions, created in 2016, status Draft.
  • Generic arrays, created in 2016 and later updated, status Draft.
  • Older typed-array style proposals, also not adopted.

Those pages are useful historical context, not a shipping roadmap.

The more current work is exploratory. The PHP Foundation has published detailed work on:

  • Full reified generics.
  • Dedicated collection syntax.
  • Static-analysis-only approaches.
  • Erased generic type declarations.
  • Fully erased type declarations.
  • Generic arrays.
  • Compile-time or abstract-interface-focused generics.

The hard parts are not syntax bikeshedding. The hard parts are runtime behavior, type inference, variance, compound types, performance, reflection, arrays, autoloading, and how much checking PHP itself should perform.

Reified generics

Reified generics keep type information at runtime.

Conceptually:

$users = new Collection<User>();

$users instanceof Collection<User>; // Would need runtime meaning.

Benefits:

  • Runtime enforcement.
  • Reflection could expose type arguments.
  • Frameworks could inspect generic types natively.
  • Errors would come from PHP, not only from analyzers.

Costs:

  • More engine complexity.
  • Runtime checks.
  • Hard type inference.
  • Hard interaction with union and intersection types.
  • Hard interaction with PHP's array model and autoloading.
  • Potential memory and performance costs.

[IMAGE: Supporting visual 4 for Generics in PHP: Current Workarounds and What the Future Holds, showing Generics in PHP: Current Workarounds and What the Future Holds decisions, examples, and PHP, Generics, PHPStan. Alt: Generics in PHP: Current Workarounds and What the Future Holds generics-in-php-current-workarounds-what-future-holds visual 4]

Reified generics are the most powerful option and also the hardest.

Erased generics

Erased generics would make generic syntax part of PHP source code but erase some or all type information before runtime enforcement.

Conceptually:

interface Repository<T>
{
    public function get(int $id): T;
}

/** @implements Repository<Invoice> */
final class InvoiceRepository implements Repository
{
    public function get(int $id): Invoice
    {
        // ...
    }
}

Benefits:

  • Cleaner than PHPDoc for tools.
  • Lower runtime cost than fully reified generics.
  • Easier migration path for code already using analyzer generics.
  • Could give IDEs and analyzers a native syntax to parse.

Costs:

  • PHP itself may not enforce the generic relationship.
  • Runtime reflection may not get the full type information.
  • Developers may expect runtime safety that is not there.
  • Without an official linter or analyzer, errors may remain tool-dependent.

This is the central trade-off: should PHP add syntax that mostly helps static tools, or should generic syntax always mean runtime-enforced types?

[IMAGE: Supporting visual 5 for Generics in PHP: Current Workarounds and What the Future Holds, showing Generics in PHP: Current Workarounds and What the Future Holds decisions, examples, and PHP, Generics, PHPStan. Alt: Generics in PHP: Current Workarounds and What the Future Holds generics-in-php-current-workarounds-what-future-holds visual 5]

Compile-time or abstract generics

The PHP Foundation's 2025 compile-time generics discussion explores a smaller target: generics on interfaces and abstract classes, with concrete classes filling in the type.

Conceptually:

interface Exporter<Thing>
{
    public function export(Thing $thing): string;
}

final class UserExporter implements Exporter<User>
{
    public function export(User $thing): string
    {
        return $thing->email;
    }
}

This avoids the harder runtime form:

new Repository<User>();

The appeal is pragmatic. Many PHP use cases need generic contracts more than generic object construction. Repositories, handlers, processors, importers, exporters, normalizers, and event subscribers often fit this model.

The limitation is also clear. It would not be full generics. It would not solve every collection or factory use case.

Collections and typed arrays

Typed arrays are one of the reasons PHP developers ask for generics:

function sendAll(array<EmailMessage> $messages): void
{
}

But arrays are unusually hard in PHP because array is both list and map, mutable, copy-on-write, dynamically keyed, and used for everything from JSON payloads to dictionaries to tuples.

The PHP Foundation's generics and collections work treats object-based collections as a possible more practical path than typed arrays. A dedicated sequence, set, or dictionary type could enforce key and value types without trying to retrofit every behavior of PHP arrays.

Do not plan migrations around hypothetical typed arrays. Use native DTOs, concrete collections, PHPDoc array shapes, list<T>, and analyzer checks today.

What should teams do now?

For production PHP in 2026:

  1. Use declare(strict_types=1).
  2. Use native parameter, return, property, and constant types wherever possible.
  3. Use PHPStan or Psalm in CI.
  4. Use PHPDoc generics for collections, factories, repositories, result wrappers, and type-preserving functions.
  5. Use array shapes for fixed payloads.
  6. Use concrete DTOs when arrays become part of the domain.
  7. Add runtime validation at external boundaries.
  8. Keep generic abstractions small.
  9. Prefer concrete domain interfaces over generic domain interfaces when readability improves.
  10. Track PHP core generics work, but do not wait for it.

[IMAGE: Supporting visual 5 for Generics in PHP: Current Workarounds and What the Future Holds, showing Generics in PHP: Current Workarounds and What the Future Holds decisions, examples, and PHP, Generics, PHPStan. Alt: Generics in PHP: Current Workarounds and What the Future Holds generics-in-php-current-workarounds-what-future-holds visual 5]

The best current PHP code does not pretend generics are already native. It uses static analysis to model what PHP cannot yet express, and it keeps runtime contracts explicit where correctness matters.

FAQ

What is Generics in PHP: Current Workarounds and What the Future Holds?

Generics in PHP: Current Workarounds and What the Future Holds is a practical core php topic that should be evaluated through implementation scope, production risk, testing, documentation, and long-term maintainability.

When should a team use Generics in PHP: Current Workarounds and What the Future Holds?

Use Generics in PHP: Current Workarounds and What the Future Holds when it solves a real project constraint, improves clarity, or reduces operational risk. Avoid it when it only adds novelty or hides behavior from future maintainers.

What is the biggest risk with Generics in PHP: Current Workarounds and What the Future Holds?

The biggest risk is copying a pattern without its context. Production systems need clear boundaries, rollback options, tests, and observability before a technique becomes dependable.

How do you test Generics in PHP: Current Workarounds and What the Future Holds?

Test the smallest unit that owns the behavior, then add integration coverage for the path users or systems actually rely on. Include failure cases, configuration differences, and regression checks.

How does Generics in PHP: Current Workarounds and What the Future Holds affect SEO and AI search visibility?

It improves visibility when the article gives a direct answer, expert context, structured headings, internal links, trustworthy references, and FAQ content that matches the visible page.

Conclusion

Generics in PHP: Current Workarounds and What the Future Holds is worth doing when the implementation improves clarity, reliability, or delivery speed. It is not worth doing when it hides ownership, increases operational risk, or makes the system harder to explain.

Use the framework above as a review checklist. Then connect this topic to the rest of the project documentation so readers can move from concept to implementation without losing context.

Top