ポリシーのソースコードリーディング
がんばりましょう。
Gateファサード
解決されるクラス Illuminate/Auth/Access/Gate.php
ポリシーの登録
ポリシーのマッピングはapp/Providers/AuthServiceProvider
に記述します。
keyに関連するモデル、valueにポリシーを記述します。
protected $policies = [
// 'App\Model' => 'App\Policies\ModelPolicy',
]
boot()
の中で呼ばれているregisterPolicies()
で登録していきます。
public function registerPolicies()
{
foreach ($this->policies() as $key => $value) {
Gate::policy($key, $value);
}
}
$this->policies()
は先程のpolicies
プロパティを返します。
Illuminate/Auth/Access/Gate::policies()
が呼ばれます。
public function policy($class, $policy)
{
$this->policies[$class] = $policy;
return $this;
}
Illuminate/Auth/Access/Gate
のpolicies
プロパティに同じ形で値を格納していきます。
これにてポリシーの登録は終了。
ポリシーをつかう
主なポリシーの使用方法は3つで、
- User.phpから使う
- Controllerから使う
- ミドルウェアで使う
中身は大体同じなので、今回はミドルウェアで使うを例に見ていきます。
想定ルート、ミドルウェア
Route::get('/{book}/{comment}', 'BookController@index')->middleware('can:view,book,comment');
実際にアクセスされたURL
/1/2
book
はルートモデルバインディングで解決され、comment
は解決されないものとします。
ここでの注意点は、middleware()
内に書いた文字列のbook
やcomment
に当たる部分は必ず同じ名前でURIに記載してください。
たとえば、
Route::get('/{book}/hoge', 'BookController@index')->middleware('can:view,book,comment');
のように、middleware()
内にcomment
と書いてあるのに、get
の第一引数の中に{comment}
が含まれていないと怒られます。
ポリシーはリソース(モデル)に対しての認可を提供するので、ルートモデルバインディングで解決されたインスタンスを利用します。(なのでミドルウェアの順番も重要です)
想定ポリシー
protected $polices = [
Book::class => BookPolicy::class
]
B
BookPolicy::view()
が定義されている。
みていこう
まずは'can:view,book,comment'
がどのようにミドルウェアに変換されるか見ていきましょう。
ミドルウェアをごにょごにょしているのはここです。
protected function carry()
{
return function ($stack, $pipe) {
return function ($passable) use ($stack, $pipe) {
[$name, $parameters] = $this->parsePipeString($pipe);
$pipe = $this->getContainer()->make($name);
$parameters = array_merge([$passable, $stack], $parameters);
$carry = method_exists($pipe, $this->method)
? $pipe->{$this->method}(...$parameters)
: $pipe(...$parameters);
return $this->handleCarry($carry);
};
};
}
$pipe
には'can:view,book'
という文字列が入り、$passable
にはRequestクラス、$stack
は次のミドルウェアが入っています。
parsePipeString()
でミドルウェア名とその他に分けます。
protected function parsePipeString($pipe)
{
[$name, $parameters] = array_pad(explode(':', $pipe, 2), 2, []);
if (is_string($parameters)) {
$parameters = explode(',', $parameters);
}
return [$name, $parameters];
}
$name = 'can'
$parameters = ['view', 'book', 'comment']
$this->getContainer()->make($name)
でIlluminate/Auth/Middleware/Authorize.php
インスタンスを生成します。
(文字列とミドルウェアの対応はapp/Http/Kernel.php
に書いてあります。)
$this->method
にはhandle
が入っているので、Illuminate/Auth/Middleware/Authorize::handle()
が実行されます.
public function handle($request, Closure $next, $ability, ...$models)
{
$this->gate->authorize($ability, $this->getGateArguments($request, $models));
return $next($request);
}
$ability
にview
が入り、...$models
に残りもの(book
comment
)が渡されます。
まずはgetGateArguments()
を見ていきましょう。
protected function getGateArguments($request, $models)
{
if (is_null($models)) {
return [];
}
return collect($models)->map(function ($model) use ($request) {
return $model instanceof Model ? $model : $this->getModel($request, $model);
})->all();
}
コレクションクラスのmap()
で一つずつ処理をしていきます。
今回$models
は文字列なのでgetModel()
が呼ばれます。
protected function getModel($request, $model)
{
if ($this->isClassName($model)) {
return trim($model);
} else {
return $request->route($model, null) ?:
((preg_match("/^['\"](.*)['\"]$/", trim($model), $matches)) ? $matches[1] : null);
}
}
isClassName
で完全修飾名かどうかを判定しています。完全修飾名だとそのまま返すようです。
完全修飾名出ない場合を見ていきましょう。
public function route($param = null, $default = null)
{
$route = call_user_func($this->getRouteResolver());
if (is_null($route) || is_null($param)) {
return $route;
}
return $route->parameter($param, $default);
}
$route
には現在のルートの情報を保持するRouteクラスが入ってきます。
Routeクラスのparameters
プロパティにはルートモデルバインディングで解決されたモデルなどが入っています。
protected $parameters = [
'book' => 解決されたBookモデル,
'comment' => 2,
$route->parameter()
は第一引数をkeyに$parameters
から値を取得します。
今回のgetGateArguments()
の返り値は配列になります。中身は[解決されたBookモデル, 2]です。
$this->gate->authorize('view', [解決されたBookモデル, 2])
を見てきます。
public function authorize($ability, $arguments = [])
{
return $this->inspect($ability, $arguments)->authorize();
}
inspcet
が呼ばれて、
public function inspect($ability, $arguments = [])
{
try {
$result = $this->raw($ability, $arguments);
if ($result instanceof Response) {
return $result;
}
return $result ? Response::allow() : Response::deny();
} catch (AuthorizationException $e) {
return $e->toResponse();
}
}
raw
が呼ばれ、
public function raw($ability, $arguments = [])
{
$arguments = Arr::wrap($arguments);
$user = $this->resolveUser();
$result = $this->callBeforeCallbacks(
$user, $ability, $arguments
);
if (is_null($result)) {
$result = $this->callAuthCallback($user, $ability, $arguments);
}
return $this->callAfterCallbacks(
$user, $ability, $arguments, $result
);
}
Arr:wrap
は配列でないときは配列でラップしてくれるだけです。
$user
にはログイン済みのユーザーが入ってきます。
callBeforeCallbacks()
とcallAfterCallbacks()
は、それぞれGate::before()
、Gate::after()
で定義したコールバックが呼ばれます。
今回は関係ないのでcallAuthCallback()
を見ていきます。
protected function callAuthCallback($user, $ability, array $arguments)
{
$callback = $this->resolveAuthCallback($user, $ability, $arguments);
return $callback($user, ...$arguments);
}
resolveAuthCallback
は...
protected function resolveAuthCallback($user, $ability, array $arguments)
{
if (isset($arguments[0]) &&
! is_null($policy = $this->getPolicyFor($arguments[0])) &&
$callback = $this->resolvePolicyCallback($user, $ability, $arguments, $policy)) {
return $callback;
}
//...
}
$arguments
は[解決されたBookモデル, 2]なので、issetはtrueになり、getPolicyFor()
は、
public function getPolicyFor($class)
{
if (is_object($class)) {
$class = get_class($class);
}
if (isset($this->policies[$class])) {
return $this->resolvePolicy($this->policies[$class]);
}
//...
foreach ($this->policies as $expected => $policy) {
if (is_subclass_of($class, $expected)) {
return $this->resolvePolicy($policy);
}
}
}
ポリシーは
protected $polices = [
Book::class => BookPolicy::class
]
と登録したので、$this->policies[$class]
の返り値はBookPolicy::class
になります。
resolvePolicy
でクラス名からインスタンスを生成します。resolveAuthCallback()
に戻ります。
protected function resolveAuthCallback($user, $ability, array $arguments)
{
if (isset($arguments[0]) &&
! is_null($policy = $this->getPolicyFor($arguments[0])) &&
$callback = $this->resolvePolicyCallback($user, $ability, $arguments, $policy)) {
return $callback;
}
//...
}
resolvePolicyCallback()
はコールバックを返すみたいなのでコールバックが呼ばれるときに見てみることにします。
呼び出し元に戻ります。
protected function callAuthCallback($user, $ability, array $arguments)
{
$callback = $this->resolveAuthCallback($user, $ability, $arguments);
return $callback($user, ...$arguments);
}
コールバックを呼び出してその返り値を返しているようです。案外登場が早かったのでコールバックの中身を見てみましょう。
protected function resolvePolicyCallback($user, $ability, array $arguments, $policy)
{
return function () use ($user, $ability, $arguments, $policy) {
$result = $this->callPolicyBefore(
$policy, $user, $ability, $arguments
);
if (! is_null($result)) {
return $result;
}
$method = $this->formatAbilityToMethod($ability);
return $this->callPolicyMethod($policy, $method, $user, $arguments);
};
}
$user
はログインユーザー、$ability
はview
、$argument
は[解決されたBookモデル, 2]、$policy
はさっき解決したBookPolicyインスタンス。
ポリシーにもbefore()
がありますが今回は関係ないのでスルー。
protected function callPolicyMethod($policy, $method, $user, array $arguments)
{
if (isset($arguments[0]) && is_string($arguments[0])) {
array_shift($arguments);
}
if (! is_callable([$policy, $method])) {
return;
}
if ($this->canBeCalledWithUser($user, $policy, $method)) {
return $policy->{$method}($user, ...$arguments);
}
}
$arguments[0]
が文字列ならばここで抹消されます。
最終的に$policy->{$method}($user, ...$arguments)
でポリシー内のメソッドが呼ばれます。ポリシーのメソッドはboolを返します。
なので、callAuthCallback()
の返り値は、ポリシーの指定されたメソッドの返り値ということになります。
public function raw($ability, $arguments = [])
{
$arguments = Arr::wrap($arguments);
$user = $this->resolveUser();
if (is_null($result)) {
$result = $this->callAuthCallback($user, $ability, $arguments);
}
return $this->callAfterCallbacks(
$user, $ability, $arguments, $result
);
}
after
を指定していないので、callAuthCallback()
の返り値がそのまま返ります。
public function inspect($ability, $arguments = [])
{
try {
$result = $this->raw($ability, $arguments);
if ($result instanceof Response) {
return $result;
}
return $result ? Response::allow() : Response::deny();
} catch (AuthorizationException $e) {
return $e->toResponse();
}
}
$result
にポリシーのメソッドの返り値が格納されます。
public static function deny($message = null, $code = null)
{
return new static(false, $message, $code);
}
public static function allow($message = null, $code = null)
{
return new static(true, $message, $code);
}
Illuminate/Auth/Access/Response.php
を生成して返していますね。
public function authorize($ability, $arguments = [])
{
return $this->inspect($ability, $arguments)->authorize();
}
inspct()
はIlluminate/Auth/Access/Response
を返すので、Illuminate/Auth/Access/Response::authorize()
が呼ばれます。
public function authorize()
{
if ($this->denied()) {
throw (new AuthorizationException($this->message(), $this->code()))
->setResponse($this);
}
return $this;
}
ここでエラーレスポンスを返すか、次のミドルウェアへ処理を移すかの分岐になっていますね。
以上でポリシーの認可は終わりです!
まとめ
ポリシーはリソース(モデル)を利用するGateの糖衣のようなもの。
ルートモデルバインディングを前提としている。
before, afterなどのhookが用意されている。