tohokuaikiのチラシの裏

技術的ネタとか。

traitのメソッドをoverwriteしたんだけど、そのoverwriteされたメソッドを呼びたい場合

参考にさせていただきました

qiita.com ありがとうございました。

試行錯誤してみる

まずは基本となるtraitのAと親クラスBaseを定義

<?php
trait A
{
    public function say()
    {
        echo "I am A\n";
    }
}

class Base
{
    public function say()
    {
        echo "I am Base\n";
    }
}

で、Baseを継承しAをtraitしたクラスを作る

<?php
class Sayer extends Base
{
    use A ;

    public function say()
    {
        echo "I am Sayer\n";
    }
}

このSayerクラスのsay()メソッドからtraitのsay()メソッドをコールしたいわけです。

単純にparentを使った

<?php
class Sayer extends Base
{
    use A ;

    public function say()
    {
        echo "I am Sayer\n";
        parent::say();
        $this->sayA();
    }
}

(new Sayer())->say();

結果

I am Sayer
I am Base

そりゃー、そーですよね。parentは継承元のクラスなんで…

単純に$this->を使った

<?php
class Sayer extends Base
{
    use A;
    
    public function say()
    {
        echo "I am Sayer\n";
        $this->say();
    }
}
(new Sayer())->say();

結果

I am Sayer
I am Sayer
...(略:252回繰り返す)
I am Sayer
I am Sayer
PHP Fatal error:  Uncaught Error: Maximum function nesting level of '256' reached, aborting! in /home/vagrant/work/trait_parent.php:22

メソッドループして怒られた。…当たり前だな。

解決方法

use ... as を使う

<?php
class Sayer extends Base
{
    use A {
        A::say as sayA;
    }
    
    public function say()
    {
        echo "I am Sayer\n";
        $this->sayA();
    }
}
(new Sayer())->say();

結果

I am Sayer
I am A

無事traitのメソッドが呼び出せた。

LaravelのRequest->rules()がどの条件で掛かるのか?

が良くわからなかったので追ってみた。

ざっとBreakpointを設置して

