1. ホーム
  2. PHP

【PHP】interface と abstract class(抽象クラス)の使い方|違いと使い分けを解説

Share

PHP のオブジェクト指向を学んでいくと、必ず interface(インターフェース)abstract class(抽象クラス) という2つの仕組みに出会います。どちらも「子クラスにメソッドの実装を強制する」という似た働きを持つため、どちらを使えばいいのか分からなくなりがちです。この記事では、それぞれの基本的な書き方から、比較表で整理した違い、実際の設計での使い分けの指針、そして実装漏れやタイプミスで出るエラーメッセージの読み方までを、PHP 8 系の仕様に沿って解説します。

interface は「守るべき約束」だけを決める

interface は、「このメソッドを持っていること」という約束(メソッドの一覧)だけを書いたものです。処理の中身は一切書かず、メソッド名・引数・戻り値の型だけを宣言します。定義は interface キーワードで行い、メソッドの本体は書かずにセミコロンで終わらせます。

Notifier.php
<?php
interface Notifier
{
    // 中身は書かない。「notify() を持つこと」という約束だけ
    public function notify(string $message): void;
}

この約束を守るクラスは implements で宣言します。宣言したからには、書かれているメソッドをすべて実装しなければなりません

MailNotifier.php
<?php
class MailNotifier implements Notifier
{
    // コンストラクタプロモーション(PHP 8 以降)
    public function __construct(private string $to) {}

    public function notify(string $message): void
    {
        echo 'メール送信(' . $this->to . '): ' . $message . PHP_EOL;
    }
}

class SlackNotifier implements Notifier
{
    public function __construct(private string $webhookUrl) {}

    public function notify(string $message): void
    {
        // 実際には webhook URL へ HTTP POST する
        echo 'Slack へ送信: ' . $message . PHP_EOL;
    }
}

ここまでだと「わざわざ約束を書く意味があるのか」と感じるかもしれません。interface の価値が出るのは、それを型として使うときです。引数の型に Notifier と書いておけば、受け取ったオブジェクトが notify() を持っていることが保証されます。

index.php
<?php
// 具体的なクラス名ではなく interface を受け取る
function announce(Notifier $notifier, string $message): void
{
    $notifier->notify($message);  // notify() があることは保証済み
}

announce(new MailNotifier('user@example.com'), '登録が完了しました');
announce(new SlackNotifier('https://example.com/hook'), '登録が完了しました');
// メール送信(user@example.com): 登録が完了しました
// Slack へ送信: 登録が完了しました

announce() は具体的なクラスを一切知りません。あとから LineNotifier を追加しても、Notifier を実装してさえいれば announce() 側は変更不要です。実装を差し替えられる余地を作るのが interface の役割だと考えると、使いどころがつかみやすくなります。

複数の interface を同時に実装できる

interface はいくつでも実装できます。implements のあとにカンマ区切りで並べるだけです。「送信できる」「リトライできる」といった能力を別々の interface に分けておき、クラスごとに必要なものを組み合わせられます。

QueuedSlackNotifier.php
<?php
interface Retryable
{
    public function maxRetries(): int;
}

// 2つの約束を同時に守る
class QueuedSlackNotifier implements Notifier, Retryable
{
    public function notify(string $message): void
    {
        echo 'キューに入れました: ' . $message . PHP_EOL;
    }

    public function maxRetries(): int
    {
        return 3;
    }
}

$queued = new QueuedSlackNotifier();
var_dump($queued instanceof Notifier);  // bool(true)
var_dump($queued instanceof Retryable); // bool(true)

実装したクラスは、そのすべての interface の型として扱えます。instanceof で両方とも true になっている点が、interface が「型」であることの証拠です。なお interface 同士は extends で継承でき、こちらも複数指定できます。

AdvancedNotifier.php
<?php
// 2つの interface をまとめた新しい interface を作れる
interface AdvancedNotifier extends Notifier, Retryable
{
}

interface に書けるものは限られている

interface は「約束を書く場所」なので、書ける内容にはっきりした制限があります。メソッドは public のみで、protectedprivate は宣言できません。約束は外から呼ばれるためのものだからです。また値を保持するプロパティも持てません。一方で定数は定義でき、実装したクラスから参照できます。

