1. ホーム
  2. PHP

【PHP】コンストラクタプロモーションの使い方|引数からプロパティを一気に定義する方法

Share

クラスにプロパティを持たせようとすると、プロパティの宣言・コンストラクタの引数・$this->name = $name; の代入と、同じ名前を3回書くことになります。プロパティが5つあれば15行、名前を1つ変えれば3か所の修正です。PHP 8.0 で追加されたコンストラクタのプロパティ昇格(Constructor Property Promotion)を使うと、この3回分が引数の1行にまとまります。この記事では、基本の書き方から readonly との組み合わせ、使えない場面とそのエラーまでを解説します。

同じ名前を3回書いていた従来のクラス

まずは昇格を使わない書き方を見ておきます。名前・メールアドレス・年齢を持つだけの単純なクラスです。

User.php
<?php

class User
{
    // 1. プロパティを宣言する
    public string $name;
    private string $email;
    public int $age;

    // 2. 同じものをコンストラクタの引数に並べる
    public function __construct( string $name, string $email, int $age = 0 )
    {
        // 3. 引数をプロパティに代入する
        $this->name  = $name;
        $this->email = $email;
        $this->age   = $age;
    }
}

やっていることは「受け取った値をそのまま入れる」だけなのに、$name という語が3回登場します。プロパティを1つ足すときも3か所に手を入れる必要があり、どれか1つを書き忘れると、型宣言だけ残って値が入らないプロパティができてしまいます。

引数にアクセス修飾子を付けるだけで昇格する

書き方のルールはひとつだけです。コンストラクタの引数に public / protected / private を付ける。それだけで PHP はその引数を「引数であると同時にプロパティでもある」と解釈し、渡された値を自動で代入してくれます。先ほどのクラスを書き換えると次のようになります。

User.php
<?php

class User
{
    public function __construct(
        public string $name,
        private string $email,
        public int $age = 0,
    ) {
    }
}

プロパティの宣言も、コンストラクタ本体の代入も消えました。それでも public string $name というプロパティは確かに存在しますし、private string $email はクラスの外から触れません。型宣言もデフォルト値も、引数に書いたものがそのまま効きます。

なお引数を1行ずつ縦に並べ、最後の引数のうしろにもカンマ(末尾カンマ)を付けています。引数リストの末尾カンマは PHP 8.0 から書けるようになったもので、昇格と同じバージョンから使えるため相性が良い書き方です。

使う側のコードは、昇格の前後でまったく変わりません。呼び出し方に影響しないので、既存クラスを静かに書き換えられます。

index.php
$user = new User( 'Taro Yamada', 'taro@example.com', 30 );

echo $user->name; // Taro Yamada
echo $user->age;  // 30

// private で昇格したプロパティは外から読めない
// echo $user->email;
// Error: Cannot access private property User::$email

デフォルト値は引数にだけ付く

