PHPの継承とオーバーライドについて

採用はこちら

PHPの継承とは、あるクラスの機能を別のクラスが受け継ぐ仕組みです。

継承元のクラスを「親クラス」または「スーパークラス」、継承先のクラスを「子クラス」または「サブクラス」と呼びます。

一方、オーバーライドとは、親クラスで定義されているメソッドを、子クラス側で同じ名前で定義し直すことです。

これにより、親クラスの基本的な処理を受け継ぎつつ、子クラスごとに異なる動作を実装できます。

PHPでは、クラスを継承するときに extends を使います。

class ChildClass extends ParentClass
{
}

たとえば、Animal クラスを継承して Dog クラスを作る場合は、次のように書きます。

<?php

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

    public function sleep(): void
    {
        echo "眠ります\n";
    }
}

class Dog extends Animal
{
    public function bark(): void
    {
        echo "ワンワン\n";
    }
}

$dog = new Dog();

$dog->eat();
$dog->sleep();
$dog->bark();

Dog クラスには eat()sleep() を定義していません。

しかし、Animal クラスを継承しているため、親クラスのメソッドを利用できます。

このように、継承を使うと共通処理を親クラスにまとめ、子クラスでは個別の処理だけを書くことができます。

目次

PHPで継承を使うメリット

共通処理をまとめられる

継承の大きなメリットは、複数のクラスに共通する処理を親クラスにまとめられることです。

たとえば、一般ユーザーと管理者ユーザーがいる場合、ログインやログアウトなどの基本処理は共通です。

<?php

class User
{
    protected string $name;

    public function __construct(string $name)
    {
        $this->name = $name;
    }

    public function login(): void
    {
        echo "{$this->name} がログインしました\n";
    }

    public function logout(): void
    {
        echo "{$this->name} がログアウトしました\n";
    }
}

class AdminUser extends User
{
    public function deletePost(): void
    {
        echo "{$this->name} が投稿を削除しました\n";
    }
}

$admin = new AdminUser("田中");

$admin->login();
$admin->deletePost();
$admin->logout();

login()logout() は全ユーザーに共通する処理なので、親クラスである User に定義しています。

一方、deletePost() は管理者だけが持つ処理なので、子クラスである AdminUser に定義しています。

このように設計すると、共通処理の重複を避けられます。

子クラスごとに処理を変えられる

継承を使うと、共通の枠組みを持たせながら、子クラスごとに異なる処理を実装できます。

たとえば、動物ごとに鳴き声を変える場合を考えます。

<?php

class Animal
{
    public function speak(): void
    {
        echo "何か鳴きます\n";
    }
}

class Dog extends Animal
{
    public function speak(): void
    {
        echo "ワンワン\n";
    }
}

class Cat extends Animal
{
    public function speak(): void
    {
        echo "ニャー\n";
    }
}

$dog = new Dog();
$cat = new Cat();

$dog->speak();
$cat->speak();

Animal クラスには speak() という共通メソッドがあります。

しかし、Dog クラスと Cat クラスでは、それぞれ同じ speak() を独自に定義しています。

このように、親クラスのメソッドを子クラスで上書きすることをオーバーライドと呼びます。

オーバーライドとは

親クラスのメソッドを子クラスで上書きすること

オーバーライドは、親クラスにあるメソッドを、子クラスで同じ名前のメソッドとして定義し直す仕組みです。

<?php

class ParentClass
{
    public function message(): void
    {
        echo "親クラスのメソッドです\n";
    }
}

class ChildClass extends ParentClass
{
    public function message(): void
    {
        echo "子クラスのメソッドです\n";
    }
}

$obj = new ChildClass();
$obj->message();

このコードでは、ChildClassParentClassmessage() をオーバーライドしています。

そのため、ChildClass のインスタンスから message() を呼び出すと、親クラスではなく子クラスの message() が実行されます。

出力結果は次のようになります。

子クラスのメソッドです

オーバーライドは処理を完全に置き換えるだけではない

オーバーライドは、親クラスの処理を完全に置き換えるためだけに使うものではありません。

親クラスの処理を実行したうえで、子クラス側の処理を追加することもできます。

そのときに使うのが parent:: です。

parent::で親クラスのメソッドを呼び出す

親の処理を残したまま子クラスで処理を追加する

