[PHP]__get()とは?アクセスできないプロパティの読み取りを動的に処理する方法を徹底解説

PHP

はじめに

前回までの記事で、未定義のメソッド呼び出しを動的に処理する __call() と __callStatic() を解説してきました。今回取り上げる __get() は、それと同じ発想をプロパティの読み取りに適用したマジックメソッドです。

$obj->undefinedProperty のように、オブジェクトに存在しない(あるいは private/protected でアクセスできない)プロパティを読み取ろうとすると、通常は警告(Warning)や未定義プロパティエラーが発生します。しかし、クラスに __get() が定義されていると、その読み取り操作が自動的に __get() に転送され、開発者が自由に値を返す処理を記述できます。遅延読み込み(レイジーロード)、読み取り専用プロパティの提供、プロパティ名の別名対応など、さまざまな用途に活用できる便利な仕組みです。本記事では基本的な使い方から、__set() との連携、注意点まで詳しく解説します。


メソッド概要

項目内容
メソッド名__get()
種別マジックメソッド
シグネチャpublic function __get(string $name): mixed
引数$name — 読み取ろうとしたプロパティ名
呼び出しタイミングアクセス不可能(未定義、またはprivate/protected)なプロパティを読み取ろうとした時
戻り値任意(プロパティの値として呼び出し元に返したい値)
対応バージョンPHP 5.0以降
対になるメソッド__set()(書き込み時)、__isset()(isset()判定時)、__unset()(unset()時)

呼び出しの仕組み(イメージ図)

  $value = $obj->someProperty;
              │
              ▼
  ┌───────────────────────────┐
  │ PHPがsomePropertyという           │
  │ アクセス可能なプロパティが存在するか確認  │
  └───────────┬───────────────┘
              ▼
   ┌──────────┴──────────────┐
   │ 存在し、アクセス可能        │ 存在しない、またはprivate/protected
   ▼                          ▼
  通常通りその値を返す           __get('someProperty') が自動的に呼ばれる
                              ★この記事の対象
                              戻り値が、そのまま$valueに代入される

ポイントは、__get() がプロパティ名を文字列として受け取り、任意の値を返せるという点です。これにより、実際にはプロパティとして存在しない値を、あたかも通常のプロパティのように読み取れる「仮想プロパティ」を実現できます。


実践サンプル7選

例1:基本的な使い方

<?php

class BasicGetDemo
{
    private array $data = [
        'name' => '太郎',
        'age'  => 30,
    ];

    public function __get(string $name): mixed
    {
        // アクセスできないプロパティの読み取りを、内部の配列から返す
        echo "__get('{$name}') が呼ばれました" . PHP_EOL;
        return $this->data[$name] ?? null;
    }
}

$demo = new BasicGetDemo();
echo $demo->name . PHP_EOL;
echo $demo->age . PHP_EOL;

例2:privateプロパティへの読み取り専用アクセスを提供するクラス

<?php

class ReadOnlyUser
{
    private string $name;
    private string $email;

    public function __construct(string $name, string $email)
    {
        $this->name = $name;
        $this->email = $email;
    }

    /**
     * privateプロパティを外部から読み取れるようにしつつ、
     * __set()を定義しないことで書き込みを不可能にする
     */
    public function __get(string $name): mixed
    {
        if (property_exists($this, $name)) {
            return $this->$name;
        }

        throw new OutOfRangeException("プロパティ '{$name}' は存在しません");
    }
}

$user = new ReadOnlyUser('花子', 'hanako@example.com');
echo $user->name . PHP_EOL;
echo $user->email . PHP_EOL;

例3:遅延読み込み(レイジーロード)を実現するクラス

<?php

class LazyLoadedArticle
{
    private array $loaded = [];

    public function __construct(private int $id)
    {
    }

    /**
     * 重い処理(DBアクセス等)を、実際にプロパティが
     * 初めて読み取られるまで遅延させる
     */
    public function __get(string $name): mixed
    {
        if (!isset($this->loaded[$name])) {
            echo "{$name} を読み込み中..." . PHP_EOL;
            $this->loaded[$name] = $this->loadFromDatabase($name);
        }

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

    private function loadFromDatabase(string $field): string
    {
        // 実際にはデータベースアクセス等が入る想定
        return "記事{$this->id}の{$field}の内容";
    }
}

$article = new LazyLoadedArticle(1);
echo $article->title . PHP_EOL;   // 初回は読み込みが発生
echo $article->title . PHP_EOL;   // 2回目はキャッシュから返される

例4:計算済みの仮想プロパティを提供するクラス

<?php

class Rectangle
{
    public function __construct(
        private float $width,
        private float $height
    ) {
    }

    /**
     * areaやperimeterのような、実際にはプロパティとして
     * 保持していない値を、読み取り時に計算して返す
     */
    public function __get(string $name): float
    {
        return match ($name) {
            'width'     => $this->width,
            'height'    => $this->height,
            'area'      => $this->width * $this->height,
            'perimeter' => 2 * ($this->width + $this->height),
            default     => throw new OutOfRangeException("プロパティ '{$name}' は存在しません"),
        };
    }
}

$rect = new Rectangle(5, 3);
echo "面積: {$rect->area}, 周長: {$rect->perimeter}" . PHP_EOL;

例5:__get()と__set()を組み合わせた完全なプロパティ制御

<?php

class ControlledProperties
{
    private array $data = [];