HttpStatus.php
<?php
interface HttpStatus
{
    const OK        = 200;
    const NOT_FOUND = 404;

    // public 以外は書けない
    // protected function log(): void;  // ← エラーになる
}

echo HttpStatus::OK; // 200

実装コードを一切持てないため、new Notifier() のようにインスタンス化することもできません。あくまで「クラスが守る約束」を書いた設計図の一部です。

abstract class は「共通処理を持った未完成のクラス」

abstract class(抽象クラス)は、普通のクラスとほとんど同じだけれど、一部のメソッドだけ中身が決まっていないクラスです。クラス宣言の前に abstract を付け、中身を書かないメソッドにも abstract を付けて本体を省略します。

AbstractNotifier.php
<?php
abstract class AbstractNotifier
{
    public function __construct(protected string $appName) {}

    // 共通処理。中身を持つ普通のメソッド
    public function notify(string $message): void
    {
        $this->send($this->format($message));
    }

    protected function format(string $message): string
    {
        return '[' . $this->appName . '] ' . $message;
    }

    // 中身は書かない。子クラスに実装を強制する
    abstract protected function send(string $message): void;
}

「送信する直前にアプリ名を付ける」という共通処理は親が持ち、「実際にどこへ送るか」だけを子クラスに委ねている形です。子クラスは extends で継承し、abstract なメソッドを実装します。

LogNotifier.php
<?php
class LogNotifier extends AbstractNotifier
{
    // abstract なので実装が必須
    protected function send(string $message): void
    {
        echo 'ログ: ' . $message . PHP_EOL;
    }
}

class ConsoleNotifier extends AbstractNotifier
{
    public function __construct(string $appName, private bool $withTime = false)
    {
        // 親のコンストラクタを明示的に呼ぶ
        parent::__construct($appName);
    }

    protected function send(string $message): void
    {
        echo ($this->withTime ? date('H:i:s') . ' ' : '') . $message . PHP_EOL;
    }
}

(new LogNotifier('MyApp'))->notify('バックアップが完了しました');
// ログ: [MyApp] バックアップが完了しました

(new ConsoleNotifier('MyApp'))->notify('処理を開始します');
// [MyApp] 処理を開始します

2つの子クラスはどちらも notify() を書いていませんが、親の実装がそのまま使えるので呼び出せます。書式を変えたくなったら親の format() を直すだけで、両方に反映されます。共通のコードを1か所にまとめつつ、変わる部分だけを子に書かせるのが abstract class の使い方です。

単体では new できない

抽象クラスは中身の決まっていないメソッドを含んでいるため、そのままインスタンス化することはできません。試みると致命的なエラーになります。

ng.php
<?php
$notifier = new AbstractNotifier('MyApp');
// Fatal error: Uncaught Error: Cannot instantiate abstract class AbstractNotifier

ちなみに、abstract なメソッドが1つもなくても abstract を付けたクラスは new できません。「このクラスは必ず継承して使ってほしい」という意思表示としても使えるということです。

継承できる親クラスは1つだけ

PHP は単一継承なので、extends で指定できるクラスは常に1つです。抽象クラスであっても例外ではありません。ここが、いくつでも実装できる interface との決定的な違いになります。

ng.php
<?php
// これは書けない(PHP は多重継承をサポートしていない)
class LogNotifier extends AbstractNotifier, AbstractLogger
{
}

// interface なら何個でも並べられる
class LogNotifier extends AbstractNotifier implements Notifier, Retryable
{
}

extendsimplements を同時に書く場合は、extends を先に書くと決まっています。順序を逆にすると構文エラーになります。

interface と abstract class の違いを整理する

ここまでの内容を表にまとめます。「実装コードを持てるかどうか」と「いくつ指定できるか」の2点が、そのまま使い分けの判断材料になります。