<?php
class FormRequestServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap the application services.
     *
     * @return void
     */
    public function boot()
    {
        $this->app->afterResolving(ValidatesWhenResolved::class, function ($resolved) {
            $resolved->validateResolved();
        });

の $resolved->validateResolved();で登録しているみたい。GETメソッドの場合は個々を実行せず、PATCHメソッドの時はこの部分を実行していた。

$this->app->afterResolving() の実装

このメソッドを追ってみるとsrc/Illuminate/Container/Container.php

<?php
class Container implements ArrayAccess, ContainerContract
{
    public function afterResolving($abstract, Closure $callback = null)
    {
        if (is_string($abstract)) {
            $abstract = $this->getAlias($abstract);
        }

        if ($abstract instanceof Closure && is_null($callback)) {
            $this->globalAfterResolvingCallbacks[] = $abstract;
        } else {
            $this->afterResolvingCallbacks[$abstract][] = $callback;
        }
    }

だったんだけど、こっからよくわかんなかった…

LaravelのPolicyで丸二日ほど悩む…効かないってか効きすぎるというか…

やりたいこと

apiのrouteに対して適宜認可を掛ける。対象になるのは、usersテーブル。これはLaravelのデフォルトのユーザーテーブル。

show/editできるのは、管理者とその当該ユーザーのみとか。

やったこと

routes/api.php

<?php
Route::middleware('auth')->group(function () {
    Route::resources([
        'user' => 'UserController',

として、(このauthはログイン状態を見ているだけ)

app/Http/Controllers/UserController.php にリソースコントローラの認可を設定

<?php
class UserController extends Controller
{
    /**
     * UserController constructor.
     */
    public function __construct()
    {
        $this->authorizeResource(User::class, 'user');
    }

で、ポリシーファイル app/Policies/UserPolicy.php

<?php
class UserPolicy
{
    use HandlesAuthorization;

    /**
     * Determine whether the user can view the model.
     *
     * @param  \App\User  $user
     * @param  \App\User  $model
     * @return mixed
     */
    public function view(User $user, User $model)
    {
         return $this->viewAny($user) || $user->id === $model->id;
    }

を作ってサービスプロバイダーに登録 app/Providers/AuthServiceProvider.php

<?php
class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array
     */
    protected $policies = [
        App\User::class => App\Policies\UserPolicy::class,

症状

403エラーが返ってくる

{ exception: "Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException",
  file: "/home/vagrant/myapp/vendor/laravel/framework/src/Illuminate/Foundation/Exceptions/Handler.php"
  line: 223
  message: "This action is unauthorized." }
  trace : {...}
}

みたいな。

対処方法

ControllerのメソッドDIを変えると全然違う

<?php
public function show($id)

の場合だと、403になる。というか、そもそもPolicyのview()まで来ていない。

<?php
public function show($id)

の場合だと、403になる。というか、そもそもUserPolicy::view()まで来ていない。

<?php
public function show(Request $request, User $user)

とすると、Policy::view()まで来るようになる。

ということで

Policyを使って認可する場合は、Controllerのメソッド引数も考慮しなければならないようだ。
LaravelってDIの魔法みたいなことするけど、こういうわけわからない挙動もあるのどうなんだ…

俺的な静的Facadeの作り方

LaravelでオレオレUtililyクラスを使いたいと思った。Staticメソッドを自分が使う分だけのもの。

で、Facadeを使うと良いかなと思って、LaravelのFacadeを作り方を調べたんだけど、なんか、ServiceProviderを登録したりFacadeクラスと実行クラスを分けたりとかなんだりかんだりとややこしそうだった。

参考にしたのはこのあたり。 LaravelのFacade(ファサード)でオリジナルの処理クラスを定義する入門編
【Laravel 5.4】ファサード の作り方(でも多用は良くない) - Qiita LaravelのFacadeの作り方 - Qiita

で、結局これだけでいいじゃんっていうのが以下。

Laravelバージョン

Laravel Framework 6.18.26

Facade敬称クラスを作る

app/Facades/ITTUtil.php

<?php
namespace App\Facades;

use Illuminate\Support\Facades\Facade;

class ITTUtil extends Facade
{
    const FACADE_BIND_KEY = 'ittutil';

    protected static function getFacadeAccessor() {
        return self::FACADE_BIND_KEY;
    }

    /**
     * @return array List of Japanese Prefectures.
     */
    public static function pref()
    {
        return [
            1 => '北海道', 2 => '青森県', 3 => '岩手県', 4 => '宮城県',
        ];
    }
}

ServiceProviderに登録(これ、不要だった)

なんか、参考記事ではServiceProviderを新しく作って…ってやってたけど、AppServiceProviderがあるのでこれ使えばいいんじゃないか?と思って app/Providers/AppServiceProvider.php

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->bind(
            'ittutil'
        );
    }

で、次にconfig/app.phpに登録

config/app.php

<?php

    'aliases' => [

        'ITTUtil' => App\Facades\ITTUtil::class,

なんか、これだけでイケた。

たぶん、DIとか使わなければいけるんだろう。

LaravelのPolicyを二重にしたらダメだった

$ ./artisan --version
Laravel Framework 6.18.26

です。

Policy使ってアクセス制限

こんな感じ。

<?php
class UserController extends Controller
{
    /**
     * UserController constructor.
     */
    public function __construct()
    {
        $this->authorizeResource(User::class);
    }


    /**
     * @param Request $request
     * @return \Illuminate\Http\JsonResponse
     * @throws \Illuminate\Auth\Access\AuthorizationException
     */
    public function index(Request $request)
    {
        $this->authorize('viewAny');
        $users = User::all();
        return response()->json(['users' => $users]);
    }

そしたら、UserPolicyで

<?php
class UserPolicy
{
    use HandlesAuthorization;

    /**
     * Determine whether the user can view any models.
     *
     * @param  \App\User  $user
     * @return mixed
     */
    public function viewAny(User $user)
    {
        return true;
    }

ってしてるのにエラーが出る。 なんでUserController::index()で$this->authorize('viewAny')をわざわざ入れたかって言うのは、Controllerメソッドとの対応が https://reffect.co.jp/laravel/laravel-gate-policy-understand#Policy-3 このページの表を見ても無かったから。

あれー、って思って、実装している
laravel\framework\src\Illuminate\Foundation\Auth\Access\AuthorizesRequests.php を見てみると、

<?php
trait AuthorizesRequests
{
...
    protected function resourceAbilityMap()
    {
        return [
            'index' => 'viewAny',
            'show' => 'view',
            'create' => 'create',
            'store' => 'create',
            'edit' => 'update',
            'update' => 'update',
            'destroy' => 'delete',
        ];
    }

なー、indexとviewAnyが対応してるやん…

2回やってもture返すならいいんじゃないかと思ったけど、2回やるとダメなんね。

ということで、 $this->authorize('viewAny'); を取って解決。