PHPのインターフェースについて

採用はこちら

PHPのインターフェースとは、クラスに対して「このメソッドを必ず実装してください」と約束させるための仕組みです。

簡単に言うと、インターフェースはクラスが備えるべき機能を定義するための契約です。

通常のクラスのように具体的な処理を書くのではなく、「この名前のメソッドを持っていること」「この引数や戻り値で使えること」を定義します。

例えば、ログを記録するクラスに対して、必ず log() メソッドを持たせたい場合は、次のようにインターフェースを定義できます。

interface Logger
{
    public function log(string $message): void;
}

この Logger インターフェースを実装するクラスは、必ず log() メソッドを実装する必要があります。

class FileLogger implements Logger
{
    public function log(string $message): void
    {
        file_put_contents('app.log', $message . PHP_EOL, FILE_APPEND);
    }
}

implements Logger と書くことで、FileLogger クラスは Logger インターフェースのルールに従うことになります。

目次

インターフェースの基本構文

interfaceで定義する

インターフェースは interface キーワードを使って定義します。

interface インターフェース名
{
    public function メソッド名(引数): 戻り値の型;
}

実装する側のクラスでは、implements を使います。

class クラス名 implements インターフェース名
{
    public function メソッド名(引数): 戻り値の型
    {
        // 実際の処理
    }
}

具体例は次の通りです。

interface PaymentGateway
{
    public function pay(int $amount): bool;
}
class CreditCardPayment implements PaymentGateway
{
    public function pay(int $amount): bool
    {
        echo "{$amount}円をクレジットカードで支払いました。";
        return true;
    }
}

この場合、CreditCardPayment クラスは PaymentGateway インターフェースを実装しているため、pay() メソッドを必ず持つ必要があります。

インターフェースでは基本的に処理を書かない

インターフェースでは、通常、メソッドの中身は書きません。

interface Logger
{
    public function log(string $message): void;
}

これは正しい書き方です。

一方、次のようにメソッドの中に処理を書くことはできません。

interface Logger
{
    public function log(string $message): void
    {
        echo $message;
    }
}

実際の処理は、インターフェースを実装するクラス側に書きます。

class DatabaseLogger implements Logger
{
    public function log(string $message): void
    {
        echo "DBにログを保存しました: {$message}";
    }
}

インターフェースを使う目的

実装ルールを統一できる

インターフェースを使うと、複数のクラスに共通のルールを持たせることができます。

例えば、ログの保存先にはいろいろな種類があります。

ファイルに保存する場合もあれば、データベースに保存する場合もあります。外部APIやSlackに送信するケースもあります。

それぞれ処理内容は異なりますが、「ログを記録する」という操作は共通しています。

interface Logger
{
    public function log(string $message): void;
}

ファイルに保存するクラスは次のように実装できます。

class FileLogger implements Logger
{
    public function log(string $message): void
    {
        file_put_contents('app.log', $message . PHP_EOL, FILE_APPEND);
    }
}

データベースに保存するクラスも、同じ Logger インターフェースを実装できます。

class DatabaseLogger implements Logger
{
    public function log(string $message): void
    {
        echo "データベースにログを保存しました: {$message}";
    }
}

Slackに通知するクラスも同じです。

class SlackLogger implements Logger
{
    public function log(string $message): void
    {
        echo "Slackにログを送信しました: {$message}";
    }
}

このように、処理内容が違っても、Logger を実装しているクラスは必ず log() メソッドを持つことが保証されます。

具体的なクラスに依存しないコードを書ける

インターフェースの大きなメリットは、具体的なクラスに依存せずにコードを書けることです。

例えば、次の関数を見てください。

function writeLog(Logger $logger, string $message): void
{
    $logger->log($message);
}

この関数は、引数に Logger 型を指定しています。

つまり、Logger インターフェースを実装しているクラスであれば、どのクラスでも受け取れます。

writeLog(new FileLogger(), 'ファイルにログを保存します');
writeLog(new DatabaseLogger(), 'DBにログを保存します');
writeLog(new SlackLogger(), 'Slackに通知します');

writeLog() 関数は、渡されたオブジェクトが FileLogger なのか、DatabaseLogger なのか、SlackLogger なのかを知る必要がありません。

ただ、Logger を実装しているなら log() メソッドを持っていることは分かっています。

これにより、コードの柔軟性が高くなります。

インターフェースを使わない場合の問題

具体クラスに依存しやすくなる

インターフェースを使わない場合、コードが特定のクラスに強く依存しやすくなります。

例えば、次のように FileLogger を直接型指定したとします。

function writeLog(FileLogger $logger, string $message): void
{
    $logger->log($message);
}

この関数は FileLogger しか受け取れません。

あとからログの保存先をデータベースに変更したくなった場合、関数の型指定を変更する必要があります。

