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
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) givenRead/Write Mechanics: Assigning to a
@property-writeor@property-readannotation 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
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 intDocBlock 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
@propertyand@methodannotations. - 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
@propertyor@methodannotation, 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:
/**
* 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:
// 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
];