objects namespaces and application architecture

Magic Methods

Magic methods are special PHP methods that are called automatically by the engine in certain situations. Their names begin with double underscores, such as __construct(), __toString(), __get(), __call(), and __invoke().

Magic methods are powerful because they let objects react to language features such as construction, property access, method calls, string conversion, cloning, serialization, and debugging. They are also risky because they can hide behaviour that is not obvious from normal code.

As a junior developer, you should recognise common magic methods, understand when frameworks use them, and avoid using them to make unclear APIs.

Constructors Are Magic Methods

The most common magic method is __construct(). PHP calls it when a new object is created.

PHP example
<?php

declare(strict_types=1);

final class EmailAddress
{
    public function __construct(
        public readonly string $value,
    ) {
        if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException('Email address is not valid.');
        }
    }
}

$email = new EmailAddress('ada@example.com');

echo $email->value . PHP_EOL;

// Prints:
// ada@example.com

Constructors are a good place to require valid initial state. An object should not be created in a broken state and fixed later if that can be avoided.

String Conversion With __toString()

__toString() is called when an object is used as a string.

PHP example
<?php

declare(strict_types=1);

final readonly class OrderReference
{
    public function __construct(
        private int $number,
    ) {
        if ($number <= 0) {
            throw new InvalidArgumentException('Order number must be positive.');
        }
    }

    public function __toString(): string
    {
        return 'ORD-' . str_pad((string) $this->number, 6, '0', STR_PAD_LEFT);
    }
}

$reference = new OrderReference(42);

echo $reference . PHP_EOL;

// Prints:
// ORD-000042

Use __toString() for safe, unsurprising string representations. Do not make it run database queries, send requests, or perform expensive work.

Dynamic Property Access With __get() And __set()

__get() is called when reading an inaccessible or missing property. __set() is called when writing one.

Frameworks and ORMs sometimes use these methods for model attributes, but they can make code harder to understand because properties appear to exist even when they are not declared.

PHP example
<?php

declare(strict_types=1);

final class AttributeBag
{
    /** @param array<string, mixed> $attributes */
    public function __construct(
        private array $attributes = [],
    ) {
    }

    public function __get(string $name): mixed
    {
        if (!array_key_exists($name, $this->attributes)) {
            throw new OutOfBoundsException('Unknown attribute: ' . $name);
        }

        return $this->attributes[$name];
    }

    public function __set(string $name, mixed $value): void
    {
        $this->attributes[$name] = $value;
    }
}

$bag = new AttributeBag(['name' => 'Ada']);
$bag->role = 'admin';

echo $bag->name . ' is ' . $bag->role . PHP_EOL;

// Prints:
// Ada is admin

If you control the class, prefer real typed properties and methods unless dynamic access is genuinely useful.

Dynamic Method Calls With __call()

__call() is called when code tries to call an inaccessible or missing instance method. __callStatic() does the same for static calls.

This is common in fluent query builders, proxy objects, and some framework facades. It can be useful, but it makes static analysis, autocomplete, and refactoring harder.

PHP example
<?php

declare(strict_types=1);

final class CommandRecorder
{
    /** @var list<string> */
    private array $commands = [];

    /** @param list<mixed> $arguments */
    public function __call(string $name, array $arguments): self
    {
        $this->commands[] = $name . '(' . count($arguments) . ')';

        return $this;
    }

    /** @return list<string> */
    public function commands(): array
    {
        return $this->commands;
    }
}

$recorder = new CommandRecorder();
$recorder->where('status', 'paid')->orderBy('created_at');

echo implode(', ', $recorder->commands()) . PHP_EOL;

// Prints:
// where(2), orderBy(1)

For normal application services, explicit methods are better. A missing method should usually be an error, not an invitation to guess behaviour.

Invokable Objects With __invoke()

__invoke() lets an object be called like a function. It is common for single-action classes, handlers, middleware, validators, and callbacks.

PHP example
<?php

declare(strict_types=1);

final class Slugify
{
    public function __invoke(string $title): string
    {
        $slug = strtolower(trim($title));
        $slug = preg_replace('/[^a-z0-9]+/', '-', $slug) ?? '';

        return trim($slug, '-');
    }
}