function writeLog(DatabaseLogger $logger, string $message): void
{
    $logger->log($message);
}

しかし、インターフェースを使えば、次のように書けます。

function writeLog(Logger $logger, string $message): void
{
    $logger->log($message);
}

これなら、FileLoggerDatabaseLoggerSlackLogger など、Logger を実装したクラスを自由に差し替えられます。

変更に弱い設計になりやすい

具体的なクラスに依存しているコードは、仕様変更に弱くなります。

例えば、決済処理で最初はクレジットカードだけに対応していたとします。

class OrderService
{
    public function __construct(private CreditCardPayment $payment)
    {
    }
}

この設計では、OrderServiceCreditCardPayment に直接依存しています。

あとからPayPal決済や銀行振込を追加したい場合、OrderService の中身を変更しなければならない可能性があります。

インターフェースを使うと、この問題を避けやすくなります。

interface PaymentMethod
{
    public function pay(int $amount): bool;
}
class OrderService
{
    public function __construct(private PaymentMethod $payment)
    {
    }

    public function checkout(int $amount): void
    {
        $this->payment->pay($amount);
    }
}

この場合、OrderService は具体的な決済方法ではなく、PaymentMethod という抽象的な契約に依存します。

そのため、決済方法を差し替えやすくなります。

インターフェースは型として使える

引数の型に指定できる

PHPでは、インターフェースを型として使えます。

interface Mailer
{
    public function send(string $to, string $message): bool;
}

このインターフェースを実装するクラスを作ります。

class SmtpMailer implements Mailer
{
    public function send(string $to, string $message): bool
    {
        echo "{$to} にSMTPでメールを送信しました。";
        return true;
    }
}

次に、Mailer インターフェースをコンストラクタの型として使います。

class MailService
{
    public function __construct(private Mailer $mailer)
    {
    }

    public function notifyUser(string $email): void
    {
        $this->mailer->send($email, 'お知らせがあります。');
    }
}

この MailService は、SmtpMailer に直接依存していません。

依存しているのは Mailer インターフェースです。

$mailer = new SmtpMailer();
$service = new MailService($mailer);

$service->notifyUser('user@example.com');

あとから別のメール送信サービスに変更したい場合も、Mailer を実装した別クラスを作れば差し替えられます。

class SendGridMailer implements Mailer
{
    public function send(string $to, string $message): bool
    {
        echo "{$to} にSendGridでメールを送信しました。";
        return true;
    }
}
$mailer = new SendGridMailer();
$service = new MailService($mailer);

MailService 側のコードは変更せずに済みます。

戻り値の型にも指定できる

インターフェースは、引数だけでなく戻り値の型としても使えます。

interface UserRepository
{
    public function findById(int $id): ?array;
}
class UserRepositoryFactory
{
    public function create(): UserRepository
    {
        return new DatabaseUserRepository();
    }
}

このように書くと、create() メソッドは UserRepository を実装したオブジェクトを返すことを保証できます。

インターフェースのメソッドはpublic

実装側もpublicにする必要がある

インターフェースに定義するメソッドは、基本的に public です。

interface Exporter
{
    public function export(array $data): string;
}

実装するクラス側でも、同じように public にする必要があります。

class CsvExporter implements Exporter
{
    public function export(array $data): string
    {
        return 'CSVデータ';
    }
}

これは正しい実装です。

一方、次のように private にするとエラーになります。

class CsvExporter implements Exporter
{
    private function export(array $data): string
    {
        return 'CSVデータ';
    }
}

インターフェースは外部から利用される契約を定義するものです。

そのため、インターフェースで public として定義されたメソッドを、実装クラス側で privateprotected にしてアクセス範囲を狭くすることはできません。

メソッドの型は互換性が必要

基本は同じシグネチャで実装する

インターフェースで定義したメソッドは、実装クラス側で互換性のある形で実装する必要があります。

初心者のうちは、基本的に「同じ引数・同じ戻り値で実装する」と考えると分かりやすいです。

interface Calculator
{
    public function add(int $a, int $b): int;
}

正しい実装は次の通りです。

class SimpleCalculator implements Calculator
{
    public function add(int $a, int $b): int
    {
        return $a + $b;
    }
}

一方、次のように引数や戻り値の型が明らかに合っていない場合はエラーになります。

class SimpleCalculator implements Calculator
{
    public function add(string $a, string $b): string
    {
        return $a . $b;
    }
}

Calculator インターフェースでは int を受け取り、int を返すと定義されています。

そのため、実装側で string を受け取り、string を返すように変えてしまうと、契約に違反します。

厳密には完全一致ではなく互換性が重要

ただし、PHPの型システムを厳密に見ると、「必ず完全一致しなければならない」というより、互換性があることが重要です。