子クラスで親クラスのメソッドをオーバーライドすると、通常は子クラスのメソッドが優先されます。

しかし、子クラスのメソッド内で親クラスのメソッドを呼び出したい場合は、parent::メソッド名() を使います。

<?php

class User
{
    public function login(): void
    {
        echo "ログイン処理\n";
    }
}

class AdminUser extends User
{
    public function login(): void
    {
        parent::login();

        echo "管理者用のログを記録\n";
    }
}

$admin = new AdminUser();
$admin->login();

出力結果は次のようになります。

ログイン処理
管理者用のログを記録

AdminUser クラスでは login() をオーバーライドしていますが、その中で parent::login() を呼び出しています。

これにより、親クラスのログイン処理を実行したあとに、管理者用の追加処理を実行できます。

parent::は実務でもよく使う

parent:: は、親クラスの基本処理を残しつつ、子クラス固有の処理を追加したい場合によく使います。

たとえば、保存処理や初期化処理などで使われます。

public function save(): void
{
    parent::save();

    // 子クラス固有の追加処理
}

「親の処理を完全に消す」のではなく、「親の処理に追加する」場合は、parent:: を使うと分かりやすい設計になります。

コンストラクタの継承とオーバーライド

子クラスでコンストラクタを書くと親のコンストラクタは自動実行されない

PHPでは、コンストラクタ __construct() も継承やオーバーライドの対象になります。

ただし、子クラスで __construct() を定義した場合、親クラスのコンストラクタは自動では呼び出されません。

親クラスの初期化処理も実行したい場合は、子クラスのコンストラクタ内で parent::__construct() を明示的に呼ぶ必要があります。

<?php

class User
{
    protected string $name;

    public function __construct(string $name)
    {
        $this->name = $name;
        echo "Userのコンストラクタ\n";
    }
}

class AdminUser extends User
{
    private string $role;

    public function __construct(string $name, string $role)
    {
        parent::__construct($name);

        $this->role = $role;
        echo "AdminUserのコンストラクタ\n";
    }
}

$admin = new AdminUser("佐藤", "super_admin");

出力結果は次のようになります。

Userのコンストラクタ
AdminUserのコンストラクタ

parent::__construct($name); を書くことで、親クラスのコンストラクタが実行されます。

親の初期化を忘れると不具合の原因になる

子クラスでコンストラクタを定義すると、親クラスの初期化処理は自動で実行されません。

そのため、親クラスで必要なプロパティを初期化している場合、parent::__construct() を呼び忘れると不具合につながります。

class AdminUser extends User
{
    public function __construct(string $name, string $role)
    {
        $this->role = $role;
    }
}

このように書くと、親クラス側の $name 初期化処理が実行されません。

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

class AdminUser extends User
{
    public function __construct(string $name, string $role)
    {
        parent::__construct($name);

        $this->role = $role;
    }
}

アクセス修飾子と継承

public、protected、privateの違い

PHPのアクセス修飾子には、主に publicprotectedprivate があります。

修飾子クラス外から子クラスから同じクラス内から
public使える使える使える
protected使えない使える使える
private使えない使えない使える

継承で特に重要なのは、protectedprivate の違いです。

protected は、定義したクラス自身と、その子クラスからアクセスできます。

一方、private は定義したクラス自身からしかアクセスできません。

子クラスからも直接アクセスできません。

protectedは子クラスから使える

<?php

class User
{
    protected string $name = "山田";
}

class AdminUser extends User
{
    public function showName(): void
    {
        echo $this->name;
    }
}

$admin = new AdminUser();
$admin->showName();

$nameprotected なので、子クラスである AdminUser からアクセスできます。

privateは子クラスから直接使えない

<?php

class User
{
    private string $password = "secret";
}

class AdminUser extends User
{
    public function showPassword(): void
    {
        echo $this->password;
    }
}

$admin = new AdminUser();
$admin->showPassword();

このコードは期待通りには動きません。

$passwordprivate なので、User クラスの中からしか直接アクセスできないためです。

親クラスの内部データを子クラスから使いたい場合は、protected にするか、protected または public なメソッドを用意します。

<?php

class User
{
    private string $password = "secret";

    protected function getPassword(): string
    {
        return $this->password;
    }
}

