Skip to content

Commit a9bf7bc

Browse files
committed
Enhance generics documentation and tests for class, interface, and trait inheritance; add single-line inline trait use example
1 parent 1632629 commit a9bf7bc

3 files changed

Lines changed: 128 additions & 1 deletion

File tree

docs/generics/generics-and-bounds.md

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -663,7 +663,7 @@ $producers->add(new Producer(new Car()));
663663

664664
---
665665

666-
## Class Inheritance (`@extends`, `@implements`, and `@use`)
666+
## Class, Interface, & Trait Inheritance (`@extends`, `@implements`, `@use`)
667667

668668
When a child class extends a generic parent class, implements a generic interface, or uses a generic trait, declare the template mapping using any of the recognized inherited template annotations:
669669

@@ -673,6 +673,8 @@ When a child class extends a generic parent class, implements a generic interfac
673673
| **Interface Implementation** | `@implements`, `@template-implements`, `@phpstan-implements`, `@psalm-implements` |
674674
| **Trait Usage** | `@use`, `@template-use`, `@phpstan-use` |
675675

676+
### 1. Interface Implementation (`@implements` / `@template-implements`)
677+
676678
```php
677679
/**
678680
* Generic Interface
@@ -711,6 +713,106 @@ $processor->process(new Dog());
711713
// Throws: TypeError: CatProcessor::process(): Argument $item (template T = Cat) must be of type Cat
712714
```
713715

716+
### 2. Class Extension (`@extends` / `@template-extends`)
717+
718+
```php
719+
/**
720+
* @template T
721+
*/
722+
abstract class BaseRepository
723+
{
724+
/**
725+
* @param T $entity
726+
*/
727+
public function save(mixed $entity): void
728+
{
729+
// ...
730+
}
731+
}
732+
733+
/**
734+
* Fulfills T = User via @template-extends
735+
*
736+
* @template-extends BaseRepository<User>
737+
*/
738+
class UserRepository extends BaseRepository
739+
{
740+
}
741+
742+
$userRepo = new UserRepository();
743+
744+
// Valid Save
745+
$userRepo->save(new User('Alice'));
746+
747+
// Invalid Save
748+
$userRepo->save(new Product('SKU-100'));
749+
// Throws: TypeError: UserRepository::save(): Argument $entity (template T = User) must be of type User
750+
```
751+
752+
### 3. Generic Traits (`@use` / `@template-use` / `@phpstan-use`)
753+
754+
When a class uses a generic Trait, declare the template binding either at the **class level** or **directly above the inline `use Trait;` statement**:
755+
756+
#### Generic Trait Definition (`ItemLoggerTrait.php`)
757+
758+
```php
759+
/**
760+
* @template T
761+
*/
762+
trait ItemLoggerTrait
763+
{
764+
/**
765+
* @param T $item
766+
*/
767+
public function logItem(mixed $item): bool
768+
{
769+
return true;
770+
}
771+
}
772+
```
773+
774+
#### Option A: Class-Level Trait Annotation (`@use` / `@template-use`)
775+
776+
```php
777+
/**
778+
* Class docblock binds T = Dog for the trait
779+
*
780+
* @use ItemLoggerTrait<Dog>
781+
*/
782+
class ClassLevelLogService
783+
{
784+
use ItemLoggerTrait;
785+
}
786+
787+
$service = new ClassLevelLogService();
788+
789+
$service->logItem(new Dog()); // Valid
790+
791+
$service->logItem(new Car());
792+
// Throws: TypeError: Argument $item (template T = Dog) must be of type Dog, Car given
793+
```
794+
795+
#### Option B: Inline Statement Trait Annotation (`/** @use */ use Trait;`)
796+
797+
```php
798+
class InlineLogService
799+
{
800+
/**
801+
* Inline statement docblock binds T = Dog
802+
*
803+
* @use ItemLoggerTrait<Dog>
804+
*/
805+
use ItemLoggerTrait;
806+
}
807+
808+
$service = new InlineLogService();
809+
810+
$service->logItem(new Dog()); // Valid
811+
812+
$service->logItem(new Car());
813+
// Throws: TypeError: Argument $item (template T = Dog) must be of type Dog, Car given
814+
```
815+
714816
---
715817

716818
## Real-World Example 1: Generic Collections (`Collection<T>`)
@@ -994,3 +1096,4 @@ processCovariantConsumer(new Consumer(new Dog()));
9941096
processCovariantConsumer(new Consumer(new Car()));
9951097
// Throws: TypeError: processCovariantConsumer() expects Consumer<covariant Animal>, but Consumer<Car> was given
9961098
```
1099+
```
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypePHP\Tests\Fixtures\Generics;
6+
7+
use TypePHP\Tests\Fixtures\Domain\Dog;
8+
9+
class SingleLineInlineTraitUseService
10+
{
11+
/** @use GenericItemLoggerTrait<Dog> */
12+
use GenericItemLoggerTrait;
13+
}

tests/TypeChecking/Generics/GenericTraitsUseAnnotationTest.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use TypePHP\Tests\Fixtures\Domain\Dog;
88
use TypePHP\Tests\Fixtures\Generics\ClassLevelTraitService;
99
use TypePHP\Tests\Fixtures\Generics\InlineTraitUseService;
10+
use TypePHP\Tests\Fixtures\Generics\SingleLineInlineTraitUseService;
1011
use TypePHP\TypePHP;
1112

1213
describe('Generic Traits with @use, @template-use, and @phpstan-use Annotations', function () {
@@ -34,5 +35,15 @@
3435
->toThrow(TypeError::class, 'must be of type TypePHP\Tests\Fixtures\Domain\Dog')
3536
;
3637
});
38+
39+
test('pre-binds generic template T with single-line docblock (/** @use Trait<T> */ use Trait;)', function () {
40+
$service = new SingleLineInlineTraitUseService();
41+
42+
expect(TypePHP::getGenericType($service))->toBe(Dog::class);
43+
44+
expect($service->logItem(new Dog()))->toBeTrue();
45+
expect(fn () => $service->logItem(new Car()))
46+
->toThrow(TypeError::class, 'must be of type TypePHP\Tests\Fixtures\Domain\Dog');
47+
});
3748
});
3849
});

0 commit comments

Comments
 (0)