PHPでは、戻り値の型についてはより具体的な型を返せる場合があります。

例えば、次のようなクラス関係があるとします。

class Animal
{
}

class Dog extends Animal
{
}

インターフェースでは Animal を返すと定義します。

interface AnimalFactory
{
    public function create(): Animal;
}

実装クラスで、より具体的な Dog を返すことは可能です。

class DogFactory implements AnimalFactory
{
    public function create(): Dog
    {
        return new Dog();
    }
}

DogAnimal の一種なので、戻り値として互換性があります。

このように、実務では「インターフェースで定義したメソッドと互換性のある形で実装する」と理解しておくと正確です。

インターフェースとプロパティ

PHP 8.3以前では主にメソッドと定数を定義する

従来のPHPでは、インターフェースは主にメソッドと定数を定義するものでした。

例えば、次のようなメソッド定義は一般的です。

interface UserRepository
{
    public function findById(int $id): ?array;
    public function save(array $user): void;
}

また、定数を定義することもできます。

interface HttpStatus
{
    public const OK = 200;
    public const NOT_FOUND = 404;
    public const SERVER_ERROR = 500;
}
echo HttpStatus::OK;

一方で、PHP 8.3以前では、通常のプロパティをインターフェースに直接定義することはできませんでした。

interface UserRepository
{
    public string $table;
}

このような書き方は、PHP 8.3以前では使えません。

PHP 8.4以降ではプロパティ要件を宣言できる

PHP 8.4以降では、プロパティフックの導入により、インターフェースでプロパティ要件を宣言できるようになりました。

例えば、概念的には次のような形で「読み取り可能なプロパティを持つこと」を定義できます。

interface HasName
{
    public string $name { get; }
}

これは、「このインターフェースを実装するクラスは、読み取り可能な $name プロパティを提供する必要がある」という意味です。

また、読み書き可能なプロパティを要求することもできます。

interface HasTitle
{
    public string $title { get; set; }
}

ただし、これはPHP 8.4以降の機能です。

PHP 8.3以前の環境では使えません。

そのため、実務では使用しているPHPのバージョンを確認する必要があります。

初心者はまずメソッド中心で理解するとよい

PHP 8.4以降ではインターフェースにプロパティ要件を書けますが、初心者の段階では、まず次のように理解するとよいです。

インターフェースは、基本的に「このメソッドを実装してください」と定義するものです。

interface Notifier
{
    public function notify(string $message): void;
}

プロパティ要件は比較的新しい機能なので、まずはメソッドの契約を理解することが重要です。

複数のインターフェースを実装できる

implementsで複数指定できる

PHPのクラスは、複数のインターフェースを実装できます。

interface Logger
{
    public function log(string $message): void;
}

interface Notifier
{
    public function notify(string $message): void;
}

この2つのインターフェースを、1つのクラスで実装できます。

class SlackService implements Logger, Notifier
{
    public function log(string $message): void
    {
        echo "Slackログ: {$message}";
    }

    public function notify(string $message): void
    {
        echo "Slack通知: {$message}";
    }
}

implements Logger, Notifier のように、カンマ区切りで複数のインターフェースを指定します。

クラス継承は1つだけ、インターフェースは複数可能

PHPでは、クラスの継承は基本的に1つだけです。

class Child extends ParentClass
{
}

しかし、インターフェースは複数実装できます。

class SomeClass implements A, B, C
{
}

これにより、クラスに複数の役割や能力を持たせることができます。

インターフェースの継承

interfaceはinterfaceを継承できる

インターフェースは、別のインターフェースを継承できます。

interface Readable
{
    public function read(): string;
}

interface Writable
{
    public function write(string $data): void;
}

これらをまとめた Storage インターフェースを作ることができます。

interface Storage extends Readable, Writable
{
    public function delete(string $key): void;
}

Storage を実装するクラスは、read()write()delete() のすべてを実装する必要があります。

class FileStorage implements Storage
{
    public function read(): string
    {
        return 'データを読み込みました';
    }

    public function write(string $data): void
    {
        echo 'データを書き込みました';
    }

    public function delete(string $key): void
    {
        echo "{$key} を削除しました";
    }
}

このように、インターフェース同士を組み合わせることで、より大きな契約を作ることができます。

インターフェースとクラス継承の違い

クラス継承は「何であるか」を表しやすい

クラス継承は、あるクラスが別のクラスの一種であることを表すときに使います。

class Animal
{
    public function eat(): void
    {
        echo '食べます';
    }
}

class Dog extends Animal
{
    public function bark(): void
    {
        echo '吠えます';
    }
}

この場合、DogAnimal の一種です。

つまり、「犬は動物である」という関係を表しています。

インターフェースは「何ができるか」を表しやすい