class AdminUser extends User
{
    public function showPassword(): void
    {
        echo $this->getPassword();
    }
}

ただし、パスワードのような機密情報をそのまま返す設計は実務では避けるべきです。

この例は、あくまでアクセス修飾子の違いを説明するためのものです。

オーバーライド時のアクセス修飾子のルール

アクセス範囲を狭くすることはできない

子クラスでメソッドをオーバーライドする場合、アクセス範囲を親クラスより狭くすることはできません。

たとえば、親クラスで public だったメソッドを、子クラスで protectedprivate に変更することはできません。

これはNGです。

<?php

class User
{
    public function login(): void
    {
        echo "ログイン";
    }
}

class AdminUser extends User
{
    protected function login(): void
    {
        echo "管理者ログイン";
    }
}

親クラスの login()public なので、子クラスでも public にする必要があります。

アクセス範囲を広げることはできる

一方、親クラスで protected だったメソッドを、子クラスで public にすることは可能です。

<?php

class User
{
    protected function checkPermission(): void
    {
        echo "権限確認";
    }
}

class AdminUser extends User
{
    public function checkPermission(): void
    {
        echo "管理者権限確認";
    }
}

この場合、アクセス範囲を広げているため問題ありません。

オーバーライド時の型の互換性

親メソッドと互換性のあるシグネチャにする必要がある

PHPでは、メソッドをオーバーライドするとき、親クラスのメソッドと互換性のあるシグネチャにする必要があります。

シグネチャとは、メソッド名、引数、戻り値の型などを含むメソッドの形のことです。

たとえば、親クラスで string を返すと定義しているのに、子クラスで int を返すように変更すると互換性がありません。

<?php

class Animal
{
    public function speak(): string
    {
        return "何か鳴きます";
    }
}

class Dog extends Animal
{
    public function speak(): int
    {
        return 123;
    }
}

これはNGです。

親クラスの speak()string を返すという約束をしています。

その子クラスである Dogint を返すと、呼び出し側の期待を壊してしまいます。

戻り値型はより具体的にできる

オーバーライド時、戻り値型は親クラスより具体的にできます。

<?php

class Animal
{
}

class Dog extends Animal
{
}

class AnimalFactory
{
    public function create(): Animal
    {
        return new Animal();
    }
}

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

親クラスでは Animal を返しています。

子クラスでは Dog を返しています。

DogAnimal の一種なので、この変更は問題ありません。

引数型はより広くできる

引数型については、子クラス側でより広い型にできます。

<?php

class Animal
{
}

class Dog extends Animal
{
}

class Trainer
{
    public function train(Dog $dog): void
    {
        echo "犬を訓練します";
    }
}

class FlexibleTrainer extends Trainer
{
    public function train(Animal $animal): void
    {
        echo "動物を訓練します";
    }
}

親クラスでは Dog を受け取るメソッドでした。

子クラスでは、より広い Animal を受け取るようにしています。

AnimalDog より広い型なので、呼び出し側の期待を壊しません。

このようなルールは、戻り値型の共変性、引数型の反変性と呼ばれます。

初心者のうちは、まず次のように覚えるとよいです。

戻り値型は、より具体的にできる
引数型は、より広くできる

finalで継承やオーバーライドを禁止する

finalメソッドはオーバーライドできない

親クラスのメソッドに final を付けると、子クラスでそのメソッドをオーバーライドできなくなります。

<?php

class User
{
    final public function getId(): int
    {
        return 1;
    }
}

class AdminUser extends User
{
    public function getId(): int
    {
        return 999;
    }
}

このコードはエラーになります。

User クラスの getId()final で定義されているため、子クラスで上書きできません。

finalクラスは継承できない

クラス自体に final を付けると、そのクラスを継承できなくなります。

<?php

final class Config
{
    public function getAppName(): string
    {
        return "My App";
    }
}

class CustomConfig extends Config
{
}

このコードもエラーになります。

Configfinal class なので、他のクラスは Config を継承できません。

finalは重要な処理を守るために使う

final は、子クラスで勝手に変更されたくない処理に使います。

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

  • 認証処理
  • 決済処理
  • ID生成処理
  • セキュリティに関わる処理
  • アプリケーション全体の設定処理

ただし、何でも final にすると拡張しづらくなります。

変更されたくない明確な理由がある場合に使うとよいでしょう。