細かい点ですが、マニュアルに明記されている挙動があります。昇格した引数に書いたデフォルト値は引数にだけコピーされ、プロパティのデフォルト値にはなりません。実用上は「コンストラクタを通れば必ず値が入る」ので気になりませんが、リフレクションでプロパティを調べたときに hasDefaultValue() が false を返す点は覚えておくと迷わずに済みます。一方、引数に付けたアトリビュート(#[...])は、プロパティと引数の両方にコピーされます。

昇格に使える条件と使えない場面

昇格できるのは「通常のクラスの __construct() の引数」だけで、そこにもいくつか制限があります。実際に PHP が出すエラーメッセージとあわせて整理します。

書き方可否とエラーメッセージ
public / protected / private を付ける昇格する(基本の書き方)
readonly だけを付ける昇格する。可視性は public になる(PHP 8.1 以降)
型宣言を付ける書ける。?string のような nullable も可
デフォルト値を付ける書ける(引数のデフォルト値になる)
__construct() 以外のメソッドの引数不可:Cannot declare promoted property outside a constructor
抽象クラス・インターフェースのコンストラクタ不可:Cannot declare promoted property in an abstract constructor
callable 型を指定する不可:Property X::$y cannot have type callable
可変長引数(...$args)に付ける不可:Cannot declare variadic promoted property
static を付ける不可:Cannot use the static modifier on a parameter
var を使う不可:構文エラーになる

callable が使えないのは、昇格の都合ではなくプロパティ側の制限です。PHP ではそもそもプロパティの型に callable を指定できません($obj->prop() がメソッド呼び出しと区別できなくなるため)。昇格した引数はプロパティになるので、同じ制限を受けます。コールバックを受け取りたいときは Closure 型を指定するか、型宣言を省いて mixed として受けることになります。

Runner.php
class Runner
{
    public function __construct(
        // private callable $handler, // Fatal error になる
        private Closure $handler,     // これなら昇格できる
    ) {
    }

    public function run( string $value ): string
    {
        return ( $this->handler )( $value );
    }
}

$runner = new Runner( strtoupper(...) );
echo $runner->run( 'php' ); // PHP

抽象クラスやインターフェースで昇格できないのは、これらのコンストラクタが「本体を持たない宣言」だからです。値を代入する場所がないので、シグネチャだけを定義し、プロパティは実装クラス側で昇格させます。

readonly と組み合わせて値オブジェクトを短く書く

昇格がいちばん効いてくるのが、PHP 8.1 で追加された readonly との併用です。readonly は初期化後の変更を禁止する修飾子で、金額や座標のような「作ったら変わらない値」を表すクラスと相性があります。昇格と組み合わせると、値オブジェクトが数行で書けます。

Money.php
<?php

final class Money
{
    public function __construct(
        public readonly int $amount,
        public readonly string $currency = 'JPY',
    ) {
    }

    public function add( Money $other ): static
    {
        // 自分を書き換えず、新しいインスタンスを返す
        return new static( $this->amount + $other->amount, $this->currency );
    }
}

$price = new Money( 1200 );

echo $price->amount;   // 1200
echo $price->currency; // JPY

$price->amount = 500;
// Error: Cannot modify readonly property Money::$amount

昇格を使わずに同じものを書こうとすると、public readonly int $amount; の宣言と引数と代入で、やはり3回同じ名前を書くことになります。readonly なプロパティは型宣言が必須なので、昇格側でも型を省略できない点に注意してください。

ちなみに readonly は、可視性の修飾子を伴わず単独で書いても昇格が起こります。その場合の可視性は public です。ただし読み手にとっては public readonly と明示されているほうが分かりやすいので、省略せずに書くことをおすすめします。

昇格する引数としない引数は混ぜられる

すべての引数を昇格させる必要はありません。マニュアルにもあるとおり、昇格させる引数とさせない引数を好きな順番で混ぜられます。「プロパティとして持ち続けたい値」と「初期化のときだけ使いたい値」を区別できるわけです。

また、コンストラクタ本体を空にする必要もありません。昇格した引数の代入は本体が実行される前に完了しているので、本体では $this->name のようにプロパティとして参照できます。バリデーションや、他の値から組み立てるプロパティの初期化はここで書きます。

Article.php
<?php

class Article
{
    // 昇格させずに自分で宣言するプロパティ
    private string $slug;

    public function __construct(
        public string $title,          // 昇格する
        private string $body,          // 昇格する
        int $publishedYear,            // 昇格しない(チェックに使うだけ)
        public ?string $excerpt = null // 昇格する(null を許可)
    ) {
        if ( $title === '' ) {
            throw new InvalidArgumentException( 'タイトルは必須です。' );
        }

        if ( $publishedYear < 2000 ) {
            throw new InvalidArgumentException( '公開年が古すぎます。' );
        }

        // 昇格したプロパティは、本体に入る前に代入済み
        $this->slug = strtolower( str_replace( ' ', '-', $this->title ) );
    }

    public function slug(): string
    {
        return $this->slug;
    }
}

$article = new Article( 'Hello PHP', '本文です。', 2026 );

echo $article->slug(); // hello-php

$publishedYear には修飾子を付けていないので、ただの引数です。チェックが終われば捨てられ、プロパティとしては残りません。「渡してもらう必要はあるが、保持する必要はない」値をプロパティにしないで済むのは、昇格を使うときの地味な利点です。

名前付き引数と組み合わせると読みやすい

昇格と同じ PHP 8.0 で追加された名前付き引数(Named Arguments)も、あわせて使うと効果が大きい機能です。呼び出し側で 引数名: 値 と書けるので、引数が増えたクラスでも「どの値が何なのか」が読む側に分かります。

index.php
// 位置で渡す:2026 や null が何を指すのか分かりにくい
$article = new Article( 'Hello PHP', '本文です。', 2026, null );

// 名前付き引数:読むだけで対応が分かる
$article = new Article(
    title:         'Hello PHP',
    body:          '本文です。',
    publishedYear: 2026,
);

// デフォルト値がある引数は飛ばせるので、順番も自由
$article = new Article(
    body:          '本文です。',
    title:         'Hello PHP',
    publishedYear: 2026,
    excerpt:       'PHP 8 の新機能の話です。',
);

ここで1つ意識しておきたいのが、引数名が外部に公開される名前になるということです。名前付き引数で呼ばれているクラスの引数名を変更すると、呼び出し側が Unknown named parameter というエラーで落ちます。昇格を使っている場合、引数名はそのままプロパティ名でもあるため、リネームの影響は二重になります。公開するライブラリのクラスほど、最初の命名を慎重に決めてください。

エラーになるとき

昇格は仕様がはっきりしているぶん、間違えたときのエラーメッセージも具体的です。よく遭遇するものを挙げておきます。

PHP 8.0 より前では Parse error になる

昇格は PHP 8.0.0 で追加された構文です。PHP 7 系で実行すると、引数の位置にある private が解釈できず Parse error(構文エラー)になります。構文エラーはファイルを読み込む段階で起きるため、そのファイル全体が実行されません。class_exists() などで回避することもできないので、レンタルサーバーなど PHP のバージョンを選べない環境に配布するコードでは、動作要件を先に確認してください。

readonly を組み合わせる場合は、さらに PHP 8.1 以上が必要です。php -v やスクリプト内の PHP_VERSION で、動かす環境のバージョンを確認しておきましょう。

プロパティを二重に宣言すると Fatal error

従来の書き方から移行するときにいちばん多いのが、これです。コンストラクタの引数に修飾子を付けたのに、クラス上部のプロパティ宣言を消し忘れると、同じプロパティを2回宣言したことになり Cannot redeclare で止まります。

NG 例
class User
{
    private int $x; // これを消し忘れている

    public function __construct( private int $x )
    {
    }
}

// Fatal error: Cannot redeclare User::$x

昇格した時点でプロパティは定義済みなので、上の宣言行は不要です。逆にいえば、このエラーが出たら「昇格がちゃんと効いている」証拠でもあります。同じ理由で、本体に残った $this->x = $x; もエラーにはなりませんが、まったくの無駄な代入になるので消しておきましょう。

static は付けられない

昇格できるのはインスタンスのプロパティだけです。public static int $count のような静的プロパティは作れません。これは昇格の制限というより引数の制限で、Cannot use the static modifier on a parameter というエラーになります。静的プロパティが必要なときは、これまでどおりクラス本体で宣言してください。

null を渡したいのに型が合わない

デフォルト値に null を書いたのに型を ? 付きにしていないと、Cannot use null as default value for parameter で止まります。これは昇格に限らず引数全般の話ですが、「プロパティのデフォルトは null にしておく」という従来の癖のまま書くと踏みやすいポイントです。null を許可するなら ?string のように明示します。

Profile.php
class Profile
{
    public function __construct(
        // public string $bio = null, // Fatal error になる
        public ?string $bio = null,   // nullable を明示する
    ) {
    }
}

コンストラクタ以外で使ってしまう

「セッターの引数にも private を付ければプロパティになるのでは」と考えて書くと、Cannot declare promoted property outside a constructor になります。昇格が働くのは __construct() のときだけで、他のメソッドや通常の関数では使えません。セッターは従来どおり、引数を受け取って本体で代入します。

まとめ

コンストラクタのプロパティ昇格は、__construct() の引数に public / protected / private を付けるだけで、その引数をプロパティとしても定義し、値の代入まで済ませてくれる PHP 8.0 の機能です。プロパティ宣言・引数・代入という3か所の重複が1か所にまとまり、クラスの見通しがはっきり良くなります。型宣言もデフォルト値も引数にそのまま書けて、呼び出し側のコードは一切変わりません。

昇格する引数としない引数は混ぜられ、コンストラクタ本体でバリデーションや他のプロパティの初期化も書けます。readonly(PHP 8.1 以降)と組み合わせれば値オブジェクトが数行で済み、名前付き引数と併用すれば呼び出し側も読みやすくなります。

一方で使えるのは通常のクラスの __construct() だけで、抽象クラスやインターフェースのコンストラクタ、callable 型、可変長引数、static には使えません。移行の際はプロパティ宣言と本体の代入を消し忘れないことだけ気をつければ、あとはエラーメッセージがはっきり教えてくれます。まずは「引数をそのまま代入しているだけ」のクラスから書き換えてみてください。

参考ページ