PHPのオブジェクトの継承について

採用はこちら

PHPのオブジェクト指向では、あるクラスの性質や処理を、別のクラスに引き継がせることができます。

この仕組みを継承といいます。

継承元になるクラスを親クラス、継承して作られるクラスを子クラスと呼びます。

たとえば、「管理者ユーザー」は「ユーザー」の一種です。

このような関係は、継承で表現できます。

class User
{
    public function login(): void
    {
        echo 'ログインしました';
    }
}

class AdminUser extends User
{
    public function deletePost(): void
    {
        echo '投稿を削除しました';
    }
}

$admin = new AdminUser();

$admin->login();      // 親クラスのメソッドを使える
$admin->deletePost(); // 子クラス独自のメソッドを使える

AdminUser クラスは User クラスを継承しているため、User クラスに定義された login() メソッドを使えます。

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

class 子クラス名 extends 親クラス名
{
    // 子クラス独自の処理
}
目次

PHPで継承を使うメリット

共通処理をまとめられる

継承を使うと、複数のクラスで共通する処理を親クラスにまとめられます。

class User
{
    public function login(): void
    {
        echo 'ログインしました';
    }

    public function logout(): void
    {
        echo 'ログアウトしました';
    }
}

class AdminUser extends User
{
}

class MemberUser extends User
{
}

この例では、AdminUserMemberUserUser を継承しているため、login()logout() を使えます。

同じ処理を何度も書く必要がないため、コードの重複を減らせます。

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

継承では、親クラスのメソッドを子クラスで上書きできます。

これをオーバーライドといいます。

class User
{
    public function getRole(): string
    {
        return '一般ユーザー';
    }
}

class AdminUser extends User
{
    public function getRole(): string
    {
        return '管理者';
    }
}

$user = new User();
$admin = new AdminUser();

echo $user->getRole();  // 一般ユーザー
echo $admin->getRole(); // 管理者

同じ getRole() というメソッド名でも、通常のユーザーと管理者ユーザーで異なる結果を返せます。

親クラスの型で子クラスを扱える

継承を使うと、親クラスの型で子クラスのオブジェクトを扱えます。

class User
{
    public function getName(): string
    {
        return '田中';
    }
}

class AdminUser extends User
{
}

function showUserName(User $user): void
{
    echo $user->getName();
}

$admin = new AdminUser();

showUserName($admin);

showUserName() の引数は User 型ですが、AdminUserUser を継承しているため渡すことができます。

このように、親クラスの型で複数の子クラスを扱えるのも、継承の重要な特徴です。

継承で引き継がれるもの

publicとprotectedは子クラスで利用できる

PHPでは、子クラスは親クラスの publicprotected のプロパティ・メソッド・定数を利用できます。

class User
{
    public string $name = '田中';

    protected string $role = 'user';

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

    protected function getRole(): string
    {
        return $this->role;
    }
}

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

$admin = new AdminUser();

echo $admin->name;      // publicなので外部からアクセスできる
echo $admin->getName(); // publicメソッドなので外部から呼び出せる

$admin->showRole();     // 子クラス内からprotectedメソッドを使える

public はクラスの外部からもアクセスできます。

一方、protected はクラスの外部からはアクセスできませんが、自分のクラスと子クラスの中から利用できます。

privateは子クラスから直接アクセスできない

private は、定義されたクラスの内部からしか直接アクセスできません。

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

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

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

このコードは正しく動きません。

$passwordUser クラスの private プロパティなので、子クラスである AdminUser から直接アクセスできないためです。

ただし、親クラスの publicprotected メソッドを通じて、親クラス内の private プロパティを扱うことはできます。

