|
| 1 | +# Magic Annotations (`@property` & `@method`) |
| 2 | + |
| 3 | +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. |
| 4 | + |
| 5 | +--- |
| 6 | + |
| 7 | +## Class-Level Magic Properties (`@property`, `@property-read`, `@property-write`) |
| 8 | + |
| 9 | +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: |
| 10 | + |
| 11 | +```php |
| 12 | +<?php |
| 13 | + |
| 14 | +declare(strict_types=1); |
| 15 | + |
| 16 | +namespace App\DTOs; |
| 17 | + |
| 18 | +/** |
| 19 | + * @property positive-int $score |
| 20 | + * @property-write non-empty-string $username |
| 21 | + * @property-read list<string> $tags |
| 22 | + */ |
| 23 | +class UserDTO |
| 24 | +{ |
| 25 | + private array $storage = []; |
| 26 | + |
| 27 | + public function __set(string $name, mixed $value): void |
| 28 | + { |
| 29 | + $this->storage[$name] = $value; |
| 30 | + } |
| 31 | + |
| 32 | + public function __get(string $name): mixed |
| 33 | + { |
| 34 | + return $this->storage[$name] ?? null; |
| 35 | + } |
| 36 | +} |
| 37 | + |
| 38 | +$user = new UserDTO(); |
| 39 | + |
| 40 | +// Valid dynamic property assignment |
| 41 | +$user->score = 100; |
| 42 | +$user->username = 'Alice'; |
| 43 | + |
| 44 | +// Invalid dynamic property assignment ($score = -50 violates positive-int) |
| 45 | +$user->score = -50; |
| 46 | +// Throws: TypeError: Property UserDTO::$score must be of type positive-int, negative int (-50) given |
| 47 | +``` |
| 48 | + |
| 49 | +> **Read/Write Mechanics:** Assigning to a `@property-write` or `@property-read` annotation will validate the incoming value against the declared type constraint. |
| 50 | +
|
| 51 | +--- |
| 52 | + |
| 53 | +## Class-Level Magic Methods (`@method`) |
| 54 | + |
| 55 | +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: |
| 56 | + |
| 57 | +```php |
| 58 | +<?php |
| 59 | + |
| 60 | +declare(strict_types=1); |
| 61 | + |
| 62 | +namespace App\Services; |
| 63 | + |
| 64 | +/** |
| 65 | + * @phpstan-type StatusUnion 'active'|'pending' |
| 66 | + * |
| 67 | + * @method positive-int processOrder(positive-int $id, non-empty-string $sku) |
| 68 | + * @method static list<int> fetchBatch(int ...$ids) |
| 69 | + * @method bool updateStatus(StatusUnion $status) |
| 70 | + */ |
| 71 | +class OrderService |
| 72 | +{ |
| 73 | + public function __call(string $name, array $arguments): mixed |
| 74 | + { |
| 75 | + return $arguments[0] ?? null; |
| 76 | + } |
| 77 | + |
| 78 | + public static function __callStatic(string $name, array $arguments): mixed |
| 79 | + { |
| 80 | + return $arguments; |
| 81 | + } |
| 82 | +} |
| 83 | + |
| 84 | +$service = new OrderService(); |
| 85 | + |
| 86 | +// Valid Dynamic Call |
| 87 | +$service->processOrder(42, 'SKU-99'); |
| 88 | + |
| 89 | +// Invalid Argument ($id = -5 violates positive-int) |
| 90 | +$service->processOrder(-5, 'SKU-99'); |
| 91 | +// Throws: TypeError: OrderService::processOrder(): Argument $id must be of type positive-int |
| 92 | + |
| 93 | +// Invalid Static Variadic Argument ('invalid' violates int) |
| 94 | +OrderService::fetchBatch(1, 2, 'invalid'); |
| 95 | +// Throws: TypeError: OrderService::fetchBatch(): Argument $ids[2] must be of type int |
| 96 | +``` |
| 97 | + |
| 98 | +--- |
| 99 | + |
| 100 | +## DocBlock Inheritance for Magic Annotations |
| 101 | + |
| 102 | +Child classes automatically inherit magic property and method annotations declared across their entire object hierarchy: |
| 103 | + |
| 104 | +* **Parent Classes:** A child class extending a parent inherits all parent `@property` and `@method` annotations. |
| 105 | +* **Interfaces:** A class implementing an interface inherits magic annotations declared on the interface. |
| 106 | +* **Traits:** A class using a trait inherits all magic annotations declared on the trait. |
| 107 | +* **Overriding:** If a child class redeclares an `@property` or `@method` annotation, the child's annotation takes precedence. |
| 108 | + |
| 109 | +--- |
| 110 | + |
| 111 | +## Best Practice: Quoted Literals in `@method` Signatures |
| 112 | + |
| 113 | +`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. |
| 114 | + |
| 115 | +**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: |
| 116 | + |
| 117 | +```php |
| 118 | +/** |
| 119 | + * Recommended: Clean & Grammar-Safe via @phpstan-type |
| 120 | + * |
| 121 | + * @phpstan-type StatusUnion 'active'|'pending' |
| 122 | + * |
| 123 | + * @method bool setStatus(StatusUnion $status) |
| 124 | + */ |
| 125 | +class OrderService |
| 126 | +{ |
| 127 | + public function __call(string $name, array $arguments) { ... } |
| 128 | +} |
| 129 | +``` |
| 130 | + |
| 131 | +--- |
| 132 | + |
| 133 | +## Configuration Toggles |
| 134 | + |
| 135 | +Magic property and magic method validations are enabled by default. You can fine-tune or disable them in your `typephp.php` configuration file: |
| 136 | + |
| 137 | +```php |
| 138 | +// typephp.php |
| 139 | +return [ |
| 140 | + /* |
| 141 | + |-------------------------------------------------------------------------- |
| 142 | + | Magic Annotations (@property & @method) |
| 143 | + |-------------------------------------------------------------------------- |
| 144 | + */ |
| 145 | + 'magic_properties' => true, // Set to false to disable dynamic @property checks |
| 146 | + 'magic_methods' => true, // Set to false to disable dynamic @method checks |
| 147 | +]; |
| 148 | +``` |
0 commit comments