@@ -328,6 +328,110 @@ public function isSuccess(): bool
328328 }
329329}
330330
331+ // ─── Generics (@template / @extends) ───────────────────────────────────────
332+
333+ /**
334+ * A generic repository — the base class declares template parameters
335+ * that child classes fill in with concrete types via @extends.
336+ *
337+ * @template T
338+ */
339+ class Repository
340+ {
341+ /** @var T|null */
342+ protected $ cached = null ;
343+
344+ /** @return T */
345+ public function find (int $ id )
346+ {
347+ return $ this ->cached ;
348+ }
349+
350+ /** @return T|null */
351+ public function findOrNull (int $ id )
352+ {
353+ return $ this ->cached ;
354+ }
355+
356+ /** @return T */
357+ public function first ()
358+ {
359+ return $ this ->cached ;
360+ }
361+ }
362+
363+ /**
364+ * Concrete repository: @extends tells the engine that T = User.
365+ * All inherited methods now return User instead of the abstract T.
366+ *
367+ * @extends Repository<User>
368+ */
369+ class UserRepository extends Repository
370+ {
371+ public function findByEmail (string $ email ): ?User
372+ {
373+ return null ;
374+ }
375+ }
376+
377+ /**
378+ * A generic collection with two template parameters.
379+ *
380+ * @template TKey of array-key
381+ * @template-covariant TValue
382+ */
383+ class TypedCollection
384+ {
385+ /** @var array<TKey, TValue> */
386+ protected array $ items = [];
387+
388+ /** @return TValue */
389+ public function first () { return reset ($ this ->items ); }
390+
391+ /** @return ?TValue */
392+ public function last () { return end ($ this ->items ) ?: null ; }
393+
394+ /** @return static */
395+ public function filter (callable $ fn ): static { return $ this ; }
396+
397+ /** @return int */
398+ public function count (): int { return count ($ this ->items ); }
399+
400+ /** @return array<TKey, TValue> */
401+ public function all (): array { return $ this ->items ; }
402+ }
403+
404+ /**
405+ * A user collection — TKey = int, TValue = User.
406+ *
407+ * @extends TypedCollection<int, User>
408+ */
409+ class UserCollection extends TypedCollection
410+ {
411+ public function adminsOnly (): self
412+ {
413+ return $ this ;
414+ }
415+ }
416+
417+ /**
418+ * Chained generics: this extends UserRepository, which extends
419+ * Repository<User>. Grandparent methods resolve through the chain.
420+ */
421+ class CachingUserRepository extends UserRepository
422+ {
423+ public function clearCache (): void {}
424+ }
425+
426+ /**
427+ * Demonstrates @phpstan-extends (the PHPStan-prefixed variant).
428+ *
429+ * @phpstan-extends TypedCollection<string, Response>
430+ */
431+ class ResponseCollection extends TypedCollection
432+ {
433+ }
434+
331435// ─── Container (conditional return types) ───────────────────────────────────
332436
333437class Container
@@ -693,3 +797,56 @@ function handleIntersection(User&Loggable $entity): void
693797
694798$ user = $ users [0 ];
695799$ user ->getEmail (); // assigned from array access → User
800+
801+
802+ // ═══════════════════════════════════════════════════════════════════════════
803+ // Generics — @template / @extends type resolution
804+ // ═══════════════════════════════════════════════════════════════════════════
805+ //
806+ // When a parent class declares @template parameters and a child class
807+ // provides concrete types via @extends, all inherited methods and
808+ // properties have their template types replaced with the real types.
809+
810+
811+ // ── Basic @extends — Repository<User> ───────────────────────────────────────
812+
813+ $ repo = new UserRepository ();
814+ $ repo ->find (1 )->getEmail (); // find() returns T → User
815+ $ repo ->first ()->getName (); // first() returns T → User
816+ $ repo ->findOrNull (1 )?->getEmail(); // findOrNull() returns ?T → ?User
817+ $ repo ->findByEmail ('a@b.c ' ); // own method still works
818+
819+
820+ // ── Two Template Parameters — TypedCollection<int, User> ────────────────────
821+
822+ $ users = new UserCollection ();
823+ $ users ->first ()->getEmail (); // first() returns TValue → User
824+ $ users ->last ()?->getName(); // last() returns ?TValue → ?User
825+ $ users ->adminsOnly (); // own method returns self
826+ $ users ->filter (fn ($ u ) => true ); // filter() returns static → UserCollection
827+ $ users ->count (); // count() returns int (non-template, unchanged)
828+
829+
830+ // ── Chained / Grandparent Generics ──────────────────────────────────────────
831+
832+ $ cachingRepo = new CachingUserRepository ();
833+ $ cachingRepo ->find (1 )->getEmail (); // grandparent Repository<User>::find() → User
834+ $ cachingRepo ->first ()->getName (); // grandparent first() → User
835+ $ cachingRepo ->clearCache (); // own method
836+
837+
838+ // ── @phpstan-extends Variant ────────────────────────────────────────────────
839+
840+ $ responses = new ResponseCollection ();
841+ $ responses ->first ()->getStatusCode (); // first() returns TValue → Response
842+ $ responses ->last ()?->getBody(); // last() returns ?TValue → ?Response
843+
844+
845+ // ── Property Type Substitution ──────────────────────────────────────────────
846+ // Inherited properties with template types are also substituted.
847+
848+ class PropertyDemo extends UserRepository {
849+ function test () {
850+ $ this ->cached ->getEmail (); // $cached has type T → User
851+ }
852+ }
0 commit comments