privateメソッドは通常の意味ではオーバーライドされない

privateメソッドは子クラスから見えない

PHPでは、親クラスの private メソッドは子クラスからアクセスできません。

そのため、子クラスで同じ名前のメソッドを定義しても、通常の意味で親の private メソッドをオーバーライドしたことにはなりません。

<?php

class ParentClass
{
    private function test(): void
    {
        echo "Parent private test\n";
    }

    public function run(): void
    {
        $this->test();
    }
}

class ChildClass extends ParentClass
{
    public function test(): void
    {
        echo "Child public test\n";
    }
}

$obj = new ChildClass();
$obj->run();

この場合、出力結果は次のようになります。

Parent private test

ChildClass にも test() はありますが、ParentClassrun() 内で呼ばれる test() は、親クラス自身の private function test() です。

子クラスで差し替えたい処理はprotectedにする

子クラスで処理を差し替えたい場合は、親クラスのメソッドを private ではなく protected または public にします。

<?php

class ParentClass
{
    protected function test(): void
    {
        echo "Parent protected test\n";
    }

    public function run(): void
    {
        $this->test();
    }
}

class ChildClass extends ParentClass
{
    protected function test(): void
    {
        echo "Child protected test\n";
    }
}

$obj = new ChildClass();
$obj->run();

出力結果は次のようになります。

Child protected test

この場合は、子クラスの test() が親クラスの test() をオーバーライドしています。

staticメソッドの継承とオーバーライド

staticメソッドもオーバーライドできる

PHPでは、static メソッドも継承できます。

<?php

class User
{
    public static function getType(): string
    {
        return "user";
    }
}

class AdminUser extends User
{
    public static function getType(): string
    {
        return "admin";
    }
}

echo User::getType();
echo AdminUser::getType();

出力結果は次のようになります。

user
admin

AdminUser クラスでは、User クラスの getType() をオーバーライドしています。

staticメソッドでは$thisは使えない

static メソッドは、インスタンスを作らずに呼び出せるメソッドです。

そのため、static メソッドの中では $this を使えません。

class User
{
    public static function getName(): string
    {
        return $this->name;
    }
}

これはNGです。

$this はインスタンス自身を表すため、インスタンスを前提としない static メソッドでは使えません。

self::、parent::、static::の違い

self::は定義されているクラスを指す

self:: は、そのコードが書かれているクラス自身を指します。

class Base
{
    public static function who(): void
    {
        echo self::class;
    }
}

class Child extends Base
{
}

Child::who();

この場合、出力は次のようになります。

Base

who()Base クラスに定義されているため、self::classBase を指します。

parent::は親クラスを指す

parent:: は、親クラスを指します。

class BaseController
{
    public function beforeAction(): void
    {
        echo "共通の前処理\n";
    }
}

class UserController extends BaseController
{
    public function beforeAction(): void
    {
        parent::beforeAction();

        echo "ユーザー画面用の前処理\n";
    }
}

parent::beforeAction() は、親クラスである BaseControllerbeforeAction() を呼び出しています。

static::は実行時に呼び出されたクラスを指す

static:: は、実行時に呼び出されたクラスを指します。

この仕組みは、Late Static Bindings、つまり遅延静的束縛と呼ばれます。

class Base
{
    public static function whoSelf(): void
    {
        echo self::class . PHP_EOL;
    }

    public static function whoStatic(): void
    {
        echo static::class . PHP_EOL;
    }
}

class Child extends Base
{
}

Child::whoSelf();
Child::whoStatic();

出力結果は次のようになります。

Base
Child

self:: はメソッドが定義されている Base を指します。

一方、static:: は実際に呼び出された Child を指します。

プロパティの継承

publicとprotectedのプロパティは子クラスで使える

PHPでは、メソッドだけでなくプロパティも継承されます。

ただし、子クラスから直接利用できるのは public または protected のプロパティです。

<?php

class Product
{
    protected string $name;
    protected int $price;

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

    public function getLabel(): string
    {
        return "{$this->name}: {$this->price}円";
    }
}

class DigitalProduct extends Product
{
    private string $downloadUrl;

