PHP: Magic Methods and Enums
PHP has a set of "magic methods" that start with
__—you never call them explicitly; PHP triggers them automatically at specific moments. They make objects behave more intelligently, like built-in types. Add PHP 8.1 enums to the mix, and this lesson takes your PHP skills to the next level.
1. __toString() — Turning an Object into a String
PHP
<?php
class User {
public function __construct(
public string $name,
public string $email
) {}
// Automatically called when the object is echoed
public function __toString(): string {
return "{$this->name} <{$this->email}>";
}
}
$user = new User("John", "john@example.com");
echo $user; // John <john@example.com>
// No need to write $user->name—just echo the object directly
?>
💡 Tip:
__toString() lets you define "how this object should be represented as a string." Use it for logging, debugging output, and template rendering.
2. __get() and __set() — Accessing Non-Existent Properties
PHP
<?php
class UserModel {
private array $data = [];
private array $changes = [];
// Called when reading a non-existent property
public function __get(string $name): mixed {
return $this->data[$name] ?? null;
}
// Called when assigning to a non-existent property
public function __set(string $name, mixed $value): void {
$this->changes[$name] = true;
$this->data[$name] = $value;
}
public function getChanges(): array {
return array_keys($this->changes);
}
}
$user = new UserModel();
$user->name = "John"; // Triggers __set()
$user->email = "j@j.com"; // Triggers __set()
echo $user->name; // John — triggers __get()
echo $user->name; // John
print_r($user->getChanges()); // ['name', 'email'] — track which fields were modified
?>
(1) __isset() and __unset()
PHP
<?php
class Config {
private array $data = ['app_name' => 'MyApp'];
public function __isset(string $name): bool {
return isset($this->data[$name]);
}
public function __unset(string $name): void {
unset($this->data[$name]);
}
}
$c = new Config();
var_dump(isset($c->app_name)); // true
var_dump(isset($c->missing)); // false
unset($c->app_name);
var_dump(isset($c->app_name)); // false
?>
3. __call() — Calling Non-Existent Methods
▶ サンプル: Dynamic Query Builder
PHP
<?php
class QueryBuilder {
private array $where = [];
private ?string $orderBy = null;
// Triggered when calling a non-existent method
public function __call(string $name, array $arguments): static {
if (str_starts_with($name, 'whereBy')) {
// whereByName("John") → WHERE name = "John"
$field = lcfirst(substr($name, 7));
$this->where[] = "{$field} = '{$arguments[0]}'";
} elseif ($name === 'orderByDesc') {
$this->orderBy = "{$arguments[0]} DESC";
}
return $this;
}
public function toSQL(): string {
$sql = "SELECT * FROM table";
if ($this->where) {
$sql .= " WHERE " . implode(" AND ", $this->where);
}
if ($this->orderBy) {
$sql .= " ORDER BY {$this->orderBy}";
}
return $sql;
}
}
$q = new QueryBuilder();
$q->whereByName("John")
->whereByStatus("active")
->orderByDesc("created_at");
echo $q->toSQL();
// SELECT * FROM table WHERE name = 'John' AND status = 'active' ORDER BY created_at DESC
?>
💡 Tip: Frameworks like Laravel's Eloquent make heavy use of
__call() to implement dynamic methods. Methods like whereByName() that look like they exist but aren't actually defined are powered by it.
4. __clone() — When an Object Is Cloned
PHP
<?php
class ShoppingCart {
private array $items = [];
public function __construct(
public string $owner
) {}
public function addItem(string $name, int $qty): void {
$this->items[] = compact('name', 'qty');
}
public function getItems(): array {
return $this->items;
}
// Automatically called when cloned
public function __clone(): void {
$this->owner = "Copy of {$this->owner}";
// The items array is shallow-copied by default, but you can do deep copies here
}
}
$cart1 = new ShoppingCart("John");
$cart1->addItem("PHP Tutorial", 2);
$cart2 = clone $cart1;
echo $cart2->owner; // Copy of John
// items are also copied
print_r($cart2->getItems()); // Has the PHP Tutorial item
?>
5. __sleep() and __wakeup() — Serialization
PHP
<?php
class DatabaseConnection {
private $connection;
public function __construct(
private string $dsn,
private string $user,
private string $pass
) {
$this->connect();
}
private function connect(): void {
$this->connection = "connected({$this->dsn})";
}
// Called on serialize(): return which property names to serialize
public function __sleep(): array {
// Don't serialize $connection (resource types can't be serialized)
return ['dsn', 'user', 'pass'];
}
// Called on unserialize(): re-establish the connection
public function __wakeup(): void {
$this->connect();
}
}
?>
6. Enums (PHP 8.1)
Writing "pending" "approved" "rejected" in functions is error-prone and hard to track. Enums are your best friend:
PHP
<?php
enum OrderStatus: string {
case PENDING = 'pending';
case APPROVED = 'approved';
case SHIPPED = 'shipped';
case DELIVERED = 'delivered';
case CANCELLED = 'cancelled';
}
// Using enums
function updateOrder(int $id, OrderStatus $status): void {
echo "Order {$id} updated to {$status->value}<br>";
}
updateOrder(1, OrderStatus::APPROVED); // ✅
// updateOrder(1, "approved"); // ❌ TypeError!
// updateOrder(1, "approvved"); // ❌ Hard to even typo this
?>
▶ サンプル: Enums + match — The Power Duo
PHP
<?php
enum UserRole: string {
case ADMIN = 'admin';
case EDITOR = 'editor';
case MEMBER = 'member';
case GUEST = 'guest';
// Enum methods
public function label(): string {
return match($this) {
self::ADMIN => 'Administrator',
self::EDITOR => 'Editor',
self::MEMBER => 'Member',
self::GUEST => 'Guest',
};
}
public function canEdit(): bool {
return match($this) {
self::ADMIN, self::EDITOR => true,
default => false,
};
}
}
$role = UserRole::ADMIN;
echo $role->label(); // Administrator
echo $role->canEdit() ? 'Can edit' : 'Cannot edit'; // Can edit
echo $role->value; // admin
?>
(1) Enum Methods
PHP
<?php
// UserRole::cases() — get all enum values
print_r(array_map(fn($r) => $r->value, UserRole::cases()));
// ['admin', 'editor', 'member', 'guest']
// UserRole::from() — create enum from value (throws exception on mismatch)
$role = UserRole::from('admin'); // UserRole::ADMIN
// $role = UserRole::from('super'); // ValueError
// UserRole::tryFrom() — safe creation (returns null on mismatch)
$role = UserRole::tryFrom('super'); // null
?>
💡 Tip: PHP 8.1+: use enums whenever your data has a fixed set of possible values.
OrderStatus::class has IDE autocompletion—"pending" doesn't. That's the difference.
❓ よくある質問
Q Do
__get and __set hurt performance?A There's a small overhead since every access goes through a function call. For high-volume data operations (millions of records), explicit public/private properties are faster. For flexible scenarios like ORMs/Models,
__get/__set is perfect.Q When should I use enums vs. class constants?
A PHP 8.1+: prefer enums—they give you type safety (passing the wrong value throws an error), autocompletion, and match-friendly exhaustiveness checking. Class constants are better for "just a set of related config values."
Q Can enum classes define methods?
A Absolutely! The
label() and canEdit() examples above prove it. Enums upgrade your code from "passing strings around" to "passing types with behavior."📖 まとめ
__toString()defines how an object converts to a string__get()__set()intercept access to non-existent properties__call()captures calls to dynamic, non-existent methods__clone()triggers onclone $obj, letting you do deep copies__sleep()/__wakeup()control serialization- PHP 8.1 enums
enum X: string { case A = 'a'; }provide type safety cases()/from()/tryFrom()are essential enum methods- Enums +
match{}is the power duo
📝 練習問題
- Write a
DynamicConfigclass that uses__get/__set/__isset/__unsetto let you "operate on a config array as if it had properties." - Write an
OrderStatusenum (Pending→Paid→Shipped→Delivered→Cancelled). Give each status acanTransitionTo(OrderStatus $target)method that defines allowed state transitions. - Write a simple ORM class that uses
__call()to implement dynamic query methods likewhereByFieldName($value).