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
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
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
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
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
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
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
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
OrderReferenceclass that validates a positive order number __toString()to format the reference asORD-000123- an invokable formatter class that accepts an
OrderReferenceand 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
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.