default eye-catch image.

flattenでcollectionを平坦化する

Laravelで多次元配列を1次元化するflatten()が便利だった. 連想配列の値のみを収集して1次元配列にしてくれる。 $ ./vendor/bin/sail artisan tinker Psy Shell v0.11.1 (PHP 8.1.2 — cli) by Justin Hileman >>> $collection = collect([ \'hoge\' => [1, 2, 3], ... \'fuga\' => \'aiueo\', ... \'foo\' => 1, ... \'bar\' => null ... ]); => IlluminateSupportCollection {#3529 all: [ \"hoge\" => [ 1, 2, 3, ], \"fuga\" => \"aiueo\", \"foo\" => 1, \"bar\" => null, ], } >>> $collection->flatten(); => IlluminateSupportCollection {#3525 all: [ 1, 2, 3, \"aiueo\", 1, null, ], }

default eye-catch image.

Laravel8 Jetstreamを導入した状態でsocialiteによるSNS認証を両立させる

Laravel8が大きく変わっていたので前回の記事で再入門した。 sailコマンドでコンテナの外からartisanコマンドを叩けて便利。 [clink url=\"https://ikuty.com/2021/05/16/laravel8-sail/\"] Laravel5,6あたりでSocialiteパッケージによりSNS認証を簡単に実装することができた. Laravel8+JetstreamにSocialiteを導入してSNS認証してみた. Jetstreamをインストールし,Jetstreamのrouteがある状態でSocialiteが機能するようにした. JetstreamのAuthはlaravel/uiのようにお手軽にrouteを変更できない様子. 今回はそれには触れず, 最低限の修正でJetstreamとSocialiteを両立させてみる. [arst_toc tag=\"h4\"] Jetstream導入 sailコマンド経由でインストールしていく。 composer, artisanだけでなく, npmもsailで実行できる. # jetstreamをインストールする $ ./vendor/bin/sail composer require laravel/jetstream # livewireをインストールする migrationファイルを作成する $ ./vendor/bin/sail artisan jetstream:install livewire # 作成したmigrationを実行する $ ./vendor/bin/sail artisan migrate # npm install , npm run dev $ ./vendor/bin/sail npm install $ ./vendor/bin/sail npm run dev migrationで作られたテーブル達を確認する. sailからmysqlを叩くことはできそうだが、さらに-eオプションでSQLを続けられなかった。 sail mysqlでいつものmysql clientに繋がる. sailはあくまでもユーザインターフェースなのでこれで良いか. $ ./vendor/bin/sail mysql mysql> show tables; +------------------------+ | Tables_in_example_app | +------------------------+ | failed_jobs | | migrations | | password_resets | | personal_access_tokens | | sessions | | users | +------------------------+ http://localhostを叩くと、認証機能が追加されていることを確認できる。 registerから登録してログインすると認証後URL (./dashboard) にredirectされる. profileに進むとまぁ普通に使いそうな機能が既にインプリメントされていることがわかる. routeの確認 Jetstreamをインストールした直後にJetstreamにより作られたrouteを確認してみる. いやー.. too much過ぎだろう... $ ./vendor/bin/sail artisan route:list +--------+----------+----------------------------------+---------------------------------+---------------------------------------------------------------------------------+-----------------------------------------------------------+ | Domain | Method | URI | Name | Action | Middleware | +--------+----------+----------------------------------+---------------------------------+---------------------------------------------------------------------------------+-----------------------------------------------------------+ | | GET|HEAD | / | | Closure | web | | | GET|HEAD | api/user | | Closure | api | | | | | | | AppHttpMiddlewareAuthenticate:sanctum | | | GET|HEAD | dashboard | dashboard | Closure | web | | | | | | | AppHttpMiddlewareAuthenticate:sanctum | | | | | | | IlluminateAuthMiddlewareEnsureEmailIsVerified | | | GET|HEAD | forgot-password | password.request | LaravelFortifyHttpControllersPasswordResetLinkController@create | web | | | | | | | AppHttpMiddlewareRedirectIfAuthenticated:web | | | POST | forgot-password | password.email | LaravelFortifyHttpControllersPasswordResetLinkController@store | web | | | | | | | AppHttpMiddlewareRedirectIfAuthenticated:web | | | GET|HEAD | livewire/livewire.js | | LivewireControllersLivewireJavaScriptAssets@source | | | | GET|HEAD | livewire/livewire.js.map | | LivewireControllersLivewireJavaScriptAssets@maps | | | | POST | livewire/message/{name} | livewire.message | LivewireControllersHttpConnectionHandler | web | | | GET|HEAD | livewire/preview-file/{filename} | livewire.preview-file | LivewireControllersFilePreviewHandler@handle | web | | | POST | livewire/upload-file | livewire.upload-file | LivewireControllersFileUploadHandler@handle | web | | | | | | | IlluminateRoutingMiddlewareThrottleRequests:60,1 | | | GET|HEAD | login | login | LaravelFortifyHttpControllersAuthenticatedSessionController@create | web | | | | | | | AppHttpMiddlewareRedirectIfAuthenticated:web | | | POST | login | | LaravelFortifyHttpControllersAuthenticatedSessionController@store | web | | | | | | | AppHttpMiddlewareRedirectIfAuthenticated:web | | | | | | | IlluminateRoutingMiddlewareThrottleRequests:login | | | POST | logout | logout | LaravelFortifyHttpControllersAuthenticatedSessionController@destroy | web | | | GET|HEAD | register | register | LaravelFortifyHttpControllersRegisteredUserController@create | web | | | | | | | AppHttpMiddlewareRedirectIfAuthenticated:web | | | POST | register | | LaravelFortifyHttpControllersRegisteredUserController@store | web | | | | | | | AppHttpMiddlewareRedirectIfAuthenticated:web | | | POST | reset-password | password.update | LaravelFortifyHttpControllersNewPasswordController@store | web | | | | | | | AppHttpMiddlewareRedirectIfAuthenticated:web | | | GET|HEAD | reset-password/{token} | password.reset | LaravelFortifyHttpControllersNewPasswordController@create | web | | | | | | | AppHttpMiddlewareRedirectIfAuthenticated:web | | | GET|HEAD | sanctum/csrf-cookie | | LaravelSanctumHttpControllersCsrfCookieController@show | web | | | GET|HEAD | two-factor-challenge | two-factor.login | LaravelFortifyHttpControllersTwoFactorAuthenticatedSessionController@create | web | | | | | | | AppHttpMiddlewareRedirectIfAuthenticated:web | | | POST | two-factor-challenge | | LaravelFortifyHttpControllersTwoFactorAuthenticatedSessionController@store | web | | | | | | | AppHttpMiddlewareRedirectIfAuthenticated:web | | | | | | | IlluminateRoutingMiddlewareThrottleRequests:two-factor | | | GET|HEAD | user/confirm-password | password.confirm | LaravelFortifyHttpControllersConfirmablePasswordController@show | web | | | | | | | AppHttpMiddlewareAuthenticate | | | POST | user/confirm-password | | LaravelFortifyHttpControllersConfirmablePasswordController@store | web | | | | | | | AppHttpMiddlewareAuthenticate | | | GET|HEAD | user/confirmed-password-status | password.confirmation | LaravelFortifyHttpControllersConfirmedPasswordStatusController@show | web | | | | | | | AppHttpMiddlewareAuthenticate | | | PUT | user/password | user-password.update | LaravelFortifyHttpControllersPasswordController@update | web | | | | | | | AppHttpMiddlewareAuthenticate | | | GET|HEAD | user/profile | profile.show | LaravelJetstreamHttpControllersLivewireUserProfileController@show | web | | | | | | | AppHttpMiddlewareAuthenticate | | | | | | | IlluminateAuthMiddlewareEnsureEmailIsVerified | | | PUT | user/profile-information | user-profile-information.update | LaravelFortifyHttpControllersProfileInformationController@update | web | | | | | | | AppHttpMiddlewareAuthenticate | | | POST | user/two-factor-authentication | two-factor.enable | LaravelFortifyHttpControllersTwoFactorAuthenticationController@store | web | | | | | | | AppHttpMiddlewareAuthenticate | | | | | | | IlluminateAuthMiddlewareRequirePassword | | | DELETE | user/two-factor-authentication | two-factor.disable | LaravelFortifyHttpControllersTwoFactorAuthenticationController@destroy | web | | | | | | | AppHttpMiddlewareAuthenticate | | | | | | | IlluminateAuthMiddlewareRequirePassword | | | GET|HEAD | user/two-factor-qr-code | two-factor.qr-code | LaravelFortifyHttpControllersTwoFactorQrCodeController@show | web | | | | | | | AppHttpMiddlewareAuthenticate | | | | | | | IlluminateAuthMiddlewareRequirePassword | | | GET|HEAD | user/two-factor-recovery-codes | two-factor.recovery-codes | LaravelFortifyHttpControllersRecoveryCodeController@index | web | | | | | | | AppHttpMiddlewareAuthenticate | | | | | | | IlluminateAuthMiddlewareRequirePassword | | | POST | user/two-factor-recovery-codes | | LaravelFortifyHttpControllersRecoveryCodeController@store | web | | | | | | | AppHttpMiddlewareAuthenticate | | | | | | | IlluminateAuthMiddlewareRequirePassword | +--------+----------+----------------------------------+---------------------------------+---------------------------------------------------------------------------------+-----------------------------------------------------------+ Socialite導入 Laravel5とか6あたりではSocialiteパッケージを導入することでSNS認証を簡単に作れた. Laravel8+Jetstreamでも同じように作れるのか試してみた. 以下の記事を参考にさせていただきました. 【Laravel】JetstreamでSNS認証(ソーシャルログイン) # Socialite インストール ./vendor/bin/sail composer require laravel/socialite # google用provider インストール ./vendor/bin/sail composer require socialiteproviders/google OAuth idとsecret を取得しておく. (id,secretの発行にはこちらを参考にさせていただきました.) Callback redirect先のURLとして http://localhost/login/google/callback を登録する. Socialite実装 .envにOAuth認証id,secret,redirectURLを書く. .env自体はhostから編集すれば良い. GOOGLE_KEY=\"*****-*******.apps.googleusercontent.com\" GOOGLE_SECRET=\"****-****\" GOOGLE_REDIRECT_URI=\"http://localhost/login/google/callback\" config/servicesに以下の設定を追加する. \'google\' => [ \'client_id\' => env(\'GOOGLE_KEY\'), \'client_secret\' => env(\'GOOGLE_SECRET\'), \'redirect\' => env(\'GOOGLE_REDIRECT_URI\'), ], Routeを追加する. Laravel7までとLaravel8ではRouteの書き方が異なる. Laravel7までは app/Providers/RouteServiceProvider.php に名前空間が定義されているため, Routeに書くコントローラの名前空間を書かなくても自動的に解決してくれた. 例えば, LoginController::class と書くと, 自動的にApp/Http/Controllers/LoginController::class と解釈された. Laravel8では, 名前空間を省略できなくなった. Route::prefix(\'login/{provider}\')->where([\'provider\'=> \'google\'])->group(function(){ Route::get(\'/\',[AppHttpControllersAuthLoginController::class, \'redirectToProvider\'])->name(\'sns_login.redirect\'); Route::get(\'/callback/\',[AppHttpControllersAuthLoginController::class, \'handleProviderCallback\'])->name(\'sns_login.callback\'); }); Socialite Providerを config/app.php のproviders に追加する /* * Socialite Providerをconfig/app.php の providers に追加する */ \'providers\' => [ ... SocialiteProvidersManagerServiceProvider::class, ... ], app/Providers/EventServiceProvider.php を以下の通り変更する. <?php namespace AppProviders; use IlluminateAuthEventsRegistered; use IlluminateAuthListenersSendEmailVerificationNotification; use IlluminateFoundationSupportProvidersEventServiceProvider as ServiceProvider; use IlluminateSupportFacadesEvent; use SocialiteProvidersManagerSocialiteWasCalled; //追加 class EventServiceProvider extends ServiceProvider { /** * The event listener mappings for the application. * * @var array */ protected $listen = [ Registered::class => [ SendEmailVerificationNotification::class, ], // 追加 SocialiteProvidersManagerSocialiteWasCalled::class => [ \'SocialiteProviders\\Google\\GoogleExtendSocialite@handle\', ], ]; /** * Register any events for your application. * * @return void */ public function boot() { // } } SNS認証によるログインを担うコントローラを自力で作成する. $ ./vendor/bin/sail artisan make:controller Auth\\LoginController Controller created successfully. 作成したコントローラの中身は以下の通り. <?php namespace AppHttpControllersAuth; use AppHttpControllersController; use AppModelsUser; use IlluminateHttpRequest; use LaravelSocialiteFacadesSocialite; use IlluminateSupportFacadesHash; use IlluminateSupportStr; class LoginController extends Controller { // メディア側へのリダイレクト public function redirectToProvider(Request $request) { $provider = $request->provider; return Socialite::driver($provider)->redirect(); } // メディア側から返されるユーザー情報 public function handleProviderCallback(Request $request) { $provider = $request->provider; $sns_user = Socialite::driver($provider)->user(); $sns_email = $sns_user->getEmail(); $sns_name = $sns_user->getName(); // 登録済ならログイン。未登録ならアカウント登録してログイン if(!is_null($sns_email)) { $user = User::firstOrCreate( // Userモデルに、レコードがあれば取得、なければ保存 [ \'email\' => $sns_email ], [ \'email\' => $sns_email, \'name\' => $sns_name, \'password\' => Hash::make(Str::random()) ]); auth()->login($user); session()->flash(\'oauth_login\', $provider.\'でログインしました。\'); return redirect(\'/\'); } return \'情報が取得できませんでした。\'; } } viewを作成する. ファイル名は app/View/auth/login.blade.php. Routeで書いた sns_login_redirect ページに遷移するリンクがあるだけ. <div> <a href=\"{{ route(\'sns_login.redirect\', \'google\') }}\">Google </div> Welcomeページのログインを修正 普通は何らかの画面が既にあってそこにSocialiteを組み込むと思うが, 今回は何もないので, とりあえずWelcomeページのログインをSocialite用に書き換えてみる. Jetstreamのrouteを変えようとしたが闇が深そうなので見なかったことにする. ちょっとJetstreamは出来が良くないのかなー.. デフォルトのWelcomeページのログインは, Jetstreamが生成する /login に合わせて作られてある. このままだと, Jetstreamが作った認証機構が動く. 例えば以下のように変更するとWelcomeページのログインをSocialiteのものに差し替えることができる. route(\'login\')をroute(\'sns_login.redirect\',\'google\')に変更した. また, registerは不要なので, registerへの遷移リンクを削除した. <body class=\"antialiased\"> <div class=\"relative flex items-top justify-center min-h-screen bg-gray-100 dark:bg-gray-900 sm:items-center py-4 sm:pt-0\"> @if (Route::has(\'sns_login.redirect\')) <div class=\"hidden fixed top-0 right-0 px-6 py-4 sm:block\"> @auth <a href=\"{{ url(\'/dashboard\') }}\" class=\"text-sm text-gray-700 underline\">Dashboard @else <a href=\"{{ route(\'sns_login.redirect\',\'google\') }}\" class=\"text-sm text-gray-700 underline\">Log in @endauth </div> @endif ... 動作確認 未ログインの状態で http://localhost を開くと, Welcome画面が表示され, Login への遷移リンクが表示される. Loginを押下すると, Googleのログイン画面に遷移する. アカウントを選択すると, http://localhost/login/google/callback にredirectがかかる. もし当サイトにアカウントがなければ,アカウントを作成する. アカウントがあれば,そのユーザでログインする. 晴れて, Googleアカウントと同じメールアドレスを持つユーザでログインした状態でダッシュボード(./dashbaord)が開く.

default eye-catch image.

Laravel8 sailで環境構築

とにかく進歩が早いLaravel。 セマンティックバージョニングになった6あたりから結構な速度で機能を乗せて来た感がある. 付いていくのがなかなか大変というのはある. 開けた口に無理やり食べ物を押し込んでくるような強引さの中にセンスの良さを感じ取れるので、 ちょっと付いて行ってみることにする. [arst_toc tag=\"h4\"] Laravel sail Laravel公式が用意するDocker開発環境を操作する軽量なコマンドラインインターフェース. ポイントは、コンテナの外部からコンテナ内のLaravelに対してコマンドを実行できる点. dockerコマンドをラップし、コンテナの内部で実行した結果を応答する仕組みとなっている. フルスタックフレームワークであるLaravelらしく何でも内包してしまう. composerやartisanコマンド実行のために、わざわざdockerコマンドを叩くのは辛い. sailが無いとdockerコマンドを叩きまくるか、コンテナに入って作業する必要がある. sailを使うことで、コンテナの中に入らず外からかsailコマンドを実行できる. こんな風にするとdockerの上位に来る仕組みを作れるのか、と結構感動. sailでプロジェクトを作る 既存のプロジェクトにsailを導入するパターンと、新規にプロジェクトを作成するパターンの2通りがある. 今回は新規にプロジェクトを作成していく. https://laravel.build/example-app というURLはShellScriptのコードを返す. withの後ろにインストールしたいミドエウウェアを指定する. 今回はmysqlだけ. カンマ区切りで複数指定可. $ mkdir -p ~/hoge && cd ~/hoge $ curl -s \"https://laravel.build/example-app?with=mysql\" | bash $ cd example-app && ./vendor/bin/sail up ちなみに、https://laravel.build/example-appは以下のShellScriptを返す. そのShellScriptは何をやっているかというと. laravelsail/php80-composerというイメージからコンテナを起動する. laravel newコマンドでプロジェクトを作成する. artisan sail:installコマンドを実行する. ディレクトリのOwnerを変更する. (パスワードが要求される) docker info > /dev/null 2>&1 # Ensure that Docker is running... if [ $? -ne 0 ]; then echo \"Docker is not running.\" exit 1 fi docker run --rm -v $(pwd):/opt -w /opt laravelsail/php80-composer:latest bash -c \"laravel new example-app && cd example-app && php ./artisan sail:install --with=mysql\" cd example-app CYAN=\'33[0;36m\' LIGHT_CYAN=\'33[1;36m\' WHITE=\'33[1;37m\' NC=\'33[0m\' echo \"\" if sudo -n true 2>/dev/null; then sudo chown -R $USER: . echo -e \"${WHITE}Get started with:${NC} cd example-app && ./vendor/bin/sail up\" else echo -e \"${WHITE}Please provide your password so we can make some final adjustments to your application\'s permissions.${NC}\" echo \"\" sudo chown -R $USER: . echo \"\" echo -e \"${WHITE}Thank you! We hope you build something incredible. Dive in with:${NC} cd example-app && ./vendor/bin/sail up\" fi sailでコンテナを立ち上げる 要はdocker-compose upをラップしたsail upコマンドを叩く. PHPのbundlerであるcomposerの仕様上, vendor 以下にモジュールがインストールされる. sailコマンドも ./vendor/bin/ に入っている. そこで ./vendor/bin/sail up を実行する. $ cd example-app $ ./vendor/bin/sail up dockerそのものなので, Ctrl+Cで落ちる. もちろん、./vendor/bin/sail up -d によりバックグラウンドで立ち上がる. $ ./vendor/bin/sail up -d ブラウザからhttp://localhostを開く あっさり開けた. ちなみに Dockerfile内で /usr/local/bin/start-containerを実行している. start-container内ではsupervisordによりLaravelのビルトインサーバをデーモン化している. #!/usr/bin/env bash if [ ! -z \"$WWWUSER\" ]; then usermod -u $WWWUSER sail fi if [ ! -d /.composer ]; then mkdir /.composer fi chmod -R ugo+rw /.composer if [ $# -gt 0 ];then exec gosu $WWWUSER \"$@\" else /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf fi supervisord.confは以下の通り. [supervisord] nodaemon=true user=root logfile=/var/log/supervisor/supervisord.log pidfile=/var/run/supervisord.pid [program:php] command=/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80 user=sail environment=LARAVEL_SAIL=\"1\" stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 sailでLaravelのバージョンを確認してみる 試しにコンテナの外からsailコマンドでartisan --versionを実行してみる. まるでコンテナの外からartisanコマンドを打っているような感覚. 良いと思う. $ ./vendor/bin/sail artisan --version Laravel Framework 8.41.0

default eye-catch image.

PHPで統計アプリを作れるか否か

LaravelをAPIサーバにして同期的にsklearnのPCAを実行するアプリを作ってみました。 jQyery/bootstrap/chart.jsがフロント、APIサーバはLaravel+MySQL。 Laravel製APIがGET/POSTに対してPythonコードを実行します(Shellで...)。 exec()でPythonを起動するため無茶苦茶重いし、 ろくにエラーハンドリングできません。 結論から書けば同期的なアプリをこの構造で作るのは無理があります。 バックエンドが無茶苦茶重くてどうせバッチ実行になるのであれば、 上記の問題は結構問題なくなって、これでも良いかなと思い始めます。 MS系のInteroperabilityで、多言語が動的に結合するやつがありますが、 あんな感じでLL言語をglueできれば楽なのになと思います。 PSRの多言語拡張みたいなやつで、PHPからPythonのクラスを使うとか...

default eye-catch image.

Laravel Accessor/Mutatorを使って透過的にフィールドを暗号化/復号するサンプル

DBに入っているデータを決まった書式/形式に変換して表示したり、 逆に逆変換して保存する例は多いかと思います。 変換,逆変換の実装方法は以下みたいな感じかと..。 いずれも変換/逆変換の存在を忘れて仕様が抜けたり、 同じことを他でも書くコードクローンが発生する原因になる。 Controllerにダラダラと変換/逆変換を書く EloquentにオレオレSetter/Getterを書く Accessor/Mutatorを使うことで上記の原因を無くすことができます。 Accessor/Mutator Eloquentのメンバ変数(つまり、テーブルのフィールド)へのアクセスを ある規則をもってEloquentに定義したSetter/Getterを仲介するように強制できます。 [clink implicit=\"false\" url=\"https://laravel.com/docs/5.8/eloquent-mutators\" imgurl=\"http://laravel.jp/assets/img/logo-head.png\" title=\"Eloquent: Mutators Introduction\" excerpt=\"Accessors and mutators allow you to format Eloquent attribute values when you retrieve or set them on model instances. For example, you may want to use the Laravel encrypter to encrypt a value while it is stored in the database, and then automatically decrypt the attribute when you access it on an Eloquent model.\"] Accessors and mutators allow you to format Eloquent attribute values when you retrieve or set them on model instances. For example, you may want to use the Laravel encrypter to encrypt a value while it is stored in the database, and then automatically decrypt the attribute when you access it on an Eloquent model. In addition to custom accessors and mutators, Eloquent can also automatically cast date fields to Carbon instances or even cast text fields to JSON. 暗号化/復号 サンプル 標題の通りですが、Accessor/Mutatorを使ってフィールドを暗号化/復号してみます。 Cryptファサードを使ってAES-256-CBCの暗号化/復号を行う対です。 secretvalueというフィールドにAES256CBCで暗号化して書き込み、復号して読み込みます。 class User extends Authenticatable { use Notifiable; /** * Get the user\'s secretvalue. * * @param string $value * @return string */ public function getSecretvalueAttribute($value) { return decrypt($value); } /** * Set the user\'s secretvalue. * * @param string $value * @return string */ public function setSecretvalueAttribute($value) { $this->attributes[\'secretvalue\'] = encrypt($value); $this->save(); } } 透過的に呼び出す例です。 Userのsecretvalueフィールドに\"hogehoge\"という値を設定しています。 hogehogeという平文を暗号化してsecretvalueフィールドに書き込む処理は使う側には見えません。 Route::get(\'/sample/setvalue\',function(){ AppUser::find(1)->secretvalue = \'hogehoge\'; }); Userのsecretvalueフィールドを読み込んで出力しています。 暗号化済み文字列を復号する処理は使う側には見えません。 Route::get(\'/sample/getvalue\',function(){ echo AppUser::find(1)->secretvalue; }); より広い用途で使える 暗号化/復号はかなり直球な使い方ですが、ビジネスロジック内の定型処理など 積極的に使おうとするとAccessor/Mutatorに掃き出せるケースがありそうです。

default eye-catch image.

Model Binding と 1枚のBladeで CRUD する

1枚のBladeで確認画面付きCRUDを実現できると、Bladeの枚数が格段に少なくなって良さそう。 その前にまずModelBindingで単なるUserを1枚のBladeでCRUDしてみる。 1枚のBladeが複数の機能で使われることになり、Bladeの中に要素と制御が増えていくため、 実は、Bladeの枚数が増えたとしても1つのBladeを単純にした方が良いのかもしれないが、 1度作っておくとずっと使えるかもしれないので、そこまでやってみる。 やること Laravelに最初から付いてくるUserを使って、name,email,passwordのCRUDをする。 URL(route)は以下。showのパラメタをOptionalにして、あればUpdate、なければCreateする。 Update、Createは、本質的に分けるべきと考えてURLを別にしてある。 firstOrNew()を使うと、あればUserインスタンスを読み込んでくれる。 なければインスタンスを作る。ただしレコードは作らない。新規作成操作時にレコードを作成する。 <?php /* |-------------------------------------------------------------------------- | Web Routes |-------------------------------------------------------------------------- | | Here is where you can register web routes for your application. These | routes are loaded by the RouteServiceProvider within a group which | contains the \"web\" middleware group. Now create something great! | */ Route::get(\'/user/{user?}\',\'UserController@show\'); Route::post(\'/user/\',\'UserController@add\')->name(\'postAddUser\'); Route::post(\'/user/{user}\',\'UserController@edit\')->name(\'postEditUser\'); コントローラ コントローラは以下。無条件に保存するだけなのでほとんど何も書いてない。 条件が増えてくるとそれなりに行数が増える。 ModelBindingの良さは、タイプヒンティングでEloquentのインスタンスを受けられること。 変数を受けてEloquentインスタンスを探す手間がバッサリ無い。 RequestValidatorは載せていません。 <?php namespace AppHttpControllers; use AppHttpRequestsAddUserRequest; use AppHttpRequestsEditUserRequest; use AppUser; class UserController extends Controller { public function show($id=null) { $user = User::firstOrNew([\'id\'=>$id]); return view(\'user\',compact(\'user\')); } public function add(User $user,AddUserRequest $request) { $user->fill($request->only([\'name\',\'email\',\'password\']))->save(); return view(\'user\',compact(\'user\')); } public function edit(User $user,EditUserRequest $request) { $user->fill($request->only([\'name\',\'email\',\'password\']))->save(); return view(\'user\',compact(\'user\')); } } Blade 肝心のBladeは以下。これだけなのに結構書かないといけない。 laravelcollective/htmlは大分前にLaravelから外れていて、使わない方が良いのかも。 自力でHTMLを書くのと大して労力が変わらない可能性がある。 結構書かないといけないから1枚にしたいのか、複数枚でよければあまり書かなくて良いのか、 微妙なところ。Laravel5.7なのでBootstrap4。validation用のクラスが全然違う。 親Blade(layouts.app)は何でも良いので載せていません。 @extends(\'layouts.app\') @section(\'content\') @if (isset($user->id))編集 @else 追加 @endif @if ($user->wasRecentlyCreated) {!! Form::model($user,[\'route\'=>[\'postEditUser\',$user->id],\'class\'=>\'form-horizontal\'])!!} @else {!! Form::model($user,[\'route\'=>[\'postAddUser\'],\'class\'=>\'form-horizontal\'])!!} @endif {!! Form::label(\'name\', \'名前 :\') !!} @if($errors->has(\'name\')) {!! Form::text(\'name\',$user->name,[\'class\'=>\'form-control is-invalid\']) !!} @else {!! Form::text(\'name\',$user->name,[\'class\'=>\'form-control\']) !!} @endif {!! $errors->first(\'name\') !!} {!! Form::label(\'email\', \'email :\') !!} @if($errors->has(\'email\')) {!! Form::email(\'email\',$user->email,[\'class\'=>\'form-control is-invalid\']) !!} @else {!! Form::email(\'email\',$user->email,[\'class\'=>\'form-control\']) !!} @endif {!! $errors->first(\'email\') !!} {!! Form::label(\'password\', \'password :\') !!} @if($errors->has(\'password\')) {!! Form::password(\'password\',[\'class\'=>\'form-control is-invalid\']) !!} @else {!! Form::password(\'password\',[\'class\'=>\'form-control\']) !!} @endif {!! $errors->first(\'password\') !!} @if($user->wasRecentlyCreated) {!! Form::submit(\'保存\',[\'class\'=>\'btn btn-primary form-control col-sm-2\']) !!} @else {!! Form::submit(\'新規作成\',[\'class\'=>\'btn btn-primary form-control col-sm-2\']) !!} @endif {!! Form::close() !!} @endsection まとめ relationもないし懸案の確認画面もないので、単純。 次回、has a、has many relation版と、確認画面付きの版を試します。

default eye-catch image.

ミドルウエア

この記事は自分の勉強のために書いています。 ソースはLaravel5.7の公式ドキュメントです。 新しいことは何もないので通常はそちらを参照してください。 [clink implicit=\"false\" url=\"https://readouble.com/laravel/5.7/ja/middleware.html\" imgurl=\"http://laravel.jp/assets/img/header.jpg\" title=\"Laravel 5.7 ミドルウェア\" excerpt=\"ミドルウェア\"] [arst_toc tag=\"h4\"] アプリケーションに送られてきたリクエストを途中でフィルタするのがミドルウェア。 認証とかCORSとかCSRFとか、デフォルトでミドルウェアが用意されているけれども、 ミドルウェアを自作することができる。 ミドルウェアの定義 新しいミドルウェアの作成 ミドルウェアを作成するartisanコマンドは以下の通り。 こうすると、app/Http/Middlewareの中にCheckCostクラスが作られる。 $ php artisan make:middleware CheckCost; CheckCostクラスの中でhandle()メソッドを実装する。 $requestを$nextに流す際に$closureを評価するように動作し、 結果として$requestと$nextの間のフィルタ処理を$closureに書くことができる。 <?php namespace AppHttpMiddleware; use Closure; class CheckCost { /** * 送信されたきたリクエストをフィルタする * * @param IlluminateHttpRequest $request * @param Closure $next * @return mixed */ public function handle($request, $closure $next) { if ($request->value < 100) { return redirect('hoge'); } return $next($request); } } ミドルウェアを複数使用するとき、 前の段のミドルウェアを通過した後、次の段のミドルウェアを通過する。 Before Middleware/ After Middleware 公式だと仰々しい名前が付いているのだけれども、 $nextを呼んだ後に処理をするか、呼ぶ前に処理をするか、の違いを表現できる。 つまり、リクエストを完了する前の処理なのか、後の処理なのか。 <?php namespace AppHttpMiddleware; use Closure; class CheckCost { /** * Before Middleware * リクエストを評価する前に処理する * * @param IlluminateHttpRequest $request * @param Closure $next * @return mixed */ public function handle($request, $closure $next) { //アクションを実行 return $next($request); } } namespace AppHttpMiddleware; use Closure; class CheckCost { /** * After Middleware * リクエストを評価した後に処理する * * @param IlluminateHttpRequest $request * @param Closure $next * @return mixed */ public function handle($request, $closure $next) { $response = $next($request); return $response; } } 登録 グローバルミドルウェア 要は全てのHTTPリクエストについてミドルウェアを通すやり方。 App/Http/Kernel.phpに書く。Kernelクラスにある$middlewareという配列に グローバルミドルウェアにしたいやつを並べていく。 <?php namespace AppHttp; use IlluminateFoundationHttpKernel as HttpKernel; class Kernel extends HttpKernel { protected $middleware = [ ... ] } 特定のルートのみにミドルウェアを設定 特定のルートに限定してミドルウェアを設定する場合もApp/Http/Kernel.phpに書く。 まず、$routeMiddlewareにミドルウェアの短縮キーを書く <?php $routeMiddleware = [ \'auth\' => AppHttpMiddlewareAuthenticate::class, \'auth.basic\' => IlluminateAuthMiddlewareAuthenticateWithBasicAuth::class, ... ]; そして、routes.phpでそれぞれのルートに定義した短縮キーを指定する。 短縮キーではなく完全なクラス名を書いても良いみたい。 ミドルウェアを複数書くと、その順番に通すことになる。 Route::get(\'admin/profile\', function() { })->middleware(\'auth\'); Route::get2(\'admin/profile2\', function() { })->middleware(\'CheckCost::class\'); Route::get3(\'admin/profile3\', function() { })->middleware(\'auth\',\'guest\'); ミドルウェアグループ 複数のミドルウェアをグループ化して、そのグループのミドルウェアを一気に当てることができる。 App/Http/Kernel.phpに書く。Kernelクラスにある$middlewareGroupという配列に グループ名をキー、ミドルウェアの配列を値として書いていく。以下のように。 <?php $middlewareGroup = [ \'web\' => [ \'auth\' => AppHttpMiddlewareAuthenticate::class, \'auth.basic\' => IlluminateAuthMiddlewareAuthenticateWithBasicAuth::class, ... ], ... ]; routes.phpの中では、それぞれのルートに定義したグループ名を与える。 例えば、以下だと\'/\'に対して\'web\'ミドルウェアグループを与えている。 つまり\'/\'に\'web\'ミドルウェアグループに定義したミドルウェアが順番にあたる。 Route::get(\'/\',function(){ })->middleware(\'web\'); routes/web.php に書いたルートには自動的に\'web\'ミドルウェアグループがあたる。 ミドルウェアパラメタ ミドルウェアパラメタの書き方 ミドルウェア実行時に引数を与えることができる。 書き方は以下の通り。handle()をオーバーライドする際に、引数として受けるパラメタを追加する。 例えば、パラメタ$paramを受け取るミドルウェアは以下の通り。 クエリパラメタにある$valが$paramである場合に限り処理を書いている。 <?php namespace AppHttpMiddleware; use Closure; class CheckCost { /** * 送信されたきたリクエストをフィルタする * * @param IlluminateHttpRequest $request * @param Closure $next * @return mixed */ public function handle($request, $closure $next, $param) { if ($request->val == $param) { return redirect(\'hoge\'); } return $next($request); } } ミドルウェア指定時に以下のようにパラメタを渡す。 T.B.D. 終了処理ミドルウェア T.B.D. サンプル 実行時間を計測するミドルウェアの試作 こちらを参考に試しに作ってみた。 https://qiita.com/niisan-tokyo/items/663300f8df1c6c89f0ae $ php artisan make:middleware TimerMiddleware Middleware created successfully. TimerMiddleware本体はこちら。 <?php namespace AppHttpMiddleware; use Closure; class TimerMiddleware { /** * Handle an incoming request. * * @param IlluminateHttpRequest $request * @param Closure $next * @return mixed */ public function handle($request, Closure $next) { $before = microtime(true); $res = $next($request); Log::debug(mictotime(true) - $before); return $res; } } ミドルウェアの登録は、app/Http/Kernel.phpの$routeMiddlewareに追加。 protected $routeMiddleware = [ .. AppHttpMiddlewareTimerMiddleware::class, ]; ルートの定義はこちら。 Route::get(\'timer\',function(){ })->middleware(\'timer\'); /timer にアクセスすると、storages/logs/laravel.logにログが残る。 ... [2019-01-29 16:05:07] local.DEBUG: 0.019489049911499

default eye-catch image.

ルーティング

この記事は自分の勉強のために書いています。 ソースはLaravel5.7の公式ドキュメントです。 新しいことは何もないので通常はそちらを参照してください。 [clink implicit=\"false\" url=\"https://readouble.com/laravel/5.7/ja/routing.html\" imgurl=\"http://laravel.jp/assets/img/header.jpg\" title=\"Laravel 5.7 ルーティング\" excerpt=\"基本的なルーティング\"] [arst_toc tag=\"h4\"] 基本的なルーティング routes.phpの書き方 HTTP動詞に対応させたroutes.phpの書き方。RESTfulAPIを書く場合は必要なんだと思う。 anyを使うと全ての動詞に対応する。matchを使うと対応する動詞を選べる。 No 動詞 文法 1 GET Route::get($url,$callback); 2 POST Route::post($url,$callback); 3 PUT Route::put($url,$callback); 4 PATCH Route::patch($url,$callback); 5 DELETE Route::delete($url,$callback); 6 OPTIONS Route::options($url,$callback); 7 全て Route::any($url,$callback); 8 選んだ動詞 Route::match($array_of_verb,$url,$callback); リダイレクトルート 単純にリダイレクトをかける場合は、わざわざコントローラを通さなくて良い。 (コントローラを通してた...)。 通常302で、301もルーティングだけで対応可能。 No HTTP Status Code 文法 1 302 Route::redirect(\'here\',\'there\'); or Route::redirect(\'here\',\'there\',302); 2 302 Route::redirect(\'here\',\'there\',301); or Route::permanentRedirect(\'hear\',\'there\'); Viewルート ルートから直接ビューを返す場合はコントローラを介さなくてもよい。 No文法 1Route::view($url,$view_name); 2Route::view($url,$view_name,$array_of_parameters); ex, Route::view($url,$view,[\'param1\'=>100,\'param2\'=>200]); CSRF保護 webルートに定義したPOST,PUT,DELETEへのルートへ送信するフォームは 一緒にCSRFトークンを送る必要がある(CSRFは別にまとめる予定)。 ルートパラメータ URLの動作に必要なパラメタをクエリ文字列ではなくURLに含める書き方。 必須パラメータとしておけば省略は不可となり存在チェックを省略できる。 必須パラメータ Curly bracketsでパラメタ名を囲んで定義する。 受けるクロージャはそのパラメタ名をもつ引数を使用する。 Route::get(\'application/{application_id}\', function($application_id) { return $application_id; }); 任意パラメータ Curly brackets内のパラメタ名を\"?\"で終わらすと任意パラメータになる。 受けるクロージャでは必ずデフォルト引数を設定する。 Route::get(\'user/{name?}\',function($name = null) { return $name; }); 正規表現制約 ルートパラメタのフォーマットを正規表現で制約できる(そんなことできるんだ...)。 書き方は以下の通り。URLとクロージャは触らない。 Route::get(\'user/{name}\', function($name) { return $name; })->where(\'name\',\'[A-Za-z]+\'); グローバル制約 RouteServiceProviderのbootメソッドに全てのルートパラメタを制約するルールを書ける。 例えば以下のように書いておくと、全てのルートの\"id\"という名前のクエリパラメタが数値に制約される。 /** * */ public function boot() { Route::pattern(\'id\',\'[0-9]+\'); parent::boot(); } /** * routes.php */ Route::get(\'user/{id}\',function($id) { return $id; // $idは数値に制約 }); 名前付きルート 定義したルートに名前を付けておくことで、そのルートのURLを名前から引くことができる。 名前付きルート 例えばuser/{id}に対するルートにmypageという名前を付けるには以下。 Route::get(\'user/{id}\',function($id) { return $id; })->name(\'mypage\'); 名前付きルートへのURL取得 このルートへのURLはroute()により取得できる。 ルートが必須/任意パラメータを必要とする場合は第2引数で値を与える。 redirect()->route(\'mypage\',[\'id\'=>100]); ルートグループ 複数のルートで共通の属性(=ミドルウェアと名前空間)をまとめて書ける。 グループはネストすることができる。ミドルウェアと名前空間は別途記載。 ミドルウェアによるグループ化 複数のルートで共通のミドルウェアを外出しする書き方は以下。 (もちろん個別に書くこともできる) Route::middleware([\'first\',\'second\'])->group(function() { Route::get(\'/\',function(){ // firstミドルウェア、secondミドルウェアを順番に使用 }); Route::get(\'/hoge/{id}\',function($id){ // firstミドルウェア、secondミドルウェアを順番に使用 }); }); 名前空間によるグループ化 複数のルートで同じPHP名前空間を使用する書き方は以下。 こうすることで、各コントローラのファイルの先頭に名前空間を書かなくてもよくなる。 Route::namespace(\'Namespace1\')->group(function() { // AppHttpControllersNamespace1 名前空間下のコントローラ }); })/ <?php namespace App/Http/Controllers/Namespace1; // <- これが不要になる サブドメインによるグループ化 複数のルートが同じサブドメイン配下にあるような配置にする書き方は以下。(こんなことできるのか!)。 Route::domain(\'{subdomainname}.myapp.test\')->group(function() { Route::get(\'user/{id}\',function($id) { // subdomainname.myapp.test/user/100 }); }); ルートプレフィックスによるグループ化 複数のルートが同じ文字列からスタートするように配置する書き方は以下。 Route::prefix(\'sameprefix\')->group(function() { Route::get(\'user/{id}\',function($id) { // /sameprefix/user/100 }); Route::get(\'group/{id}\',function($id) { // /sameprefix/group/200 }); }); ルート名プレフィックスによるグループ化 複数のルートのルート名が同じ文字列からスタートするように配置する書き方は以下。 Route::name(\'sameprefix\')->group(function() { Route::get(\'user/{id}\',function($id) { // \'sameprefix.user\' というルート名 })->name(\'user\'); Route::get(\'group/{id}\',function($id) { // \'sameprefix.group\' というルート名 }); }); モデル結合ルート ルートで与えた情報(多くの場合でモデルID)をコントローラに渡して コントローラの中でモデルインスタンスを引くという使い方がほとんどなので、 ルートに含めたモデルIDからモデルインスタンスを解決してコントローラでそれを使うことができる。 当然、クロージャを直接書いても同じ。 暗黙のモデル結合ルート どうやってルートパラメータとモデルインスタンスを紐づけるかというと、 なんと、PHPのタイプヒントを使って紐づける。 以下、$userがAppUserクラスであるとタイプヒントされている。 ルートパラメータにuserがあるから、クロージャの中ではAppUserのインスタンスを $userを使って引いたオブジェクトを使用できる。 わざわざ$userを使ってAppUser::find($user)と書いたりエラーケースを書くのを省略できる。 AppUser::find($user)が見つからなかったらルートは404を返す。すごいー。 Route::get(\'users/{user}\',function(AppUser $user) { // AppUser $user を使用できる。 }); キー名がカスタマイズされたモデル結合ルート モデルの主キーがidではないときimplicitにidからModelを引くことが出来ない。 モデル側でgetRouteKeyName()をオーバーライドして主キーを返せば良い。 ルートパラメータを新しい主キーであるものとしてインスタンスを探してくれる。 Route::get(\'users/{user}\',function(AppUser $user) { // AppUser.userid が $user であるインスタンスを使用できる。 }); /** * モデルの中 * */ public function getRouteKeyName() { return \'userid\'; } 明示的なモデル結合ルート ルート名とタイプヒントを使ってimplicitにモデル結合する方法とは別に、 RouteServiceProviderクラスのboot()メソッドの中でexplicitにモデル結合を定義できる。 タイプヒントがなくても良いというのならexplicitに書く意味もあると思うのだけど、 タイプヒントが必要なのであれば、implicitな記法に対して冗長な気がするんだけども...。 /** * RouteServiceProvider * */ public function boot() { parent::boot(); Route::model(\'user\',AppUser::class); } /** * routes.php * */ Route::get(\'user/{user}\',function(AppUser $user) { }); 依存解決ロジックのカスタマイズ 「ルートパラメータをモデルID、タイプヒントをモデルクラスとして、モデルIDを主キー(または別のキー) を使って検索する」という単純なロジックとは異なるロジックを定義する方法が用意されている。 例えばモデルを主キーとは関係ないフィールドで検索したいときは以下みたいに書く。 /** * RouteServiceProvider * */ public function boot() { parent::boot(); Route::bind(\'user\', function($value) { return AppUser::where(\'name\',$value)->first() ?? abort(404); }); } モデル側でresoluveRoutingBind()をオーバーライドすることでも実現可能。 /** * モデル * */ public function resolveRoutingBind($value) { return $this->where(\'name\',$value)->first() ?? abort(404); } その他 フォールバックルート フォールバックルートを書いておくと、routes.phpに書いたどのルートにも当たらなかったときに使われる。 通常404になるところを受ける。書く場合は最後に配置しないといけない。 Route::fallback(function(){ // do something when 404. });

default eye-catch image.

Laravel Mutator Accessor

たぶん Delphi/C# が起源だと思うんだが、データに紐づく規則や処理をモデルに寄せるパターン(Microsoft:The Repository Pattern)と同等の機能がPHP FrameworkであるLaravelのORM(Eloquent)にあり、流石に超便利だったのでメモっておく。 setter側をMutator、getter側をAccessorという。Mutator, Accessorを簡単にまとめると以下のような感じ。 Eloquentインスタンスに対して値をセットする際に必ずあるメソッド(Mutator)を経由する機能 Eloquentインスタンスから値をゲットする際に必ずあるメソッド(Accessor)を経由する機能 本家のドキュメントは以下のように続く。 Accessors and mutators allow you to format Eloquent attribute values when you retrieve or set them on model instances. For example, you may want to use the Laravel encrypter to encrypt a value while it is stored in the database, and then automatically decrypt the attribute when you access it on an Eloquent model. データに紐づくルールや処理をモデルに寄せることでコントローラの肥大化を防ぐ機能の一つだが、コントローラとモデルの間の取り決めを規約によって定めることで、コントローラからもモデルからも扱いやすくなる。 例えば、usersテーブルにfirst_nameという名前のフィールドがあったとする。 フィールドには小文字で入力し、使うときは必ず先頭1文字を大文字として使う、というルールがある場合Userモデル側を以下のような規約に従って書く。 <?php namespace App; use IlluminateDatabaseEloquentModel; class User extends Model { /* * Get the user\'s first name. * * @param string $value * @return string */ public function getFirstNameAttribute($value) { return ucfirst($value); } /* * Set the user\'s first name. * * @param string $value * @return void */ public function setFirstNameAttribute($value) { $this->attributes[\'first_name\'] = strtolower($value); } } コントローラからfirst_nameフィールドの値を取るコードは以下の通りだが、必ずAccessorを経由し値が返る。フィールドの値が何であれ先頭は必ず大文字になる。ブラウザ等に表示する際に加工する必要がない。 <?php $user = AppUser::find(1); $firstname = $user->first_name; ブラウザ等からデータが入って来たとき、それをusers.first_nameフィールドに格納する処理を書く際、必ずMutatorを経由する。 「小文字にする」という処理を一切意識しないで良い。 <?php $user = AppUser::find(1); $user->first_name = \'Sally\'; コントローラより上は涼しい顔をして重い処理の記述を省略できる。データに紐づくルールが重ければ重いほどモデルに寄せる効果が大きくなる。 例えば、データを暗号化して格納するケースにおいては、暗号化/復号処理をModelに隠蔽しControllerより上からは透過的に平文として扱うなど。 Eloquentは流石に雄弁だ。

default eye-catch image.

Laravel5 Form Request Validationによるコントローラの軽量化

フォームのビジネスロジック(検証や保存)をコントローラに書くと、どうでも良いコードでコントローラが重くなってくる。フォームが複数ある場合、だいたい似たり寄ったりのコードが量産されていく。本エントリでは、Laravel5 の Form Request Validation によってコントローラからバリデーションロジックを完全分離する方法について記述する。 Form Request Validation - バリデーションの分離 流れは以下の通り。 AppHttpRequestsRequestクラスを派生し、ContactRequestクラスを作成する。 コントローラメソッドにて、AppHttpRequestsRequestクラスのインスタンスではなく ContractRequestクラスのインスタンスを受けるようにする。 コントローラメソッドに入ったときには既にContactRequestクラスによるバリデーションが完了していて、データが正しい前提で処理できる。 ContactRequestクラス、ContactRequestTraitクラスの例を以下に示す。 <?php namespace AppHttpRequests; class ContactRequest extends Request { use ConfirmRequestTrait; /** * Determine if the user is authorized to make this request. * * @return bool */ public function authorize() { return true; } /** * Get the validation rules that apply to the request. * * @return array */ public function rules() { return [ \'name\' => \'required\', \'email\' => \'required|email\', \'subject\' => \'required\', \'content\' => \'required\', ]; } /** * Set custom messages for validator errors. * * @return array */ public function messages() { return [ // ]; } /** * Set custom attributes for validator errors. * * @return array */ public function attributes() { return [ \'name\' => \'お名前\', \'email\' => \'メールアドレス\', \'subject\' => \'件名\', \'content\' => \'内容\', ]; } } <?php namespace AppHttpRequests; use IlluminateContractsValidationValidator; trait ConfirmRequestTrait { /** * Set custom messages for validator errors. * * @param IlluminateContractsValidationFactory $factory * * @return IlluminateContractsValidationValidator */ public function validator($factory) { // 値検証前の処理 if (method_exists($this, \'beforeValidate\')) { $this->beforeValidate(); } $validator = $factory->make( $this->all(), $rules, $this->messages(), $this->attributes() ); $validator->after(function ($validator) { $failed = $validator->failed(); // 値検証後の処理 if (method_exists($this, \'afterValidate\')) { $this->afterValidate($validator); } }); return $validator; } /** * Format the errors from the given Validator instance. * * @param IlluminateContractsValidationValidator $validator * * @return array */ protected function formatErrors(Validator $validator) { $errors = parent::formatErrors($validator); return $errors; } } Form Request Validation - コントローラ側 ポイントは、storeメソッドの引数に ContactRequestヒントが渡されていること。sotreメソッドに入る前にContactRequestによりバリデーションが実行され、バリデーションに成功した時に限り、sotre()メソッドに入る。バリデーションが失敗すると、その前のアドレスにリダイレクトされる。コントローラから一切のバリデーションロジックを排除できた。 <?php namespace App¥Http¥Controllers; use App¥Http¥Requests¥ContactRequest; use App¥Repositories¥ContactRepository; class ContactController extends Controller { /** /* @var ContactRepository */ protected $repository; /** /* @constructor /* param ContactRepository $repository */ public function __construct(ContactRepository $repository) { $this->repository = $repository; } /* * */ public function index() { ... } /** * @var ContactRequest $request * */ public function store(ContactRequest $request) { } }