- PHP
PHPクラスの書き方|コンストラクタ・static・継承をPHP8対応で解説
PHPのクラスとは、データ(プロパティ)と処理(メソッド)をひとまとめにした設計図で、class クラス名 { ... } と書いて定義し、new クラス名() で実体(インスタンス)を作ります。 PHP 8 系では、コンストラクタの引数に public などを付けるだけでプロパティの宣言と代入が済む「コンストラクタ昇格」や、書き換えを防ぐ readonly が加わり、同じ内容をずっと短く書けるようになりました。
この記事では、同じクラスを「旧来の書き方」と「PHP 8 系の書き方」で並べて比べ、各機能がどのバージョンから使えるかを一覧表にしました。public・protected・private の違い、self・static・parent の違い、抽象クラス・インターフェース・トレイトの使い分けも表で整理しています。コードはすべて PHP 8.4.21(CLI)で実行し、出力とエラーメッセージをそのまま載せています(エラー文中のファイルパスは /app/ に短縮)。
PHPクラスの早見表
書き方と対応バージョンをまとめた表です。 構文を思い出したいときは、この表から該当の見出しへ進んでください。
| やりたいこと | 書き方 | 使えるバージョン |
|---|---|---|
| クラスを定義する | class Task { ... } |
すべて |
| インスタンスを作る | $task = new Task('資料を作る'); |
すべて |
| プロパティ・メソッドを使う | $task->title / $task->complete() |
すべて |
| 型付きプロパティ | public string $title; |
7.4 以降 |
| コンストラクタ | public function __construct(string $title) { ... } |
すべて(型は 7.0 以降) |
| コンストラクタ昇格 | public function __construct(public string $title) {} |
8.0 以降 |
| 名前付き引数 | new Point(y: 5) |
8.0 以降 |
| 書き換え不可のプロパティ | public readonly int $id |
8.1 以降 |
| 列挙型 | enum Status: string { case New = 'new'; } |
8.1 以降 |
| クラス全体を書き換え不可に | readonly class Point { ... } |
8.2 以降 |
| 型付きクラス定数 | public const string VERSION = '1.0'; |
8.3 以降 |
| オーバーライドの明示 | #[\Override] |
8.3 以降 |
| プロパティフック | public string $email { set { ... } } |
8.4 以降 |
| 読み取りだけ公開 | public private(set) int $count = 0; |
8.4 以降 |
| 静的プロパティ・メソッド | self::$next / Counter::issued() |
すべて |
| クラス定数 | Counter::PREFIX |
すべて |
| 継承 | class User extends Model { ... } |
すべて |
| 親の処理を呼ぶ | parent::__construct($title); |
すべて |
| インターフェースを実装 | class CsvRow implements Exportable { ... } |
すべて |
| トレイトを使う | use HasTimestamps; |
5.4 以降 |
php.net のサポート状況ページ(今回確認)によると、2026年10月時点でサポートが続いているのは PHP 8.2〜8.5 で、8.2 はセキュリティサポートが 2026年12月31日までです。これから書くコードは 8.3 以降を前提にすると、この記事の書き方のほとんどを使えます。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「PHPクラスの書き方|コンストラクタ・static・継承をPHP8対応で解説」
クラスの基本:定義・インスタンス化・メソッド呼び出し
まずは最小限の完成形です。task.php として保存し、php task.php で実行できます。
<?php
declare(strict_types=1);
class Task
{
// プロパティ(型付き・初期値あり)
public string $title;
public bool $done = false;
// コンストラクタ:new したときに自動で呼ばれる
public function __construct(string $title)
{
$this->title = $title;
}
// メソッド
public function complete(): void
{
$this->done = true;
}
public function label(): string
{
return ($this->done ? '[x] ' : '[ ] ') . $this->title;
}
}
$task = new Task('資料を作る'); // インスタンス化
echo $task->label(), "\n";
$task->complete();
echo $task->label(), "\n";
var_dump($task instanceof Task);
var_dump($task);
実行結果:
[ ] 資料を作る
[x] 資料を作る
bool(true)
object(Task)#1 (2) {
["title"]=>
string(15) "資料を作る"
["done"]=>
bool(true)
}
用語の対応表
| 用語 | 意味 | コード上の例 |
|---|---|---|
| クラス | 設計図 | class Task { ... } |
| インスタンス(オブジェクト) | 設計図から作った実体 | $task = new Task('...') |
| プロパティ | インスタンスが持つデータ | public string $title; |
| メソッド | クラスの中に書いた関数 | public function complete(): void |
| コンストラクタ | new のときに自動で呼ばれるメソッド |
__construct() |
$this |
メソッドの中で「自分自身のインスタンス」を指す変数 | $this->title |
-> |
インスタンスのプロパティ・メソッドを使う演算子 | $task->label() |
:: |
クラスに属するもの(定数・static)を使う演算子 | Counter::PREFIX |
declare(strict_types=1); は、型の自動変換を行わず、型が合わないときにエラーで知らせてくれる宣言です。 ファイルの先頭に書いておくと、型の取り違えに早く気づけます。この記事のコードは、旧来の書き方の例を除いてすべてこの宣言を付けています。
同じクラスを旧来の書き方と PHP 8 の書き方で比べる
商品名と価格を持つ Item クラスを、2通りの書き方で並べます。
旧来の書き方(PHP 5〜7.3 でも動く形)
<?php
// 旧来の書き方(PHP 5〜7.3 でも動く形)
class Item
{
/** @var string */
private $name;
/** @var int */
private $price;
public function __construct($name, $price)
{
$this->name = $name;
$this->price = $price;
}
public function getName()
{
return $this->name;
}
public function getPrice()
{
return $this->price;
}
public function priceWithTax()
{
return (int) floor($this->price * 1.1);
}
}
$item = new Item('ノート', 300);
echo $item->getName(), ' ', $item->priceWithTax(), "円\n";
$bad = new Item('ペン', '百円'); // 型の指定がないので通ってしまう
var_dump($bad->getPrice());
実行結果:
ノート 330円
string(6) "百円"
型をコメント(@var)で書いているだけなので、価格に文字列の '百円' を渡してもそのまま通ります。 気づくのは、後で計算に使ったときです。
PHP 8.1 以降の書き方(コンストラクタ昇格 + readonly)
<?php
declare(strict_types=1);
// PHP 8.1 以降の書き方(コンストラクタ昇格 + readonly)
final class Item
{
public function __construct(
public readonly string $name,
public readonly int $price,
) {
}
public function priceWithTax(): int
{
return (int) floor($this->price * 1.1);
}
}
$item = new Item('ノート', 300);
echo $item->name, ' ', $item->priceWithTax(), "円\n";
try {
$bad = new Item('ペン', '百円');
} catch (TypeError $e) {
echo get_class($e), ': ', $e->getMessage(), "\n";
}
実行結果:
ノート 330円
TypeError: Item::__construct(): Argument #2 ($price) must be of type int, string given, called in /app/new.php on line 23
同じ動きのクラスが、およそ半分の行数になりました。 しかも、型の合わない値は new の時点で TypeError として知らせてくれます。
何が変わったか
| 項目 | 旧来の書き方 | PHP 8 系の書き方 | 使えるバージョン |
|---|---|---|---|
| プロパティの型 | コメント(@var)で書く |
string $name と宣言する |
7.4 |
| 引数・戻り値の型 | なし | int $price、: int |
7.0(void は 7.1) |
| プロパティの宣言と代入 | 宣言・引数・代入の3か所に書く | コンストラクタの引数に public などを付けて1か所で済む(コンストラクタ昇格) |
8.0 |
| 値を守る方法 | private にしてゲッターを書く |
public readonly で読み取りだけ公開 |
8.1 |
| 引数リストの末尾カンマ | 不可 | 可(差分が見やすくなる) | 8.0 |
| 継承の制限 | 指定なし | final で継承しない意図を示す |
すべて(5.0 以降) |
コンストラクタ昇格は、引数に public・protected・private のどれかを付けると、同名のプロパティの宣言と $this->name = $name; の代入を PHP が自動で行ってくれる仕組みです。
PHP 8 系で使えるクラス機能とバージョン一覧
| バージョン | リリース | クラスまわりの主な機能 |
|---|---|---|
| 7.4 | 2019年11月 | 型付きプロパティ |
| 8.0 | 2020年11月 | コンストラクタ昇格、名前付き引数、static 戻り値型、nullsafe 演算子 ?->、match 式、union 型、$obj::class |
| 8.1 | 2021年11月 | readonly プロパティ、enum、new を引数の初期値に書ける、final クラス定数 |
| 8.2 | 2022年12月 | readonly クラス、動的プロパティの非推奨化、トレイトの定数 |
| 8.3 | 2023年11月 | 型付きクラス定数、#[\Override]、readonly プロパティを __clone 内で再初期化可能 |
| 8.4 | 2024年11月 | プロパティフック、非対称可視性 private(set)、new 直後のかっこなしメソッド呼び出し |
| 8.5 | 2025年11月 | (この記事の範囲では 8.4 までの書き方がそのまま使える) |
8.2〜8.5 のリリース日は php.net のサポート状況ページで今回確認したものです。7.4〜8.1 のリリース年月と各機能の導入バージョンは一般に知られた仕様で、動作は PHP 8.4.21 で確認しました(8.5 での実行は未実行です)。
プロパティと型:未初期化エラーを避ける
型付きプロパティ(7.4 以降)は、初期値を持たないまま読み取るとエラーになります。 これは「値がまだ入っていない」ことを知らせてくれる動きです。次のクラスで、よくあるエラーをまとめて再現しました。
<?php
declare(strict_types=1);
function attempt(string $label, callable $fn): void
{
try {
$fn();
echo "$label => OK\n";
} catch (Throwable $e) {
echo "$label => ", get_class($e), ': ', $e->getMessage(), "\n";
}
}
class Profile
{
public string $name; // 初期値なし
public ?string $nickname = null; // null 許容で初期化済み
public function __construct(public readonly int $id = 1) {}
}
attempt('1 未初期化の読み取り', function () { $p = new Profile(); echo $p->name; });
attempt('2 null許容で初期化済み', function () { $p = new Profile(); var_dump($p->nickname); });
attempt('3 readonly 再代入', function () { $p = new Profile(); $p->id = 2; });
attempt('4 型の違う代入', function () { $p = new Profile(); $p->name = 123; });
attempt('5 未定義メソッド', function () { $p = new Profile(); $p->save(); });
attempt('7 nullにメソッド', function () { $p = null; $p->save(); });
abstract class Shape { abstract public function area(): float; }
attempt('8 抽象クラスをnew', function () { new Shape(); });
interface Printable { public function print(): string; }
attempt('9 インターフェースをnew', function () { new Printable(); });
実行結果:
1 未初期化の読み取り => Error: Typed property Profile::$name must not be accessed before initialization
NULL
2 null許容で初期化済み => OK
3 readonly 再代入 => Error: Cannot modify readonly property Profile::$id
4 型の違う代入 => TypeError: Cannot assign int to property Profile::$name of type string
5 未定義メソッド => Error: Call to undefined method Profile::save()
7 nullにメソッド => Error: Call to a member function save() on null
8 抽象クラスをnew => Error: Cannot instantiate abstract class Shape
9 インターフェースをnew => Error: Cannot instantiate interface Printable
エラーメッセージと直し方の対応表
| メッセージ | 原因 | 直し方 |
|---|---|---|
Typed property Profile::$name must not be accessed before initialization |
型付きプロパティに値を入れる前に読んだ | コンストラクタで必ず代入する。値がない状態を許すなら ?string $name = null にする |
Cannot modify readonly property Profile::$id |
readonly プロパティに2回目の代入をした |
値を変えた新しいインスタンスを作る(後述の withX() の形) |
Cannot assign int to property Profile::$name of type string |
型の合わない値を代入した | 代入前に (string) などで変換するか、型を見直す |
Call to undefined method Profile::save() |
メソッド名の打ち間違い、または未定義 | クラス定義のメソッド名を確認する |
Call to a member function save() on null |
変数が null なのにメソッドを呼んだ |
null の可能性があるなら $p?->save() を使うか、事前に確認する |
Cannot instantiate abstract class Shape |
抽象クラスを直接 new した |
継承した具体的なクラスを new する |
Cannot instantiate interface Printable |
インターフェースを直接 new した |
実装したクラスを new する |
未初期化と null は別の状態である点がポイントです。public ?string $nickname;(初期値なし)は未初期化で、読むとエラーになります。= null と書いて初めて「null が入っている」状態になります。
動的プロパティは 8.2 から非推奨
クラスに宣言していないプロパティへ代入すると、8.2 以降は非推奨の通知が出ます。
class Box { public int $n = 1; }
$a = new Box();
$a->label = 'x';
Deprecated: Creation of dynamic property Box::$label is deprecated in /app/misc.php on line 43
プロパティ名の打ち間違い($a->lable = ... など)に気づけるようになる変更です。使うプロパティはクラスに宣言しておくと、この通知は出ません。
readonly と値を変えない設計
readonly プロパティは、クラスの中で1回だけ代入でき、その後は変更できません。 8.2 からはクラス全体に readonly を付けて、すべてのプロパティをまとめて書き換え不可にできます。
<?php
declare(strict_types=1);
// PHP 8.2:readonly クラス(全プロパティが readonly になる)
final readonly class Point
{
public function __construct(public int $x = 0, public int $y = 0)
{
}
// 値を変えたいときは、新しいインスタンスを返す
public function withX(int $x): static
{
return new static($x, $this->y);
}
}
$p1 = new Point(1, 2);
$p2 = $p1->withX(10);
echo "{$p1->x},{$p1->y} / {$p2->x},{$p2->y}\n";
var_dump($p1 == $p2, $p1 == new Point(1, 2), $p1 === new Point(1, 2));
// PHP 8.0:名前付き引数と nullsafe 演算子
$p3 = new Point(y: 5);
echo "{$p3->x},{$p3->y}\n";
$none = null;
var_dump($none?->x);
// オブジェクトは参照のように渡る(コピーしたいときは clone)
class Box { public int $n = 1; }
$a = new Box(); $b = $a; $c = clone $a;
$b->n = 2;
echo $a->n, ' ', $c->n, "\n";
実行結果:
1,2 / 10,2
bool(false)
bool(true)
bool(false)
0,5
NULL
2 1
読み取れることを整理します。
| 出力 | わかること |
|---|---|
1,2 / 10,2 |
withX() は元の $p1 を変えずに、新しいインスタンスを返している |
$p1 == new Point(1, 2) が true |
== は「同じクラスで、プロパティの値が等しいか」を比べる |
$p1 === new Point(1, 2) が false |
=== は「同じインスタンスか」を比べる |
0,5 |
名前付き引数で、2番目の引数だけを指定できる |
NULL |
?-> は左側が null ならエラーにせず null を返す |
2 1 |
$b = $a は同じインスタンスを指す。clone は別のインスタンスを作る |
変数にオブジェクトを代入しても、コピーは作られません。 $b を通して変更すると $a から見ても変わります。この動きが意図しない変更につながることがあるので、値を表すクラス(座標、期間、金額など)は readonly にしておくと安心して受け渡しできます。
static とクラス定数
static を付けたプロパティ・メソッドは、インスタンスではなくクラスそのものに属します。 new しなくても クラス名:: で使え、静的プロパティはすべてのインスタンスで共有されます。
<?php
declare(strict_types=1);
class Counter
{
public const PREFIX = 'ID-'; // クラス定数
private static int $next = 1; // 静的プロパティ(クラスで1つ)
public readonly string $id;
public function __construct()
{
$this->id = self::PREFIX . self::$next++;
}
public static function issued(): int // 静的メソッド
{
return self::$next - 1;
}
}
$a = new Counter();
$b = new Counter();
echo $a->id, ' ', $b->id, "\n";
echo Counter::issued(), "\n";
echo Counter::PREFIX, "\n";
class Util
{
public string $name = 'util';
public static function hello(): string
{
return $this->name;
}
}
try {
Util::hello();
} catch (Error $e) {
echo get_class($e), ': ', $e->getMessage(), "\n";
}
実行結果:
ID-1 ID-2
2
ID-
Error: Using $this when not in object context
静的メソッドの中では $this を使えません。 静的メソッドはインスタンスなしで呼ばれるため、「自分自身のインスタンス」が存在しないからです。
インスタンスのものと static のものの違い
| 項目 | インスタンスのプロパティ・メソッド | static のプロパティ・メソッド | クラス定数 |
|---|---|---|---|
| 属する先 | インスタンスごと | クラスに1つ | クラスに1つ |
| 外からの使い方 | $obj->name / $obj->run() |
Counter::$next / Counter::issued() |
Counter::PREFIX |
| クラス内での使い方 | $this->name |
self::$next / static::$next |
self::PREFIX / static::PREFIX |
| 値の変更 | できる | できる | できない |
$this |
使える | 使えない | ― |
| 向いている用途 | そのインスタンスの状態と振る舞い | 生成用のメソッド、状態を持たない補助処理 | 固定の値 |
静的プロパティは「どこからでも書き換えられる共有の値」になるため、多用するとテストや処理の追跡に手間がかかります。状態はインスタンスに持たせ、static は生成用メソッド(User::fromArray() のような形)や定数的な用途に絞ると、見通しのよいコードになります。
public・protected・private の違い
アクセス修飾子は、プロパティやメソッドを「どこから使えるか」を決めるキーワードです。
<?php
declare(strict_types=1);
class Account
{
public string $name = 'sato';
protected string $rank = 'gold';
private string $token = 'abc123';
public function show(): string
{
return "{$this->name}/{$this->rank}/{$this->token}"; // クラスの中からは全部読める
}
}
class AdminAccount extends Account
{
public function peek(): string
{
return $this->rank; // protected は子クラスから読める
}
}
$a = new AdminAccount();
echo $a->name, "\n"; // public
echo $a->show(), "\n";
echo $a->peek(), "\n";
$base = new Account();
foreach (['rank', 'token'] as $prop) {
try {
echo $base->$prop, "\n";
} catch (Error $e) {
echo get_class($e), ': ', $e->getMessage(), "\n";
}
}
実行結果:
sato
sato/gold/abc123
gold
Error: Cannot access protected property Account::$rank
Error: Cannot access private property Account::$token
| 修飾子 | クラスの中 | 子クラスの中 | クラスの外 | 使いどころ |
|---|---|---|---|---|
public |
使える | 使える | 使える | 外に公開する機能 |
protected |
使える | 使える | 使えない | 子クラスに拡張を任せる部分 |
private |
使える | 使えない | 使えない | そのクラスだけで完結する内部の処理 |
public private(set)(8.4) |
読み書きできる | 読める | 読める | 読み取りは公開し、変更はクラス内に限る |
なお、子クラスのメソッドから親の private プロパティ($this->token)を読もうとした場合は、「見えていない=存在しない」扱いになり、Warning: Undefined property: AdminAccount::$token という警告が出ることを確認しました。迷ったら private から始め、必要になったときに広げると、後から変更しやすいクラスになります。
継承(extends)と self・static・parent の違い
継承は、既存のクラス(親)の機能を引き継いで、新しいクラス(子)を作る仕組みです。子クラスで同じ名前のメソッドを書くと、親の実装を上書き(オーバーライド)できます。
<?php
declare(strict_types=1);
class Model
{
public static function createSelf(): self
{
return new self(); // 書いたクラス(Model)で固定
}
public static function createStatic(): static
{
return new static(); // 呼び出されたクラスで決まる(遅延静的束縛)
}
public function describe(): string
{
return 'Model';
}
}
class User extends Model
{
public function describe(): string
{
return 'User < ' . parent::describe(); // 親の実装を呼ぶ
}
}
echo get_class(User::createSelf()), "\n";
echo get_class(User::createStatic()), "\n";
echo (new User())->describe(), "\n";
実行結果:
Model
User
User < Model
User::createSelf() と呼んでも、new self() は書かれている場所のクラス(Model)を作ります。new static() は呼び出しに使われたクラス(User)を作ります。定数でも同じ違いが出ることを、別のコードで確認しました。
class Animal
{
public const SOUND = '...';
public function bySelf(): string { return self::SOUND; }
public function byStatic(): string { return static::SOUND; }
}
class Dog extends Animal
{
public const SOUND = 'ワン';
}
$d = new Dog();
echo $d->bySelf(), ' / ', $d->byStatic(), "\n";
... / ワン
self・static・parent・$this の比較表
| キーワード | 指すもの | 決まるタイミング | よく使う場面 |
|---|---|---|---|
$this |
いま動いているインスタンス | 実行時 | インスタンスのプロパティ・メソッド |
self |
そのコードが書かれているクラス | 書いた時点で固定 | 定数、static メンバー、子で変えさせたくない処理 |
static |
呼び出しに使われたクラス | 実行時(遅延静的束縛) | 子クラスでも正しく動く生成メソッド、戻り値型 : static |
parent |
親クラス | 書いた時点で固定 | 親のコンストラクタ・メソッドを呼ぶ |
継承で出会うエラー
親と子でメソッドの引数や戻り値の型が合わない場合、クラスを読み込んだ時点でエラーになります。
class Base { public function find(int $id): ?array { return null; } }
class Child extends Base {
public function find(string $id): ?array { return null; }
}
Fatal error: Declaration of Child::find(string $id): ?array must be compatible with Base::find(int $id): ?array in /app/sig_err.php on line 4
final を付けたクラスを継承した場合と、#[\Override](8.3)を付けたメソッドの名前を打ち間違えた場合は、それぞれ次のとおりです。
Fatal error: Class LongMemo cannot extend final class Memo in /app/final_err.php on line 3
Fatal error: Child::bodi() has #[\Override] attribute, but no matching parent method exists in /app/override_err.php on line 5
#[\Override] は「このメソッドは親のメソッドを上書きしている」と宣言する属性です。 親にその名前のメソッドがなければエラーになるので、打ち間違いや、親側のメソッド名が変わったことにすぐ気づけます。
抽象クラス・インターフェース・トレイトの使い分け
共通化の方法は3つあります。1つのコードで全部を使った完成形です。
<?php
declare(strict_types=1);
// インターフェース:「このメソッドを持つ」という約束だけを決める
interface Exportable
{
public function export(): string;
}
// トレイト:複数のクラスに同じ実装を配る
trait HasTimestamps
{
private ?string $createdAt = null;
public function touch(string $now): void
{
$this->createdAt = $now;
}
public function createdAt(): ?string
{
return $this->createdAt;
}
}
// 抽象クラス:共通の実装と「子が必ず書くメソッド」を両方持つ
abstract class Document implements Exportable
{
use HasTimestamps;
public function __construct(protected string $title)
{
}
abstract protected function body(): string; // 子クラスが実装する
public function export(): string // 共通の流れは親が持つ
{
return "# {$this->title}\n" . $this->body();
}
}
final class Memo extends Document
{
public function __construct(string $title, private string $text)
{
parent::__construct($title); // 親のコンストラクタを呼ぶ
}
#[\Override]
protected function body(): string
{
return $this->text;
}
}
final class CsvRow implements Exportable, Countable
{
public function __construct(private array $cells)
{
}
public function export(): string
{
return implode(',', $this->cells);
}
public function count(): int
{
return count($this->cells);
}
}
function output(Exportable $target): void // 型はインターフェースで受ける
{
echo $target->export(), "\n";
}
$memo = new Memo('買い物', '牛乳、卵');
$memo->touch('2026-10-03 10:00');
output($memo);
echo $memo->createdAt(), "\n";
$row = new CsvRow(['A001', 'ノート', 300]);
output($row);
echo count($row), "\n";
var_dump($memo instanceof Exportable, $memo instanceof Document, $row instanceof Document);
実行結果:
# 買い物
牛乳、卵
2026-10-03 10:00
A001,ノート,300
3
bool(true)
bool(true)
bool(false)
output() 関数は引数の型を Exportable にしているので、継承関係のない Memo と CsvRow のどちらも受け取れます。 「export() を持っていること」だけを条件にできるのが、インターフェースの強みです。CsvRow は PHP 組み込みの Countable も実装しているため、count($row) が使えています。
3つの比較表
| 項目 | 抽象クラス | インターフェース | トレイト |
|---|---|---|---|
| キーワード | abstract class / extends |
interface / implements |
trait / use |
| 実装(処理の中身)を持てるか | 持てる | 持てない(シグネチャのみ) | 持てる |
| プロパティを持てるか | 持てる | 持てない(定数は可。8.4 からはフック付きプロパティの宣言も可) | 持てる |
| 1つのクラスで使える数 | 1つ | 複数 | 複数 |
型として使えるか(引数・instanceof) |
使える | 使える | 使えない |
| 関係の意味 | 「〜の一種である」 | 「〜ができる」 | 「この実装を取り込む」 |
どれを選ぶかの判断表
| やりたいこと | 選ぶもの |
|---|---|
| 関係のないクラス同士に、同じメソッドを持つことを約束させたい | インターフェース |
| 引数の型を「特定のクラス」ではなく「できること」で指定したい | インターフェース |
| 共通の処理の流れを親に置き、一部だけ子に書かせたい | 抽象クラス |
| 継承関係のない複数のクラスに、同じ実装をそのまま配りたい | トレイト |
| 迷ったとき | まずインターフェース。共通の実装が必要になってから抽象クラスやトレイトを足す |
実装し忘れ・トレイトの衝突で出るエラー
インターフェースのメソッドを実装しなかった場合です。
interface Exportable { public function export(): string; }
final class Broken implements Exportable {}
Fatal error: Class Broken contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (Exportable::export) in /app/types_err.php on line 3
2つのトレイトに同じ名前のメソッドがある場合は、どちらを使うかを指定します。
<?php
trait A { public function hello(): string { return 'A'; } }
trait B { public function hello(): string { return 'B'; } }
class C {
use A, B {
A::hello insteadof B; // hello() は A のものを使う
B::hello as helloB; // B のものは別名で残す
}
}
$c = new C();
echo $c->hello(), $c->helloB(), "\n";
AB
指定せずに use A, B; とだけ書いた場合は、次のエラーになりました。
Fatal error: Trait method B::hello has not been applied as C::hello, because of collision with A::hello in /app/trait_conf.php on line 4
enum:クラス定数から列挙型へ(PHP 8.1)
「受付」「発送済み」のような決まった選択肢は、8.1 で加わった enum(列挙型)で表すと、型として扱えるようになります。旧来のクラス定数と並べて比べます。
<?php
declare(strict_types=1);
// 旧来:クラス定数で状態を表す
class OrderStatusOld
{
public const NEW = 'new';
public const SHIPPED = 'shipped';
}
function labelOld(string $status): string
{
return $status === OrderStatusOld::NEW ? '受付' : '発送済み';
}
echo labelOld('shiped'), "\n"; // つづり間違いでも通ってしまう
// PHP 8.1 以降:enum(列挙型)
enum OrderStatus: string
{
case New = 'new';
case Shipped = 'shipped';
case Cancelled = 'cancelled';
public function label(): string
{
return match ($this) {
self::New => '受付',
self::Shipped => '発送済み',
self::Cancelled => 'キャンセル',
};
}
}
final class Order
{
public function __construct(
public readonly int $id,
public OrderStatus $status = OrderStatus::New,
) {
}
}
$order = new Order(id: 1);
echo $order->status->label(), "\n";
$order->status = OrderStatus::from('shipped'); // 文字列から変換
echo $order->status->label(), ' / ', $order->status->value, "\n";
var_dump(OrderStatus::tryFrom('shiped')); // 見つからなければ null
echo count(OrderStatus::cases()), "\n";
try {
OrderStatus::from('shiped');
} catch (ValueError $e) {
echo get_class($e), ': ', $e->getMessage(), "\n";
}
try {
$order->status = 'shipped';
} catch (TypeError $e) {
echo get_class($e), ': ', $e->getMessage(), "\n";
}
実行結果:
発送済み
受付
発送済み / shipped
NULL
3
ValueError: "shiped" is not a valid backing value for enum OrderStatus
TypeError: Cannot assign string to property Order::$status of type OrderStatus
1行目に注目してください。旧来の書き方では、つづりを間違えた 'shiped' が「発送済み」として通っています。 enum では、存在しない値は from() が ValueError で知らせ、プロパティの型を OrderStatus にしておけば文字列の代入自体を TypeError で止めてくれます。
| メソッド・プロパティ | 動き |
|---|---|
OrderStatus::New |
ケースを直接指定する |
OrderStatus::from('new') |
値からケースを得る。該当なしは ValueError |
OrderStatus::tryFrom('new') |
値からケースを得る。該当なしは null |
OrderStatus::cases() |
すべてのケースの配列 |
$status->value |
対応する値('new' など) |
$status->name |
ケース名('New' など) |
enum にはメソッドを書けるので、表示名のような「ケースごとに決まる情報」を enum 自身に持たせられます。
PHP 8.4 のプロパティフックと非対称可視性
8.4 では、プロパティの読み書きに処理をはさめる「プロパティフック」が加わりました。これまでゲッター・セッターのメソッドで書いていた検証や整形を、プロパティの宣言に直接書けます。
<?php
declare(strict_types=1);
// PHP 8.4:プロパティフックと非対称可視性
final class Member
{
// set フック:代入時に整形・検証する
public string $email {
set(string $value) {
if (!str_contains($value, '@')) {
throw new InvalidArgumentException("メールアドレスの形式ではありません: $value");
}
$this->email = strtolower($value);
}
}
// get フック:他のプロパティから計算する(値を保存しない仮想プロパティ)
public string $fullName {
get => $this->lastName . ' ' . $this->firstName;
}
// 非対称可視性:読むのは public、書くのはクラスの中だけ
public private(set) int $loginCount = 0;
public function __construct(
public string $lastName,
public string $firstName,
string $email,
) {
$this->email = $email;
}
public function login(): void
{
$this->loginCount++;
}
}
$m = new Member('佐藤', '花子', 'Hanako@Example.COM');
echo $m->email, "\n";
echo $m->fullName, "\n";
$m->login();
echo $m->loginCount, "\n";
try {
$m->email = 'not-an-email';
} catch (InvalidArgumentException $e) {
echo get_class($e), ': ', $e->getMessage(), "\n";
}
try {
$m->loginCount = 99;
} catch (Error $e) {
echo get_class($e), ': ', $e->getMessage(), "\n";
}
try {
$m->fullName = '田中 太郎';
} catch (Error $e) {
echo get_class($e), ': ', $e->getMessage(), "\n";
}
// PHP 8.4:new の直後にかっこなしでメソッドを呼べる
echo new Member('鈴木', '一郎', 'ichiro@example.com')->fullName, "\n";
実行結果:
hanako@example.com
佐藤 花子
1
InvalidArgumentException: メールアドレスの形式ではありません: not-an-email
Error: Cannot modify private(set) property Member::$loginCount from global scope
Error: Property Member::$fullName is read-only
鈴木 一郎
旧来のゲッター・セッターとの対応
| やりたいこと | 8.3 までの書き方 | 8.4 の書き方 |
|---|---|---|
| 代入時に検証・整形する | private プロパティ + setEmail() メソッド |
set フック |
| 他の値から計算して返す | getFullName() メソッド |
get フックだけの仮想プロパティ |
| 外からは読めるが書けない | private プロパティ + ゲッター、または readonly |
public private(set) |
| 使う側の書き方 | $m->setEmail('...') / $m->getFullName() |
$m->email = '...' / $m->fullName |
php.net のプロパティフックのページ(今回確認)には、フックを持つプロパティには値を保存する「backed」と、保存しない「virtual」の2種類があること、フックは readonly プロパティと併用できないことが書かれています。実際に両方を付けて試した結果は次のとおりです。
class X { public readonly string $v { get => 'a'; } }
Fatal error: Hooked properties cannot be readonly in /app/ro_hook.php on line 2
「読み取りだけ公開したい、かつフックも使いたい」場合は、readonly の代わりに public private(set) を組み合わせます。
readonly と private(set) の使い分け
| 項目 | readonly |
public private(set) |
|---|---|---|
| 使えるバージョン | 8.1 | 8.4 |
| 外からの読み取り | できる(public の場合) |
できる |
| クラスの中での変更 | 最初の1回だけ | 何度でもできる |
| フックとの併用 | できない | できる |
| 向いている用途 | 作った後は変わらない値(ID、座標など) | クラスのメソッドを通して変化する値(回数、状態など) |
クラスを書くときのチェックリスト
| 確認すること | ねらい |
|---|---|
ファイル先頭に declare(strict_types=1); を書いたか |
型の取り違えを早く見つける |
| プロパティ・引数・戻り値に型を付けたか | エラーを new や代入の時点で受け取る |
| 型付きプロパティは、コンストラクタで必ず初期化しているか | 未初期化エラーを避ける |
変わらない値は readonly にしたか |
意図しない書き換えを防ぐ |
アクセス修飾子は private から検討したか |
後から変更しやすくする |
継承を想定しないクラスに final を付けたか |
拡張の範囲を明確にする |
上書きするメソッドに #[\Override] を付けたか(8.3 以降) |
名前の打ち間違いに気づく |
決まった選択肢を文字列ではなく enum にしたか(8.1 以降) |
存在しない値を型で防ぐ |
| 引数の型をインターフェースで受けられないか | 差し替えやテストをしやすくする |
よくある質問
Q. コンストラクタ昇格と、普通にプロパティを宣言する書き方は混ぜて使えますか?
A. 使えます。この記事の Member クラスのように、昇格させる引数(public string $lastName)と、通常の引数(string $email)を同じコンストラクタに並べられます。フックを付けたいプロパティや、コンストラクタの中で加工してから代入したい値は、通常のプロパティとして宣言するとよいでしょう。
Q. self と static はどちらを使えばよいですか?
A. 継承されても動きを変えたくないなら self、子クラスから呼ばれたときに子クラスとして動いてほしいなら static を選びます。new self() は書いたクラス、new static() は呼び出しに使われたクラスのインスタンスを作ります。生成用の静的メソッドでは、戻り値型も含めて static にしておくと、子クラスでそのまま使えます。
Q. 抽象クラスとインターフェースは、どちらから使い始めればよいですか? A. まずはインターフェースをおすすめします。「このメソッドを持つ」という約束だけを決めるので、後からの変更がしやすく、1つのクラスが複数実装できます。複数のクラスで同じ処理の流れが重なってきたら、その共通部分を抽象クラスやトレイトにまとめる、という順番が進めやすい形です。
Q. プロパティフックは PHP 8.3 以前でも使えますか?
A. 使えません。プロパティフックと private(set) は 8.4 で加わった構文なので、8.3 以前では構文エラーになります(8.3 以前での実行は今回は未実行です)。8.3 以前では、private プロパティとゲッター・セッターのメソッド、または readonly で同じ目的を達成できます。
まとめ
- クラスは
class 名前 { ... }で定義し、newでインスタンスを作る。->はインスタンスのもの、::はクラスに属するもの(定数・static)に使う - PHP 8.0 のコンストラクタ昇格と 8.1 の
readonlyで、宣言・引数・代入の3か所書きが1か所になる。型を付ければ、合わない値はTypeErrorで知らせてくれる - 型付きプロパティは初期化前に読むと
must not be accessed before initializationになる。コンストラクタで代入するか、= nullなどの初期値を置く selfは書いたクラス、staticは呼び出されたクラス、parentは親クラス。staticメソッドの中では$thisを使えない- インターフェースは約束、抽象クラスは共通の流れ、トレイトは実装の配布。迷ったらインターフェースから始める
- 決まった選択肢は
enum(8.1)にすると、存在しない値をValueError・TypeErrorで防げる - 8.4 のプロパティフックと
private(set)で、ゲッター・セッターをプロパティの宣言にまとめられる(readonlyとの併用は不可)
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「PHPクラスの書き方|コンストラクタ・static・継承をPHP8対応で解説」
株式会社RJCでは、PHPのクラス設計やPHP 8系の新しい書き方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。