    public function __construct(string $name, int $price, string $downloadUrl)
    {
        parent::__construct($name, $price);

        $this->downloadUrl = $downloadUrl;
    }

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

$product = new DigitalProduct("電子書籍", 1200, "https://example.com/download");

echo $product->getLabel();

DigitalProduct は、Product$name$pricegetLabel() を利用しています。

ただし、$name$priceprotected なので、子クラスからアクセスできます。

クラス定数の継承とオーバーライド

クラス定数も子クラスで再定義できる

PHPでは、クラス定数も継承できます。

<?php

class User
{
    public const TYPE = "user";
}

class AdminUser extends User
{
    public const TYPE = "admin";
}

echo User::TYPE;
echo AdminUser::TYPE;

出力結果は次のようになります。

user
admin

このように、子クラスで同じ名前のクラス定数を定義することもできます。

finalなクラス定数は再定義できない

PHP 8.1以降では、final を付けたクラス定数を子クラスで再定義できません。

<?php

class User
{
    final public const TYPE = "user";
}

class AdminUser extends User
{
    public const TYPE = "admin";
}

このコードはエラーになります。

TYPEfinal なクラス定数として定義されているためです。

抽象クラスとオーバーライド

abstractで子クラスに実装を強制できる

継承と一緒によく使われる仕組みに、抽象クラスがあります。

抽象クラスは、直接インスタンス化せず、子クラスに共通仕様を持たせるために使います。

<?php

abstract class Payment
{
    abstract public function pay(int $amount): void;

    public function log(int $amount): void
    {
        echo "{$amount}円の支払い処理を記録しました\n";
    }
}

class CreditCardPayment extends Payment
{
    public function pay(int $amount): void
    {
        echo "クレジットカードで{$amount}円支払いました\n";
    }
}

class BankTransferPayment extends Payment
{
    public function pay(int $amount): void
    {
        echo "銀行振込で{$amount}円支払いました\n";
    }
}

$payment = new CreditCardPayment();
$payment->pay(5000);
$payment->log(5000);

Payment クラスでは、pay() を抽象メソッドとして定義しています。

抽象メソッドは、子クラス側で必ず実装する必要があります。

そのため、CreditCardPaymentBankTransferPayment は、それぞれ独自の pay() を定義しています。

共通処理と個別処理を分けられる

抽象クラスを使うと、共通処理と個別処理を分けて設計できます。

上の例では、log() はすべての支払い方法で共通する処理です。

一方、pay() は支払い方法ごとに異なる処理です。

このような場合に、抽象クラスは便利です。

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

継承は実装を受け継ぐ仕組み

クラス継承は、親クラスに定義された実装を子クラスが受け継ぐ仕組みです。

class AdminUser extends User
{
}

AdminUserUser の一種として扱われ、User に定義されたメソッドやプロパティを利用できます。

インターフェースは実装すべき内容を決める契約

インターフェースは、「このメソッドを必ず持つ」という契約を定義するために使います。

<?php

interface Notifiable
{
    public function send(string $message): void;
}

class EmailNotification implements Notifiable
{
    public function send(string $message): void
    {
        echo "メール送信: {$message}\n";
    }
}

class SlackNotification implements Notifiable
{
    public function send(string $message): void
    {
        echo "Slack送信: {$message}\n";
    }
}

EmailNotificationSlackNotification は、どちらも Notifiable を実装しています。

そのため、どちらのクラスも send() メソッドを持つ必要があります。

extendsとimplementsの違い

クラスを継承する場合は extends を使います。

class AdminUser extends User
{
}

インターフェースを実装する場合は implements を使います。

class EmailNotification implements Notifiable
{
}

クラスの継承は基本的に1つの親クラスしか指定できません。

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

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

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

class User implements Loggable, Notifiable
{
    public function log(string $message): void
    {
    }

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

トレイトとの違い

PHPはクラスの多重継承ができない

PHPでは、1つのクラスが直接継承できる親クラスは1つだけです。

class Child extends ParentClass
{
}

次のように、複数の親クラスを同時に継承することはできません。

class Child extends ParentA, ParentB
{
}

これはNGです。

コード再利用にはトレイトを使うこともある

PHPには、クラス間でコードを再利用するための仕組みとしてトレイトがあります。

<?php

trait LoggerTrait
{
    public function log(string $message): void
    {
        echo "[LOG] {$message}\n";
    }
}

class OrderService
{
    use LoggerTrait;

