はじめに
これまでの記事で、プロパティの「読み取り」を処理する __get() と、「書き込み」を処理する __set() を解説してきました。今回取り上げる __isset() は、isset() 関数や empty() 関数でプロパティの存在確認を行おうとした際に動的に処理を行う、3つ目のマジックメソッドです。
isset($obj->property) や empty($obj->property) は、通常のプロパティに対しては自然に動作しますが、__get() によって仮想的に提供されているプロパティに対しては、デフォルトでは正しく判定できません。この問題を解決するのが __isset() です。__get() と組み合わせて使うことで、仮想プロパティに対しても自然な isset() の挙動を実現できます。本記事では基本的な使い方から、empty() との関係、実践的な活用例まで詳しく解説します。
メソッド概要
| 項目 | 内容 |
|---|---|
| メソッド名 | __isset() |
| 種別 | マジックメソッド |
| シグネチャ | public function __isset(string $name): bool |
| 引数 | $name — 存在確認しようとしたプロパティ名 |
| 呼び出しタイミング | アクセス不可能なプロパティに対して isset() または empty() が呼ばれた時 |
| 戻り値 | プロパティが「存在する」とみなす場合は true |
| 対応バージョン | PHP 5.1.0以降 |
| 関連するメソッド | __get()、__set()、__unset() |
__get()だけでは不十分な理由(イメージ図)
class VirtualProps {
public function __get($name) {
return $name === 'title' ? 'サンプル' : null;
}
// __isset()を定義していない場合...
}
$obj = new VirtualProps();
isset($obj->title);
│
▼
┌───────────────────────────┐
│ __isset()が未定義の場合: │
│ PHPは__get()を呼び出さずに │
│ 常に false を返してしまう │
│ (実際には値が取得できるのに!) │
└───────────────────────────┘
┌───────────────────────────┐
│ __isset()を定義した場合: │
│ __isset('title') が呼ばれ、 │
│ 明示的にtrue/falseを制御できる │
│ ★この記事の対象 │
└───────────────────────────┘
ポイントは、isset() はデフォルトでは __get() を呼び出さないという点です。__get() だけを定義していても isset() の判定には使われないため、仮想プロパティに対して正しい isset() の挙動を実現したい場合、__isset() の実装が必須になります。
実践サンプル7選
例1:基本的な使い方
<?php
class BasicIssetDemo
{
private array $data = ['name' => '太郎', 'age' => null];
public function __get(string $name): mixed
{
return $this->data[$name] ?? null;
}
public function __isset(string $name): bool
{
// 配列のキーとして存在するかどうかを判定する
echo "__isset('{$name}') が呼ばれました" . PHP_EOL;
return isset($this->data[$name]);
}
}
$demo = new BasicIssetDemo();
var_dump(isset($demo->name)); // true
var_dump(isset($demo->age)); // false(値がnullのため)
var_dump(isset($demo->nonexistent)); // false
例2:__isset()がない場合との挙動の違いを確認するデモ
<?php
class WithoutIsset
{
public function __get(string $name): mixed
{
return $name === 'virtual' ? '仮想の値' : null;
}
// __isset()を定義していない
}
class WithIsset
{
public function __get(string $name): mixed
{
return $name === 'virtual' ? '仮想の値' : null;
}
public function __isset(string $name): bool
{
return $name === 'virtual';
}
}
$a = new WithoutIsset();
$b = new WithIsset();
var_dump(isset($a->virtual)); // false(__get()の結果を反映していない)
var_dump(isset($b->virtual)); // true(__isset()が正しく判定している)
例3:キーの存在確認を伴うデータコンテナクラス
<?php
class DataContainer
{
private array $storage = [];
public function set(string $key, mixed $value): void
{
$this->storage[$key] = $value;
}
public function __get(string $name): mixed
{
return $this->storage[$name] ?? null;
}
public function __set(string $name, mixed $value): void
{
$this->storage[$name] = $value;
}
/**
* array_key_exists()を使うことで、
* 値がnullの場合とキー自体が存在しない場合を区別する
*/
public function __isset(string $name): bool
{
return array_key_exists($name, $this->storage);
}
public function __unset(string $name): void
{
unset($this->storage[$name]);
}
}
$container = new DataContainer();
$container->nullable = null;
var_dump(isset($container->nullable)); // false(isset()の仕様上nullはfalse扱い)
var_dump(array_key_exists('nullable', (array)$container)); // キー自体は存在する
例4:empty()との組み合わせで有効/無効フラグを判定するクラス
<?php
class FeatureFlags
{
private array $flags = ['new_ui' => true, 'beta_mode' => false];
public function __get(string $name): mixed
{
return $this->flags[$name] ?? null;
}
/**
* __isset()はempty()の判定にも使われる
* empty($obj->prop)は、__isset()がfalseを返すか、
* __get()が返した値がfalsyである場合にtrueとなる
*/
public function __isset(string $name): bool
{
return isset($this->flags[$name]);
}
}
$flags = new FeatureFlags();
var_dump(empty($flags->beta_mode)); // true(存在はするが値がfalse)
var_dump(empty($flags->new_ui)); // false(存在し、値もtrue)
var_dump(empty($flags->unknown)); // true(そもそも存在しない)
例5:キャッシュの存在確認を遅延読み込みと組み合わせるクラス
<?php
class LazyCacheEntity
{
private array $cache = [];
private array $available = ['profile', 'settings'];
public function __get(string $name): mixed
{
if (!isset($this->cache[$name]) && in_array($name, $this->available, true)) {
echo "{$name} を読み込み中..." . PHP_EOL;
$this->cache[$name] = "{$name}のデータ";
}
return $this->cache[$name] ?? null;
}
/**
* 実際に読み込まなくても、
* "読み込み可能かどうか"だけを事前に判定できる
*/
public function __isset(string $name): bool
{
return in_array($name, $this->available, true);
}
}
$entity = new LazyCacheEntity();
var_dump(isset($entity->profile)); // trueだが、まだ読み込み処理は発生しない
echo $entity->profile . PHP_EOL; // ここで初めて読み込みが発生する
例6:ArrayAccessインターフェースと組み合わせた併用パターン
<?php
class HybridAccessObject implements ArrayAccess
{
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]);
}
// ArrayAccessインターフェースの実装(配列風アクセスにも対応)
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);
}
public function offsetUnset(mixed $offset): void
{
$this->__unset($offset);
}
}
$obj = new HybridAccessObject();
$obj->name = '太郎'; // プロパティとしてのアクセス
var_dump(isset($obj->name)); // __isset()経由
var_dump(isset($obj['name'])); // offsetExists()経由(内部で__isset()を再利用)
例7:テスト容易性を考慮した存在確認ロジックの分離
<?php
class TestableContainer
{
private array $items = [];
public function has(string $key): bool
{
// 実際の判定ロジックは独立したpublicメソッドに切り出す
return array_key_exists($key, $this->items) && $this->items[$key] !== null;
}
public function __get(string $name): mixed
{
return $this->items[$name] ?? null;
}
public function __set(string $name, mixed $value): void
{
$this->items[$name] = $value;
}
/**
* __isset()はhas()メソッドに処理を委譲することで、
* ロジックの重複を避けつつユニットテストもしやすくする
*/
public function __isset(string $name): bool
{
return $this->has($name);
}
}
$container = new TestableContainer();
$container->key1 = 'value';
var_dump($container->has('key1')); // 通常のメソッドとして直接テスト可能
var_dump(isset($container->key1)); // マジックメソッド経由でも同じ結果
関連機能との比較
| 機能 | 役割 | __isset()との違い |
|---|---|---|
__isset() | アクセス不可能なプロパティの存在確認を処理 | 本記事の対象。isset()/empty()呼び出し時に発動する |
__get() | アクセス不可能なプロパティの読み取りを処理 | 「読み取り」時に発動する。isset()判定には単独では使われない |
__set() | アクセス不可能なプロパティへの書き込みを処理 | 「書き込み」時に発動する |
__unset() | unset()による削除を処理 | プロパティの「削除」時に発動する、対になるメソッド |
ArrayAccess::offsetExists() | 配列風アクセス($obj['key'])での存在確認 | プロパティアクセス(->)ではなく配列アクセス([])が対象 |
よくある落とし穴(注意点)
__get()だけ定義して__isset()を忘れるとisset()が常にfalseを返す これがこの関数の最も重要な注意点です。仮想プロパティを提供する場合、読み取り用の__get()だけでなく、存在確認用の__isset()もセットで実装しなければ、isset()やempty()が意図通りに動作しません(例2を参照)。isset()はnull値を「存在しない」とみなす PHPのisset()の仕様上、値がnullの場合はfalseを返します。__isset()内でisset($this->data[$name])を使うと、この仕様が引き継がれます。値がnullであっても「キー自体は存在する」ことを区別したい場合は、array_key_exists()を使う必要があります(例3を参照)。empty()は__isset()と__get()の両方に影響されるempty($obj->prop)は、まず__isset()がfalseを返すか、__isset()がtrueを返した場合でも__get()の返す値がfalsy(0、''、falseなど)であればtrueになります。この二段階の評価を理解しておく必要があります(例4を参照)。- 存在確認のためだけに重い処理を実行してしまう
__isset()の実装が、実際のデータ取得と同じ重い処理(DBアクセスなど)を行ってしまうと、単なる存在確認のたびに無駄なコストが発生します。可能な限り、存在確認は軽量な判定にとどめる設計が望ましいです(例5を参照)。 unset()との組み合わせを忘れる プロパティの削除もサポートしたい場合、__isset()や__get()/__set()に加えて__unset()も実装する必要があります。4つのメソッドは一貫性を持ってセットで実装することが推奨されます。
まとめ
| 観点 | まとめ |
|---|---|
| 何をするメソッドか | アクセス不可能なプロパティに対するisset()/empty()の判定を動的に処理する |
| 主な用途 | 仮想プロパティへのisset()対応、データコンテナの存在確認、ArrayAccessとの連携 |
| 連携が必須なメソッド | __get()(これがないと存在確認だけしても意味がない) |
isset()の仕様 | null値は「存在しない」扱いになる点を理解しておく必要がある |
| 注意点 | __get()との対での実装、empty()の二段階評価、重い処理を避けること |
__isset() は、__get() によって提供される仮想プロパティに対して、自然な isset()/empty() の挙動を実現するために欠かせないマジックメソッドです。4つのプロパティオーバーロード用マジックメソッド(__get()、__set()、__isset()、__unset())は、基本的にセットで実装することを心がけ、一貫性のある振る舞いを提供しましょう。
