15.

PHP のコンストラクタ|__construct とプロパティ昇格・readonly

編集
この記事の要点
  • コンストラクタの名前は __construct(アンダースコア 2 個)。クラス名と同じ名前は使わない
  • PHP 8 のコンストラクタプロパティ昇格で、宣言と代入を 1 行にまとめられる
  • オーバーロードはできない。作り方が複数あるなら static のファクトリメソッドにする
  • 継承したら parent::__construct() を自分で呼ぶ。自動では呼ばれない
  • コンストラクタで DB 接続や通信をしない。受け取るだけにするとテストしやすい

基本

class User
{
    public string $name;
    public int $age;

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

$u = new User('田中', 30);
echo $u->name;        // 田中

$u2 = new User('鈴木');           // 既定値を使う
$u3 = new User(age: 25, name: '佐藤');   // 名前付き引数(PHP 8 以降)

PHP 4 時代の「クラス名と同じ名前のメソッド」は PHP 8 で削除されました。__construct 以外はただのメソッドとして扱われます。

コンストラクタプロパティ昇格(PHP 8)

// 従来: 同じ名前を 3 回書く
class User
{
    private string $name;
    public function __construct(string $name) { $this->name = $name; }
}

// PHP 8: 引数に可視性を書くだけでプロパティになる
class User
{
    public function __construct(
        private string $name,
        private int $age = 0,
    ) {}
}

// readonly を付ければ生成後に変更できない(PHP 8.1 以降)
class Point
{
    public function __construct(
        public readonly int $x,
        public readonly int $y,
    ) {}
}

$p = new Point(1, 2);
echo $p->x;      // 1
// $p->x = 5;    // Error: Cannot modify readonly property Point::$x

値を保持するだけのクラスは、この書き方で数行に収まります。値オブジェクトには readonly を付けて、あとから壊されないようにしておくと安全です。

オーバーロードはできない

class Point
{
    public function __construct(int $x, int $y) {}
    public function __construct(string $text) {}   // Fatal error: Cannot redeclare Point::__construct()
}

方法 1: 既定値と可変長引数

class Point
{
    public function __construct(
        public readonly int $x = 0,
        public readonly int $y = 0,
    ) {}
}

new Point();          // (0, 0)
new Point(1);         // (1, 0)
new Point(y: 5);      // (0, 5)

方法 2: static のファクトリメソッド(推奨)

class Point
{
    private function __construct(
        public readonly int $x,
        public readonly int $y,
    ) {}

    public static function of(int $x, int $y): static
    {
        return new static($x, $y);
    }

    public static function fromText(string $text): static
    {
        [$x, $y] = array_map('intval', explode(',', $text));
        return new static($x, $y);
    }

    public static function origin(): static
    {
        return new static(0, 0);
    }
}

$a = Point::of(1, 2);
$b = Point::fromText('3,4');
$c = Point::origin();

作り方ごとに名前が付くので、呼び出し側を読むだけで意図が分かります。__constructprivate にすれば、他の作り方を封じられます。

継承したときは parent を呼ぶ

class Base
{
    public function __construct(protected string $name) {}
}

class Admin extends Base
{
    public function __construct(string $name, private int $level)
    {
        parent::__construct($name);    // ← これを書かないと $name が設定されない
    }
}

class Broken extends Base
{
    public function __construct(private int $level) {}    // parent を呼び忘れ
}

$b = new Broken(3);
// $b->name にアクセスすると
// Error: Typed property Base::$name must not be accessed before initialization

子クラスで __construct定義しなければ親のものがそのまま使われます。定義した瞬間に親の初期化は行われなくなるため、呼び忘れが最頻出のバグです。

デストラクタ

class FileHandle
{
    private $fp;

    public function __construct(string $path)
    {
        $this->fp = fopen($path, 'r');
    }

    public function __destruct()
    {
        if (is_resource($this->fp)) {
            fclose($this->fp);
        }
    }
}

$h = new FileHandle('data.txt');
unset($h);        // 参照が 0 になった時点で __destruct が呼ばれる

__destruct参照がすべて無くなったときか、スクリプト終了時に呼ばれます。ただし呼ばれる順序は保証されず、循環参照があると遅れます。確実に閉じたい資源は、明示的な close() メソッドを用意してください。

コンストラクタに書かないほうがよいこと

// 悪い例: 生成しただけで通信・DB 接続が走る
class ReportService
{
    private array $rates;