    public function createOrder(): void
    {
        $this->log("注文を作成しました");
    }
}

$service = new OrderService();
$service->createOrder();

トレイトは継承とは異なります。

親子関係を作るのではなく、共通のメソッドをクラスに取り込むための仕組みです。

「AはBの一種」という関係ではないけれど、共通の処理だけ使い回したい場合に便利です。

実務での継承の使い方

ECサイトの商品クラスの例

ECサイトでは、通常商品、ダウンロード商品、定期購入商品など、似た性質を持つクラスを扱うことがあります。

共通する項目や処理は親クラスにまとめ、商品ごとの違いを子クラスに書くと分かりやすくなります。

<?php

class Product
{
    protected string $name;
    protected int $price;

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

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

    public function getPrice(): int
    {
        return $this->price;
    }

    public function getDescription(): string
    {
        return "{$this->name} は {$this->price}円です。";
    }
}

class DownloadProduct extends Product
{
    private string $fileUrl;

    public function __construct(string $name, int $price, string $fileUrl)
    {
        parent::__construct($name, $price);

        $this->fileUrl = $fileUrl;
    }

    public function getDescription(): string
    {
        return parent::getDescription() . " ダウンロード商品です。";
    }

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

$product = new DownloadProduct(
    "マーケティング資料PDF",
    3000,
    "https://example.com/file.pdf"
);

echo $product->getDescription();

出力結果は次のようになります。

マーケティング資料PDF は 3000円です。 ダウンロード商品です。

DownloadProductProduct を継承しています。

そして、getDescription() をオーバーライドし、親クラスの説明文に「ダウンロード商品です。」という文言を追加しています。

コントローラーの共通処理の例

Webアプリケーションでは、複数のコントローラーで共通する処理を親クラスにまとめることがあります。

<?php

class BaseController
{
    protected function render(string $view, array $data = []): void
    {
        echo "ビュー {$view} を表示します\n";
    }

    protected function requireLogin(): void
    {
        echo "ログインチェックを行います\n";
    }
}

class MyPageController extends BaseController
{
    public function index(): void
    {
        $this->requireLogin();

        $this->render("mypage/index", [
            "title" => "マイページ",
        ]);
    }
}

$controller = new MyPageController();
$controller->index();

render()requireLogin() は、複数のコントローラーで使う可能性がある共通処理です。

このような処理を BaseController にまとめておくと、各コントローラーでは個別の画面処理だけを書けます。

継承を使うべきケース

明確な「AはBの一種」という関係がある場合

継承を使うべき代表的なケースは、「AはBの一種」と言える関係がある場合です。

たとえば、次のような関係です。

Dog は Animal の一種
AdminUser は User の一種
CreditCardPayment は Payment の一種
DownloadProduct は Product の一種

このような関係であれば、継承は自然です。

class AdminUser extends User
{
}

AdminUserUser の一種なので、継承関係として分かりやすいです。

共通処理と個別処理を分けたい場合

複数のクラスに共通する処理があり、それぞれ一部だけ動作を変えたい場合にも継承は有効です。

たとえば、支払い処理では、ログ出力は共通でも、実際の支払い方法はクレジットカード、銀行振込、QR決済などで異なります。

このような場合は、抽象クラスやオーバーライドを使うことで、共通処理と個別処理を分けられます。

継承を使いすぎると起きる問題

継承階層が深くなると理解しにくくなる

継承は便利ですが、使いすぎると設計が複雑になります。

たとえば、次のように継承階層が深くなると、どこで何が定義されているのか分かりにくくなります。

BaseModel
  └── UserModel
        └── AdminUserModel
              └── SuperAdminUserModel

このような設計では、親クラスを少し変更しただけで、多くの子クラスに影響が出る可能性があります。

また、メソッドがどのクラスで定義され、どのクラスでオーバーライドされているのかを追いにくくなります。

「共通処理があるから継承」は危険

共通処理があるからといって、必ず継承すべきとは限りません。

重要なのは、「子クラスは本当に親クラスの一種なのか」という点です。

たとえば、次の継承は自然です。

class CreditCardPayment extends Payment
{
}

クレジットカード決済は、支払い方法の一種だからです。

一方、次のような継承は不自然です。

class Order extends Logger
{
}

注文はログ出力機能の一種ではありません。

この場合、継承ではなく、ログ出力用のクラスを別に持たせる方が自然です。

<?php

class Logger
{
    public function log(string $message): void
    {
        echo "[LOG] {$message}\n";
    }
}

class OrderService
{
    private Logger $logger;