項目interfaceabstract class
役割「このメソッドを持つ」という約束(型)を決める共通処理を持った未完成のクラスを用意する
使う側の書き方implementsextends
いくつ指定できるかいくつでも実装できる1つだけ(単一継承)
実装コード(メソッドの中身)持てない持てる(抽象メソッドと通常メソッドを混在できる)
プロパティ持てない持てる(初期値や状態を保持できる)
定数持てる持てる
コンストラクタ宣言はできるが中身は書けない持てる。子から parent::__construct() で呼べる
メソッドの可視性public のみpublic / protected / privateabstract メソッドに private は不可)
new できるかできないできない

表の最後の行のとおり、どちらも単体ではインスタンス化できません。この点は共通なので、「new できないほう」という覚え方では区別できないことに注意してください。

どちらを使うか迷ったときの判断基準

機能の違いは表のとおりですが、実際に設計するときは「日本語にしたときどう読めるか」で考えると迷いにくくなります。

「〜ができる」なら interface

能力や役割を表すものは interface が向いています。「通知を送れる(Notifier)」「JSON に変換できる(JsonSerializable)」「数えられる(Countable)」のように、「〜できる」と言い換えられる性質がその目印です。こうした能力はクラスの本質とは独立していて、まったく系統の違うクラスが同じ能力を持つこともあります。継承の親子関係に縛られず、いくつでも組み合わせられる interface のほうが素直に表現できます。

もう1つの目印は、あとから実装を差し替える予定があるかです。メール送信を Slack 通知に切り替えたい、テストのときだけ何もしないダミーに置き換えたい——こうした要求があるなら、呼び出し側は interface を受け取るように書いておきます。具体的なクラス名に依存しないコードは、変更に強くなります。

「〜の一種」で共通の土台があるなら abstract class

一方、LogNotifierAbstractNotifier の一種である、というように 「〜の一種」と読める関係で、しかも共有したい実装コードがあるなら abstract class です。先ほどの format() のように「どの子クラスでも同じで、1か所にまとめたい処理」が存在するかどうかが決め手になります。共有したいコードが何もないのであれば、abstract class にする理由はありません。interface で十分です。

また、abstract class はプロパティを持てるので、子クラス共通の状態を扱えます。上の例の $appName のように、全部の子クラスが同じ初期化を必要とするなら、親のコンストラクタでまとめて受け取ってしまうほうが記述量も減ります。ただし継承は1回しか使えない貴重な枠なので、「共通処理を配りたいだけ」という理由で安易に extends するのは避けてください。

trait とはどう違うのか

3つ目の選択肢として trait(トレイト)もあります。trait は実装コードを複数のクラスに配るための仕組みで、いくつでも取り込めます。継承の枠を消費せずにコードを再利用できるのが強みですが、trait は型ではありませんuse SomeTrait; したクラスを $obj instanceof SomeTrait で判定することはできず、引数の型に指定することもできません。

やりたいこと使うもの
型として扱いたい・実装を差し替えたいinterface
共通の土台と状態を持たせ、一部だけ子に書かせたいabstract class
同じコードを継承関係のないクラスに配りたいtrait

実務では、この3つを組み合わせることも珍しくありません。型としての約束は interface で決め、共通実装は abstract class か trait で配るのが基本の形です。抽象クラスに interface を実装させると、両方の利点をそのまま受け取れます。

AbstractNotifier.php
<?php
interface Notifier
{
    public function notify(string $message): void;
}

// 約束は interface、共通実装は abstract class が受け持つ
abstract class AbstractNotifier implements Notifier
{
    public function __construct(protected string $appName) {}

    // interface の約束を、共通の実装で満たしてしまう
    public function notify(string $message): void
    {
        $this->send('[' . $this->appName . '] ' . $message);
    }

    abstract protected function send(string $message): void;
}

class LogNotifier extends AbstractNotifier
{
    protected function send(string $message): void
    {
        error_log($message);
    }
}

var_dump(new LogNotifier('MyApp') instanceof Notifier); // bool(true)

子クラスは send() を書くだけで、自動的に Notifier 型として扱えるようになります。呼び出し側は Notifier だけを見ていればよく、抽象クラスを継承しない独自の実装クラスを別に用意することもできます。

実装漏れやタイプミスで出るエラーの読み方

