はじめに
これまでの記事で、プロパティオーバーロードに関わる3つのマジックメソッド、__get()(読み取り)、__set()(書き込み)、__isset()(存在確認)を解説してきました。今回取り上げる __unset() は、この一連のグループを締めくくる最後のメソッドで、unset() によってプロパティを削除しようとした際に動的に処理を行います。
unset($obj->property) は、通常のプロパティに対しては自然に動作し、そのプロパティをオブジェクトから取り除きます。しかし、__get()/__set() によって仮想的に提供されているプロパティに対しては、__unset() を定義しない限り、削除の意図を正しく反映できません。本記事では、__unset() の基本的な使い方から、他の3つのマジックメソッドと合わせた完全な実装パターン、実践的な活用例まで詳しく解説します。
メソッド概要
| 項目 | 内容 |
|---|---|
| メソッド名 | __unset() |
| 種別 | マジックメソッド |
| シグネチャ | public function __unset(string $name): void |
| 引数 | $name — 削除しようとしたプロパティ名 |
| 呼び出しタイミング | アクセス不可能なプロパティに対して unset() が呼ばれた時 |
| 戻り値 | なし(void) |
| 対応バージョン | PHP 5.1.0以降 |
| 関連するメソッド | __get()、__set()、__isset() |
プロパティオーバーロード4兄弟の全体像(イメージ図)
アクセス不可能なプロパティ "virtualProp" に対する操作
読み取り $value = $obj->virtualProp; → __get('virtualProp')
書き込み $obj->virtualProp = 'value'; → __set('virtualProp', 'value')
存在確認 isset($obj->virtualProp); → __isset('virtualProp')
削除 unset($obj->virtualProp); → __unset('virtualProp')
★この記事の対象
この4つのマジックメソッドが揃って初めて、
仮想プロパティが「本物のプロパティ」のように
一貫した振る舞いをするようになる
ポイントは、__unset() がこの4つ組の最後のピースであるという点です。__get()、__set()、__isset() を実装していても __unset() を忘れると、unset() 操作だけが期待通りに動作しない、という不完全な実装になってしまいます。
実践サンプル7選
例1:基本的な使い方
<?php
class BasicUnsetDemo
{
private array $data = ['name' => '太郎', 'age' => 30];
public function __get(string $name): mixed
{
return $this->data[$name] ?? null;
}
public function __set(string $name, mixed $value): void
{
$this->data[$name] = $value;
}
public function __isset(string $name): bool
{
return isset($this->data[$name]);
}
public function __unset(string $name): void
{
// 内部の配列からキーを削除する
echo "__unset('{$name}') が呼ばれました" . PHP_EOL;
unset($this->data[$name]);
}
}
$demo = new BasicUnsetDemo();
var_dump(isset($demo->name)); // true
unset($demo->name);
var_dump(isset($demo->name)); // false
例2:__unset()がない場合との挙動の違いを確認するデモ
<?php
class WithoutUnset
{
private array $data = ['key' => 'value'];
public function __get(string $name): mixed
{
return $this->data[$name] ?? null;
}
public function __isset(string $name): bool
{
return isset($this->data[$name]);
}
// __unset()を定義していない
}
$obj = new WithoutUnset();
unset($obj->key); // __unset()が未定義のため何も起こらない(PHPはデフォルトの挙動にフォールバックする)
var_dump(isset($obj->key)); // 内部データは変更されず、trueのままの可能性がある
例3:削除時に関連データもクリーンアップするクラス
<?php
class CachedEntity
{
private array $data = [];
private array $cacheTimestamps = [];
public function __set(string $name, mixed $value): void
{
$this->data[$name] = $value;
$this->cacheTimestamps[$name] = time();
}
public function __get(string $name): mixed
{
return $this->data[$name] ?? null;
}
public function __isset(string $name): bool
{
return isset($this->data[$name]);
}
/**
* プロパティ削除時に、関連する付随データ(タイムスタンプ)も
* 一緒にクリーンアップする
*/
public function __unset(string $name): void
{
unset($this->data[$name], $this->cacheTimestamps[$name]);
}
public function getCacheTimestamps(): array
{
return $this->cacheTimestamps;
}
}
$entity = new CachedEntity();
$entity->title = 'サンプル';
unset($entity->title);
print_r($entity->getCacheTimestamps()); // titleのタイムスタンプも消えている
例4:削除を禁止する読み取り専用コレクション
<?php
class ProtectedCollection
{
private array $items;
public function __construct(array $items)
{
$this->items = $items;
}
public function __get(string $name): mixed
{
return $this->items[$name] ?? null;
}
public function __isset(string $name): bool
{
return isset($this->items[$name]);
}
/**
* __unset()を定義し、常に例外をスローすることで
* プロパティの削除を一切禁止する
*/
public function __unset(string $name): void
{
throw new LogicException('このコレクションの要素は削除できません');
}
}
$collection = new ProtectedCollection(['key1' => 'value1']);
try {
unset($collection->key1);
} catch (LogicException $e) {
echo 'エラー: ' . $e->getMessage() . PHP_EOL;
}
例5:削除履歴を記録する監査ログ付きコンテナ
<?php
class AuditedContainer
{
private array $data = [];
private array $auditLog = [];
public function __set(string $name, mixed $value): void
{
$this->data[$name] = $value;
}
public function __get(string $name): mixed
{
return $this->data[$name] ?? null;
}
public function __isset(string $name): bool
{
return isset($this->data[$name]);
}
/**
* 削除前の値を記録してから実際に削除する
*/
public function __unset(string $name): void
{
$this->auditLog[] = [
'action' => 'delete',
'property' => $name,
'old_value' => $this->data[$name] ?? null,
'time' => date('Y-m-d H:i:s'),
];
unset($this->data[$name]);
}
public function getAuditLog(): array
{
return $this->auditLog;
}
}
$container = new AuditedContainer();
$container->secret = '機密データ';
unset($container->secret);
print_r($container->getAuditLog());
例6:ArrayAccessと組み合わせた完全な配列風オブジェクト
<?php
class FullFeaturedBag implements ArrayAccess, Countable
{
private array $data = [];
public function __get(string $name): mixed { return $this->data[$name] ?? null; }
public function __set(string $name, mixed $value): void { $this->data[$name] = $value; }
public function __isset(string $name): bool { return isset($this->data[$name]); }
public function __unset(string $name): void { unset($this->data[$name]); }
public function offsetExists(mixed $offset): bool { return $this->__isset($offset); }
public function offsetGet(mixed $offset): mixed { return $this->__get($offset); }
public function offsetSet(mixed $offset, mixed $value): void { $this->__set($offset, $value); }
/**
* ArrayAccessのoffsetUnset()からも__unset()のロジックを再利用する
*/
public function offsetUnset(mixed $offset): void { $this->__unset($offset); }
public function count(): int { return count($this->data); }
}
$bag = new FullFeaturedBag();
$bag->a = 1;
$bag['b'] = 2;
echo count($bag) . PHP_EOL; // 2
unset($bag->a, $bag['b']);
echo count($bag) . PHP_EOL; // 0
例7:必須プロパティの削除だけを防ぐ選択的な保護
<?php
class PartiallyProtectedEntity
{
private array $data;
private array $protectedKeys = ['id', 'created_at'];
public function __construct(array $initialData)
{
$this->data = $initialData;
}
public function __get(string $name): mixed
{
return $this->data[$name] ?? null;
}
public function __set(string $name, mixed $value): void
{
$this->data[$name] = $value;
}
public function __isset(string $name): bool
{
return isset($this->data[$name]);
}
/**
* 保護対象のキー以外は自由に削除できるが、
* 重要なキーの削除だけは拒否する
*/
public function __unset(string $name): void
{
if (in_array($name, $this->protectedKeys, true)) {
throw new LogicException("'{$name}' は削除できない保護されたプロパティです");
}
unset($this->data[$name]);
}
}
$entity = new PartiallyProtectedEntity(['id' => 1, 'name' => '太郎']);
unset($entity->name); // 通常通り削除できる
try {
unset($entity->id); // 保護されているため例外になる
} catch (LogicException $e) {
echo 'エラー: ' . $e->getMessage() . PHP_EOL;
}
関連機能との比較
| 機能 | 役割 | __unset()との違い |
|---|---|---|
__unset() | アクセス不可能なプロパティの削除を処理 | 本記事の対象。unset()呼び出し時に発動する |
__get() | アクセス不可能なプロパティの読み取りを処理 | 「読み取り」時に発動する |
__set() | アクセス不可能なプロパティへの書き込みを処理 | 「書き込み」時に発動する |
__isset() | isset()/empty()の判定を処理 | 「存在確認」時に発動する |
ArrayAccess::offsetUnset() | 配列風アクセス(unset($obj['key']))での削除 | プロパティアクセス(->)ではなく配列アクセス([])が対象 |
よくある落とし穴(注意点)
__unset()を定義し忘れると削除操作が無視される__get()、__set()、__isset()を実装していても__unset()がない場合、unset($obj->property)を呼び出しても内部データが変更されず、削除が無視されたかのような挙動になります(例2を参照)。- 削除後の状態と
__isset()の整合性を保つ__unset()でデータを削除した後、__isset()が正しくfalseを返すように、同じ内部データソースを参照する実装にしておく必要があります。別々のデータ構造を使っていると、削除したはずなのにisset()がtrueのままになる、といった不整合が生じます。 - 関連データのクリーンアップ漏れに注意する プロパティに付随するキャッシュやメタデータがある場合、
__unset()内でそれらも一緒に削除しないと、不要なデータが残り続けてしまいます(例3を参照)。 - 4つのマジックメソッドの実装に一貫性を持たせる
__get()、__set()、__isset()、__unset()は、同じ内部データ構造(多くの場合は1つの連想配列)を一貫して操作するように設計するのが基本です。それぞれが異なるロジックで動作すると、予測しにくい挙動につながります。 - 削除を禁止したい場合は明確に例外をスローする 不変オブジェクトや、特定の重要なプロパティを保護したい場合、
__unset()内で黙って何もしない(サイレントに無視する)のではなく、明確な例外をスローすることで、意図しない削除の試みを呼び出し元に正しく伝えることができます(例4・例7を参照)。
まとめ
| 観点 | まとめ |
|---|---|
| 何をするメソッドか | アクセス不可能なプロパティに対してunset()が呼ばれた際に動的に処理する |
| 主な用途 | 仮想プロパティの削除サポート、関連データのクリーンアップ、削除履歴の記録、削除の禁止 |
| プロパティオーバーロードにおける位置づけ | __get()/__set()/__isset()と並ぶ、4つ組の最後のメソッド |
| 削除後の整合性 | __isset()が正しくfalseを返すよう、同じデータソースを参照すること |
| 注意点 | 定義漏れによる削除操作の無視、関連データのクリーンアップ、保護したいプロパティへの例外対応 |
__unset() は、プロパティオーバーロードの仕組みを完成させる最後のピースです。これまでの4回の記事で見てきた __get()・__set()・__isset()・__unset() は、どれか1つだけを実装するのではなく、常にセットで一貫性を持たせて実装することが、予測可能で保守しやすいクラス設計の鍵となります。