$slugify = new Slugify();

echo $slugify('PHP Magic Methods') . PHP_EOL;

// Prints:
// php-magic-methods

__invoke() is clearest when the class has one obvious job.

Debugging With __debugInfo()

__debugInfo() controls what appears when an object is dumped with var_dump().

PHP example
<?php

declare(strict_types=1);

final class ApiToken
{
    public function __construct(
        private string $token,
    ) {
    }

    /** @return array<string, string> */
    public function __debugInfo(): array
    {
        return ['token' => '[hidden]'];
    }
}

var_dump(new ApiToken('secret-token'));

This is useful for hiding secrets during debugging. It does not replace proper secret handling in logs and errors.

Serialization With __serialize() And __unserialize()

__serialize() and __unserialize() control how an object is serialized and restored.

PHP example
<?php

declare(strict_types=1);

final class SessionUser
{
    public function __construct(
        private int $id,
        private string $email,
        private string $temporaryToken,
    ) {
    }

    /** @return array{id: int, email: string} */
    public function __serialize(): array
    {
        return [
            'id' => $this->id,
            'email' => $this->email,
        ];
    }

    /** @param array{id: int, email: string} $data */
    public function __unserialize(array $data): void
    {
        $this->id = $data['id'];
        $this->email = $data['email'];
        $this->temporaryToken = '';
    }
}

$serialized = serialize(new SessionUser(10, 'ada@example.com', 'temporary'));

echo str_contains($serialized, 'temporary') ? 'leaked' : 'hidden';
echo PHP_EOL;

// Prints:
// hidden

Only serialize objects deliberately. Serialized data can become difficult to change when class names and property structures evolve.

When Magic Methods Are A Problem

Magic methods can make code feel convenient at first and confusing later. Watch for:

  • dynamic properties where real typed properties would be clearer
  • __call() hiding misspelled method names
  • side effects inside __toString()
  • serialized objects stored long-term without versioning
  • framework magic being used in domain logic
  • objects that are hard to inspect because behaviour is hidden behind magic

Frameworks sometimes use magic methods well because they provide documentation, conventions, and tooling. Your own domain classes should usually be more explicit.

What You Should Be Able To Do

After this lesson, you should be able to recognise common magic methods and explain when PHP calls them. You should be able to use __construct(), __toString(), and __invoke() safely, and be cautious with __get(), __set(), and __call().

For junior work, the practical skill is reading framework code without being surprised by magic, while keeping your own application code clear and explicit.

Practice

Practice: Build Safe Magic Methods

Create a small PHP example using __construct(), __toString(), and __invoke().

Task

Build:

  • an OrderReference class that validates a positive order number
  • __toString() to format the reference as ORD-000123
  • an invokable formatter class that accepts an OrderReference and returns a display message

Use strict types. Keep the expected output in the PHP code block as printed lines or comments.

Check Your Work

Run cases for:

  • a valid order reference
  • an invalid order number
  • calling the formatter object like a function

Afterward, explain why these magic methods are safe and predictable.

Show solution
PHP example
<?php

declare(strict_types=1);

final readonly class OrderReference
{
    public function __construct(
        private int $number,
    ) {
        if ($number <= 0) {
            throw new InvalidArgumentException('Order number must be positive.');
        }
    }

    public function __toString(): string
    {
        return 'ORD-' . str_pad((string) $this->number, 6, '0', STR_PAD_LEFT);
    }
}

final class FormatOrderReference
{
    public function __invoke(OrderReference $reference): string
    {
        return 'Order reference: ' . $reference;
    }
}

$reference = new OrderReference(123);
$formatter = new FormatOrderReference();

echo $reference . PHP_EOL;
echo $formatter($reference) . PHP_EOL;

try {
    new OrderReference(0);
} catch (InvalidArgumentException $exception) {
    echo $exception->getMessage() . PHP_EOL;
}

// Prints:
// ORD-000123
// Order reference: ORD-000123
// Order number must be positive.

These magic methods are predictable because they do small, local work. The constructor validates state, __toString() returns a string without side effects, and __invoke() makes sense because the formatter has one obvious job.