PHPのトレイトは、PHP 5.4で導入された比較的以前よりある機能です。もちろん、私もLaravelなどではよく使っています。
が、Laravelの枠組み内だからこそ使っている感が拭えず……。その証拠に、独自Webアプリケーションでは全くと言って良いほど使っていません。
結構、私と同じような方もいらっしゃるのではないでしょうか!?
本ページは、PHPのトレイトの解説を通じ、私自身も理解を深める目的で作りました。
トレイトが初めての方はもちろん、ちゃんと理解しておきたいという方の参考にしていただければ幸いです。
トレイトとは!?
概要
トレイト(Trait)とは、複数のクラスで同じメソッドやプロパティを共有するための機能です。
PHPは単一継承しかサポートしていないため、その不便を軽減するために導入されたようです。
トレイトを定義
トレイトを定義する際には、class
キーワードの代わりにtrait
キーワードを使用します。
コードを見た方が早いですね。
以下では、トレイトクラスであるExampleTrait
にプロパティとメソッドを定義しています。
trait ExampleTrait {
public $greeting = "Hello from Trait!";
public function sayHello() {
echo $this->greeting;
}
}
トレイトの使用
定義したTrait
をクラスで使用する際には、use
キーワードを用います。以下は先ほどのトレイトをMyClass
で利用しています。
class MyClass {
use ExampleTrait;
}
$obj = new MyClass();
$obj->sayHello(); // 出力: Hello from Trait!
このようにして、トレイト内のプロパティやメソッドをクラスに取り込み、そのクラスのインスタンスからアクセスすることができます。
抽象クラスに近い!?
トレイトは、直接インスタンス化することができません。
trait ExampleTrait {
public $greeting = "Hello from Trait!";
public function sayHello() {
echo $this->greeting;
}
}
// 以下はできない
// $trait = new ExampleTrait();
後に考えを改めることになりますが、最初は抽象クラスに近いのかなと当初の私は感じました(実際、後述しますが、抽象メソッドも定義できます)。
しかしながら、トレイトには継承機能がありません。他のトレイトの利用はできます。
いろいろな使い方
先ほどはベーシックな例を挙げましたが、いろいろな使い方ができます。
複数のトレイトを使用する
複数のトレイトを、一つのクラスで使用することもできます。
trait TraitOne {
public function methodOne() {
echo "Method from TraitOne";
}
}
trait TraitTwo {
public function methodTwo() {
echo "Method from TraitTwo";
}
}
class MyClass {
use TraitOne, TraitTwo;
}
$obj = new MyClass();
$obj->methodOne(); // 出力: Method from TraitOne
$obj->methodTwo(); // 出力: Method from TraitTwo
このように、,
でくぎることで複数のトレイトを利用することが可能です。
トレイト間で競合するメソッドがある場合
複数のトレイトを一つのクラスで使用する際、同名のメソッドが存在すると競合が発生し、Fatal errorとなります。
この場合、insteadof
キーワードを使い、どのメソッドを使用するか明示的に指定する必要があります。
trait TraitA {
public function conflictingMethod() {
echo "From TraitA";
}
}
trait TraitB {
public function conflictingMethod() {
echo "From TraitB";
}
}
class MyClass {
use TraitA, TraitB {
TraitA::conflictingMethod insteadof TraitB;
}
}
この例では、MyClass
では TraitA
の conflictingMethod
が使用され、TraitB
のものは除外されます。
メソッドのエイリアスとしての使用
as
キーワードを使用して、取り込むメソッドの名前のエイリアス(別名)設定することも可能です。
trait TraitExample {
public function exampleMethod() {
echo "From Trait";
}
}
class MyClass {
use TraitExample { exampleMethod as aliasMethod; }
}
$obj = new MyClass();
$obj->aliasMethod(); // 出力: From Trait
$obj->exampleMethod(); // 出力: From Trait
13行目を見ると分かるとおり、あくまでエイリアスの「追加」です。元の名称での利用ができなくなるわけではありません。
抽象メソッドを使う
抽象クラスと同様、トレイトも抽象メソッドを実装することが可能です。
抽象メソッドを持つトレイを使用するクラスでは、その抽象メソッドに対する具体的な実装を提供する必要があります。
もし実装し忘れると、Fatal errorが発生します。
trait Printable {
// 抽象メソッドの定義
abstract public function getContent();
public function printContent() {
echo $this->getContent();
}
}
class Article {
use Printable;
private $content;
public function __construct($content) {
$this->content = $content;
}
// 抽象メソッドの実装
public function getContent() {
return $this->content;
}
}
$article = new Article("Hello, World!");
$article->printContent(); // 出力: Hello, World!
上記の例では、Printable
トレイトに抽象メソッド getContent
を定義しています。
Article
クラスはこのトレイトを使用しているので、getContent
の具体的な実装をする必要があります。
クラスとの違い・使い分けが分からない!?
トレイトは、「継承できない&同時にいくつも利用できる抽象クラス」のようなイメージを持たれる方も多いかもしれません。
正直な所、クラスとどう使い分ければ良いのか。どういった利点があるのか今一掴みづらいところです。
そこでトレイトの考え方を、自分なりに解釈してみました。
1トレイト=1ファイル=1機能
トレイトは1つのファイルに1機能(役割)として記述し、その機能に関しては完結するものとして作った方が良さそうです。
そうすれば、その機能を複数のクラスで使うことができます。
クラスとは別軸として、独立した機能としての便利さを感じました。
“Does-a”または”Has-a”関係
クラスは”Is-a”関係。つまりは「〜は〜である」という関係で継承します。Dog
はAnimal
であるため、Dog
クラスがAnimal
クラスを継承する……という考えです。
対するトレイトは、”Does-a”または”Has-a”関係、つまり「〜が〜の機能を持つ/行う」感じで使うもの、と考えるのが良さそうです。
例えばログの機能を提供するためのLoggerTrait
を多くのクラスに実装したい……という場合はトレイトが好ましいかもしれません。
実用的なコード例
私は過去にキャッシュに関する独自クラスを作ったことがありました。
その簡易版のようなクラスを例に、トレイトに書き直してみたいと思います。
こんなクラスです。
- キャッシュを管理するクラス
- キャッシュグループを利用できる(グループ毎に読み込み・保存を分ける)
- DBの結果などをはじめ、いろいろなキャッシュを保存する用途を想定
修正前
何の変哲も無い、キャッシュを管理するクラスです。
class Cache {
private $cacheGroup;
public function __construct(string $group) {
$this->cacheGroup = $group;
}
public function setCache($key, $value) {
echo "Saving {$value} to cache with key {$key} in group {$this->cacheGroup}\n";
// 実際のキャッシュ保存処理はここに...
}
public function getCache($key) {
echo "Fetching data from cache with key {$key} in group {$this->cacheGroup}\n";
// 実際のキャッシュ取得処理はここに...
return "Sample Data"; // デモ用のデータ
}
public function clearGroupCache() {
echo "Clearing cache in group {$this->cacheGroup}\n";
// 実際のキャッシュグループのクリア処理はここに...
}
}
これを利用するクラスが以下です。
class User {
private $cache;
public function __construct() {
$this->cache = new Cache("user_group");
}
public function setCache($key, $value) {
$this->cache->setCache($key, $value);
}
public function getCache($key) {
return $this->cache->getCache($key);
}
public function clearCache() {
$this->cache->clearGroupCache();
}
}
class Product {
private $cache;
public function __construct() {
$this->cache = new Cache("product_group");
}
public function setCache($key, $value) {
$this->cache->setCache($key, $value);
}
public function getCache($key) {
return $this->cache->getCache($key);
}
public function clearCache() {
$this->cache->clearGroupCache();
}
}
このようなCache
クラスだと、キャッシュ機能をつけたいだけなのに、利用するクラスごとにsetCache()
, getCache()
などの実装をしています。
getCache()
などを作ってインスタンスごと返してやればそんな実装は不要ですが、カプセル化したいです。
もしくはCache
クラスを継承すれば良いですが、”is-a”の観点から矛盾してしまいますのでやってはいけません。
Traitを使った例
こんな時にトレイトが使えそうです。
まずはキャッシュができるトレイト(特性)ということで、Cacheble
と名前を変えて、以下の様にしました。
trait Cacheable {
abstract protected function getCacheGroup(): string;
public function setCache($key, $value) {
$group = $this->getCacheGroup();
echo "Saving {$value} to cache with key {$key} in group {$group}\n";
// 実際のキャッシュ保存処理はここに...
}
public function getCache($key) {
$group = $this->getCacheGroup();
echo "Fetching data from cache with key {$key} in group {$group}\n";
// 実際のキャッシュ取得処理はここに...
return "Sample Data"; // デモ用のデータ
}
public function clearGroupCache() {
$group = $this->getCacheGroup();
echo "Clearing cache in group {$group}\n";
// 実際のキャッシュグループのクリア処理はここに...
}
}
User
クラスとProduct
クラスを作成します。これらのクラスは上記のCacheable
トレイトを使用します。
class User {
use Cacheable;
protected function getCacheGroup(): string {
return "user_group";
}
}
class Product {
use Cacheable;
protected function getCacheGroup(): string {
return "product_group";
}
}
簡単ですね!これだけで両クラスにキャッシュ機能が追加されました。クラスが増えても、手間はあまりかかりません。
こんな感じで使います。
$user = new User();
$user->setCache('userId_123', '山田太郎');
$user->getCache('userId_123');
$user->clearGroupCache();
$product = new Product();
$product->setCache('productId_456', 'おいしい羊羹');
$product->getCache('productId_456');
$product->clearGroupCache();
適所であれば、クラスよりも柔軟なコードが書けますね。
まとめ
以上、PHPのトレイとの説明でした。
昔からの癖で、ついついクラスで解決するコーディングスタイルが身についてしまい、自作のWebアプリケーションではさっぱりでした。
しかし、今回トレイトを研究することで、改めてトレイト良いなと。しかも、内容が身についた気がします。
今後はLaravel以外でも、トレイトの機能をもっと使っていきたいと思います。
この記事が、少しでもあなたの役にも立てばうれしいです。
コメント