Skip to content
Merged
21 changes: 21 additions & 0 deletions docs/advanced/how-it-works.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ Whenever your application loads a PHP file via `require`, `include`, or Composer

---

## Project Root Resolution & Web Server Independence

To ensure consistent configuration loading across CLI commands, test runners (Pest, PHPUnit), and production web servers (where `getcwd()` points to `public/`), `Config::getProjectRoot()` searches upwards from the library's directory to locate `vendor/autoload.php` or `composer.json`.

Once located, the project root path is memoized in static memory. All relative `include`, `exclude`, and `cache_dir` configuration globs resolve reliably against the true application root directory across all PHP SAPIs (`cli`, `fpm`, `frankenphp`, `swoole`).

---

## Stream Interception

TypePHP registers a custom stream wrapper for PHP's native `file://` protocol using `stream_wrapper_register()`.
Expand Down Expand Up @@ -83,6 +91,18 @@ If the file is included, TypePHP parses the source code into an AST using `nikic

---

## Tooling Annotation Normalization & Tag Priority Hierarchy

Third-party packages often define both broad IDE docblocks and strict tool-specific contracts on the same signature (such as `@param mixed $element` alongside `@phpstan-param T $element` in Doctrine Collections).

`DocblockExtractor` normalizes and evaluates tag definitions using a **3-Tier Priority System**:

1. **Tool-Specific Annotations Take Precedence:** `@phpstan-param` and `@psalm-param` override `@param`; `@phpstan-return` and `@psalm-return` override `@return`; `@phpstan-var` overrides `@var`.
2. **Inherited Template Extraction:** Collects class, interface, and trait template mappings across all recognized variations (`@extends`, `@template-extends`, `@phpstan-extends`, `@psalm-extends`, `@implements`, `@template-implements`, `@use`, `@template-use`).
3. **Variance Modifiers:** Extracts class-level `@template-covariant` and `@template-contravariant` tags to configure the runtime variance engine.

---

## Zero Line-Drift Formatting and Caching

A common issue with AST code injection is that adding new statements pushes subsequent code down, causing line numbers in error stack traces to drift.
Expand Down Expand Up @@ -226,3 +246,4 @@ TypePHP gives you granular control so you can choose where and when to pay the p
* **Selective Path Whitelisting:** Type-check only mission-critical domain logic (`app/Domain/**`) while bypassing non-critical files completely.
* **Granular Toggles:** Turn off array checking (`inline_vars.arrays => false`) or scalar checking (`inline_vars.scalars => false`) on high-frequency internal loops while maintaining strict parameter and return boundaries (`params => true`, `returns => true`).
* **Environment Master Switch:** Disable TypePHP completely in environment builds (`enabled => false`) for 100% un-transformed, native PHP execution speed.
```
39 changes: 35 additions & 4 deletions docs/core-concepts/function-contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,39 @@ registerUser(-5, 'Alice', 'admin');

---

## Tooling Annotation Priority Hierarchy (`@phpstan-*` > `@psalm-*` > `@*`)

Modern PHP packages and frameworks (such as **Doctrine Collections**, **Symfony**, and **Laravel**) frequently declare both broad IDE-fallback annotations and strict static analysis contracts on the exact same method signature:

```php
/**
* @param mixed $element // Broad fallback for standard IDEs
* @phpstan-param positive-int $element // Refined contract for static analyzers
*
* @return mixed
* @phpstan-return list<positive-int>
*/
public function add(mixed $element): mixed;
```

When multiple tool annotations are declared on the same parameter or return value, TypePHP resolves the active contract using a deterministic **3-Tier Priority Hierarchy**:

$$\text{1. } \mathbf{@phpstan\text{-}*} \quad \longrightarrow \quad \text{2. } \mathbf{@psalm\text{-}*} \quad \longrightarrow \quad \text{3. } \mathbf{@* \text{ (Standard)}}$$

### Why Priority Matters in Real-World Codebases

1. **Refined Contracts Take Precedence:** Tool-specific annotations (`@phpstan-param`, `@psalm-return`) contain specific type constraints (such as generic templates, array shapes, or integer bounds) that standard `@param mixed` omits. TypePHP always enforces the tighter, intended contract.
2. **Third-Party Framework Compatibility:** Libraries like Doctrine Collections declare `@phpstan-param T $element` on `Collection::add` alongside native `mixed $element`. TypePHP automatically prioritizes `@phpstan-param`, making generic collections enforce types at runtime without manual wrapper code.

### Tooling Priority Matrix Across Boundary Contracts

| Boundary Type | Priority 1 (Highest) | Priority 2 | Priority 3 (Fallback) |
| :--- | :--- | :--- | :--- |
| **Parameters** | `@phpstan-param` | `@psalm-param` | `@param` |
| **Return Values** | `@phpstan-return` | `@psalm-return` | `@return` |

---

## PHP 8.0+ Named Arguments

TypePHP natively supports PHP 8.0+ Named Arguments. Because parameter contracts are mapped by parameter name rather than argument position index, you can pass named arguments in any order, and TypePHP will accurately validate each parameter:
Expand Down Expand Up @@ -257,7 +290,7 @@ TypePHP fully validates class constructor arguments, supporting both standard co

### Promoted Properties (PHP 8.0+)

Annotate promoted properties in the constructor's docblock using standard `@param` tags:
Annotate promoted properties in the constructor's docblock using standard `@param` or `@phpstan-param` tags:

```php
class Order
Expand All @@ -284,7 +317,7 @@ new Order(-1, 'SKU-99', 5);

