Skip to content

Commit 739840c

Browse files
authored
Internal improvements 2 (#18)
* Add tests for class-string template bounds and generic template validations and fix implementation * Add methods to extract types from class-level docblocks and implement tests for magic methods * Enhance magic method and property handling - Implemented validation for magic methods and properties in ParamChecker and ReturnChecker. - Added support for dynamic @method and @Property annotations in DocblockExtractor. - Introduced new tests for inherited magic methods and properties. - Improved error handling for non-existent methods and properties. - Updated configuration to manage magic methods and properties validation. - Refactored SpecialTypeResolver to handle reflection context more gracefully. - Added new fixtures for testing dynamic method and property behavior. * fix php stan errors * Add magic annotations documentation and configuration options * Add file exclusion check in class hierarchy for property parsing
1 parent a23adac commit 739840c

32 files changed

Lines changed: 1486 additions & 79 deletions

docs/.vitepress/config.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export default defineConfig({
3030
{ text: 'Function Contracts', link: '/core-concepts/function-contracts' },
3131
{ text: 'Property Validation', link: '/core-concepts/property-validation' },
3232
{ text: 'Inline Variables', link: '/core-concepts/inline-variables' },
33+
{ text: 'Magic Annotations', link: '/core-concepts/magic-annotations' },
3334
]
3435
},
3536
{
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
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+
```

docs/getting-started/configuration.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,16 @@ return [
3434
'params' => true,
3535
'returns' => true,
3636

37+
/*
38+
|--------------------------------------------------------------------------
39+
| Magic Annotations (@property & @method)
40+
|--------------------------------------------------------------------------
41+
| Enforces class-level annotations for dynamic properties and magic methods
42+
| routed through __get, __set, __call, and __callStatic.
43+
*/
44+
'magic_properties' => true,
45+
'magic_methods' => true,
46+
3747
/*
3848
|--------------------------------------------------------------------------
3949
| Respect Ignore Docblock Tags
@@ -107,6 +117,22 @@ return [
107117

108118
---
109119

120+
## Configuration Reference
121+
122+
Key options explained:
123+
124+
| Configuration Option | Default | Description |
125+
| :--- | :--- | :--- |
126+
| **`'enabled'`** | `true` | Global master switch for runtime type enforcement. |
127+
| **`'params'`** | `true` | Enforces parameter `@param` contracts on physical functions and methods. |
128+
| **`'returns'`** | `true` | Enforces return `@return` contracts on physical functions and methods. |
129+
| **`'magic_properties'`** | `true` | Enforces class-level `@property`, `@property-read`, and `@property-write` annotations on dynamic assignments (`__set`). |
130+
| **`'magic_methods'`** | `true` | Enforces class-level `@method` annotations on dynamic method calls (`__call` / `__callStatic`). |
131+
| **`'respect_ignore_tags'`** | `true` | Respects `@typephp-ignore` and `@typephp-ignore-file` tags. Set to `false` in CI/CD to force audit checks. |
132+
| **`'cache'`** | `true` | Pre-transforms and caches PHP files on disk (`typephp-cache/`) for OPcache optimization. |
133+
134+
---
135+
110136
## Inline Variable Categories Reference (`inline_vars`)
111137

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

docs/getting-started/quick-start.md

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ TypePHP enforces PHPDoc type contracts at runtime during execution. Below is an
66

77
## What is TypePHP?
88

9-
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.
9+
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.
1010

1111
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.
1212

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

3030
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.
3131
2. **Method-Level Suppression:** Add `@typephp-ignore` to specific legacy methods or un-refactored functions without removing their PHPDoc annotations.
32-
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.
32+
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.
3333

3434
---
3535

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

193193
---
194194

195+
## Class-Level Magic Annotations (`@property` & `@method`)
196+
197+
TypePHP validates dynamic property writes (`__set`) and dynamic method calls (`__call`) against class-level `@property` and `@method` annotations:
198+
199+
```php
200+
/**
201+
* @phpstan-type StatusUnion 'active'|'pending'
202+
*
203+
* @property positive-int $score
204+
* @method bool updateStatus(StatusUnion $status)
205+
*/
206+
class DynamicModel
207+
{
208+
private array $storage = [];
209+
210+
public function __set(string $name, mixed $value): void
211+
{
212+
$this->storage[$name] = $value;
213+
}
214+
215+
public function __call(string $name, array $arguments): mixed
216+
{
217+
return true;
218+
}
219+
}
220+
221+
$model = new DynamicModel();
222+
223+
// Invalid Dynamic Property Assignment ($score = -50 violates positive-int)
224+
$model->score = -50;
225+
// Throws: TypeError: Property DynamicModel::$score must be of type positive-int
226+
227+
// Invalid Dynamic Method Argument ($status = 'archived' violates StatusUnion)
228+
$model->updateStatus('archived');
229+
// Throws: TypeError: DynamicModel::updateStatus(): Argument $status must be of type ('active' | 'pending')
230+
```
231+
232+
---
233+
195234
## PHP 8.4 Property Hooks & Asymmetric Visibility
196235

197236
TypePHP validates incoming and returned values on PHP 8.4 Property Hooks:

docs/troubleshooting.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,28 @@ TypePHP injects guard rails at the call site where assignments happen.
5555

5656
---
5757

58+
### Why is my `@method` annotation with quoted literals like `'active'|'pending'` not being enforced?
59+
60+
`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.
61+
62+
**Solution:** Use a local `@phpstan-type` alias to define the union string literal or shape, and reference the alias in your `@method` annotation:
63+
64+
```php
65+
/**
66+
* Best Practice: Clean & Grammar-Safe via @phpstan-type
67+
*
68+
* @phpstan-type StatusUnion 'active'|'pending'
69+
*
70+
* @method bool updateStatus(StatusUnion $status)
71+
*/
72+
class OrderService
73+
{
74+
public function __call(string $name, array $arguments) { ... }
75+
}
76+
```
77+
78+
---
79+
5880
### Why is my Pest or PHPUnit test suite running slower with JIT enabled?
5981

6082
During CLI test execution, a single short-lived PHP process runs your tests.

src/Command/ConfigInitCommand.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,16 @@ private static function getTemplate(): string
6464
'params' => true,
6565
'returns' => true,
6666
67+
/*
68+
|--------------------------------------------------------------------------
69+
| Magic Annotations (@property & @method)
70+
|--------------------------------------------------------------------------
71+
| Enforces class-level annotations for dynamic properties and magic methods
72+
| routed through __get, __set, __call, and __callStatic.
73+
*/
74+
'magic_properties' => true,
75+
'magic_methods' => true,
76+
6777
/*
6878
|--------------------------------------------------------------------------
6979
| Respect Ignore Docblock Tags

0 commit comments

Comments
 (0)