はじめに
この記事は、PHP Advent Calender 2019 の17日目の記事です。
みなさん、Trait使っていますか?
社内でTraitの使い所がいまいち分からないんだよね〜という話が出ていたので、Traitの使われかたを分類し、Laravelの実際のコードを例として理解を深めていけたらと思います。
Traitとは
そもそもTraitってどんな機能でしょうか?
PHP 5.4.0 以降では、コードを再利用するための「トレイト」という仕組みが導入されました。
トレイトは、PHP のような単一継承言語でコードを再利用するための仕組みのひとつです。 トレイトは、単一継承の制約を減らすために作られたもので、 いくつかのメソッド群を異なるクラス階層にある独立したクラスで再利用できるようにします。 トレイトとクラスを組み合わせた構文は複雑さを軽減させてくれ、 多重継承や Mixin に関連するありがちな問題を回避することもできます。
トレイトはクラスと似ていますが、トレイトは単にいくつかの機能をまとめるためだけのものです。 トレイト自身のインスタンスを作成することはできません。 昔ながらの継承に機能を加えて、振る舞いを水平方向で構成できるようになります。 つまり、継承しなくてもクラスのメンバーに追加できるようになります。
要するにクラスという単位とは別の単位で機能をまとめて再利用ができるようにする仕組みです。
Traitの使い所
Traitを使うときれいに書けるパターンとして以下があると思っています。
- 機能をパーツとして使い回すパターン
- Interfaceと一緒に使うことでデフォルトの実装を提供するパターン
- 機能をグルーピングすることによって、可読性を向上するパターン
他にも使い方があると思いますが、この3つはとっつきやすく、すぐ使えるパターンではないかと思うので紹介していきます。
機能をパーツとして使い回すパターン
一番基本的なやつですね。他のクラスと機能を共通化するときに、共通化する部分をTraitとして切り出しておいて、必要なときにuseします。
Laravelでもたくさんのところに使われていますが、特にController周りで使われています。登録やログイン等、一般的なWebサービスに必要だが実装が煩雑になりやすい部分を簡単に実装できるようにTraitが用意されています。
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use App\User;
use Illuminate\Foundation\Auth\RegistersUsers;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
class RegisterController extends Controller
{
/*
|--------------------------------------------------------------------------
| Register Controller
|--------------------------------------------------------------------------
|
| This controller handles the registration of new users as well as their
| validation and creation. By default this controller uses a trait to
| provide this functionality without requiring any additional code.
|
*/
use RegistersUsers;
... 省略 ...
<?php
namespace Illuminate\Foundation\Auth;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
trait RegistersUsers
{
use RedirectsUsers;
/**
* Show the application registration form.
*
* @return \Illuminate\Http\Response
*/
public function showRegistrationForm()
{
return view('auth.register');
}
/**
* Handle a registration request for the application.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function register(Request $request)
{
$this->validator($request->all())->validate();
event(new Registered($user = $this->create($request->all())));
$this->guard()->login($user);
return $this->registered($request, $user)
?: redirect($this->redirectPath());
}
/**
* Get the guard to be used during registration.
*
* @return \Illuminate\Contracts\Auth\StatefulGuard
*/
protected function guard()
{
return Auth::guard();
}
/**
* The user has been registered.
*
* @param \Illuminate\Http\Request $request
* @param mixed $user
* @return mixed
*/
protected function registered(Request $request, $user)
{
//
}
}
Interfaceと一緒に使うことでデフォルトの実装を提供するパターン
Interfaceのデフォルト実装をTraitにもたせるパターンです。Java8ではInterfaceがデフォルト実装を持てるなんて話もありますね。InterfaceとそのInterfaceを実装するためのTraitをセットで作り、クラスに対してInterfaceとTraitの両方を使うことで、簡単にInterfaceに沿った実装をさせることができます。
以下はログインユーザーとしてLaravelのLogin機構にわたすことのできるクラスのInterfaceとそれを実装するためのTraitです。
<?php
namespace Illuminate\Contracts\Auth;
interface Authenticatable
{
/**
* Get the name of the unique identifier for the user.
*
* @return string
*/
public function getAuthIdentifierName();
/**
* Get the unique identifier for the user.
*
* @return mixed
*/
public function getAuthIdentifier();
/**
* Get the password for the user.
*
* @return string
*/
public function getAuthPassword();
/**
* Get the token value for the "remember me" session.
*
* @return string
*/
public function getRememberToken();
/**
* Set the token value for the "remember me" session.
*
* @param string $value
* @return void
*/
public function setRememberToken($value);
/**
* Get the column name for the "remember me" token.
*
* @return string
*/
public function getRememberTokenName();
}
<?php
namespace Illuminate\Auth;
trait Authenticatable
{
/**
* The column name of the "remember me" token.
*
* @var string
*/
protected $rememberTokenName = 'remember_token';
/**
* Get the name of the unique identifier for the user.
*
* @return string
*/
public function getAuthIdentifierName()
{
return $this->getKeyName();
}
/**
* Get the unique identifier for the user.
*
* @return mixed
*/
public function getAuthIdentifier()
{
return $this->{$this->getAuthIdentifierName()};
}
/**
* Get the password for the user.
*
* @return string
*/
public function getAuthPassword()
{
return $this->password;
}
/**
* Get the token value for the "remember me" session.
*
* @return string|null
*/
public function getRememberToken()
{
if (! empty($this->getRememberTokenName())) {
return (string) $this->{$this->getRememberTokenName()};
}
}
/**
* Set the token value for the "remember me" session.
*
* @param string $value
* @return void
*/
public function setRememberToken($value)
{
if (! empty($this->getRememberTokenName())) {
$this->{$this->getRememberTokenName()} = $value;
}
}
/**
* Get the column name for the "remember me" token.
*
* @return string
*/
public function getRememberTokenName()
{
return $this->rememberTokenName;
}
}
更にTraitにあるデフォルト実装を一部いじりたい場合はそこだけ上書きするなんてこともできますので、結構柔軟に使うことができます。詳しくは過去記事を御覧ください。
Traitの関数にクラス内のローカルな名前をつけて呼び出す
機能をグルーピングすることによって、可読性を向上するパターン
これは今まで自分はやったことがなかったのですが、Laravelのコードを読んでいるときに偶然見つけました。
同じクラスにたくさんの機能を持たせなければいけないときに、機能をグルーピングし、それぞれ名前をつけることができ、他のファイルに分離することができます。
LaravelではTestCaseクラスでこれをやっています。
<?php
namespace Illuminate\Foundation\Testing;
use Carbon\Carbon;
use Carbon\CarbonImmutable;
use Illuminate\Console\Application as Artisan;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Facade;
use Illuminate\Support\Str;
use Mockery;
use Mockery\Exception\InvalidCountException;
use PHPUnit\Framework\TestCase as BaseTestCase;
use Throwable;
abstract class TestCase extends BaseTestCase
{
use Concerns\InteractsWithContainer,
Concerns\MakesHttpRequests,
Concerns\InteractsWithAuthentication,
Concerns\InteractsWithConsole,
Concerns\InteractsWithDatabase,
Concerns\InteractsWithExceptionHandling,
Concerns\InteractsWithSession,
Concerns\MocksApplicationServices;
... 省略 ...
}
機能をそれぞれConcernsとして分割しています。これの良いなと思うところはTraitにすることによって関数のグルーピングに名前をつけることができるという点です。
クラスが大きくなってくるとコンテキストによって様々な振る舞いをします。もちろんそうならないようにクラスを分割するなどは大前提ですが、一つのクラスが様々な振る舞いをする場合、その文脈ごとにTraitで関数をグルーピングすることで、ぐっとコードが読みやすくなると思います。
まとめ
Traitの使い所について、3つのパターンで類型化し、Laravelのフレームワーク内のソースコードを題材に解説してみました。
Traitを使いこなして、より重複の少ない読みやすいコードを目指して行きましょう。