class User
{
    private string $name = '田中';

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

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

$admin = new AdminUser();
$admin->showName(); // 田中

この例では、AdminUser から $name に直接アクセスしているわけではありません。

親クラスの getName() メソッドを呼び出し、そのメソッド内で private プロパティを扱っています。

アクセス修飾子の違い

継承を理解するうえで、publicprotectedprivate の違いは非常に重要です。

修飾子クラス外部からアクセス子クラスからアクセス主な用途
publicできるできる外部から呼び出したいメソッド
protectedできないできる子クラスで使わせたい処理や値
privateできない直接はできないそのクラス内だけで管理したい値や処理

実務では、プロパティを安易に public にするよりも、private にして getter や setter を用意する設計がよく使われます。

class User
{
    private string $name;

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

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

このようにすると、外部から直接プロパティを書き換えられないため、クラス内部の状態を安全に管理しやすくなります。

メソッドのオーバーライド

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

子クラスでは、親クラスで定義されたメソッドを同じ名前で定義し直すことができます。

これをオーバーライドといいます。

class User
{
    public function getDashboardUrl(): string
    {
        return '/mypage';
    }
}

class AdminUser extends User
{
    public function getDashboardUrl(): string
    {
        return '/admin';
    }
}

$user = new User();
$admin = new AdminUser();

echo $user->getDashboardUrl();  // /mypage
echo $admin->getDashboardUrl(); // /admin

通常ユーザーは /mypage、管理者ユーザーは /admin に移動させる、といった処理を表現できます。

オーバーライドでは型の互換性が必要

親クラスのメソッドをオーバーライドするときは、引数や戻り値の型に互換性が必要です。

次のコードは正しくありません。

class User
{
    public function getRole(): string
    {
        return 'user';
    }
}

class AdminUser extends User
{
    public function getRole(): array
    {
        return ['admin'];
    }
}

親クラスの getRole()string を返すのに、子クラスでは array を返しているため、互換性がありません。

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

class AdminUser extends User
{
    public function getRole(): string
    {
        return 'admin';
    }
}

戻り値の型や引数の型は、親クラスのメソッドと矛盾しないように定義する必要があります。

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

子クラスでメソッドを上書きしながら、親クラスの処理も使いたい場合は parent:: を使います。

class User
{
    public function login(): void
    {
        echo 'ログイン処理を実行しました。';
    }
}

class AdminUser extends User
{
    public function login(): void
    {
        parent::login();
        echo '管理者画面へ移動しました。';
    }
}

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

実行結果は次のようになります。

ログイン処理を実行しました。管理者画面へ移動しました。

parent::login() によって、親クラスの login() を呼び出しています。

コンストラクタの継承

子クラスにコンストラクタがなければ親クラスのコンストラクタが使われる

コンストラクタとは、オブジェクトを作成したときに自動で実行されるメソッドです。

PHPでは __construct() を使います。

class User
{
    protected string $name;

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

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

class AdminUser extends User
{
}

$admin = new AdminUser('佐藤');

echo $admin->getName(); // 佐藤

AdminUser クラスにはコンストラクタがありません。

この場合、親クラスである User のコンストラクタが使われます。

子クラスでコンストラクタを書く場合はparent::__construct()を呼ぶ

子クラスで独自のコンストラクタを定義すると、親クラスのコンストラクタは自動では呼ばれません。

そのため、親クラスの初期化処理も実行したい場合は parent::__construct() を明示的に呼び出します。

class User
{
    protected string $name;

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

class AdminUser extends User
{
    private int $level;

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

    public function showInfo(): void
    {
        echo "{$this->name}さんの管理者レベルは{$this->level}です";
    }
}

$admin = new AdminUser('鈴木', 5);
$admin->showInfo();

この例では、親クラスの $nameUser のコンストラクタで初期化し、子クラス独自の $levelAdminUser のコンストラクタで初期化しています。

親クラスのコンストラクタがprivateの場合は注意する

親クラスのコンストラクタが private の場合、子クラスから通常の形で利用することはできません。

class User
{
    private function __construct()
    {
    }
}

class AdminUser extends User
{
}

このような設計は、継承して自由にインスタンスを作る用途には向きません。

private コンストラクタは、シングルトンやファクトリメソッドなど、特別な設計で使われることがあります。

初心者のうちは、継承させたいクラスのコンストラクタは public または必要に応じて protected にすると理解しやすいです。

abstractを使った抽象クラス

abstract classとは

abstract を使うと、直接インスタンス化できないクラスを作れます。

このようなクラスを抽象クラスといいます。

抽象クラスは、共通処理を持たせながら、一部の処理を子クラスに実装させたい場合に使います。

abstract class User
{
    protected string $name;

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

    public function getProfile(): string
    {
        return "名前:{$this->name}";
    }

