Magic Methods
Magic methods let you hook into special behaviors—construction, destruction, property access, string conversion, and more. They're called automatically by the runtime when certain actions occur.
Magic methods have a distinct syntax: no fn keyword, no visibility modifiers. Just the method name and body.
Syntax
type User {
name string
// Magic method - no fn keyword
new($name string) {
$this.name = $name;
}
// Regular method - has fn keyword
fn greet() {
echo "Hello, {$this.name}";
}
}
The absence of fn marks a method as magic. This makes them visually distinct and easy to spot.
Constructor: new
Called when you create an instance of a type.
type Connection {
host string
port i32
socket Socket
new($host string, $port i32) {
$this.host = $host;
$this.port = $port;
$this.socket = Socket::connect($host, $port);
}
}
// Usage
$conn = Connection("localhost", 8080); // Calls new()
Compared to fn new()
You can still use the static method pattern if you prefer:
type User {
name string
fn new($name string): User {
return User { name: $name };
}
}
$user = User::new("Alice"); // Explicit call
The magic new method is called implicitly when you use the type as a constructor:
$user = User("Alice"); // Calls magic new()
Both patterns are valid. Use magic new for implicit construction, fn new() for explicit factory-style creation.
Destructor: destruct
Called when an object is destroyed—either explicitly with delete or when it goes out of scope.
type FileHandle {
path string
handle i32
new($path string) {
$this.path = $path;
$this.handle = open_file($path);
}
destruct() {
close_file($this.handle);
echo "Closed file: {$this.path}";
}
}
// Automatic destruction at scope exit
{
$file = FileHandle("data.txt");
// ... use file ...
} // destruct() called here
// Explicit destruction with delete
$file = FileHandle("data.txt");
delete $file; // destruct() called immediately
The delete Keyword
Use delete to explicitly destroy objects and free resources:
delete $variable; // Destroy variable, call destruct()
delete $object; // Destroy object, call destruct()
delete $array[$index]; // Remove array element
This is useful for:
- Releasing resources early (files, sockets, locks)
- Breaking reference cycles
- Explicit memory management
Property Access: get and set
Intercept property reads and writes. The $undefined parameter tells you whether the property exists on the type.
get
Called when reading a property.
type DynamicObject {
data HashMap<string, mixed>
name string
get($name string, $undefined bool): mixed {
if $undefined {
// Property doesn't exist as a field
return $this.data.get($name);
}
// Let it fall through to the actual field
}
}
$obj = DynamicObject();
$obj.data.insert("custom", "value");
echo $obj.name; // $undefined = false, returns actual field
echo $obj.custom; // $undefined = true, returns from data map
set
Called when writing to a property.
type DynamicObject {
data HashMap<string, mixed>
name string
set($name string, $value mixed, $undefined bool) {
if $undefined {
// Property doesn't exist as a field
$this.data.insert($name, $value);
} else {
// Could add validation, logging, etc.
echo "Setting {$name} to {$value}";
}
}
}
$obj = DynamicObject();
$obj.name = "Alice"; // $undefined = false
$obj.custom = "value"; // $undefined = true, stored in data map
Use Cases
- Lazy loading: Load data on first access
- Validation: Check values before setting
- Computed properties: Calculate values on the fly
- Proxies: Forward access to another object
- Dynamic properties: Store arbitrary key-value pairs
Method Interception: call
Called when invoking a method that doesn't exist.
type Proxy {
target mixed
call($name string, $args []mixed): mixed {
echo "Calling {$name} with {$args.len()} args";
// Forward to target
return $this.target.call_method($name, $args);
}
}
$proxy = Proxy(some_object);
$proxy.anything("a", "b"); // Calls call("anything", ["a", "b"])
Use Cases
- Proxies and decorators: Wrap objects with additional behavior
- Remote procedure calls: Forward method calls over network
- Method chaining builders: Handle arbitrary method names
- Testing mocks: Record and replay method calls
String Conversion: to_string
Called when the object needs to be converted to a string. Implements the Display trait automatically.
type Point {
x f64
y f64
to_string(): string {
return "({$this.x}, {$this.y})";
}
}
$p = Point { x: 3.0, y: 4.0 };
echo $p; // Calls to_string(): "(3.0, 4.0)"
$s = string($p); // Explicit conversion
$msg = "Point: {$p}"; // String interpolation
Trait Mapping
Defining to_string() automatically implements the Display trait:
// This is equivalent to:
type Point {
x f64
y f64
@Display
fn fmt(&$this, $f &Formatter) {
write!($f, "({}, {})", $this.x, $this.y);
}
}
Array Access: index
Called when using array syntax on the object. Implements the Index trait automatically.
type Matrix {
data [][]f64
rows i32
cols i32
index($row i32, $col i32): f64 {
return $this.data[$row][$col];
}
}
$m = Matrix::new(3, 3);
$value = $m[1, 2]; // Calls index(1, 2)
With Mutable Access
For setting values, combine with set:
type Vector {
data []f64
index($i i32): f64 {
return $this.data[$i];
}
// Handle $vec[i] = value
set($name string, $value mixed, $undefined bool) {
if $name.starts_with("[") {
$i = parse_index($name);
$this.data[$i] = $value;
}
}
}
Trait Mapping
Defining index() automatically implements the Index trait.
Comparison: compare
Called for comparison operations. Implements the Comparable trait automatically.
type Version {
major i32
minor i32
patch i32
compare($other &Self): i32 {
if $this.major != $other.major {
return $this.major - $other.major;
}
if $this.minor != $other.minor {
return $this.minor - $other.minor;
}
return $this.patch - $other.patch;
}
}
$v1 = Version { major: 1, minor: 2, patch: 3 };
$v2 = Version { major: 1, minor: 3, patch: 0 };
if $v1 < $v2 { // Calls compare()
echo "v1 is older";
}
$versions.sort(); // Uses compare() for sorting
Return Values
- Negative:
$thisis less than$other - Zero:
$thisequals$other - Positive:
$thisis greater than$other
Trait Mapping
Defining compare() automatically implements the Comparable trait, enabling:
<,>,<=,>=operators- Sorting with
.sort() - Min/max functions
Complete Example
Here's a type using multiple magic methods:
type SmartDict {
data HashMap<string, mixed>
access_count i32
new() {
$this.data = HashMap::new();
$this.access_count = 0;
}
destruct() {
echo "Dict destroyed after {$this.access_count} accesses";
}
get($name string, $undefined bool): mixed {
$this.access_count += 1;
return $this.data.get($name);
}
set($name string, $value mixed, $undefined bool) {
$this.access_count += 1;
$this.data.insert($name, $value);
}
index($key string): mixed {
return $this.data.get($key);
}
to_string(): string {
return "SmartDict({$this.data.len()} items)";
}
}
// Usage
$dict = SmartDict();
$dict.name = "example"; // set()
echo $dict.name; // get()
echo $dict["name"]; // index()
echo $dict; // to_string()
delete $dict; // destruct()
Trait Mappings Summary
| Magic Method | Implements Trait | Enables |
|---|---|---|
to_string() |
Display |
echo, string conversion, interpolation |
index() |
Index |
Array access syntax $obj[$key] |
compare() |
Comparable |
Comparison operators, sorting |
When you define a magic method that maps to a trait, you don't need to add the @Trait annotation—it's automatic.
Best Practices
Use magic methods sparingly. They add implicit behavior that can surprise readers. When possible, prefer explicit methods.
Document magic behavior. If your type has magic methods, document what triggers them and what they do.
Keep magic methods simple. Complex logic in get/set makes code hard to reason about.
Prefer traits for standard behaviors. If you're just implementing Display or Comparable, consider using the trait annotation directly for clarity.
Be careful with call. Method interception is powerful but can make code hard to understand and debug.
// Good: Clear, explicit
type User {
name string
to_string(): string {
return "User: {$this.name}";
}
}
// Avoid: Too much magic
type MagicObject {
get($name string, $undefined bool): mixed { ... }
set($name string, $value mixed, $undefined bool) { ... }
call($name string, $args []mixed): mixed { ... }
// Now anything can happen - hard to understand
}
Use delete for resource cleanup. When you have resources that need explicit cleanup (files, sockets, locks), use destruct() and delete to make cleanup explicit.