一方、インターフェースは「そのクラスが何をできるか」を表すのに向いています。

interface Flyable
{
    public function fly(): void;
}
class Bird implements Flyable
{
    public function fly(): void
    {
        echo '飛びます';
    }
}

この場合、BirdFlyable を実装しています。

つまり、「鳥は飛べる」という能力を表しています。

extendsとimplementsの違い

クラス継承とインターフェースの違いを整理すると、次のようになります。

項目クラス継承インターフェース
キーワードextendsimplements
主な目的既存の処理や性質を引き継ぐ実装すべき契約を定義する
メソッドの中身書ける基本的に書かない
複数指定1つのクラスだけ継承可能複数実装可能
関係性「AはBの一種」「AはBできる」

例えば、犬は動物の一種なので、次のような継承は自然です。

class Dog extends Animal
{
}

一方、「通知できる」「ログを出せる」「エクスポートできる」といった能力は、インターフェースで表現しやすいです。

class SlackService implements Notifier, Logger
{
}

インターフェースと抽象クラスの違い

抽象クラスは共通処理を持てる

インターフェースと似たものに、抽象クラスがあります。

抽象クラスは abstract class で定義します。

abstract class Animal
{
    abstract public function makeSound(): void;

    public function sleep(): void
    {
        echo '眠ります';
    }
}

抽象クラスでは、未実装のメソッドと実装済みのメソッドを混在させることができます。

class Dog extends Animal
{
    public function makeSound(): void
    {
        echo 'ワン';
    }
}

この例では、makeSound() は子クラスで実装する必要があります。

一方、sleep() は抽象クラス側に実装済みなので、子クラスでそのまま使えます。

インターフェースは契約を定義する

インターフェースは、基本的に「何を実装すべきか」という契約を定義します。

interface Notifier
{
    public function notify(string $message): void;
}

このインターフェースを実装するクラスは、notify() メソッドを実装する必要があります。

class EmailNotifier implements Notifier
{
    public function notify(string $message): void
    {
        echo "メール通知: {$message}";
    }
}

抽象クラスとの違いを整理する

インターフェースと抽象クラスの違いは次の通りです。

項目インターフェース抽象クラス
キーワードinterfaceabstract class
使用方法implementsextends
主な目的契約を定義する共通処理や基本構造を定義する
通常メソッドの実装基本的に不可可能
プロパティPHP 8.4以降はプロパティ要件を宣言可能通常プロパティや抽象プロパティを持てる
複数指定複数実装できる1つだけ継承できる

実務では、次のように考えると分かりやすいです。

インターフェースは、「この機能を持っているべき」という契約を定義したいときに使います。

抽象クラスは、「共通処理を持たせたうえで、一部だけ子クラスに実装させたい」ときに使います。

インターフェースとトレイトの違い

トレイトは実装を使い回す仕組み

PHPには、trait という仕組みもあります。

トレイトは、複数のクラスで使い回したい処理をまとめるための機能です。

trait Timestampable
{
    public function now(): string
    {
        return date('Y-m-d H:i:s');
    }
}

このトレイトをクラスで使うには、use を使います。

class Post
{
    use Timestampable;
}

class Comment
{
    use Timestampable;
}

これにより、Post クラスでも Comment クラスでも now() メソッドを使えるようになります。

インターフェースは型として使えるがトレイトは型として使えない

インターフェースは型として使えます。

function writeLog(Logger $logger): void
{
    $logger->log('ログを書き込みます');
}

一方、トレイトは型として使うものではありません。

function process(Timestampable $object): void
{
}

このように、トレイト名をインターフェースのように型指定することはできません。

インターフェースとトレイトの役割

インターフェースとトレイトの違いを整理すると、次のようになります。

項目インターフェーストレイト
目的実装すべき契約を定義する実装済みの処理を使い回す
キーワードinterfacetrait
使用方法implementsuse
処理の中身基本的に書かない書ける
型として使えるか使える使えない

インターフェースは「何をできるべきか」を決めるものです。

トレイトは「同じ処理を複数のクラスで使い回す」ためのものです。

インターフェースの実用例

決済処理で使う例

ECサイトでは、複数の決済方法を扱うことがあります。

例えば、クレジットカード、PayPal、銀行振込などです。

それぞれ処理内容は違いますが、「支払う」という操作は共通しています。

このような場合、インターフェースを使うときれいに設計できます。

interface PaymentMethod
{
    public function pay(int $amount): bool;
}

クレジットカード決済の実装です。

class CreditCardPayment implements PaymentMethod
{
    public function pay(int $amount): bool
    {
        echo "{$amount}円をクレジットカードで支払いました。";
        return true;
    }
}

PayPal決済の実装です。

