tcds-io / php-better-generics
PHP library to better work with generics
Requires
- php: ^8.4
- phpstan/phpdoc-parser: ^2.0
Requires (Dev)
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^10.5
- slevomat/coding-standard: ^8.16
- symfony/var-dumper: ^7.2
This package is auto-updated.
Last update: 2026-05-03 20:33:13 UTC
README
PHP 8.4+ library for working with generics at runtime: typed collections, lazy object proxies, and a reflection layer that understands @template, @extends, @implements, generic types, and array shapes from PHPDoc.
Why
PHP doesn't have native generics — only static analyzers (PHPStan, Psalm) do. This library bridges that gap at runtime by:
- Reading the PHPDoc you already write for static analysis (
@template T,@param list<Foo> $items,@extends Collection<User>) and exposing it through aReflectionClassAPI that resolves real FQNs. - Providing thin, well-typed
ArrayList<T>/Map<K, V>collections so@template-style code is also nice to write. - Offering
lazyOf()proxies on top of PHP 8.4's native lazy objects.
Install
composer require tcds-io/php-better-generics
Single runtime dependency: phpstan/phpdoc-parser (zero deps of its own).
Typed collections
use function Tcds\Io\Generic\listOf; use function Tcds\Io\Generic\mapOf; $users = listOf($alice, $bob, $carol); $names = $users ->filter(fn (User $u) => $u->isActive()) ->map(fn (User $u) => $u->name); $byEmail = $users->indexedBy(fn (User $u) => $u->email); // Map<string, User> $first = $users->first(fn (User $u) => $u->isAdmin()); $count = $users->count(fn (User $u) => $u->isActive());
MutableArrayList adds push, pop, set, removeAt, clear. Map implements ArrayAccess and Countable; MutableMap adds put, putAll, remove, clear.
Lazy proxies
use function lazyOf; use function lazyBufferOf; // Single proxy: $user is fully typed as User; constructor only runs on first access. $user = lazyOf(User::class, fn () => $userRepository->find($id)); // Buffered loader: collects keys, then loads in one batch when the buffer fills. $loader = lazyBufferOf( User::class, fn (array $ids) => $userRepository->findMany($ids), maxBufferSize: 50, ); $users = $orderIds->map(fn (string $orderId) => $loader->lazyOf($orderId)); // Each access fetches up to 50 users at once instead of N+1.
Generic-aware reflection
Read the PHPDoc, get back resolved types.
use Tcds\Io\Generic\Reflection\ReflectionClass; /** @template T */ class Collection { /** @return list<T> */ public function items(): array { /* ... */ } } /** @extends Collection<User> */ class UserCollection extends Collection {} $reflection = new ReflectionClass(UserCollection::class); $type = $reflection->getMethod('items')->getReturnType(); // GenericReflectionType('list', [User::class]) // — `T` resolved through `@extends Collection<User>`.
Supported out of the box:
@template Tand@template T of Foo(bounds parsed but not yet enforced)@param,@return,@var@extends Foo<X>,@implements Foo<X>— including transitive inheritance through PHPextends@phpstan-typealiases- Generics, array shapes (
array{name: string}), unions, intersections, nullables,Foo[] - Short-name resolution through
usestatements (includinguse A as B,use A\{B, C}, grouped imports) self,static,parentresolution against the declaring class
Requirements
- PHP 8.4+
Development
composer install
composer tests # cs:check + test:stan + test:unit
License
MIT — see composer.json.