    abstract public function getRole(): string;
}

getProfile() には具体的な処理があります。

一方、getRole()abstract メソッドなので、処理の中身がありません。

抽象メソッドは子クラスで実装する

抽象メソッドを持つクラスを継承した場合、子クラス側でそのメソッドを実装する必要があります。

class AdminUser extends User
{
    public function getRole(): string
    {
        return '管理者';
    }
}

class MemberUser extends User
{
    public function getRole(): string
    {
        return '会員';
    }
}

$admin = new AdminUser('佐藤');
$member = new MemberUser('田中');

echo $admin->getProfile(); // 名前:佐藤
echo $admin->getRole();    // 管理者

echo $member->getProfile(); // 名前:田中
echo $member->getRole();    // 会員

抽象クラスを使うと、「共通する処理は親クラスに置き、子クラスごとに異なる処理は子クラスで実装する」という設計ができます。

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

final classは継承できない

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

final class User
{
    public function login(): void
    {
        echo 'ログインしました';
    }
}

class AdminUser extends User
{
}

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

User クラスが final なので、AdminUser クラスは User を継承できません。

final class は、「このクラスはこれ以上拡張させたくない」という場合に使います。

final methodはオーバーライドできない

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

class User
{
    final public function login(): void
    {
        echo 'ログインしました';
    }
}

class AdminUser extends User
{
    public function login(): void
    {
        echo '管理者としてログインしました';
    }
}

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

login() メソッドが final なので、子クラスで上書きできません。

重要な処理や、変更されると困る処理には final を使うことがあります。

staticメソッドと継承

staticメソッドも継承できる

static メソッドは、オブジェクトを作らずにクラスから直接呼び出せるメソッドです。

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

class AdminUser extends User
{
}

echo AdminUser::getType(); // user

AdminUserUser を継承しているため、Userstatic メソッドを呼び出せます。

子クラスでオーバーライドすることもできます。

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

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

echo AdminUser::getType(); // admin

self::とstatic::の違い

継承で static メソッドを扱うときは、self::static:: の違いに注意が必要です。

self:: は、そのメソッドが定義されているクラスを指します。

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

    public static function showType(): string
    {
        return self::getType();
    }
}

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

echo AdminUser::showType(); // user

AdminUser::showType() と呼んでいますが、showType() の中では self::getType() を使っています。

そのため、User::getType() が呼ばれます。

一方、static:: を使うと、実際に呼び出されたクラスを考慮します。

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