### Property `@var` Fallback for Un-Annotated Constructors

If a constructor parameter is un-annotated (or lacks a `@param` tag), TypePHP automatically inspects the corresponding class property's `@var` docblock to infer the parameter contract:
If a constructor parameter is un-annotated (or lacks a `@param` tag), TypePHP automatically inspects the corresponding class property's `@var` / `@phpstan-var` docblock to infer the parameter contract:

```php
class User
Expand Down Expand Up @@ -329,8 +362,6 @@ getUserStatus(-10);
// Throws: TypeError: getUserStatus(): Return value['id'] must be of type positive-int
```

> **PHPStan and Psalm Compatibility:** TypePHP also recognizes `@phpstan-param`, `@phpstan-return`, `@psalm-param`, and `@psalm-return` annotations.

---

## Fluent `$this` Identity Returns
Expand Down
170 changes: 158 additions & 12 deletions docs/generics/generics-and-bounds.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,43 @@ collectSameType(10, 20, 'invalid');

---

## Tooling Template Priority Hierarchy (`@phpstan-template-*` > `@psalm-template-*` > `@template-*`)

When third-party packages or framework classes declare both general IDE template docblocks and strict tool-specific annotations on the same class, TypePHP resolves the active generic contract using a deterministic **3-Tier Priority Hierarchy**:

$$\begin{aligned}
\mathbf{Priority\ 1\ (Highest):} & \quad \text{@phpstan-template-covariant} \ > \ \text{@phpstan-template-contravariant} \ > \ \text{@phpstan-template} \\
\mathbf{Priority\ 2:} & \quad \text{@psalm-template-covariant} \ > \ \text{@psalm-template-contravariant} \ > \ \text{@psalm-template} \\
\mathbf{Priority\ 3\ (Base):} & \quad \text{@template-covariant} \ > \ \text{@template-contravariant} \ > \ \text{@template}
\end{aligned}$$

### Why Priority Matters for Templates

Authors often write a broad `@template T` for generic IDE docblocks, and then declare `@phpstan-template T of Animal` to specify strict upper bounds for static analyzers. TypePHP always extracts the tool-specific annotation so that runtime bound enforcement matches the author's intended contract:

```php
/**
* Standard tag has no bound, but @phpstan-template enforces Animal bound:
*
* @template T
* @phpstan-template T of Animal
* @phpstan-template-covariant T
*/
class BoundedProducer
{
public function __construct(public mixed $item) {}
}

// Valid: Dog extends Animal
new BoundedProducer(new Dog());

// Invalid: Car does not extend Animal
new BoundedProducer(new Car());
// Throws: TypeError: BoundedProducer::__construct(): Argument $item (template T) must be of type Animal, Car given
```

---

## Multiple Generic Templates (`@template T`, `@template U`)

Functions and classes are not limited to a single template parameter. You can declare multiple independent generic templates (such as `T`, `U`, `K`, `V`):
Expand Down Expand Up @@ -163,24 +200,24 @@ $users = new Collection();
/** @var Dictionary<string, Product> $catalog */
$catalog = new Dictionary();

// Single-Template Smart Fallback (No template name needed!)
// Single-Template Smart Fallback (No template name needed!)
$userType = TypePHP::getGenericType(object: $users); // Returns 'App\Models\User'

// Multi-Template Explicit Inspection
// Multi-Template Explicit Inspection
$keyType = TypePHP::getGenericType(object: $catalog, template: 'K'); // Returns 'string'
$valueType = TypePHP::getGenericType(object: $catalog, template: 'V'); // Returns 'App\Models\Product'

// Inherited Generic Classes (@extends BaseRepository<User>)
$userRepo = new UserRepository();
$repoType = TypePHP::getGenericType(object: $userRepo); // Returns 'App\Models\User'

// Inspect all bound template parameters as an array
// Inspect all bound template parameters as an array
$types = TypePHP::getGenericTypes(object: $catalog); // Returns ['K' => 'string', 'V' => 'App\Models\Product']

// Inspect Declared Variance ('covariant', 'contravariant', or 'invariant')
// Inspect Declared Variance ('covariant', 'contravariant', or 'invariant')
$variance = TypePHP::getGenericVariance(object: $producer); // Returns 'covariant'