    public function __get(string $name): mixed
    {
        return $this->data[$name] ?? null;
    }

    /**
     * __get()とセットで__set()を定義することで、
     * 読み取りと書き込みの両方を制御できる
     */
    public function __set(string $name, mixed $value): void
    {
        $this->data[$name] = $value;
    }

    public function __isset(string $name): bool
    {
        return isset($this->data[$name]);
    }
}

$obj = new ControlledProperties();
$obj->title = 'サンプル';
echo $obj->title . PHP_EOL;
var_dump(isset($obj->title));
var_dump(isset($obj->nonexistent));

例6:スネークケースとキャメルケースの別名対応を提供するクラス

<?php

class FlexibleNamingModel
{
    private array $attributes = [
        'first_name' => '太郎',
        'last_name'  => '山田',
    ];

    /**
     * $model->firstName と $model->first_name の
     * どちらの書き方でも同じ値を取得できるようにする
     */
    public function __get(string $name): mixed
    {
        $snakeCase = strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $name));

        return $this->attributes[$snakeCase] ?? $this->attributes[$name] ?? null;
    }
}

$model = new FlexibleNamingModel();
echo $model->firstName . PHP_EOL;   // キャメルケースでアクセス
echo $model->first_name . PHP_EOL;  // スネークケースでもアクセス可能

例7:存在しないプロパティへのアクセスを検知するデバッグ用クラス

<?php

class StrictPropertyAccess
{
    public string $definedProperty = '定義済みの値';

    /**
     * タイプミスなどによる未定義プロパティへのアクセスを、
     * サイレントに失敗させず明確に例外として通知する
     */
    public function __get(string $name): never
    {
        throw new OutOfBoundsException(
            sprintf('未定義のプロパティ %s::$%s へアクセスしようとしました', static::class, $name)
        );
    }
}

$obj = new StrictPropertyAccess();
echo $obj->definedProperty . PHP_EOL; // 通常のアクセス(__get()は呼ばれない)

try {
    echo $obj->typoProperty; // タイプミス → __get()が発動し例外がスローされる
} catch (OutOfBoundsException $e) {
    echo 'エラー: ' . $e->getMessage() . PHP_EOL;
}

関連機能との比較

機能役割__get()との違い
__get()アクセス不可能なプロパティの読み取りを処理本記事の対象。プロパティの「読み取り」時に発動する
__set()アクセス不可能なプロパティへの書き込みを処理「書き込み」時に発動する、対になるマジックメソッド
__isset()isset()/empty()の判定を処理プロパティの「存在確認」時に発動する
__unset()unset()による削除を処理プロパティの「削除」時に発動する
__call()未定義メソッドの呼び出しを処理プロパティではなく、メソッド呼び出しを対象とする(別記事で解説済み)

よくある落とし穴(注意点)

  1. __get()だけ定義してもisset()は正しく動作しない __get() を定義しても、isset($obj->property) の判定はデフォルトでは false を返します。isset() に対応させたい場合は、別途 __isset() も定義する必要があります(例5を参照)。
  2. 配列プロパティへの直接的な変更が反映されない __get() は値のコピーを返す(参照渡しではない)ため、$obj->arrayProperty[] = 'value' のような配列への直接的な追加は、意図通りに動作しません。このような操作が必要な場合は、__get() を &__get() のように参照返しにするか、専用の追加メソッドを用意する必要があります。
  3. 無限再帰に注意する __get() 内で、同じ未定義プロパティに再びアクセスしようとすると、無限再帰に陥る可能性があります。PHPは同一プロパティ名への再帰呼び出しを検知して防ぐ仕組みを持っていますが、意図しない挙動の原因になり得るため、内部データは別のプロパティ(例では $data 配列)に格納するのが安全です。
  4. IDEの補完やコード解析ツールが効きにくくなる __call() などと同様に、動的に提供されるプロパティはIDEに認識されません。PHPDocの @property アノテーションを使って補完情報を提供する工夫が推奨されます。
  5. 明示的なgetterメソッドの方が意図が伝わりやすい場合が多い __get() は柔軟ですが、コードを読む人にとって「このクラスにはどんなプロパティがあるのか」が分かりにくくなります。単純なアクセスであれば、明示的な getName() のようなメソッドや、PHP 8.1以降の readonly プロパティの方が、意図が伝わりやすく保守しやすい選択肢です。

まとめ

観点まとめ
何をするメソッドか存在しない、またはアクセスできないプロパティが読み取られた際に動的に処理する
主な用途遅延読み込み、読み取り専用アクセスの提供、計算済み仮想プロパティ、別名対応
連携するメソッド__set()(書き込み)、__isset()(存在確認)、__unset()(削除)
発動条件未定義、またはアクセス権限のないプロパティの読み取り時
注意点isset()との連携には__isset()が必要、配列プロパティの直接変更の制約、IDE補完の効きにくさ

__get() は、プロパティアクセスを動的に制御できる便利な仕組みですが、その柔軟性ゆえにコードの見通しを悪くするリスクも持っています。遅延読み込みや仮想プロパティなど、__get() ならではのメリットが活きる場面に絞って活用し、単純なケースでは明示的なgetterや readonly プロパティを選ぶという使い分けを心がけましょう。

タイトルとURLをコピーしました