Tip
Implement type-safe wrapper methods (getString(), getInt(), etc.) instead of relying on __get() and __set() directly. Wrapper methods provide compile-time type checking and eliminate the need for callers to narrow mixed return values.
Tier 0 · Story 08Complete
Tyhp supports all of PHP's magic methods with additional type safety requirements. Magic methods that return mixed in PHP must have their return values type-narrowed before use. The compiler enforces correct signatures, auto-generates certain magic methods for generic classes, and prioritizes extension methods over __call for compile-time-safe dispatch.
All standard PHP magic methods are supported in Tyhp. They must follow PHP's expected signatures but can use Tyhp's enhanced type system. The following table lists every supported magic method and its Tyhp-specific behavior.
__construct()Constructor. Supports Tyhp constructor property promotion with modifiers like readonly. The return type is required: : void, or : parent(args) for explicit parent constructor invocation.
__destruct()Destructor. Called when an object is garbage collected. Used internally by DisposableScope for automatic resource disposal via the := operator.
__get(string $name): mixedCalled when reading an inaccessible or non-existent property. Returns mixed — must be narrowed before use in a type-safe context.
__set(string $name, mixed $value): voidCalled when writing to an inaccessible or non-existent property. Type-safe wrapper methods are encouraged instead of direct __set usage.
__isset(string $name): boolCalled by isset() or empty() on inaccessible or non-existent properties.
__unset(string $name): voidCalled by unset() on inaccessible or non-existent properties.
__call(string $name, array $arguments): mixedCalled when invoking inaccessible or non-existent instance methods. Returns mixed. Extension methods are checked BEFORE __call — if an extension method matches the call, it takes priority.
__callStatic(string $name, array $arguments): mixedCalled when invoking inaccessible or non-existent static methods. Returns mixed. Same extension method priority applies as __call.
__toString(): stringCalled when an object is used in a string context. Must return string — the compiler enforces this return type strictly.
__invoke(...$args): mixedCalled when an object is used as a function ($obj()). Return type is mixed — must be narrowed before use.
__clone(): voidCalled after an object is cloned with clone. You may implement it yourself. For PHP < 8.5, the compiler may emit a clone wrapper when with updates readonly properties.
__debugInfo(): arrayCalled by var_dump() to get the properties to display. Must return an array.
__serialize(): arrayCalled during serialization. Must return an array containing the object's serialized state.
__unserialize(array $data): voidCalled during unserialization. Receives the array returned by __serialize().
Magic methods like __get(), __call(), __callStatic(), and __invoke() return mixed in PHP. In Tyhp, the mixed type must be narrowed before the value can be used in a typed context. This means you must use type narrowing (is checks, instanceof) or type assertions before using the return value.
<?tyhp
class DynamicConfig {
private array $data = [];
public function __get(string $name): mixed {
return $this->data[$name] ?? null;
}
public function __set(string $name, mixed $value): void {
$this->data[$name] = $value;
}
}
DynamicConfig $config = new DynamicConfig();
$config->dbHost = 'localhost';
// ERROR: Cannot use mixed directly without narrowing
// string $value = $config->dbHost;
// OK: Narrow with is check first
mixed $raw = $config->dbHost;
if ($raw is string) {
string $value = $raw; // OK: narrowed to string
echo $value;
}
The recommended pattern is to implement type-safe wrapper methods that internally use __get/__set but expose typed interfaces to callers.
<?tyhp
class TypedConfig {
private array $data = [];
public function __get(string $name): mixed {
return $this->data[$name] ?? null;
}
// Type-safe wrapper methods — preferred approach
public function getString(string $key, string $default = ''): string {
mixed $val = $this->data[$key] ?? $default;
return $val is string ? $val : $default;
}
public function getInt(string $key, int $default = 0): int {
mixed $val = $this->data[$key] ?? $default;
return $val is int ? $val : $default;
}
}
Tyhp's extension methods are resolved at compile time and provide full type checking. When both an extension method and a __call magic method could handle a method call, the extension method takes priority. This means extension methods are always checked first — __call is only invoked when no matching extension method exists.
<?tyhp
class ApiClient {
// __call handles unknown methods at runtime
public function __call(string $name, array $args): mixed {
return $this->sendRequest($name, $args);
}
}
// Extension method — compile-time type safety.
// Members are top-level `function` forms (optional `async` only).
// Visibility/`static` cannot appear. `extension Name extends ApiClient` is optional.
extension ApiClientExtensions {
function getUsers(extends ApiClient $client): array<User> {
mixed $result = $client->sendRequest('getUsers', []);
// narrowing and type-safe return
return $result is array ? $result : [];
}
}
ApiClient $api = new ApiClient();
// This calls the extension method (type-safe, compile-time checked)
array<User> $users = $api->getUsers();
// This falls through to __call (returns mixed, requires narrowing)
mixed $result = $api->unknownMethod();
<?php
// Extension method compiles to static call:
$users = ApiClientExtensions::getUsers($api);
// __call compiles to standard PHP magic method dispatch:
$result = $api->unknownMethod();
When a class uses generics with runtime type tracking (for example typeof(T)), the Tyhp compiler adds the HasGenerics trait from the tyhp/core package (\Tyhp\Concerns\HasGenerics). Concerns\GenericObject is the legacy name. The compiler also auto-generates constructor logic to initialize the generic type information (__initGenerics__tyhpGeneric and $this->__tyhpGeneric->init(...)). Generic classes that never use typeof(T) or other tracking triggers emit no trait.
<?tyhp
class TypedCollection<T> {
private array $items = [];
public function add(T $item): void {
$this->items[] = $item;
}
public function get(int $index): T {
return $this->items[$index];
}
public function getItemType(): \Tyhp\Type {
return typeof(T);
}
}
<?php
class TypedCollection {
use \Tyhp\Concerns\HasGenerics;
private array $items = [];
public function add(mixed $item): void {
$this->items[] = $item;
}
public function get(int $index): mixed {
return $this->items[$index];
}
protected function __initGenerics__tyhpGeneric(?\Tyhp\Type ...$generics): void
{
$this->__tyhpGeneric->init(static::class, \TypedCollection::class, new \Tyhp\NamedType('T', $generics[0] ?? null));
$this->__tyhpGeneric->markBound();
}
}
The compiler strictly enforces that __toString() returns string. Unlike PHP which coerces the return value, Tyhp treats a non-string return type as a compiler error.
<?tyhp
class Money {
public function __construct(
private int $amount,
private string $currency
): void {}
// OK: returns string
public function __toString(): string {
return "{$this->amount} {$this->currency}";
}
}
// ERROR: __toString must return string
// class Bad {
// public function __toString(): int { return 42; }
// }
Tyhp constructors must declare a return type: : void, or : parent(args) to insert parent::__construct(...) at the start of the body. PHP output strips the annotation.
<?tyhp
class Base {
public function __construct(public string $name): void {}
}
class Child extends Base {
public function __construct(
string $name,
public int $age
): parent($name) {
// parent::__construct($name) is called automatically
}
}
<?php
class Base {
public function __construct(public string $name) {}
}
class Child extends Base {
public function __construct(
string $name,
public int $age
) {
parent::__construct($name);
}
}
clone ... with on readonly properties does not require you to write __clone(). On PHP 8.5+ the compiler emits native clone($obj, [...]). On PHP 8.2–8.4 it uses a compiler-generated wrapper (opt-in via build.experimentalReadonlyCloneWith). You can still write your own __clone() for other clone-time work.
<?tyhp
class Point {
public function __construct(
public readonly int $x,
public readonly int $y
): void {}
}
Point $p1 = new Point(1, 2);
Point $p2 = clone $p1 with [x => 10]; // $p2->x is 10, $p2->y is 2
Implement type-safe wrapper methods (getString(), getInt(), etc.) instead of relying on __get() and __set() directly. Wrapper methods provide compile-time type checking and eliminate the need for callers to narrow mixed return values.
Use extension methods instead of __call() whenever possible. Extension methods are resolved at compile time, provide full type safety, and compile to static method calls — while __call() loses all type information and requires runtime narrowing.
Use operator overloads instead of relying on __toString() for arithmetic or comparison operations. Operator overloads provide explicit, type-safe semantics.
Use readonly properties with clone ... with for immutable value objects. You do not need a handwritten __clone() for that pattern.
Relying on __get() and __set() for dynamic property creation. Tyhp disables dynamic property creation on classes — all properties must be declared in the class definition. Use __get/__set only for controlled access to a backing store like an array.
Using __call() for type-safe method dispatch. __call() returns mixed and provides no compile-time type information. Use function overloads or extension methods instead for type-safe dispatch patterns.
Assuming mixed return values from magic methods are a specific type without narrowing. Always use is checks or type assertions before using values returned from __get(), __call(), __callStatic(), or __invoke().
Returning a non-string value from __toString(). The compiler enforces that __toString() must have a string return type.
Assuming clone ... with on readonly works on PHP 8.2–8.4 without build.experimentalReadonlyCloneWith: true. Enable that flag, or target PHP 8.5+.
Magic methods compile to standard PHP magic methods. The compiler does not transform the magic method signatures or bodies — they pass through to PHP directly. The only additions are auto-generated generic-tracking members when needed (HasGenerics trait, __initGenerics__tyhpGeneric). Readonly clone ... with on PHP < 8.5 may emit a separate wrapper, not a replacement of your own __clone().
<?tyhp
class Entity {
public function __serialize(): array {
return ['id' => $this->id, 'name' => $this->name];
}
public function __unserialize(array $data): void {
$this->id = $data['id'];
$this->name = $data['name'];
}
public function __debugInfo(): array {
return ['id' => $this->id];
}
}
<?php
// Magic methods pass through to PHP unchanged
class Entity {
public function __serialize(): array {
return ['id' => $this->id, 'name' => $this->name];
}
public function __unserialize(array $data): void {
$this->id = $data['id'];
$this->name = $data['name'];
}
public function __debugInfo(): array {
return ['id' => $this->id];
}
}