class PaypalPayment implements PaymentMethod
{
    public function pay(int $amount): bool
    {
        echo "{$amount}円をPayPalで支払いました。";
        return true;
    }
}

銀行振込の実装です。

class BankTransferPayment implements PaymentMethod
{
    public function pay(int $amount): bool
    {
        echo "{$amount}円の銀行振込を作成しました。";
        return true;
    }
}

注文処理クラスでは、具体的な決済方法ではなく PaymentMethod に依存します。

class OrderService
{
    public function __construct(private PaymentMethod $paymentMethod)
    {
    }

    public function checkout(int $amount): void
    {
        $success = $this->paymentMethod->pay($amount);

        if ($success) {
            echo '注文が完了しました。';
        } else {
            echo '決済に失敗しました。';
        }
    }
}

使う側では、決済方法を自由に差し替えられます。

$orderService = new OrderService(new CreditCardPayment());
$orderService->checkout(5000);
$orderService = new OrderService(new PaypalPayment());
$orderService->checkout(5000);

OrderService の中身を変更せずに、決済方法を切り替えられる点が大きなメリットです。

メール送信で使う例

メール送信にもインターフェースはよく使われます。

interface MailClient
{
    public function send(string $to, string $subject, string $body): bool;
}

SMTPで送信する実装です。

class SmtpMailClient implements MailClient
{
    public function send(string $to, string $subject, string $body): bool
    {
        echo "{$to} にSMTPでメールを送信しました。";
        return true;
    }
}

外部メールサービスで送信する実装です。

class ApiMailClient implements MailClient
{
    public function send(string $to, string $subject, string $body): bool
    {
        echo "{$to} に外部APIでメールを送信しました。";
        return true;
    }
}

メール通知サービスでは、具体的な送信方法ではなく MailClient に依存します。

class NotificationService
{
    public function __construct(private MailClient $mailClient)
    {
    }

    public function sendWelcomeMail(string $email): void
    {
        $this->mailClient->send(
            $email,
            '会員登録ありがとうございます',
            'ご登録ありがとうございます。'
        );
    }
}

この設計にしておくと、SMTPから外部APIに変更したい場合でも、NotificationService のコードを変更せずに済みます。

Repositoryパターンで使う例

データベースアクセスを抽象化する場合にも、インターフェースはよく使われます。

interface ArticleRepository
{
    public function findPublished(): array;

    public function findBySlug(string $slug): ?array;
}

MySQLから記事を取得する実装です。

class MySqlArticleRepository implements ArticleRepository
{
    public function findPublished(): array
    {
        // MySQLから公開記事一覧を取得する
        return [];
    }

    public function findBySlug(string $slug): ?array
    {
        // MySQLからslugに一致する記事を取得する
        return null;
    }
}

記事サービスでは、具体的なMySQL実装ではなく ArticleRepository に依存します。

class ArticleService
{
    public function __construct(private ArticleRepository $articles)
    {
    }

    public function getPublishedArticles(): array
    {
        return $this->articles->findPublished();
    }
}

このようにすると、データ取得元をMySQLからAPIやCSVに変更したい場合にも対応しやすくなります。

インターフェースとポリモーフィズム

同じ呼び出し方で異なる動きをさせられる

インターフェースは、ポリモーフィズムと深く関係しています。

ポリモーフィズムとは、同じメソッド呼び出しでも、実際のオブジェクトによって異なる動きをすることです。

例えば、次のようなインターフェースを定義します。

interface Animal
{
    public function speak(): void;
}

犬と猫のクラスを作ります。

class Dog implements Animal
{
    public function speak(): void
    {
        echo 'ワン';
    }
}
class Cat implements Animal
{
    public function speak(): void
    {
        echo 'ニャー';
    }
}

次に、Animal 型を受け取る関数を作ります。

function makeAnimalSpeak(Animal $animal): void
{
    $animal->speak();
}

この関数には、DogCat も渡せます。

makeAnimalSpeak(new Dog());
makeAnimalSpeak(new Cat());

呼び出し側は、どちらの場合も speak() を呼んでいるだけです。

しかし、実際の動作はオブジェクトによって変わります。

Dog なら「ワン」、Cat なら「ニャー」と出力されます。

これがポリモーフィズムです。

テストでインターフェースが役立つ理由

本物の外部サービスを使わずにテストできる

インターフェースを使うと、テスト用の実装を作りやすくなります。

例えば、ユーザー情報を取得するインターフェースを定義します。

interface UserRepository
{
    public function findById(int $id): ?array;
}

本番用の実装では、データベースからユーザーを取得します。

class DatabaseUserRepository implements UserRepository
{
    public function findById(int $id): ?array
    {
        // 本来はDBから取得する
        return ['id' => $id, 'name' => '山田太郎'];
    }
}

テスト用には、DBに接続しない偽物の実装を用意できます。

