Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export default defineConfig({
{ text: 'Function Contracts', link: '/core-concepts/function-contracts' },
{ text: 'Property Validation', link: '/core-concepts/property-validation' },
{ text: 'Inline Variables', link: '/core-concepts/inline-variables' },
{ text: 'Magic Annotations', link: '/core-concepts/magic-annotations' },
]
},
{
Expand Down
148 changes: 148 additions & 0 deletions docs/core-concepts/magic-annotations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# Magic Annotations (`@property` & `@method`)

Dynamic properties and magic methods are widely used across modern PHP frameworks (such as Laravel Eloquent models, DTOs, and dynamic service repositories). TypePHP provides transparent, runtime enforcement for class-level `@property`, `@property-read`, `@property-write`, and `@method` annotations.

---

## Class-Level Magic Properties (`@property`, `@property-read`, `@property-write`)

When a property does not physically exist on a class, PHP routes property writes through `__set()`. TypePHP intercepts these dynamic assignments and validates incoming values against class-level `@property`, `@property-read`, and `@property-write` annotations declared on the class, parent classes, interfaces, or traits:

```php
<?php

declare(strict_types=1);

namespace App\DTOs;

/**
* @property positive-int $score
* @property-write non-empty-string $username
* @property-read list<string> $tags
*/
class UserDTO
{
private array $storage = [];

public function __set(string $name, mixed $value): void
{
$this->storage[$name] = $value;
}

public function __get(string $name): mixed
{
return $this->storage[$name] ?? null;
}
}

$user = new UserDTO();

// Valid dynamic property assignment
$user->score = 100;
$user->username = 'Alice';

// Invalid dynamic property assignment ($score = -50 violates positive-int)
$user->score = -50;
// Throws: TypeError: Property UserDTO::$score must be of type positive-int, negative int (-50) given
```

> **Read/Write Mechanics:** Assigning to a `@property-write` or `@property-read` annotation will validate the incoming value against the declared type constraint.

---

## Class-Level Magic Methods (`@method`)

When a method is called dynamically via `__call()` or `__callStatic()`, TypePHP intercepts the invocation and validates both incoming arguments and returned values against class-level `@method` annotations:

```php
<?php

declare(strict_types=1);

namespace App\Services;

/**
* @phpstan-type StatusUnion 'active'|'pending'
*
* @method positive-int processOrder(positive-int $id, non-empty-string $sku)
* @method static list<int> fetchBatch(int ...$ids)
* @method bool updateStatus(StatusUnion $status)
*/
class OrderService
{
public function __call(string $name, array $arguments): mixed
{
return $arguments[0] ?? null;
}

public static function __callStatic(string $name, array $arguments): mixed
{
return $arguments;
}
}

$service = new OrderService();

// Valid Dynamic Call
$service->processOrder(42, 'SKU-99');

// Invalid Argument ($id = -5 violates positive-int)
$service->processOrder(-5, 'SKU-99');
// Throws: TypeError: OrderService::processOrder(): Argument $id must be of type positive-int

// Invalid Static Variadic Argument ('invalid' violates int)
OrderService::fetchBatch(1, 2, 'invalid');
// Throws: TypeError: OrderService::fetchBatch(): Argument $ids[2] must be of type int
```

---

## DocBlock Inheritance for Magic Annotations

Child classes automatically inherit magic property and method annotations declared across their entire object hierarchy:

* **Parent Classes:** A child class extending a parent inherits all parent `@property` and `@method` annotations.
* **Interfaces:** A class implementing an interface inherits magic annotations declared on the interface.
* **Traits:** A class using a trait inherits all magic annotations declared on the trait.
* **Overriding:** If a child class redeclares an `@property` or `@method` annotation, the child's annotation takes precedence.

---

## Best Practice: Quoted Literals in `@method` Signatures

`phpdoc-parser`'s grammar for `@method` parameter signatures can encounter ambiguity when parsing unparenthesized single quotes directly inside parameter types (such as `@method bool setStatus('active'|'pending' $status)`). When `phpdoc-parser` encounters this grammar ambiguity, it drops that specific `@method` tag.

**Recommended Best Practice:** Define complex union string literals or array shapes using a local `@phpstan-type` alias, and reference the alias in your `@method` annotation:

```php
/**
* Recommended: Clean & Grammar-Safe via @phpstan-type
*
* @phpstan-type StatusUnion 'active'|'pending'
*
* @method bool setStatus(StatusUnion $status)
*/
class OrderService
{
public function __call(string $name, array $arguments) { ... }
}
```

---

## Configuration Toggles

Magic property and magic method validations are enabled by default. You can fine-tune or disable them in your `typephp.php` configuration file:

```php
// typephp.php
return [
/*
|--------------------------------------------------------------------------
| Magic Annotations (@property & @method)
|--------------------------------------------------------------------------
*/
'magic_properties' => true, // Set to false to disable dynamic @property checks
'magic_methods' => true, // Set to false to disable dynamic @method checks
];
```
28 changes: 27 additions & 1 deletion docs/getting-started/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ return [
'params' => true,
'returns' => true,

/*
|--------------------------------------------------------------------------
| Magic Annotations (@property & @method)
|--------------------------------------------------------------------------
| Enforces class-level annotations for dynamic properties and magic methods
| routed through __get, __set, __call, and __callStatic.
*/
'magic_properties' => true,
'magic_methods' => true,

/*
|--------------------------------------------------------------------------
| Respect Ignore Docblock Tags
Expand Down Expand Up @@ -107,6 +117,22 @@ return [

---

## Configuration Reference

Key options explained:

| Configuration Option | Default | Description |
| :--- | :--- | :--- |
| **`'enabled'`** | `true` | Global master switch for runtime type enforcement. |
| **`'params'`** | `true` | Enforces parameter `@param` contracts on physical functions and methods. |
| **`'returns'`** | `true` | Enforces return `@return` contracts on physical functions and methods. |
| **`'magic_properties'`** | `true` | Enforces class-level `@property`, `@property-read`, and `@property-write` annotations on dynamic assignments (`__set`). |
| **`'magic_methods'`** | `true` | Enforces class-level `@method` annotations on dynamic method calls (`__call` / `__callStatic`). |
| **`'respect_ignore_tags'`** | `true` | Respects `@typephp-ignore` and `@typephp-ignore-file` tags. Set to `false` in CI/CD to force audit checks. |
| **`'cache'`** | `true` | Pre-transforms and caches PHP files on disk (`typephp-cache/`) for OPcache optimization. |

---

## Inline Variable Categories Reference (`inline_vars`)

How each `inline_vars` toggle maps to PHPDoc type annotations:
Expand All @@ -116,7 +142,7 @@ How each `inline_vars` toggle maps to PHPDoc type annotations:
| **`'scalars'`** | Primitive & Refined Scalars | `int`, `string`, `bool`, `positive-int`, `non-empty-string`, `truthy` |
| **`'objects'`** | Class Instances & Bare Class References | `User`, `stdClass`, `class-string`, `interface-string`, `enum-string` |
| **`'generics'`** | Template & Bound Types | `Collection<User>`, `Producer<T>`, `class-string<T>` |
| **` illegible 'arrays'`** | All Arrays, Shapes, & Lists | `array{id: int}`, `int[]`, `User[]`, `list<string>`, `array<string, int>` |
| **`'arrays'`** | All Arrays, Shapes, & Lists | `array{id: int}`, `int[]`, `User[]`, `list<string>`, `array<string, int>` |
| **`'callables'`** | Callables & Closures | `callable`, `Closure`, `callable(int): string`, `static-closure` |
| **`'properties'`** | Class Property Writes | `$this->id = 1`, `UserProfile::$username = 'Alice'` |

Expand Down
43 changes: 41 additions & 2 deletions docs/getting-started/quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ TypePHP enforces PHPDoc type contracts at runtime during execution. Below is an

## What is TypePHP?

TypePHP is a transparent, pure-PHP runtime type checker that enforces extended PHPDoc type contracts (`@param`, `@return`, `@var`, `@template`, array shapes, integer ranges, and scalar refinements) during actual execution.
TypePHP is a transparent, pure-PHP runtime type checker that enforces extended PHPDoc type contracts (`@param`, `@return`, `@var`, `@template`, `@property`, `@method`, array shapes, integer ranges, and scalar refinements) during actual execution.

Unlike traditional assertion libraries that force you to write repetitive manual check calls inside every function, or validation frameworks that require custom PHP attributes and base classes, TypePHP requires **zero manual checks** and **zero new syntax**. It works transparently using your existing PHPDoc annotations.

Expand All @@ -29,7 +29,7 @@ TypePHP does not force you into an "all-or-nothing" paradigm. You do not have to

1. **Path-Level Whitelisting:** Use `include` patterns in `typephp.php` to target specific mission-critical domain modules (such as `app/Domain/Billing/**`) while completely bypassing legacy directories.
2. **Method-Level Suppression:** Add `@typephp-ignore` to specific legacy methods or un-refactored functions without removing their PHPDoc annotations.
3. **Category-Level Feature Toggles:** Granularly enable or disable specific check categories (`inline_vars.scalars`, `inline_vars.arrays`, `params`, `returns`) in `typephp.php` depending on performance or migration needs.
3. **Category-Level Feature Toggles:** Granularly enable or disable specific check categories (`inline_vars.scalars`, `inline_vars.arrays`, `params`, `returns`, `magic_properties`, `magic_methods`) in `typephp.php` depending on performance or migration needs.

---

Expand Down Expand Up @@ -192,6 +192,45 @@ $users->add(new Product('SKU-100'));

---

## Class-Level Magic Annotations (`@property` & `@method`)

TypePHP validates dynamic property writes (`__set`) and dynamic method calls (`__call`) against class-level `@property` and `@method` annotations:

```php
/**
* @phpstan-type StatusUnion 'active'|'pending'
*
* @property positive-int $score
* @method bool updateStatus(StatusUnion $status)
*/
class DynamicModel
{
private array $storage = [];

public function __set(string $name, mixed $value): void
{
$this->storage[$name] = $value;
}

public function __call(string $name, array $arguments): mixed
{
return true;
}
}

$model = new DynamicModel();

// Invalid Dynamic Property Assignment ($score = -50 violates positive-int)
$model->score = -50;
// Throws: TypeError: Property DynamicModel::$score must be of type positive-int

// Invalid Dynamic Method Argument ($status = 'archived' violates StatusUnion)
$model->updateStatus('archived');
// Throws: TypeError: DynamicModel::updateStatus(): Argument $status must be of type ('active' | 'pending')
```

---

## PHP 8.4 Property Hooks & Asymmetric Visibility

TypePHP validates incoming and returned values on PHP 8.4 Property Hooks:
Expand Down
22 changes: 22 additions & 0 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,28 @@ TypePHP injects guard rails at the call site where assignments happen.

---

### Why is my `@method` annotation with quoted literals like `'active'|'pending'` not being enforced?

`phpdoc-parser`'s grammar engine for `@method` parameter lists can encounter ambiguity when parsing unparenthesized single or double quotes directly inside parameter type signatures (such as `@method bool updateStatus('active'|'pending' $status)`). When `phpdoc-parser` encounters this grammar ambiguity, it drops that specific `@method` tag during DocBlock parsing.

**Solution:** Use a local `@phpstan-type` alias to define the union string literal or shape, and reference the alias in your `@method` annotation:

```php
/**
* Best Practice: Clean & Grammar-Safe via @phpstan-type
*
* @phpstan-type StatusUnion 'active'|'pending'
*
* @method bool updateStatus(StatusUnion $status)
*/
class OrderService
{
public function __call(string $name, array $arguments) { ... }
}
```

---

### Why is my Pest or PHPUnit test suite running slower with JIT enabled?

During CLI test execution, a single short-lived PHP process runs your tests.
Expand Down
10 changes: 10 additions & 0 deletions src/Command/ConfigInitCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,16 @@ private static function getTemplate(): string
'params' => true,
'returns' => true,

/*
|--------------------------------------------------------------------------
| Magic Annotations (@property & @method)
|--------------------------------------------------------------------------
| Enforces class-level annotations for dynamic properties and magic methods
| routed through __get, __set, __call, and __callStatic.
*/
'magic_properties' => true,
'magic_methods' => true,

/*
|--------------------------------------------------------------------------
| Respect Ignore Docblock Tags
Expand Down
Loading
Loading