// Inspect All Bound Variances as Arrays
// Inspect All Bound Variances as Arrays
$variances = TypePHP::getGenericVariances(object: $producer); // Returns ['T' => 'covariant']
```

Expand Down Expand Up @@ -626,9 +663,17 @@ $producers->add(new Producer(new Car()));

---

## Class Inheritance (`@extends` and `@implements`)
## Class, Interface, & Trait Inheritance (`@extends`, `@implements`, `@use`)

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:

| Inheritance Context | Supported Tag Variations |
| :--- | :--- |
| **Class Inheritance** | `@extends`, `@template-extends`, `@phpstan-extends`, `@psalm-extends` |
| **Interface Implementation** | `@implements`, `@template-implements`, `@phpstan-implements`, `@psalm-implements` |
| **Trait Usage** | `@use`, `@template-use`, `@phpstan-use` |

When a child class extends a generic parent class or implements a generic interface, declare the template mapping using `@extends` or `@implements` (also recognized as `@template-extends` and `@template-implements`):
### 1. Interface Implementation (`@implements` / `@template-implements`)

```php
/**
Expand All @@ -646,9 +691,9 @@ interface ProcessorInterface
}

/**
* Fulfills T = Cat via @implements
* Fulfills T = Cat via @template-implements
*
* @implements ProcessorInterface<Cat>
* @template-implements ProcessorInterface<Cat>
*/
class CatProcessor implements ProcessorInterface
{
Expand All @@ -668,6 +713,106 @@ $processor->process(new Dog());
// Throws: TypeError: CatProcessor::process(): Argument $item (template T = Cat) must be of type Cat
```

### 2. Class Extension (`@extends` / `@template-extends`)

```php
/**
* @template T
*/
abstract class BaseRepository
{
/**
* @param T $entity
*/
public function save(mixed $entity): void
{
// ...
}
}

/**
* Fulfills T = User via @template-extends
*
* @template-extends BaseRepository<User>
*/
class UserRepository extends BaseRepository
{
}

$userRepo = new UserRepository();

// Valid Save
$userRepo->save(new User('Alice'));

// Invalid Save
$userRepo->save(new Product('SKU-100'));
// Throws: TypeError: UserRepository::save(): Argument $entity (template T = User) must be of type User
```

### 3. Generic Traits (`@use` / `@template-use` / `@phpstan-use`)

When a class uses a generic Trait, declare the template binding either at the **class level** or **directly above the inline `use Trait;` statement**:

#### Generic Trait Definition (`ItemLoggerTrait.php`)

```php
/**
* @template T
*/
trait ItemLoggerTrait
{
/**
* @param T $item
*/
public function logItem(mixed $item): bool
{
return true;
}
}
```

#### Option A: Class-Level Trait Annotation (`@use` / `@template-use`)

```php
/**
* Class docblock binds T = Dog for the trait
*
* @use ItemLoggerTrait<Dog>
*/
class ClassLevelLogService
{
use ItemLoggerTrait;
}

$service = new ClassLevelLogService();

$service->logItem(new Dog()); // Valid

$service->logItem(new Car());
// Throws: TypeError: Argument $item (template T = Dog) must be of type Dog, Car given
```

#### Option B: Inline Statement Trait Annotation (`/** @use */ use Trait;`)

```php
class InlineLogService
{
/**
* Inline statement docblock binds T = Dog
*
* @use ItemLoggerTrait<Dog>
*/
use ItemLoggerTrait;
}

$service = new InlineLogService();

$service->logItem(new Dog()); // Valid

$service->logItem(new Car());
// Throws: TypeError: Argument $item (template T = Dog) must be of type Dog, Car given
```

---

## Real-World Example 1: Generic Collections (`Collection<T>`)
Expand Down Expand Up @@ -722,7 +867,7 @@ $users->add(new Product('SKU-999'));

## Real-World Example 2: Generic Repositories (`Repository<T>`)

When a class extends a generic parent class (`@extends BaseRepository<User>`), TypePHP automatically resolves and inherits the parent's generic template bindings:
When a class extends a generic parent class (`@extends BaseRepository<User>` or `@template-extends BaseRepository<User>`), TypePHP automatically resolves and inherits the parent's generic template bindings:

```php
namespace App\Repositories;
Expand All @@ -744,9 +889,9 @@ abstract class BaseRepository
}

/**
* Fulfills T = User via @extends
* Fulfills T = User via @template-extends
*
* @extends BaseRepository<User>
* @template-extends BaseRepository<User>
*/
class UserRepository extends BaseRepository
{
Expand Down Expand Up @@ -951,3 +1096,4 @@ processCovariantConsumer(new Consumer(new Dog()));
processCovariantConsumer(new Consumer(new Car()));
// Throws: TypeError: processCovariantConsumer() expects Consumer<covariant Animal>, but Consumer<Car> was given
```
```
Loading
Loading