class FakeUserRepository implements UserRepository
{
    public function findById(int $id): ?array
    {
        return ['id' => $id, 'name' => 'テストユーザー'];
    }
}

サービスクラスは、UserRepository インターフェースに依存します。

class UserService
{
    public function __construct(private UserRepository $users)
    {
    }

    public function getUserName(int $id): ?string
    {
        $user = $this->users->findById($id);

        return $user['name'] ?? null;
    }
}

テスト時には、FakeUserRepository を渡せます。

$service = new UserService(new FakeUserRepository());

echo $service->getUserName(1);

これにより、実際のデータベースに接続せずにテストできます。

テストが速くなり、外部環境に依存しにくくなります。

依存性逆転の原則とインターフェース

具体ではなく抽象に依存する

インターフェースは、依存性逆転の原則とも関係します。

依存性逆転の原則を簡単に言うと、具体的なクラスではなく、抽象に依存しようという考え方です。

悪い例を見てみます。

class ReportService
{
    private CsvExporter $exporter;

    public function __construct()
    {
        $this->exporter = new CsvExporter();
    }
}

この設計では、ReportServiceCsvExporter に直接依存しています。

Excel形式で出力したくなった場合、ReportService の中身を変更する必要があります。

インターフェースを使うと、次のように改善できます。

interface Exporter
{
    public function export(array $data): string;
}

CSV出力の実装です。

class CsvExporter implements Exporter
{
    public function export(array $data): string
    {
        return 'CSV';
    }
}

Excel出力の実装です。

class ExcelExporter implements Exporter
{
    public function export(array $data): string
    {
        return 'Excel';
    }
}

ReportService は、具体的な CsvExporter ではなく Exporter に依存します。

class ReportService
{
    public function __construct(private Exporter $exporter)
    {
    }

    public function download(array $data): string
    {
        return $this->exporter->export($data);
    }
}

使う側で、どの出力形式を使うかを決めます。

$service = new ReportService(new CsvExporter());
$service = new ReportService(new ExcelExporter());

この設計にすると、ReportService の中身を変更せずに出力形式を差し替えられます。

PHP標準の代表的なインターフェース

Countable

Countable は、オブジェクトを count() 関数で数えられるようにするためのインターフェースです。

class Cart implements Countable
{
    private array $items = [];

    public function add(string $item): void
    {
        $this->items[] = $item;
    }

    public function count(): int
    {
        return count($this->items);
    }
}
$cart = new Cart();
$cart->add('商品A');
$cart->add('商品B');

echo count($cart);

この場合、count($cart) の結果は 2 になります。

IteratorAggregate

IteratorAggregate は、オブジェクトを foreach で回せるようにするためのインターフェースです。

class ProductList implements IteratorAggregate
{
    private array $products = ['商品A', '商品B', '商品C'];

    public function getIterator(): Traversable
    {
        return new ArrayIterator($this->products);
    }
}
$list = new ProductList();

foreach ($list as $product) {
    echo $product;
}

このようにすると、ProductList オブジェクトを配列のように foreach で扱えます。

JsonSerializable

JsonSerializable は、json_encode() したときの出力内容を制御するためのインターフェースです。

class User implements JsonSerializable
{
    public function __construct(
        private int $id,
        private string $name,
        private string $email
    ) {
    }

    public function jsonSerialize(): mixed
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
        ];
    }
}
$user = new User(1, '山田太郎', 'yamada@example.com');

echo json_encode($user, JSON_UNESCAPED_UNICODE);

この例では、email はJSONに含めていません。

jsonSerialize() で返した配列だけがJSON化されます。

Laravelでのインターフェース利用例

サービスコンテナと組み合わせる

Laravelでは、インターフェースとサービスコンテナを組み合わせて使うことがよくあります。

例えば、決済処理のインターフェースを定義します。

interface PaymentGateway
{
    public function charge(int $amount): bool;
}

Stripe決済の実装を作ります。

class StripePaymentGateway implements PaymentGateway
{
    public function charge(int $amount): bool
    {
        // Stripeで決済する処理
        return true;
    }
}

サービスプロバイダで、インターフェースと実装クラスを紐づけます。

public function register(): void
{
    $this->app->bind(PaymentGateway::class, StripePaymentGateway::class);
}

これにより、コンストラクタで PaymentGateway を指定したときに、Laravelのサービスコンテナが StripePaymentGateway を注入してくれます。

class CheckoutService
{
    public function __construct(private PaymentGateway $paymentGateway)
    {
    }

    public function checkout(int $amount): void
    {
        $this->paymentGateway->charge($amount);
    }
}

あとから決済サービスを変更する場合も、バインド設定を変更すれば対応しやすくなります。

インターフェースを使うべき場面

実装を差し替える可能性がある場合