    public function __construct(Logger $logger)
    {
        $this->logger = $logger;
    }

    public function createOrder(): void
    {
        $this->logger->log("注文を作成しました");
    }
}

$logger = new Logger();
$service = new OrderService($logger);

$service->createOrder();

このように、別のオブジェクトとして機能を持たせる設計をコンポジションと呼びます。

実務では、継承よりもコンポジションの方が柔軟な設計になることも多いです。

PHPの継承とオーバーライドでよくある間違い

親クラスのコンストラクタを呼び忘れる

子クラスでコンストラクタを定義した場合、親クラスのコンストラクタは自動実行されません。

class AdminUser extends User
{
    public function __construct(string $name, string $role)
    {
        $this->role = $role;
    }
}

親クラスで必要な初期化処理がある場合は、必ず parent::__construct() を呼びます。

class AdminUser extends User
{
    public function __construct(string $name, string $role)
    {
        parent::__construct($name);

        $this->role = $role;
    }
}

privateを子クラスから使おうとする

private なプロパティやメソッドは、子クラスから直接使えません。

class User
{
    private string $name = "山田";
}

class AdminUser extends User
{
    public function showName(): void
    {
        echo $this->name;
    }
}

子クラスから使いたい場合は、protected にするか、getterメソッドを用意します。

class User
{
    private string $name = "山田";

    protected function getName(): string
    {
        return $this->name;
    }
}

class AdminUser extends User
{
    public function showName(): void
    {
        echo $this->getName();
    }
}

アクセス修飾子を狭くしてしまう

親クラスで public だったメソッドを、子クラスで protected にすることはできません。

class User
{
    public function login(): void
    {
    }
}

class AdminUser extends User
{
    protected function login(): void
    {
    }
}

このようにアクセス範囲を狭くするとエラーになります。

オーバーライド時は、親クラスと同じか、より広いアクセス範囲にする必要があります。

型の互換性を壊してしまう

親クラスのメソッドと互換性のない型に変更するとエラーになります。

class Animal
{
    public function speak(): string
    {
        return "何か鳴きます";
    }
}

class Dog extends Animal
{
    public function speak(): int
    {
        return 123;
    }
}

親クラスが string を返すと定義しているのに、子クラスで int を返すのは互換性がありません。

オーバーライドするときは、親クラスのメソッドと互換性のあるシグネチャにする必要があります。

PHPの継承とオーバーライドのまとめ

PHPの継承は、親クラスの機能を子クラスが受け継ぐ仕組みです。

class ChildClass extends ParentClass
{
}

オーバーライドは、親クラスのメソッドを子クラスで同じ名前で定義し直すことです。

class ParentClass
{
    public function method(): void
    {
        echo "親";
    }
}

class ChildClass extends ParentClass
{
    public function method(): void
    {
        echo "子";
    }
}

親クラスの処理を子クラス側で呼び出したい場合は、parent:: を使います。

parent::method();

継承とオーバーライドで特に重要なのは、次のポイントです。

ポイント内容
extendsクラスを継承する
オーバーライド親クラスのメソッドを子クラスで上書きする
parent::親クラスのメソッドを呼び出す
protected子クラスからアクセスできる
private子クラスから直接アクセスできない
final継承やオーバーライドを禁止する
型の互換性親メソッドと互換性のあるシグネチャにする

初心者のうちは、次のように覚えると分かりやすいです。

継承 = 親の機能を受け継ぐ
オーバーライド = 親の機能を子で上書きする
parent:: = 親の処理を呼び出す
protected = 子クラスでも使える
private = 子クラスでは直接使えない
final = 変更を禁止する

PHPでオブジェクト指向を理解するうえで、継承とオーバーライドは非常に重要です。

ただし、継承は便利な反面、使いすぎるとクラス同士の依存が強くなり、設計が複雑になります。

実務では、「本当に親子関係なのか」「共通処理を親クラスに置くべきか」「コンポジションやトレイトの方が自然ではないか」を考えながら使うことが大切です。

以上、PHPの継承とオーバーライドについてでした。

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

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