#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つのトレイトに同名のメソッドがあると競合します。insteadof と as で解決します。
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 トレイト名でクラスに組み込む- 複数のトレイトをカンマ区切りで一度に使える
- 同名メソッドの競合は
insteadofとasで解決する - インターフェースと組み合わせると「契約(型)+実装」を分離できる
HasTimestamps、SoftDeletesなど実務でよく使われる
次回は依存性注入——テストしやすく、変更に強いコードの設計手法を学びます。