    public static function showType(): string
    {
        return static::getType();
    }
}

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

echo AdminUser::showType(); // admin

このような仕組みを遅延静的束縛といいます。

static:: は、継承先のクラスごとに処理を切り替えたい場合に便利です。

継承とポリモーフィズム

ポリモーフィズムとは

ポリモーフィズムとは、同じメソッド呼び出しでも、オブジェクトの種類によって異なる処理を実行できる仕組みです。

abstract class User
{
    abstract public function getRole(): string;
}

class AdminUser extends User
{
    public function getRole(): string
    {
        return '管理者';
    }
}

class MemberUser extends User
{
    public function getRole(): string
    {
        return '会員';
    }
}

function showRole(User $user): void
{
    echo $user->getRole();
}

showRole(new AdminUser());  // 管理者
showRole(new MemberUser()); // 会員

showRole()User 型の引数を受け取ります。

しかし、実際には AdminUserMemberUser も渡せます。

どちらも User を継承しているためです。

そして、呼び出される getRole() の処理は、実際に渡されたオブジェクトのクラスによって変わります。

interfaceでポリモーフィズムを実現することもある

ポリモーフィズムは継承だけでなく、interface を使って実現することもできます。

interface RoleProvider
{
    public function getRole(): string;
}

class AdminUser implements RoleProvider
{
    public function getRole(): string
    {
        return '管理者';
    }
}

class MemberUser implements RoleProvider
{
    public function getRole(): string
    {
        return '会員';
    }
}

function showRole(RoleProvider $user): void
{
    echo $user->getRole();
}

showRole(new AdminUser());
showRole(new MemberUser());

共通の親クラスを持たせたい場合は継承が向いています。

一方で、「同じメソッドを持っていることだけを保証したい」場合は、interface の方が適していることもあります。

PHPは多重継承できない

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

PHPでは、1つのクラスが複数の親クラスを同時に継承することはできません。

次のような書き方はできません。

class A
{
}

class B
{
}

class C extends A, B
{
}

PHPのクラス継承は、基本的に単一継承です。

正しくは次のように、1つの親クラスだけを指定します。

class A
{
}

class C extends A
{
}

複数の機能を使い回したい場合はtraitやinterfaceを使う

複数のクラスで同じ処理を使い回したい場合は、trait を使えます。

また、複数のルールをクラスに持たせたい場合は、複数の interface を実装できます。

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

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

class UserService implements LoggerInterface, NotifierInterface
{
    public function log(string $message): void
    {
        echo "ログ:{$message}";
    }

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

クラスの継承は1つだけですが、インターフェースは複数実装できます。

継承とtraitの違い

traitは処理を使い回すための仕組み

trait は、複数のクラスで同じメソッドを使い回したいときに使います。

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

class Article
{
    use TimestampTrait;
}

class Comment
{
    use TimestampTrait;
}

$article = new Article();
echo $article->getCurrentTime();

$comment = new Comment();
echo $comment->getCurrentTime();

ArticleComment は親子関係ではありません。

しかし、どちらも TimestampTrait のメソッドを使えます。

継承は親子関係、traitは機能の共有

継承と trait の違いは次のとおりです。

仕組み主な目的
継承親子関係を表すAdminUser extends User
trait処理を複数クラスで使い回すuse TimestampTrait

単にコードを使い回したいだけなら、継承よりも trait やコンポジションの方が適している場合があります。

継承とinterfaceの違い

interfaceは実装すべきルールを定義する

interface は、「このメソッドを必ず実装してください」というルールを定義する仕組みです。

interface Authenticatable
{
    public function login(): void;
}

class User implements Authenticatable
{
    public function login(): void
    {
        echo 'ログインしました';
    }
}

Authenticatable インターフェースでは、login() メソッドを持つことを決めています。

具体的な処理は、implements したクラス側で実装します。

継承、interface、traitの使い分け

継承、interfacetrait は似ている部分もありますが、目的が異なります。

仕組み目的
継承親クラスの性質や処理を引き継ぐ
interface実装すべきメソッドのルールを決める
traitメソッドの実装を複数のクラスで使い回す

たとえば、管理者ユーザーが通常ユーザーの一種であれば、継承が自然です。

class User
{
}

class AdminUser extends User
{
}

一方で、ログイン可能であることだけを保証したい場合は、interface が向いています。

interface Loginable
{
    public function login(): void;
}

また、複数のクラスに同じメソッドを持たせたいだけなら、trait を検討できます。

継承とコンポジションの違い

継承は「is-a」の関係で使う

継承を使うかどうか迷ったときは、A is a B の関係が成り立つかを考えると分かりやすいです。

たとえば、次の関係は自然です。

AdminUser is a User
管理者ユーザーはユーザーの一種である

この場合は、継承が向いています。

class User
{
}

class AdminUser extends User
{
}

コンポジションは「has-a」の関係で使う

一方で、A has a B の関係なら、継承ではなくコンポジションを使う方が自然です。

たとえば、車とエンジンの関係を考えてみます。

Car has an Engine
車はエンジンを持っている

車はエンジンの一種ではありません。

そのため、Car extends Engine とするのは不自然です。

この場合は、Car クラスの中で Engine オブジェクトを持たせます。

class Engine
{
    public function start(): void
    {
        echo 'エンジンを始動しました';
    }
}

class Car
{
    private Engine $engine;

    public function __construct(Engine $engine)
    {
        $this->engine = $engine;
    }

    public function drive(): void
    {
        $this->engine->start();
        echo '車が走り出しました';
    }
}

$engine = new Engine();
$car = new Car($engine);

$car->drive();

このように、別のオブジェクトを部品として持つ設計をコンポジションといいます。

実務では継承よりコンポジションが適する場合も多い

単に処理を使い回したいだけで継承を使うと、不自然な設計になりやすいです。

たとえば、次のような設計はあまり自然ではありません。

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

class User extends Logger
{
}

UserLogger の一種ではありません。

「ユーザーがログを出力する機能を使う」という関係です。

この場合は、次のように Logger を部品として持たせる方が自然です。

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

class User
{
    private Logger $logger;

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

