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: $this is less than $other
  • Zero: $this equals $other
  • Positive: $this is 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.

Copyright (c) 2025 Ocean Softworks, Sharkk