インターフェースは、実装を差し替える可能性がある場面で特に役立ちます。

例えば、次のような処理です。

  • メール送信
  • 決済処理
  • ログ出力
  • キャッシュ
  • ファイル保存
  • 外部API通信
  • データベースアクセス
  • CSVやExcelなどのエクスポート
  • 通知処理

これらは、あとから実装が変わる可能性があります。

例えば、メール送信ならSMTPから外部APIに変えるかもしれません。

ログ出力なら、ファイル保存からクラウドログサービスに変えるかもしれません。

キャッシュなら、ファイルキャッシュからRedisに変えるかもしれません。

このような変更が想定される箇所では、インターフェースを使う価値があります。

複数の実装がある場合

同じ目的に対して複数の実装がある場合も、インターフェースが役立ちます。

例えば、エクスポート処理を考えます。

interface Exporter
{
    public function export(array $data): string;
}

CSV出力、JSON出力、Excel出力などを別々のクラスで実装できます。

class CsvExporter implements Exporter
{
    public function export(array $data): string
    {
        return 'CSV';
    }
}
class JsonExporter implements Exporter
{
    public function export(array $data): string
    {
        return json_encode($data, JSON_UNESCAPED_UNICODE);
    }
}
class ExcelExporter implements Exporter
{
    public function export(array $data): string
    {
        return 'Excel';
    }
}

使う側は Exporter として扱えばよいため、具体的な出力形式に依存しません。

テスト用の実装を作りたい場合

テストしやすい設計にしたい場合も、インターフェースは便利です。

本番用の実装ではデータベースや外部APIを使い、テスト用の実装では固定データを返すようにできます。

interface ArticleProvider
{
    public function getArticles(): array;
}
class ApiArticleProvider implements ArticleProvider
{
    public function getArticles(): array
    {
        // 外部APIから取得する
        return [];
    }
}
class FakeArticleProvider implements ArticleProvider
{
    public function getArticles(): array
    {
        return [
            ['title' => 'テスト記事1'],
            ['title' => 'テスト記事2'],
        ];
    }
}

このようにしておくと、外部APIに依存せずにテストできます。

インターフェースを使いすぎないことも重要

何でもインターフェース化すればよいわけではない

インターフェースは便利ですが、何でもインターフェース化すればよいわけではありません。

例えば、次のような単純なクラスを考えます。

interface UserNameFormatterInterface
{
    public function format(string $name): string;
}
class UserNameFormatter implements UserNameFormatterInterface
{
    public function format(string $name): string
    {
        return trim($name);
    }
}

このクラスの実装が1つしかなく、今後も差し替える予定がない場合、インターフェースを作ることで逆にファイル数が増え、設計が複雑になることがあります。

インターフェースを作る判断基準

インターフェースを作るかどうかは、次のような観点で判断するとよいです。

判断基準インターフェース化の価値
実装が複数ある高い
将来的に差し替える可能性がある高い
外部APIやDBに依存している高い
テスト用の実装を用意したい高い
チームで実装ルールを揃えたい高い
実装が1つだけで変更予定がない低い場合がある
単純な処理だけのクラス低い場合がある

重要なのは、インターフェースを「何となく作る」のではなく、差し替えやすさ・テストしやすさ・責務の明確化に役立つかどうかで判断することです。

良いインターフェース設計のポイント

役割を明確にする

良いインターフェースは、役割が明確です。

例えば、次のような名前は少し曖昧です。

interface UserServiceInterface
{
}

UserServiceInterface という名前だけでは、何をするためのインターフェースなのか分かりにくいです。

より具体的にすると、責務が見えやすくなります。

interface UserFinder
{
    public function findById(int $id): ?User;
}
interface UserAuthenticator
{
    public function authenticate(string $email, string $password): bool;
}
interface UserNotifier
{
    public function notify(User $user, string $message): void;
}

このように、インターフェース名から役割が分かるようにすると、コードの見通しがよくなります。

メソッドを増やしすぎない

インターフェースには、必要以上に多くのメソッドを詰め込まない方がよいです。

例えば、次のようなインターフェースは責務が多すぎます。

interface UserManager
{
    public function findUser(int $id): ?array;

    public function createUser(array $data): void;

    public function updateUser(int $id, array $data): void;

    public function deleteUser(int $id): void;

    public function sendWelcomeEmail(int $id): void;

    public function exportUsers(): string;

    public function importUsers(string $csv): void;
}

このインターフェースには、ユーザー取得、作成、更新、削除、メール送信、エクスポート、インポートまで含まれています。

責務が多すぎるため、実装クラスが不要なメソッドまで持たなければならなくなる可能性があります。

分割すると、より扱いやすくなります。

interface UserReader
{
    public function findUser(int $id): ?array;
}
interface UserWriter
{
    public function createUser(array $data): void;