    public function register(): void
    {
        $this->logger->log('ユーザー登録しました');
    }
}

継承は強力ですが、親クラスと子クラスの結びつきが強くなります。

そのため、実務では「本当に親子関係なのか」を考えてから使うことが大切です。

継承関係を判定するinstanceof

instanceofでオブジェクトの型を確認できる

instanceof を使うと、あるオブジェクトが特定のクラスのインスタンスかどうかを判定できます。

class User
{
}

class AdminUser extends User
{
}

$admin = new AdminUser();

var_dump($admin instanceof AdminUser); // true
var_dump($admin instanceof User);      // true

$adminAdminUser のオブジェクトなので、$admin instanceof AdminUsertrue です。

また、AdminUserUser を継承しているため、$admin instanceof Usertrue になります。

interfaceの判定にも使える

instanceof は、クラスだけでなく interface に対しても使えます。

interface Loginable
{
    public function login(): void;
}

class User implements Loginable
{
    public function login(): void
    {
        echo 'ログインしました';
    }
}

$user = new User();

var_dump($user instanceof Loginable); // true

特定のインターフェースを実装しているかどうかを確認したい場合にも便利です。

実務でよくある継承の例

会員種別ごとに割引率を変える例

ECサイトで、通常会員とプレミアム会員の割引率を変えたい場合を考えます。

共通する情報は親クラスにまとめ、会員種別ごとに異なる割引率は子クラスで実装します。

abstract class User
{
    protected string $name;
    protected string $email;

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

    public function getProfile(): string
    {
        return "{$this->name} / {$this->email}";
    }

    abstract public function getDiscountRate(): float;
}

通常会員クラスです。

class RegularUser extends User
{
    public function getDiscountRate(): float
    {
        return 0.05;
    }
}

プレミアム会員クラスです。

class PremiumUser extends User
{
    public function getDiscountRate(): float
    {
        return 0.15;
    }
}

利用例です。

function showDiscount(User $user): void
{
    echo $user->getProfile();
    echo '割引率:' . ($user->getDiscountRate() * 100) . '%';
}

$regular = new RegularUser('田中', 'tanaka@example.com');
$premium = new PremiumUser('佐藤', 'sato@example.com');

showDiscount($regular);
showDiscount($premium);

RegularUserPremiumUserUser の一種です。

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

管理者ユーザーと一般ユーザーの例

管理者ユーザーだけが投稿を削除できる、という例も継承で表現できます。

class User
{
    protected string $name;

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

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

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

$admin = new AdminUser('佐藤');

echo $admin->getName();
$admin->deletePost();

AdminUserUser の基本機能を持ちながら、管理者専用の deletePost() を追加しています。

継承を使うときの注意点

コードの使い回しだけを目的にしない

継承は便利ですが、「同じ処理を使い回したい」という理由だけで使うと、設計が不自然になることがあります。

継承を使う前に、次のように考えるとよいです。

AdminUser は User の一種か?
PremiumUser は User の一種か?
Car は Engine の一種か?
User は Logger の一種か?

「AはBの一種である」と自然に言える場合は、継承が候補になります。

一方で、「AはBを持っている」「AはBを使っている」という関係なら、コンポジションの方が適していることが多いです。

protectedを増やしすぎない

protected は子クラスからアクセスできるため便利です。

しかし、親クラスの内部構造を子クラスに見せすぎると、親クラスと子クラスの結びつきが強くなります。

たとえば、親クラスのプロパティを子クラスが直接使いすぎると、親クラス側でプロパティ名や構造を変えにくくなります。

class User
{
    protected string $name;
    protected string $email;
    protected string $role;
}

このように何でも protected にすると、子クラスが親クラスの内部に依存しやすくなります。

必要に応じて、private プロパティとメソッドを組み合わせるとよいです。

class User
{
    private string $name;

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

継承階層を深くしすぎない

継承階層が深くなると、どのクラスでどの処理が定義されているのか分かりにくくなります。

User
 └── PaidUser
      └── PremiumUser
           └── CorporatePremiumUser

このような設計は、最初は整理されているように見えても、後から変更しにくくなることがあります。

継承階層が深くなりそうな場合は、次のような方法も検討するとよいです。