interface と abstract class を使い始めると、それまで見たことのないエラーメッセージが出るようになります。どれも原因がはっきりしているので、文面と対処を結び付けて覚えてしまいましょう。

contains 1 abstract method と言われる

もっとも頻繁に出るのがこれです。implementsextends を書いたのに、要求されているメソッドを実装していないときに出ます。

MailNotifier.php
<?php
class MailNotifier implements Notifier
{
    public function notifiy(string $message): void  // ← notify の綴りが違う
    {
    }
}
// Fatal error: Class MailNotifier contains 1 abstract method and must therefore
// be declared abstract or implement the remaining method (Notifier::notify)

読み方のコツは、末尾の括弧の中を見ることです。(Notifier::notify) と書かれていれば「Notifier が要求している notify が足りない」という意味で、これが実装すべきメソッドの正体です。上の例のようにメソッド名を1文字打ち間違えただけでも、PHP から見れば「別のメソッドを定義して、約束のメソッドは未実装」という状態なので同じエラーになります。自分では実装したつもりなのにこのエラーが出るときは、まず綴りと大文字・小文字を疑ってください。

なお、メッセージにある「declared abstract(抽象クラスとして宣言する)」も正しい対処のひとつです。途中の階層のクラスで実装しきれないなら、そのクラス自体を abstract class にして、実装を孫クラスに先送りできます。

Cannot instantiate と言われる

Cannot instantiate abstract class AbstractNotifier は抽象クラスを、Cannot instantiate interface Notifier は interface を new しようとしたときのエラーです。原因は単純で、実体のあるクラスではなく設計図のほうを new しているということです。new AbstractNotifier(...) ではなく new LogNotifier(...) のように、具体的な子クラスを指定してください。

設定ファイルなどからクラス名を文字列で受け取って new $className() としている場合にも起こります。この場合はクラス名の指定ミスなので、渡している値を確認します。

must be compatible with と言われる

メソッド名は合っているのに、引数や戻り値の型が約束と食い違っているときのエラーです。

MailNotifier.php
<?php
class MailNotifier implements Notifier
{
    // 引数を1つ増やしてしまった
    public function notify(string $message, string $subject): void
    {
    }
}
// Fatal error: Declaration of MailNotifier::notify(string $message, string $subject): void
// must be compatible with Notifier::notify(string $message): void

このメッセージは 「自分が書いた宣言」と「守るべき宣言」が並べて表示されるのが親切なところです。must be compatible with の後ろが正解なので、その形に合わせれば解決します。どうしても引数を追加したい場合は、string $subject = '' のようにデフォルト値を付けて省略可能にすると互換性が保たれ、エラーになりません。

Access level must be public と言われる

interface のメソッドを実装するときに protectedprivate を付けると、Access level to MailNotifier::notify() must be public というエラーになります。interface のメソッドは必ず public なので、実装側でそれより狭い可視性にすることはできません。可視性は継承・実装のたびに広げることはできても、狭めることはできないというルールがあるためです。

抽象クラスでも同じで、abstract protected function send() を子クラスで private にすると同じエラーになります(public にするのは広げる方向なので問題ありません)。ちなみに abstract private function という宣言自体も、子クラスから見えず実装しようがないため Abstract function A::x() cannot be declared private というエラーになります。

まとめ

interface は「このメソッドを持つ」という約束だけを書いたもので、implements で実装します。メソッドは public のみ、実装コードやプロパティは持てませんが、いくつでも実装でき、型として使えるのが最大の利点です。abstract class は共通処理を持った未完成のクラスで、abstract なメソッドを子クラスに実装させます。プロパティやコンストラクタを持てる反面、extends できるのは1つだけです。判断に迷ったら、「〜ができる」という能力の約束なら interface、「〜の一種」で共有したい実装があるなら abstract class と考えてください。コードの再利用だけが目的なら trait という選択肢もありますが、trait は型にならない点が interface との決定的な違いです。実装漏れや綴りのミスは contains 1 abstract method、シグネチャ違いは must be compatible with というエラーで教えてくれるので、括弧の中に書かれた正解の形を見て直していきましょう。

参考ページ