SEO Metadata
SEO Title Options
- Generics in PHP: Current Workarounds and What the Future
- Generics in PHP: Current Workarounds and: Practical 2026
- Core PHP Playbook: Generics in PHP: Current Workarounds
Meta Description Options
- Learn Generics in PHP: Current Workarounds and What the Future Holds with a practical Core PHP framework, expert mistakes, implementation steps, examples.
- 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
- What Generics in PHP: Current Workarounds and What the Future Holds means
- Why it matters now
- Implementation framework
- Practical comparison
- Expert workflow
- Common mistakes
- Media and link plan
- Original technical deep dive
- FAQ
- Structured data
- Conclusion
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.
- Define the user problem and the production risk.
- Identify the smallest reliable implementation boundary.
- Keep configuration, secrets, and environment-specific behavior outside the article's core logic.
- Add tests for the behavior that would hurt if it regressed.
- Document the trade-off, not only the final code.
- Measure the result with logs, metrics, or user-facing outcomes.
- 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 area | Strong approach | Weak approach | Why it matters |
|---|---|---|---|
| Scope | Solve one clear problem | Mix unrelated concerns | Focus improves testing and search intent |
| Architecture | Put logic in explicit classes or documented boundaries | Hide behavior in templates or incidental callbacks | Future changes stay easier to review |
| Data flow | Pass prepared data into the view or endpoint | Query or compute in presentation code | Reduces regressions and performance surprises |
| Testing | Cover the risky behavior directly | Test only the happy path | Catches production failures earlier |
| Documentation | Explain trade-offs and limits | Repeat generic definitions | Builds E-E-A-T and reader trust |
| Operations | Track logs, metrics, and rollback steps | Ship without measurement | Makes 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]
Media and link plan
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.]
Trustworthy outbound links
- PHP manual - use this as the trust reference for language-level reference.
- Google Search quality guidance - use this as the trust reference for people-first content and E-E-A-T alignment.
Internal linking opportunities
- Internal guide: PHP Static Analysis With PHPStan & Psalm - use this when readers need a related Testing follow-up.
- Internal guide: Understanding PHP Stream API: File I/O - use this when readers need a related Core PHP follow-up.
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:
| Need | Current PHP answer | Runtime-enforced? |
|---|---|---|
| A list of strings | list<string> in PHPDoc | No |
| A map of IDs to users | array<int, User> in PHPDoc | No |
| A reusable typed collection | @template T plus PHPStan or Psalm | No |
| A factory returning the requested class | class-string<T> plus @return T | Partly, if you validate class names |
| A DTO with known fields | Native class properties or array shapes | Properties yes, array shape no |
| A public API contract | Native parameter and return types first, PHPDoc generics second | Native parts only |
| Full native generic classes | Not available in PHP as of May 7, 2026 | No |
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:
declare(strict_types=1);
final readonly class UserId
{
public function __construct(public int $value)
{
}
}
function findUser(UserId $id): User
{
// ...
}
This is not valid 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.
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:
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:
/**
* @param User $user
* @return Invoice
*/
function createInvoice($user)
{
// ...
}
Better:
declare(strict_types=1);
function createInvoice(User $user): Invoice
{
// ...
}
Use PHPDoc where the native signature cannot express the full contract.
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.
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."
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.
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:
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>:
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:
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.
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.
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:
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:
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:
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.
declare(strict_types=1);
interface Event
{
}
/**
* @template TEvent of Event
*/
interface EventHandler
{
/**
* @param TEvent $event
*/
public function handle(Event $event): void;
}
Concrete handler:
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:
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.
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.
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.
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:
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:
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:
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.
#[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:
| Pattern | Problem |
|---|---|
array everywhere with no PHPDoc | Type information disappears immediately |
Collection<mixed> | Usually just a typed way to say "unknown" |
| Generic repositories in domain interfaces | Concrete repository interfaces are clearer |
| Mutable covariant collections | Type unsafe if values can be added |
| Runtime reflection hacks pretending to be generics | Complex, slow, and still not native |
| Copying Psalm-only annotations into PHPStan-only projects without checking support | Tool-specific syntax can drift |
| Docblocks that contradict native signatures | The 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:
- Use
declare(strict_types=1). - Use native parameter, return, property, and constant types wherever possible.
- Use PHPStan or Psalm in CI.
- Use PHPDoc generics for collections, factories, repositories, result wrappers, and type-preserving functions.
- Use array shapes for fixed payloads.
- Use concrete DTOs when arrays become part of the domain.
- Add runtime validation at external boundaries.
- Keep generic abstractions small.
- Prefer concrete domain interfaces over generic domain interfaces when readability improves.
- 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.