  • interface で振る舞いを分ける
  • trait で共通処理を切り出す
  • コンポジションで機能を部品化する
  • 条件分岐ではなくクラスの責務を見直す

親クラスには安定した共通処理を置く

親クラスの変更は、子クラスに影響します。

そのため、頻繁に変わる処理を親クラスに入れると、継承している子クラス全体に影響が広がる可能性があります。

親クラスには、できるだけ安定した共通処理を置くのが理想です。

たとえば、ユーザーの名前やメールアドレスを扱う処理は共通化しやすいですが、キャンペーンごとの割引計算など、頻繁に変わる処理は別クラスに分けた方がよい場合もあります。

継承でよくあるエラー

privateプロパティに子クラスからアクセスしている

よくある間違いが、親クラスの private プロパティに子クラスから直接アクセスすることです。

class User
{
    private string $name = '田中';
}

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

これは正しくありません。

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

class User
{
    private string $name = '田中';

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

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

このようにすると、子クラスは親クラスの protected メソッドを通じて名前を取得できます。

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

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

class User
{
    protected string $name;

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

class AdminUser extends User
{
    private int $level;

    public function __construct(int $level)
    {
        $this->level = $level;
    }
}

このコードでは、親クラスの $name が初期化されません。

正しくは、parent::__construct() を呼び出します。

class AdminUser extends User
{
    private int $level;

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

オーバーライド時に戻り値の型が合っていない

親クラスと子クラスで、メソッドの戻り値型が大きく異なるとエラーになります。

class User
{
    public function getName(): string
    {
        return '田中';
    }
}

class AdminUser extends User
{
    public function getName(): array
    {
        return ['田中'];
    }
}

この例では、親クラスは string を返すのに、子クラスは array を返しているため正しくありません。

次のように、互換性のある型にします。

class AdminUser extends User
{
    public function getName(): string
    {
        return '管理者:田中';
    }
}

PHPの継承を使うときの判断基準

継承が向いているケース

継承が向いているのは、親子関係が自然に表現できる場合です。

関係継承が向いているか
管理者ユーザーはユーザーの一種向いている
プレミアム会員は会員の一種向いている
PDF請求書は請求書の一種向いている
車はエンジンの一種向いていない
ユーザーはメール送信機能の一種向いていない

ポイントは、「AはBの一種である」と自然に言えるかどうかです。

継承以外を検討した方がよいケース

次のような場合は、継承以外の設計を検討した方がよいです。

状況検討したい方法
複数のクラスで同じメソッドを使い回したいtrait
共通のルールだけを定義したいinterface
別のオブジェクトの機能を使いたいコンポジション
継承階層が深くなりそうinterfaceやコンポジション
親クラスの変更が多そうコンポジション

継承は「コードを短くするため」だけに使うものではありません。

クラス同士の関係を正しく表現するために使うものです。

まとめ

PHPの継承は、親クラスの性質や処理を子クラスに引き継がせる仕組みです。

基本構文は次のとおりです。

class AdminUser extends User
{
}

継承を使うと、共通処理を親クラスにまとめたり、子クラスごとに処理を変えたり、親クラスの型で複数の子クラスを扱ったりできます。

特に重要なポイントは次のとおりです。

用語・構文内容
extendsクラスを継承する
public外部からも子クラスからもアクセスできる
protected子クラスからアクセスできる
private子クラスから直接アクセスできない
オーバーライド親クラスのメソッドを子クラスで上書きする
parent::親クラスのメソッドやコンストラクタを呼び出す
abstract子クラスに実装を強制する
final継承やオーバーライドを禁止する
static::呼び出されたクラスを考慮して静的メソッドを呼ぶ
instanceofオブジェクトの型や継承関係を判定する

ただし、継承は使いすぎるとクラス同士の結びつきが強くなり、変更しにくい設計になることがあります。

継承を使うか迷ったときは、次の基準で考えると分かりやすいです。

A is a B なら継承
A has a B ならコンポジション

たとえば、AdminUserUser の一種なので継承が自然です。

一方で、UserLogger を使う場合、User extends Logger とするのではなく、UserLogger を持つ設計にする方が自然です。

PHPの継承は、単なるコードの使い回しではなく、クラス同士の関係を表現するための機能です。

親子関係として自然か、変更に強い設計になっているかを意識しながら使うことが大切です。

以上、PHPのオブジェクトの継承についてでした。

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

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