    public function updateUser(int $id, array $data): void;

    public function deleteUser(int $id): void;
}
interface UserExporter
{
    public function exportUsers(): string;
}
interface UserNotifier
{
    public function sendWelcomeEmail(int $id): void;
}

このように小さく分けると、必要な機能だけに依存できるようになります。

利用側の都合で設計する

インターフェースは、実装側の都合ではなく、利用側の都合で設計すると使いやすくなります。

例えば、あるサービスが「記事一覧を取得したい」だけなら、次のようなインターフェースで十分です。

interface PublishedArticleProvider
{
    public function getPublishedArticles(): array;
}

実装側がMySQLなのか、APIなのか、CSVなのかは関係ありません。

利用側が必要としている操作をインターフェースにすることで、余計な依存を減らせます。

インターフェース名の付け方

Interfaceを末尾につけるパターン

PHPでは、インターフェース名の付け方に絶対的なルールはありません。

よくある命名の1つが、末尾に Interface を付けるパターンです。

interface PaymentGatewayInterface
{
    public function pay(int $amount): bool;
}

この命名は分かりやすく、インターフェースであることが名前からすぐに分かります。

役割名だけにするパターン

一方で、役割名だけにするパターンもあります。

interface Logger
{
    public function log(string $message): void;
}
interface Notifier
{
    public function notify(string $message): void;
}

こちらは自然な英語として読みやすく、コードもすっきりします。

能力を表す名前にするパターン

「何ができるか」を表す名前にするのもよい方法です。

interface Exportable
{
    public function export(): string;
}
interface Searchable
{
    public function search(string $keyword): array;
}

PHP標準にも、CountableJsonSerializable のように、能力を表すインターフェースがあります。

命名では、チームやプロジェクトのルールに合わせることが大切です。

インターフェースでよくあるエラー

必要なメソッドを実装していない

インターフェースを実装したのに、必要なメソッドを書き忘れるとエラーになります。

interface Notifier
{
    public function notify(string $message): void;
}
class EmailNotifier implements Notifier
{
}

このクラスは Notifier を実装しているにもかかわらず、notify() を定義していません。

正しくは次のように書きます。

class EmailNotifier implements Notifier
{
    public function notify(string $message): void
    {
        echo "メール通知: {$message}";
    }
}

アクセス修飾子が合っていない

インターフェースで public として定義されたメソッドを、実装側で privateprotected にすることはできません。

interface Exporter
{
    public function export(array $data): string;
}
class CsvExporter implements Exporter
{
    protected function export(array $data): string
    {
        return 'CSV';
    }
}

このような実装はエラーになります。

正しくは public にします。

class CsvExporter implements Exporter
{
    public function export(array $data): string
    {
        return 'CSV';
    }
}

型の互換性がない

インターフェースで定義した型と互換性のない型に変えてしまうとエラーになります。

interface Formatter
{
    public function format(string $text): string;
}
class NumberFormatter implements Formatter
{
    public function format(int $text): int
    {
        return $text;
    }
}

この実装は、引数と戻り値の型がインターフェースと合っていません。

正しくは、互換性のある型で実装します。

class TextFormatter implements Formatter
{
    public function format(string $text): string
    {
        return trim($text);
    }
}

PHPのインターフェースまとめ

PHPのインターフェースは、クラスに対して「この機能を必ず持ってください」と約束させるための仕組みです。

基本形は次の通りです。

interface Logger
{
    public function log(string $message): void;
}
class FileLogger implements Logger
{
    public function log(string $message): void
    {
        echo $message;
    }
}

インターフェースを使うことで、実装ルールを統一できます。

また、インターフェースは型として使えるため、具体的なクラスに依存しない柔軟なコードを書けます。

決済処理、メール送信、ログ出力、キャッシュ、外部API通信、Repository、エクスポート処理など、実装を差し替える可能性がある箇所では特に有効です。

ただし、何でもインターフェース化すればよいわけではありません。

実装が1つしかなく、差し替える予定もない単純なクラスでは、インターフェースを作ることでかえって設計が複雑になる場合もあります。

インターフェースを使うかどうかは、次の観点で判断するとよいです。

観点判断
複数の実装がある使う価値が高い
将来的に差し替える可能性がある使う価値が高い
外部APIやDBに依存している使う価値が高い
テスト用の実装を作りたい使う価値が高い
単純で実装が1つだけ無理に作らなくてもよい

一言でまとめると、PHPのインターフェースは、クラスの振る舞いを保証し、変更に強い設計を作るための契約です。

特に実務では、具体的な実装に依存せず、インターフェースに依存することで、保守しやすく、テストしやすく、拡張しやすいコードを書けるようになります。

以上、PHPのインターフェースについてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次