    public function __construct()
    {
        $this->rates = (new TaxApi())->fetchAll();   // テストで差し替えられない
    }
}

// 良い例: 必要なものを受け取るだけ
class ReportService
{
    public function __construct(private TaxRateProvider $rates) {}

    public function build(): string
    {
        return $this->rates->get('JP');     // 実際に使うときに呼ぶ
    }
}
コンストラクタでやること
やってよい引数の代入、値の検証、既定値の設定
避けるDB 接続・HTTP 通信・ファイル読み込み・重い計算
避ける$this を外部に渡す(初期化途中の状態が漏れる)

値の検証を入れる

class Age
{
    public function __construct(public readonly int $value)
    {
        if ($value < 0 || $value > 150) {
            throw new InvalidArgumentException("年齢が範囲外です: {$value}");
        }
    }
}

new Age(30);      // OK
new Age(-1);      // InvalidArgumentException

コンストラクタで検証すると、そのクラスのインスタンスは常に正しい値を持つと保証できます。以降の処理で毎回チェックする必要がなくなります。

複製したときの挙動(__clone)

class Order
{
    public function __construct(
        public string $code,
        public Address $address,
    ) {}
}

$a = new Order('A-1', new Address('東京'));
$b = clone $a;
$b->address->city = '大阪';

echo $a->address->city;   // 大阪  ← 中のオブジェクトは共有されたまま

clone浅いコピーです。プロパティに入っているオブジェクトは同じ実体を指し続けます。中身まで複製したいときは __clone() を書きます。

class Order
{
    public function __construct(
        public string $code,
        public Address $address,
    ) {}

    public function __clone(): void
    {
        $this->address = clone $this->address;   // 中のオブジェクトも複製する
    }
}

$b = clone $a;
$b->address->city = '大阪';
echo $a->address->city;   // 東京   影響しない

readonly プロパティは PHP 8.3 から __clone() の中でだけ再代入できるようになりました。値オブジェクトの「一部だけ変えた新しいインスタンス」を作るときに使えます。

初期化されていないプロパティ

class User
{
    public string $name;          // 型付きで既定値なし → 未初期化状態がある

    public function __construct() {}   // $name に入れていない
}

$u = new User();
echo $u->name;
// Error: Typed property User::$name must not be accessed before initialization

var_dump(isset($u->name));    // false   未初期化は isset で false になる

型付きプロパティは null ではなく「未初期化」という状態を持ちます。null を許すなら ?string $name = null; と書き、そうでなければコンストラクタで必ず値を入れてください。

作れないときは例外を投げる

class Config
{
    private array $values;

    public function __construct(string $path)
    {
        if (!is_file($path)) {
            throw new RuntimeException("設定ファイルがありません: {$path}");
        }
        $this->values = require $path;
    }
}

try {
    $c = new Config('/no/such/file.php');
} catch (RuntimeException $e) {
    echo $e->getMessage();
}

「作れたのに中身が正しくない」インスタンスを返さないのが原則です。失敗したら例外を投げて、生成そのものを失敗させます。null を返したくなったら、それは static のファクトリメソッド(tryFrom() のような名前)で表現してください。

関連

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. 基本事項
  2. HTMLへの埋め込み
  3. 変数
  4. 可変変数
  5. 定数
  6. データ型
  7. キャスト
  8. エスケープ文字
  9. 配列
  10. 演算子
  11. 代入の際の注意点
  12. 条件分岐
  13. 繰り返し処理
  14. クラスとインスタンス
  15. コンストラクタ
  16. 関数
  17. スーパーグローバル変数
  18. スコープ
  19. staticについて
  20. yieldについて
  21. ファイルのアップロード方法
  22. DB接続方法
  23. SQL実行方法
  24. カプセル化の具体例
  25. 継承の構文
  26. オーバーライド
  27. ポリモーフィズム(多様性)の具体例
  28. 抽象クラス・メソッドの構文と具体例
  29. GET通信
  30. try catchで全てのエラーを拾う方法

最近更新/作成されたページ