#08 オブジェクト指向プログラミング入門

トレイト——横断的な機能を共有する

継承では解決できない問題

継承(第3回)でコードを再利用できることを学びました。しかし継承には限界があります。

たとえば「作成日時・更新日時を管理する機能」を複数のモデルクラスに追加したいとします。

User      → タイムスタンプが欲しい
Post      → タイムスタンプが欲しい
Product   → タイムスタンプが欲しい

継承を使うなら TimestampModel という親クラスを作って全クラスに継承させる……でも User はすでに BaseModel を継承しているかもしれません。PHPは多重継承できないので、継承だけでは解決できません。

これを解決するのがトレイト(Trait)です。


トレイトの基本

トレイトはメソッドとプロパティのセットを「ミックスイン(mix-in)」できる仕組みです。クラスに use するだけで機能を取り込めます。

trait HasTimestamps
{
    private ?DateTimeImmutable $createdAt = null;
    private ?DateTimeImmutable $updatedAt = null;

    public function setCreatedAt(): void
    {
        $this->createdAt = new DateTimeImmutable();
    }

    public function setUpdatedAt(): void
    {
        $this->updatedAt = new DateTimeImmutable();
    }

    public function getCreatedAt(): ?DateTimeImmutable
    {
        return $this->createdAt;
    }

    public function getUpdatedAt(): ?DateTimeImmutable
    {
        return $this->updatedAt;
    }
}
class User
{
    use HasTimestamps; // トレイトを組み込む

    public function __construct(
        public readonly int $id,
        public string $name,
    ) {
        $this->setCreatedAt();
        $this->setUpdatedAt();
    }
}

class Post
{
    use HasTimestamps; // 同じトレイトを別クラスにも

    public function __construct(
        public readonly int $id,
        public string $title,
    ) {
        $this->setCreatedAt();
        $this->setUpdatedAt();
    }
}
$user = new User(1, '田中太郎');
echo $user->getCreatedAt()->format('Y-m-d H:i:s');
// 2026-04-10 12:00:00

$post = new Post(1, 'OOP入門');
echo $post->getCreatedAt()->format('Y-m-d H:i:s');
// 2026-04-10 12:00:00

複数のトレイトを組み合わせる

1つのクラスに複数のトレイトを組み込めます。

trait SoftDeletes
{
    private ?DateTimeImmutable $deletedAt = null;

    public function softDelete(): void
    {
        $this->deletedAt = new DateTimeImmutable();
    }

    public function restore(): void
    {
        $this->deletedAt = null;
    }

    public function isDeleted(): bool
    {
        return $this->deletedAt !== null;
    }

    public function getDeletedAt(): ?DateTimeImmutable
    {
        return $this->deletedAt;
    }
}

trait HasUuid
{
    private string $uuid;

    public function initUuid(): void
    {
        $this->uuid = sprintf(
            '%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
            mt_rand(0, 0xffff), mt_rand(0, 0xffff),
            mt_rand(0, 0xffff),
            mt_rand(0, 0x0fff) | 0x4000,
            mt_rand(0, 0x3fff) | 0x8000,
            mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
        );
    }

    public function getUuid(): string
    {
        return $this->uuid;
    }
}
class Article
{
    use HasTimestamps, SoftDeletes, HasUuid; // カンマ区切りで複数指定

    public function __construct(
        public string $title,
        public string $body,
    ) {
        $this->initUuid();
        $this->setCreatedAt();
        $this->setUpdatedAt();
    }
}

$article = new Article('PHP入門', '本文...');
echo $article->getUuid();       // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
echo $article->isDeleted();     // false

$article->softDelete();
echo $article->isDeleted();     // true

トレイトのメソッド競合を解決する

2つのトレイトに同名のメソッドがあると競合します。insteadofas で解決します。

trait A
{
    public function hello(): string
    {
        return 'Hello from A';
    }
}

trait B
{
    public function hello(): string
    {
        return 'Hello from B';
    }
}

class MyClass
{
    use A, B {
        A::hello insteadof B; // A の hello を優先
        B::hello as helloFromB; // B の hello を別名で使えるようにする
    }
}

$obj = new MyClass();
echo $obj->hello();       // Hello from A
echo $obj->helloFromB();  // Hello from B

抽象メソッドを持つトレイト

トレイトに abstract メソッドを定義して、使用クラスに実装を強制できます。

trait Validatable
{
    // このトレイトを使うクラスは validate() を実装しなければならない
    abstract protected function validate(): bool;

    public function saveIfValid(): string
    {
        if ($this->validate()) {
            return '保存しました。';
        }
        return 'バリデーションエラーのため保存できません。';
    }
}

class UserForm
{
    use Validatable;

    public function __construct(
        private string $name,
        private string $email,
    ) {}

    protected function validate(): bool
    {
        return $this->name !== '' && filter_var($this->email, FILTER_VALIDATE_EMAIL) !== false;
    }
}

$form = new UserForm('田中太郎', 'tanaka@example.com');
echo $form->saveIfValid(); // 保存しました。

$invalidForm = new UserForm('', 'not-an-email');
echo $invalidForm->saveIfValid(); // バリデーションエラーのため保存できません。

トレイトとインターフェースの使い分け

比較トレイトインターフェース
目的実装の再利用型の保証・契約
実装メソッドの実装を持てる実装を持てない
組み合わせ複数利用可能複数実装可能
型チェックできないinstanceof で可能

トレイトとインターフェースは補完関係にあります。インターフェースで「何ができるか」を宣言し、トレイトでその「実装を提供する」パターンがよく使われます。

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

trait LoggableTrait
{
    public function log(string $message): void
    {
        // ファイルにログを書く実装
        file_put_contents(
            storage_path('logs/app.log'),
            date('[Y-m-d H:i:s] ') . $message . "\n",
            FILE_APPEND
        );
    }
}

class OrderService implements Loggable
{
    use LoggableTrait; // インターフェースの実装をトレイトで提供

    public function placeOrder(): void
    {
        // 注文処理...
        $this->log('注文が確定しました。');
    }
}

まとめ

  • トレイトはクラスに「横断的な機能」を追加する仕組み
  • use トレイト名 でクラスに組み込む
  • 複数のトレイトをカンマ区切りで一度に使える
  • 同名メソッドの競合は insteadofas で解決する
  • インターフェースと組み合わせると「契約(型)+実装」を分離できる
  • HasTimestampsSoftDeletes など実務でよく使われる

次回は依存性注入——テストしやすく、変更に強いコードの設計手法を学びます。