- 投稿日:2020-02-04T22:20:50+09:00
【Laravel】failed_jobsに入ったジョブをリランさせるSQL
Laravelでdatabaseキューを使っている場合、失敗したジョブは
failed_jobsテーブルに入ります。
リラン(再実行)したい場合は、再度jobsテーブルに入れてあげればキューワーカーが再実行してくれます。INSERT INTO jobs ( queue ,payload ,attempts ,reserved_at ,available_at ,created_at ) SELECT queue ,payload ,0 ,null ,UNIX_TIMESTAMP(now()) ,UNIX_TIMESTAMP(now()) FROM failed_jobs WHERE id = X ;上記のようなSQLで
failed_jobsに入ったレコードをjobsに戻せます。
- 投稿日:2020-02-04T20:21:45+09:00
【黒歴史】未経験半年で作成したWebアプリを、実務半年経ったのでリファクタリングしてみた
皆さんは最初に書いたコード覚えていますでしょうか。
目的
18年の12月にPHPを勉強し始め、19年の3月頃からLaravelを勉強し始めました。
その後、19年の5月20日から6月15日までLaravelを使ったオリジナルアプリを作りました。当時Twitterで行われている100DaysOfCodeというものを行っており
活動時間とかわかればいいなーと思い作りました。作った当初は良いと思っていたのですが、エンジニアとして現場に出て半年、久々に見るとひどいなと思い
何がだめだったのか、供養がてらリファクタリングしてみようとやってみました。(※厳密にはリファクタリングではありません)なお、ユーザーが全く居ないのでデータ移行は考慮してないです
もっとこう出来る等ありましたら、ぜひ教えていただけますと幸いです。
作ったもの
動作リンクはこちら
Topページ
- Twitterで登録・ログインを押すと、ソーシャルログインが行われマイページへリダイレクトします
マイページ
- 画面下部の入力欄に継続したい活動の名称を登録できます
- 登録した活動が一覧表示されます
投稿ページ
- 活動の内容や時間などを入力し、投稿するとツイートされ、活動時間、活動日数、継続日数が記録されます
- 画像には映っていませんが、投稿を削除でき、削除するとツイートも消えます。
簡単にまとめると
- トップページから、ログイン・登録をおすと、Twitter認証が行われる
- ログイン後、マイページへ移動でき、そこで活動を作成
- 作成した活動の進捗などをTwitterにつぶやく
- 合計活動時間や継続日数などが記録されていく
というものです。
環境
当時のデプロイした環境は下記になります。
PHP 7.2 FW Laravel 5.5 MySQL ConohaVPSリファクタリング
1.DB関係
マイグレーションファイルは下記のようになっています。
Schema::create('users', function (Blueprint $table) { $table->increments('id'); $table->string('twitter_id')->unique(); $table->string('twitter_name')->nullable(); $table->string('twitter_nickname')->nullable(); $table->string('twitter_avatar')->nullable(); $table->string('twitter_oauth_token')->nullable(); $table->string('twitter_oauth_token_secret')->nullable(); $table->rememberToken(); $table->timestamps(); $table->softDeletes(); }); Schema::table('users', function (Blueprint $table) { $table->string('twitter_id', 191)->change(); });Schema::create('activities', function (Blueprint $table) { $table->increments('id'); $table->integer('task_id')->nullable()->default(0); $table->integer('user_id'); $table->string('name')->nullable()->default(null); $table->string('hour')->nullable()->default(0); $table->integer('continuation_days')->nullable()->default(0); $table->timestamps(); $table->softDeletes(); }); Schema::table('activities', function (Blueprint $table) { $table->integer('days_of_activity')->nullable()->default(0); });Schema::create('tweets', function (Blueprint $table) { $table->increments('id'); $table->string('user_id')->nullable(); $table->integer('activity_id'); $table->string('tweet_id'); $table->text('body')->nullable(); $table->string('hour')->nullable()->default(0); $table->timestamps(); $table->softDeletes(); });何がだめだったか
まず、ほんとうにそのカラムが必要なのか考えていませんでした
行き当たりばったりで作った記憶があります。
正規化もよく分かっていませんでした。命名も
半年ぶりに見るとなんのカラムを表しているのかわかりませんでした。
tweetsテーブルがtweet_idを持っていたり、task_idという謎のカラムもあり自分で作ったくせに悩みました。わかりやすい命名とコメントをつけるべきでした。
また下記のように、カラムにもコメントをつけるべきでした。$table->integer('hoge')->comment('ほげ');次に型です。
tweetsテーブルのもつuser_idやhourがなぜかstring(varchar)だったりします。インデックスと制約についてですが、特に考量していませんでした。
存在は知っていましたが付け方を知らなかった気がします。修正後
tweetsテーブルをpostsテーブルに変更しました。また、活動内容を投稿するたびに
activitiesの持っていた合計時間格納用カラムに加算してましたが、postsからSQLで集計するようにしたため削除。
継続日数もロジックでなんとかなりそうだったので削除しました。
usersはidをtwitterのユーザーidにし、他のカラム名も変更しています。その他、外部キーも設定し、その他検索で使うと思われるものにindexを貼っています(この小規模ので必要かはわかりませんが)。
2.ルーティング
web.phpRoute::get('/', function () { return view('top', ['user' => Auth::user()]); })->name('top'); Route::prefix('auth/twitter')->group(function () { // ログインURL Route::get('/', 'Auth\LoginController@redirectToProvider')->name('login'); // コールバックURL Route::get('/callback', 'Auth\LoginController@handleProviderCallback'); // ログアウトURL Route::post('logout', 'Auth\LoginController@logout')->name('logout'); }); Route::group(['middleware' => ['auth', 'user.name'], 'prefix' => 'activity'], function () { Route::get('{user_name}/', 'ActivityController@index')->name('activity.index'); Route::post('{user_name}/', 'ActivityController@store')->name('activity.store'); Route::get('{user_name}/{activity}', 'ActivityController@show')->name('activity.show'); Route::patch('{user_name}/{activity}', 'ActivityController@update')->name('activity.tweet'); Route::delete('{user_name}/{activity}', 'ActivityController@destroy')->name('activity.delete'); Route::delete('{user_name}/{activity}/{id}', 'ActivityController@deleteTweet')->name('tweet.delete'); });何がだめだったか
もっとまとめられました。
修正後
web.php<?php Route::view('/', 'top')->name('top'); Route::group(['prefix' => 'auth/twitter'], function () { // ログインURL Route::get('/', 'Auth\LoginController@redirectToProvider')->name('login'); // コールバックURL Route::get('/callback', 'Auth\LoginController@handleProviderCallback'); // ログアウトURL Route::post('logout', 'Auth\LoginController@logout')->name('logout'); }); Route::group(['prefix' => '{user_name}', 'middleware' => ['auth', 'user.name']], function () { Route::resource('activity', 'ActivityController')->only(['index', 'store', 'show', 'update', 'destroy']); Route::resource('activity/{activity}/post', 'PostController')->only(['store', 'destroy']); Route::get('activity/{activity}/post/latest', 'PostController@fetchLatest')->name('post.latest'); });topページは
Route::viewというものがあるので、それを用います。
また、Route::prefixをgroupに変更
resourceでまとめられるものをまとめています。投稿に関するものを
ActivityControllerで処理していたので(継続日数や活動時間を投稿のたびに、活動へアップデートしていた)
PostControllerへ切り離しました。topで使用されている
Auth::user()ですが、他のviewで共通して使用していたのでViewComposerを用いて自動的に必要なViewに渡すようにしています。3.Controller
Controller共通
Controller内でバリデーションや登録等すべての処理をしていましたが、
FormRequestでバリデーション- コンストラクタインジェクションした、サービスクラスにバリデーション済みデータを渡す
- 処理した結果を受け取る
上記のように変更しました。
Modelはリレーション、アクセサ、スコープを書く用にしています。ActivityController.php/** * 活動の保存 * @param ActivityStoreRequest $request * @return \Illuminate\Http\RedirectResponse */ public function store(ActivityStoreRequest $request) { $result = DB::transaction(function () use ($request) { return $this->activity_service->createActivity($request->validated()); }); return redirect()->back()->with($result ? 'success' : 'error', $result ? '新規追加しました' : '追加に失敗しました'); }Login処理
下記はTwitterの認証画面で承認された際のリダイレクトを処理する
auth/twitter/callbackの部分です。Auth/LoginController.php/** * ユーザーのアクセストークンを取得する * * @return \Illuminate\Http\Response */ public function handleProviderCallback() { try { $twitter_user = Socialite::driver('twitter')->user(); //アクセストークン取得 $token = $twitter_user->token; $token_secret = $twitter_user->tokenSecret; if (!$twitter_user || !$token || !$token_secret) throw new \RuntimeException('ユーザー取得の失敗'); //ユーザーの取得または生成 $user = User::firstOrCreate(['twitter_id' => $twitter_user->id]); //最新状態に更新 $update_result = $user->update( [ 'twitter_name' => $twitter_user->name, 'twitter_nickname' => $twitter_user->nickname, 'twitter_avatar' => $twitter_user->user['profile_image_url_https'], 'twitter_oauth_token' => $token, 'twitter_oauth_token_secret' => $token_secret ] ); if (!$user || !$update_result) throw new \RuntimeException('ユーザー情報の生成または更新の失敗'); Auth::login($user, true); return redirect()->route('activity.index', $twitter_user->nickname)->with('success', 'ログインしました'); } catch (Exception $e) { Log::error($e->getMessage()); return redirect()->route('top')->with('error', 'Twitterアカウント取得に失敗しました'); } return redirect()->route('top')->with('erorr', 'エラーが発生しました。再度お試しください'); }何がだめだったか
まず、謎に通らない箇所にリダイレクト処理があります。
作成・更新がありますがトランザクションがありません。
また、取得できなかったら、作成、その後更新をしていますが、updateOrCreateで済みます。Serviceに切り出したりしたらもっとスッキリできそうな気がするのですがこれで進みました。
Auth/LoginController.php/** * ユーザーのアクセストークンを取得、ログインをする * * @return \Illuminate\Http\Response */ public function handleProviderCallback() { // twitterからアカウント情報取得 try { $account_info = Socialite::driver('twitter')->user(); } catch (\Exception $e) { Log::error($e->getMessage()); return redirect()->route('top')->with('error', 'Twitterアカウント取得に失敗しました'); } $auth_user = DB::transaction(function () use ($account_info) { $user = User::updateOrCreate(['id' => $account_info->id], [ 'name' => $account_info->name, 'nickname' => $account_info->nickname, 'avatar' => $account_info->avatar, 'token' => $account_info->token, 'token_secret' => $account_info->tokenSecret ]); Auth::login($user, true); return $user; }); return redirect()->route('activity.index', $auth_user->nickname)->with('success', 'ログインしました'); }4.フロント周り
なにがだめだったか
もともとこのアプリはHTML、CSSは無料テンプレートのを使っていたのですが、
不必要なライブラリなどたくさん読み込んでいました。
しかも、minify化(コードの圧縮化)がされていないものでした。また、jQueryを使っていたのですが、
Bladeにベタ書きしており、更にBlade自体コンポーネントへ分割していなかったので非常に見づらくなっていました。ライブラリ類は
npmからインストールし、
Laravel Mixを用いてjsのコードを圧縮しました。Bladeも
@pushや@include、@componentを使い切りだせそうなものは切り出しました
5.Log
これは未だにどうするのが良いのか、はっきりと分かってはいません
もともとは、エラー箇所にLogを書き込む処理おいているだけでした。今回は
Eloquentは作成や更新などイベントが起こるとそれをフックに、そのタイミングで処理ができるので、そこにLog処理を置きました。下記のようにすると、Userが作成されたタイミングでLogが書かれます
php artisan make:observer UserObserver --model=UserUserObserver.php<?php namespace App\Observers; use App\User; use Illuminate\Support\Facades\Log; class UserObserver { /** * Handle the user "created" event. * * @param \App\User $user * @return void */ public function created(User $user) { Log::info('ユーザーが新規作成されました', ['user' => $user->nickname]); } }6.最適化
一番最初、完成した後やったことは
.envのAPP_ENVをproductionにし、APP_DEBUGをfalseにしただけでした。なにがだめだったか
公式にかかれている最適化処理があるのに関わらず、行っていませんでした。
オートローダー最適化
composer install --optimize-autoloader --no-dev設定ローディングの最適化
php artisan config:cacheルートロードの最適化
php artisan route:cacheその他の最適化として、
Opcacheの有効化を行いました。OPcacheとは
スクリプト言語は毎回呼び出される度にプログラムの解析・解釈が行われています。 コンパイル型の言語に比べてスクリプト言語は、「呼び出し毎にプログラムを解析する」点にオーバーヘッドがあると言えます。
スクリプト言語においても、ソースコードが変更されていないのであればコンピュータが解釈をし直す必要がないのではないかと考えられます。 よって、コンピュータが解析済みの「バイトコード」をキャッシュして再利用すればよく、そういう機能をアクセラレータとよびます。 PHPのアクセラレータ機能を利用することでPHPを解釈する段取りをスキップすることができ、処理が高速になる
他に、HTMLの圧縮を行いました。
htmlmin/htmlminというパッケージを使用し、Bladeからコンパイルされたファイルを圧縮しています。
自身で作ったディレクティブ(@hogeのようなもの)があると、うまく使えません。その他個人的な
ツイートの文字数カウント
今まで、jsで
keyUpのたびにカウントする、Laravel側はmax:140のバリデーションというガバルールだったのを
twitter-text-jsを使って、Twitterと同様のカウントにしました。
Laravelも同様にパッケージを使いTwitterと同様のルールでバリデーションできるようにしました。
js:詳しくはこちら
Laravel:詳しくはこちら継続日数のカウント
今まで投稿するたびに、合計時間も活動日数も継続日数もいちいち計算して活動(
activities)のカラムへ保存していました。
投稿から取得して計算するように変更したところ、コード量がごっそり減りました。おわりに
半年前のコードが完全に他人のもので、想像以上に時間がかかりました。
頭の中で考えてとりあえず作るということのデメリットが享受できました。
今後は事前の設計をしっかりして、物を作りたいです。また半年したらこのコードもだめだなと思えるような成長を遂げていたいものです。
- 投稿日:2020-02-04T20:21:45+09:00
未経験半年で作成したWebアプリを、実務半年経ったのでリファクタリングしてみた
皆さんは最初に書いたコード覚えていますでしょうか。
目的
18年の12月にPHPを勉強し始め、19年の3月頃からLaravelを勉強し始めました。
その後、19年の5月20日から6月15日までLaravelを使ったオリジナルアプリを作りました。当時Twitterで行われている100DaysOfCodeというものを行っており
活動時間とかわかればいいなーと思い作りました。作った当初は良いと思っていたのですが、エンジニアとして現場に出て半年、久々に見るとひどいなと思い
何がだめだったのか、供養がてらリファクタリングしてみようとやってみました。(※厳密にはリファクタリングではありません->若干挙動に修正が入っているため)なお、ユーザーが全く居ないのでデータ移行は考慮してないです
もっとこう出来る等ありましたら、ぜひ教えていただけますと幸いです。
作ったもの
動作リンクはこちら
Topページ
- Twitterで登録・ログインを押すと、ソーシャルログインが行われマイページへリダイレクトします
マイページ
- 画面下部の入力欄に継続したい活動の名称を登録できます
- 登録した活動が一覧表示されます
投稿ページ
- 活動の内容や時間などを入力し、投稿するとツイートされ、活動時間、活動日数、継続日数が記録されます
- 画像には映っていませんが、投稿を削除でき、削除するとツイートも消えます。
簡単にまとめると
- トップページから、ログイン・登録をおすと、Twitter認証が行われる
- ログイン後、マイページへ移動でき、そこで活動を作成
- 作成した活動の進捗などをTwitterにつぶやく
- 合計活動時間や継続日数などが記録されていく
というものです。
環境
当時のデプロイした環境は下記になります。
PHP 7.2 FW Laravel 5.5 MySQL ConohaVPSリファクタリング
1.DB関係
マイグレーションファイルは下記のようになっています。
Schema::create('users', function (Blueprint $table) { $table->increments('id'); $table->string('twitter_id')->unique(); $table->string('twitter_name')->nullable(); $table->string('twitter_nickname')->nullable(); $table->string('twitter_avatar')->nullable(); $table->string('twitter_oauth_token')->nullable(); $table->string('twitter_oauth_token_secret')->nullable(); $table->rememberToken(); $table->timestamps(); $table->softDeletes(); }); Schema::table('users', function (Blueprint $table) { $table->string('twitter_id', 191)->change(); });Schema::create('activities', function (Blueprint $table) { $table->increments('id'); $table->integer('task_id')->nullable()->default(0); $table->integer('user_id'); $table->string('name')->nullable()->default(null); $table->string('hour')->nullable()->default(0); $table->integer('continuation_days')->nullable()->default(0); $table->timestamps(); $table->softDeletes(); }); Schema::table('activities', function (Blueprint $table) { $table->integer('days_of_activity')->nullable()->default(0); });Schema::create('tweets', function (Blueprint $table) { $table->increments('id'); $table->string('user_id')->nullable(); $table->integer('activity_id'); $table->string('tweet_id'); $table->text('body')->nullable(); $table->string('hour')->nullable()->default(0); $table->timestamps(); $table->softDeletes(); });何がだめだったか
まず、ほんとうにそのカラムが必要なのか考えていませんでした
行き当たりばったりで作った記憶があります。
正規化もよく分かっていませんでした。命名も
半年ぶりに見るとなんのカラムを表しているのかわかりませんでした。
tweetsテーブルがtweet_idを持っていたり、task_idという謎のカラムもあり自分で作ったくせに悩みました。わかりやすい命名とコメントをつけるべきでした。
また下記のように、カラムにもコメントをつけるべきでした。$table->integer('hoge')->comment('ほげ');次に型です。
tweetsテーブルのもつuser_idやhourがなぜかstring(varchar)だったりします。インデックスと制約についてですが、特に考量していませんでした。
存在は知っていましたが付け方を知らなかった気がします。修正後
tweetsテーブルをpostsテーブルに変更しました。また、活動内容を投稿するたびに
activitiesの持っていた合計時間格納用カラムに加算してましたが、postsからSQLで集計するようにしたため削除。
継続日数もロジックでなんとかなりそうだったので削除しました。
usersはidをtwitterのユーザーidにし、他のカラム名も変更しています。その他、外部キーも設定し、その他検索で使うと思われるものにindexを貼っています(この小規模ので必要かはわかりませんが)。
2.ルーティング
web.phpRoute::get('/', function () { return view('top', ['user' => Auth::user()]); })->name('top'); Route::prefix('auth/twitter')->group(function () { // ログインURL Route::get('/', 'Auth\LoginController@redirectToProvider')->name('login'); // コールバックURL Route::get('/callback', 'Auth\LoginController@handleProviderCallback'); // ログアウトURL Route::post('logout', 'Auth\LoginController@logout')->name('logout'); }); Route::group(['middleware' => ['auth', 'user.name'], 'prefix' => 'activity'], function () { Route::get('{user_name}/', 'ActivityController@index')->name('activity.index'); Route::post('{user_name}/', 'ActivityController@store')->name('activity.store'); Route::get('{user_name}/{activity}', 'ActivityController@show')->name('activity.show'); Route::patch('{user_name}/{activity}', 'ActivityController@update')->name('activity.tweet'); Route::delete('{user_name}/{activity}', 'ActivityController@destroy')->name('activity.delete'); Route::delete('{user_name}/{activity}/{id}', 'ActivityController@deleteTweet')->name('tweet.delete'); });何がだめだったか
もっとまとめられました。
修正後
web.php<?php Route::view('/', 'top')->name('top'); Route::group(['prefix' => 'auth/twitter'], function () { // ログインURL Route::get('/', 'Auth\LoginController@redirectToProvider')->name('login'); // コールバックURL Route::get('/callback', 'Auth\LoginController@handleProviderCallback'); // ログアウトURL Route::post('logout', 'Auth\LoginController@logout')->name('logout'); }); Route::group(['prefix' => '{user_name}', 'middleware' => ['auth', 'user.name']], function () { Route::resource('activity', 'ActivityController')->only(['index', 'store', 'show', 'update', 'destroy']); Route::resource('activity/{activity}/post', 'PostController')->only(['store', 'destroy']); Route::get('activity/{activity}/post/latest', 'PostController@fetchLatest')->name('post.latest'); });topページは
Route::viewというものがあるので、それを用います。
また、Route::prefixをgroupに変更
resourceでまとめられるものをまとめています。投稿に関するものを
ActivityControllerで処理していたので(継続日数や活動時間を投稿のたびに、活動へアップデートしていた)
PostControllerへ切り離しました。topで使用されている
Auth::user()ですが、他のviewで共通して使用していたのでViewComposerを用いて自動的に必要なViewに渡すようにしています。3.Controller
Controller共通
Controller内でバリデーションや登録等すべての処理をしていましたが、
FormRequestでバリデーション- コンストラクタインジェクションした、サービスクラスにバリデーション済みデータを渡す
- 処理した結果を受け取る
上記のように変更しました。
Modelはリレーション、アクセサ、スコープを書く用にしています。ActivityController.php/** * 活動の保存 * @param ActivityStoreRequest $request * @return \Illuminate\Http\RedirectResponse */ public function store(ActivityStoreRequest $request) { $result = DB::transaction(function () use ($request) { return $this->activity_service->createActivity($request->validated()); }); return redirect()->back()->with($result ? 'success' : 'error', $result ? '新規追加しました' : '追加に失敗しました'); }Login処理
下記はTwitterの認証画面で承認された際のリダイレクトを処理する
auth/twitter/callbackの部分です。Auth/LoginController.php/** * ユーザーのアクセストークンを取得する * * @return \Illuminate\Http\Response */ public function handleProviderCallback() { try { $twitter_user = Socialite::driver('twitter')->user(); //アクセストークン取得 $token = $twitter_user->token; $token_secret = $twitter_user->tokenSecret; if (!$twitter_user || !$token || !$token_secret) throw new \RuntimeException('ユーザー取得の失敗'); //ユーザーの取得または生成 $user = User::firstOrCreate(['twitter_id' => $twitter_user->id]); //最新状態に更新 $update_result = $user->update( [ 'twitter_name' => $twitter_user->name, 'twitter_nickname' => $twitter_user->nickname, 'twitter_avatar' => $twitter_user->user['profile_image_url_https'], 'twitter_oauth_token' => $token, 'twitter_oauth_token_secret' => $token_secret ] ); if (!$user || !$update_result) throw new \RuntimeException('ユーザー情報の生成または更新の失敗'); Auth::login($user, true); return redirect()->route('activity.index', $twitter_user->nickname)->with('success', 'ログインしました'); } catch (Exception $e) { Log::error($e->getMessage()); return redirect()->route('top')->with('error', 'Twitterアカウント取得に失敗しました'); } return redirect()->route('top')->with('erorr', 'エラーが発生しました。再度お試しください'); }何がだめだったか
まず、謎に通らない箇所にリダイレクト処理があります。
作成・更新がありますがトランザクションがありません。
また、取得できなかったら、作成、その後更新をしていますが、updateOrCreateで済みます。Serviceに切り出したりしたらもっとスッキリできそうな気がするのですがこれで進みました。
Auth/LoginController.php/** * ユーザーのアクセストークンを取得、ログインをする * * @return \Illuminate\Http\Response */ public function handleProviderCallback() { // twitterからアカウント情報取得 try { $account_info = Socialite::driver('twitter')->user(); } catch (\Exception $e) { Log::error($e->getMessage()); return redirect()->route('top')->with('error', 'Twitterアカウント取得に失敗しました'); } $auth_user = DB::transaction(function () use ($account_info) { $user = User::updateOrCreate(['id' => $account_info->id], [ 'name' => $account_info->name, 'nickname' => $account_info->nickname, 'avatar' => $account_info->avatar, 'token' => $account_info->token, 'token_secret' => $account_info->tokenSecret ]); Auth::login($user, true); return $user; }); return redirect()->route('activity.index', $auth_user->nickname)->with('success', 'ログインしました'); }4.フロント周り
なにがだめだったか
もともとこのアプリはHTML、CSSは無料テンプレートのを使っていたのですが、
不必要なライブラリなどたくさん読み込んでいました。
しかも、minify化(コードの圧縮化)がされていないものでした。また、jQueryを使っていたのですが、
Bladeにベタ書きしており、更にBlade自体コンポーネントへ分割していなかったので非常に見づらくなっていました。ライブラリ類は
npmからインストールし、
Laravel Mixを用いてjsのコードを圧縮しました。Bladeも
@pushや@include、@componentを使い切りだせそうなものは切り出しました
5.Log
これは未だにどうするのが良いのか、はっきりと分かってはいません
もともとは、エラー箇所にLogを書き込む処理おいているだけでした。今回は
Eloquentは作成や更新などイベントが起こるとそれをフックに、そのタイミングで処理ができるので、そこにLog処理を置きました。下記のようにすると、Userが作成されたタイミングでLogが書かれます
php artisan make:observer UserObserver --model=UserUserObserver.php<?php namespace App\Observers; use App\User; use Illuminate\Support\Facades\Log; class UserObserver { /** * Handle the user "created" event. * * @param \App\User $user * @return void */ public function created(User $user) { Log::info('ユーザーが新規作成されました', ['user' => $user->nickname]); } }6.最適化
一番最初、完成した後やったことは
.envのAPP_ENVをproductionにし、APP_DEBUGをfalseにしただけでした。なにがだめだったか
公式にかかれている最適化処理があるのに関わらず、行っていませんでした。
オートローダー最適化
composer install --optimize-autoloader --no-dev設定ローディングの最適化
php artisan config:cacheルートロードの最適化
php artisan route:cacheその他の最適化として、
Opcacheの有効化を行いました。OPcacheとは
スクリプト言語は毎回呼び出される度にプログラムの解析・解釈が行われています。 コンパイル型の言語に比べてスクリプト言語は、「呼び出し毎にプログラムを解析する」点にオーバーヘッドがあると言えます。
スクリプト言語においても、ソースコードが変更されていないのであればコンピュータが解釈をし直す必要がないのではないかと考えられます。 よって、コンピュータが解析済みの「バイトコード」をキャッシュして再利用すればよく、そういう機能をアクセラレータとよびます。 PHPのアクセラレータ機能を利用することでPHPを解釈する段取りをスキップすることができ、処理が高速になる
他に、HTMLの圧縮を行いました。
htmlmin/htmlminというパッケージを使用し、Bladeからコンパイルされたファイルを圧縮しています。
自身で作ったディレクティブ(@hogeのようなもの)があると、うまく使えません。その他個人的な
ツイートの文字数カウント
今まで、jsで
keyUpのたびにカウントする、Laravel側はmax:140のバリデーションというガバルールだったのを
twitter-text-jsを使って、Twitterと同様のカウントにしました。
Laravelも同様にパッケージを使いTwitterと同様のルールでバリデーションできるようにしました。
js:詳しくはこちら
Laravel:詳しくはこちら継続日数のカウント
今まで投稿するたびに、合計時間も活動日数も継続日数もいちいち計算して活動(
activities)のカラムへ保存していました。
投稿から取得して計算するように変更したところ、コード量がごっそり減りました。おわりに
半年前のコードが完全に他人のもので、想像以上に時間がかかりました。
頭の中で考えてとりあえず作るということのデメリットが享受できました。
今後は事前の設計をしっかりして、物を作りたいです。また半年したらこのコードもだめだなと思えるような成長を遂げていたいものです。
- 投稿日:2020-02-04T19:41:22+09:00
Docker for WindowsでLaravel+OCI8の複数プロジェクト開発環境を作ってみた
普段レガシーなシステムをごにょごにょお世話しているレガシーなプログラマーです。
中高年でもイマドキ風の開発をやってみたい!
でも仕事で使うDBはうちはORACLEが多い…
ということで、LaravelでOCIを使える環境を作ってみました。普通に開発環境を作るならLaraDockが色々揃っていますし、他に良い記事が沢山あると思います
Laravelの開発環境をDockerを使って構築するDocker Desktop on Windowsのインストール
まずはDockerの準備です。動作条件を確認。
- Windows10 pro 64bit
- メモリは4GB以上
- Hyper-Vが使えること
ほぼこちらの説明通りで設定できました。ありがとうございます!
https://qiita.com/ksh-fthr/items/6b1242c010fac7395a45ダウンロード&インストール
公式サイトからインストーラーをダウンロードします。
この時、Docker Hubのアカウントも作成することになります。サクッと登録しましょう。https://docs.docker.com/docker-for-windows/install/
docker-composeのインストール
Docker for Macは個別に入れなくてもよかったのですがWindows版は手動で入れないといけません。
1.Powershell を管理者権限で起動する。
2.以下のコマンドを実行する。DockerのUpdateのたびに実行する必要があるのがメンドイ。Invoke-WebRequest "https://github.com/docker/compose/releases/download/1.24.1/docker-compose-Windows-x86_64.exe" -UseBasicParsing -OutFile $Env:ProgramFiles\docker\docker-compose.exeShared Drivesの設定
チェックを入れて「Apply」をクリック。
Dockerを再起動してちゃんと設定できているか確認します。
docker-compose.yml作成
こんな構成で作成しました。
[lara73oci]
├ docker-compose.yml
├ [nginx]
│ ├ [sites] <=ここにサイトの設定を入れる
│ ├ Dockerfile
│ └ nginx.conf
├ [php7.3]
│ ├ Dockerfile
│ ├ instantclient-basic-linux.x64-12.2.0.1.0.zip <=Oracleからダウンロードしてきます
│ ├ instantclient-sdk-linux.x64-12.2.0.1.0.zip <=Oracleからダウンロードしてきます
│ ├ php.ini
│ └ tnsnames.ora
├ [src] <=ここにプログラム
└ [logs] <=nginxのログdocker-compose.ymlversion: '3' services: nginx: build: context: ./nginx volumes: - ./src:/var/www:cached - ./logs/nginx/:/var/log/nginx - ./nginx/sites/:/etc/nginx/sites-available ports: - "80:80" depends_on: - php-fpm php-fpm: build: context: ./php7.3 dockerfile: Dockerfile volumes: - ./php7.3/php.ini:/usr/local/etc/php/php.ini - ./php7.3/tnsnames.ora:/usr/local/instantclient/tnsnames.ora - ./src:/var/www:cached expose: - "9000"複数プロジェクトを動かしたかったのでsites-availableを使っています。
nginx
nginx/DockerfileFROM nginx:alpine COPY nginx.conf /etc/nginx/ RUN apk update \ && apk upgrade \ && apk add --no-cache bash RUN set -x ; \ addgroup -g 82 -S www-data ; \ adduser -u 82 -D -S -G www-data www-data && exit 0 ; exit 1 RUN rm /etc/nginx/conf.d/default.conf CMD ["nginx"] EXPOSE 80 443nginx/nginx.confuser www-data; worker_processes 4; pid /run/nginx.pid; daemon off; events { worker_connections 2048; multi_accept on; use epoll; } http { server_tokens off; sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 15; types_hash_max_size 2048; client_max_body_size 20M; include /etc/nginx/mime.types; default_type application/octet-stream; access_log /dev/stdout; error_log /dev/stderr; gzip on; gzip_disable "msie6"; include /etc/nginx/conf.d/*.conf; include /etc/nginx/sites-available/*.conf; open_file_cache off; # Disabled for issue 619 charset UTF-8; }サイトの設定です。
nginx/sites/default.confserver { listen 80 default_server; listen [::]:80 default_server ipv6only=on; server_name localhost; root /var/www/public; index index.php index.html index.htm; location / { try_files $uri $uri/ /index.php$is_args$args; } location ~ \.php$ { try_files $uri /index.php =404; fastcgi_pass php-fpm:9000; fastcgi_index index.php; fastcgi_buffers 16 16k; fastcgi_buffer_size 32k; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; #fixes timeouts fastcgi_read_timeout 600; include fastcgi_params; } location ~ /\.ht { deny all; } location /.well-known/acme-challenge/ { root /var/www/letsencrypt/; log_not_found off; } }複数プロジェクトで使用する場合
nginx/sitesのproject1.confをコピーして使用してください
(例)project_hoge.conf
ファイル内のproject1、project1.local を自分のプロジェクトに合わせて変更してください
もちろんDocument rootも!PHP+OCI8
Oracle instant clientの入手
Instant Client for Linux x86-64からinstantclient-basicとinstantclient-sdkをzip形式でダウンロードして
php7.3のDockerFileと同じディレクトリに置いてください
バージョンは12.2と12.1で確認しました
(例)
./php7.3/instantclient-sdk-linux.x64-12.2.0.1.0.zip
./php7.3/instantclient-basic-linux.x64-12.2.0.1.0.zipphp7.3/DockerFileの編集
入手したファイル名に合わせて13~25行目を変更してください
php7.3/tnsnames.oraの編集
ご自分の環境に合わせて記述してください
php7.3/DockerfileFROM php:7.3-fpm RUN apt-get update && \ apt-get install -y git \ libfreetype6-dev \ libjpeg62-turbo-dev \ libpng-dev && \ docker-php-ext-install gd pdo_mysql mysqli mbstring bcmath ENV LD_LIBRARY_PATH /usr/local/instantclient/ ENV TNS_ADMIN /usr/local/instantclient/ COPY instantclient-basic-linux.x64-12.2.0.1.0.zip /tmp COPY instantclient-sdk-linux.x64-12.2.0.1.0.zip /tmp RUN apt-get update && \ apt-get install -y zip unzip libaio1 wget && \ unzip /tmp/instantclient-basic-linux.x64-12.2.0.1.0.zip -d /usr/local/ && \ unzip /tmp/instantclient-sdk-linux.x64-12.2.0.1.0.zip -d /usr/local/ && \ ln -s /usr/local/instantclient_12_2 /usr/local/instantclient && \ ln -s /usr/local/instantclient/libclntsh.so.12.1 /usr/local/instantclient/libclntsh.so && \ export LD_LIBRARY_PATH=/usr/local/instantclient && \ docker-php-ext-configure oci8 --with-oci8=instantclient,/usr/local/instantclient && \ docker-php-ext-install oci8 && \ docker-php-ext-configure pdo_oci --with-pdo-oci=instantclient,/usr/local/instantclient,12.2 && \ docker-php-ext-install pdo_oci && \ rm -rf /usr/local/*.zip RUN apt-get install -y \ curl \ gnupg RUN curl -sL https://deb.nodesource.com/setup_12.x | bash - RUN apt-get install -y nodejs RUN npm install npm@latest -g COPY --from=composer:latest /usr/bin/composer /usr/bin/composerLaravel用にnpmやcomposerも入るようにしています
Oracle instant client導入~OCI8のインストール部分は
LaraDockのworkspaceとphp-fpmのDockerfileに同様の設定をいれると動くと思います。
LaraDockでやりたい方はお試しあれ!/etc/hostsを変更
Windows10の場合はC:\Windows\System32\drivers\etc\hosts
管理者モードでメモ帳を起動して設定したホスト名を追記します。
これで http://project1.local/ でアクセスできるようになります。127.0.0.1 project1.local ::1 project1.local 127.0.0.1 project2.local ::1 project2.local起動
docker-compose up -dLaravelのインストールとOCI8設定
1.php-fpmに入ります
docker-compose exec php-fpm bash2.Laravelのインストール
$ cd /var/www <=設定したドキュメントルートに合うようにしてください $ composer create-project --prefer-dist laravel/laravel project1 "5.8.*" <=5.8の場合 $ composer create-project --prefer-dist laravel/laravel project1 <=最新版3.yajra/laravel-oci8のインストール
https://github.com/yajra/laravel-oci8
ドキュメント通りにLaravelのバージョンにあわせてcomposerでインストールします
Laravelのディレクトリに移動して$ composer require yajra/laravel-oci8:"5.8.*" <=Laravel5.8 $ composer require yajra/laravel-oci8:"^6.0" <=Laravel6~4.Laravelのconfig/app.phpのproviders内に追加します
config/app.php'providers' => [ // ... Yajra\Oci8\Oci8ServiceProvider::class, ],5.config/oracle.phpの作成
php artisan vendor:publish --tag=oracle6..env内のORACLEの設定
.envDB_CONNECTION=oracle DB_HOST=xxx.xxx.xxx.xxx DB_PORT=1521 DB_DATABASE=oracleのSID DB_USERNAME=xxx DB_PASSWORD=xxx以上でMySQLと同じようにDBの操作ができました。
リポジトリ
作成したOracle instant client本体なしの雛形リポジトリを置いておきます。
Docker初心者なのでここはこうしたほうがいいよと教えていただけると嬉しいです!
https://github.com/anissia0828/nginx_php73_oci参考にさせていただきました
- 投稿日:2020-02-04T14:48:47+09:00
laravel + vue.jsでSPAの開発をしていてデバックがしたい!!!
結論
SPAで変数とかの中身を見るには
laravel debugarとclock workの両方をcomposerでインストールする必要あり。STEP1 laravel debugarのインストール
これはいつも通りですね
こちらの↓READ MEを参考に開発環境にインストール
https://github.com/barryvdh/laravel-debugbarcomposer require barryvdh/laravel-debugbar --devSTEP2 clock workのインストール
これはSPAだとHTMLに返ってこないデバックの結果をChromeのデベロッパーツールで確認できるようにするツールです!
google chrome自体へのインストールとcomposerへの両方のインストールが必要これも以下を参照!
https://github.com/itsgoingd/clockwork
- google chromeへのインストール
https://chrome.google.com/webstore/detail/clockwork/dmggabnehkmmfmdffgajcflpdjlnoemp
- composerへのインストール
composer require itsgoingd/clockwork用法
インストールするとclockヘルパメソッドが使えるようになります!
使い方は以下のように変数の中身を確認したり、メソッドを組み合わせて使うことができる!これも詳しい使い方は公式ドキュメント↓参照
https://underground.works/clockwork/logging?#contentめちゃエラー出てるの恥ずかしいけどこんな感じで出ます!!!!!
デバックできずに困ってた人は参考にしてみてください~
- 投稿日:2020-02-04T14:48:47+09:00
laravel + vue.jsでSPAの開発をしていてデバッグがしたい!!!
SPAだとlaravelのエラー内容や変数とかの中身をブラウザに返すことができないけどデバッグがしたい!人用
結論
そのためには、、、
laravel debugbarとclock workの両方をcomposerでインストールする必要あり。STEP1 laravel debugbarのインストール
これはいつも通りですね
こちらの↓READ MEを参考に開発環境にインストール
https://github.com/barryvdh/laravel-debugbarcomposer require barryvdh/laravel-debugbar --devSTEP2 clock workのインストール
これはSPAだとHTMLに返ってこないデバッグの結果をChromeのデベロッパーツールで確認できるようにするツールです!
google chrome自体へのインストールとcomposerへの両方のインストールが必要これも以下を参照!
https://github.com/itsgoingd/clockwork
- google chromeへのインストール
https://chrome.google.com/webstore/detail/clockwork/dmggabnehkmmfmdffgajcflpdjlnoemp
- composerへのインストール
composer require itsgoingd/clockwork用法
インストールするとclockヘルパメソッドが使えるようになります!
使い方は以下のように変数の中身を確認したり、メソッドを組み合わせて使うことができる!これも詳しい使い方は公式ドキュメント↓参照
https://underground.works/clockwork/logging?#contentまた.envに以下をの設定を追加するとlaravel dubagbarが画面に表示されなくなります!
DEBUGBAR_ENABLED=false
めちゃエラー出てるの恥ずかしいけどこんな感じで出ます!!!!!
デバッグできずに困ってた人は参考にしてみてください~
- 投稿日:2020-02-04T14:19:10+09:00
『 Laravel DB.com 』コード書かずにCRUD作成【最新版 2020/02/01 〜利用方法まとめ〜】
「 Laravel DB.com 」 、利用方法まとめ
2020年2月1日リニューアルしたので、使用方法をまとめたいと思います。
Laravel DB.com ってなに?
「LaravelDB.com」は、ER図(テーブルの設計)を作成することで、”Migration” OR ”CRUD(テストデータ)”のコードまで生成できるサービス(ツール)です。
確認したLaravel動作環境
- 開発環境: Laravel5.5 ~ 6.x / PHP7.2.1 / MySQL5.6.38
- 確認環境: Mac: Chromeブラウザ
メリット
- インストール不要
- テーブル設計をクラウドに保存可能 (他アプリの設計に使い回せる!)
- Migration自動生成
- CRUD自動生成
- テストデータ(Faker)自動生成
- Validation自動生成(テーブル設計時のカラム型を参照して作成してる。※Enum型は自分でCODE修正必要かも)
- Webサービスだから、どのPCでも同じデータを活用して作成できる
さあ、 LaravelDB.com はじめて行きましょう!
操作方法(Youtubu)
— LaravelDB.com (@ErdLaravel)の操作方法【 CRUD: 新しいプロジェクトを用意 】
最低限Laravelがインストールした状態を準備します!
1. この時点ではまだLOGINは作らないように!
2. .env と MySQLのDB準備はしておきましょう。
【 LaravelDB.comへ ログイン 】
Googleアカウントのみログイン可能です。(画面右上 OR ページ下部の中央)
LaravelDB.com
【 LaravelDB.comの POINT!! 】
ER図を作成していきますが重要事項があります!
<<重要>>
主キーのAutoIncrementは どのテーブルも『 id 』 と固定すること!!
LaravelのEloquentModel を便利に使うためには、どのテーブルも 『 id 』 と固定するのが吉です。
理由は「 モデル名::find($id); 」と便利に使う場合は 主キーのAutoIncrement 『 id 』 名にしておかないと動作しないからです( Laravelを勉強してる人はなんとなく知ってることでしょうか )。
※ laravelDB.comのCRUDでも生成されるファイルは「 ○○○::find($id); 」を使ってるためそ!
※ laravelに最初から入っているテーブル「user」「password_resets」は作成しないこと(migrate時に上書きしてしまいます)
※慣れれば簡単ですが、少しだけ慣れる時間は必要ですね。
【 CRUDを生成してみる! 】
- まずは、ER図を簡単に作ってみましょう!! その後に右メニュー「 ER図のLoad/Save 」をクリックすると以下画面が表示されます。
![]()
- 外部キーを沢山つけてる場合は、Migrationの順番が重要なので試す場合は「OFF」がオススメです。
- 「 Laravel( CRUD BUILD) 」 ボタンをクリックでファイルが生成されます!!
【 ダウンロードファイルを確認しましょう 】
【移行方法】
Macの場合: 「Optionキー + ドラッグ&ドロップ」 → 「結合」でフォルダの上書きではなく、結合になります。
Winの場合:「ドラッグ&ドロップ」で結合できます。ダウンロードした各フォルダを上記の方法で移動しましょう!
【 LaravelのHOMEページを表示しましょう 】
私の環境では「http://localhost/ 」
【 「 コマンド 」 を3つ順番に打ちましょう! 】
<<重要>>
DBや.env設定など最低限の設定を完了しておくこと!!php artisan migrate composer update //テストデータいらない場合は打たなくてもOK php artisan db:seed以上3つです。
【 完成: ブラウザで確認しましょう! 】
http://localhost/ (URLは開発環境によって異なります) で確認しましょう!
あなたが作ったテーブルのCRUD画面へ遷移できますよ!!作成されたindex/show/editの画面
基本的な処理が最初から出来てます!捗りそうですね!
Validationがスゴイ! → テーブル設計に合わせて生成
例でざっくりテーブルをER図で書いてみました。
以下"t_gsusers"テーブルを中心に見ていきます。
生成されたコントローラーのcreate(),edit()にはこういったvalidationが挿入されます。
TGsusersController$this->validate($request, [ "name" => "required|max:128", //string('name',128) "lid" => "required|max:128", //string('lid',128) "lpw" => "required|max:128", //string('lpw',128) "m_department_id" => "required|integer", //integer('m_department_id') "m_position_id" => "required|integer", //integer('m_position_id') "m_prefectures_id" => "required|integer", //integer('m_prefectures_id') "m_kanri_id" => "required|integer", //integer('m_kanri_id') "m_lifeflg_id" => "required|integer", //integer('m_lifeflg_id') "m_test_id" => "required|integer", //integer('m_test_id') ]);{{old('name')}} → 入力項目を補完
validationで未入力等ではじかれた場合、入力した文字を消さずに表示します。
date,datetime型 → 入力フォー厶type="date"
- 画面表示側もtype='date'
- validation側もdateチェックが入ります。
ER図外部キー設定→JOIN自動生成 QueryBuilderをController側にコメントで用意
イチからQueryBuilderを使用して、JOINを書くのは大変です。ベースになる部分は生成してくれます。使用する場合はコメントになってるのでコメントを外し、上記に書かれてるコードのEloquentModelの1行をコメントして使います。
※機械的に作成してるため、より良い記述に変更は必要になります。動作した後はコードを見ると良いでしょう
- Routing
- Controller
- Views
- Model 後は、自分で変更をいれていくだけですね。
日本発 Laravelツール
日本から海外でも使われるプロダクトになることを願っております。
Twitter: LaravelDB.com
以上
- 投稿日:2020-02-04T14:19:10+09:00
最新版『 Laravel DB.com 』コード書かずにCRUD作成~利用方法まとめ~
「 Laravel DB.com 」 、利用方法まとめ
2020年2月2日リニューアルしたので、使用方法をまとめたいと思います。
Laravel DB.com ってなに?
「LaravelDB.com」は、ER図(テーブルの設計)を作成することで、”Migration” OR ”CRUD(テストデータ)”のコードまで生成できるサービス(ツール)です。
確認したLaravel動作環境
- 開発環境: Laravel5.5 ~ 6.x / PHP7.2.1 / MySQL5.6.38
- 確認環境: Mac: Chromeブラウザ
メリット
- インストール不要
- テーブル設計をクラウドに保存可能 (他アプリの設計に使い回せる!)
- Migration自動生成
- CRUD自動生成
- テストデータ(Faker)自動生成
- Validation自動生成(テーブル設計時のカラム型を参照して作成してる。※Enum型は自分でCODE修正必要かも)
- Webサービスだから、どのPCでも同じデータを活用して作成できる
さあ、 LaravelDB.com はじめて行きましょう!
操作方法(Youtubu)
— LaravelDB.com (@ErdLaravel)の操作方法【 CRUD: 新しいプロジェクトを用意 】
最低限Laravelがインストールした状態を準備します!
1. この時点ではまだLOGINは作らないように!
2. .env と MySQLのDB準備はしておきましょう。
【 LaravelDB.comへ ログイン 】
Googleアカウントのみログイン可能です。(画面右上 OR ページ下部の中央)
LaravelDB.com
【 LaravelDB.comの POINT!! 】
ER図を作成していきますが重要事項があります!
<<重要>>
主キーのAutoIncrementは どのテーブルも『 id 』 と固定すること!!
LaravelのEloquentModel を便利に使うためには、どのテーブルも 『 id 』 と固定するのが吉です。
理由は「 モデル名::find($id); 」と便利に使う場合は 主キーのAutoIncrement 『 id 』 名にしておかないと動作しないからです( Laravelを勉強してる人はなんとなく知ってることでしょうか )。
※ laravelDB.comのCRUDでも生成されるファイルは「 ○○○::find($id); 」を使ってるためそ!
※ laravelに最初から入っているテーブル「user」「password_resets」は作成しないこと(migrate時に上書きしてしまいます)
※慣れれば簡単ですが、少しだけ慣れる時間は必要ですね。
【 CRUDを生成してみる! 】
- まずは、ER図を簡単に作ってみましょう!! その後に右メニュー「 ER図のLoad/Save 」をクリックすると以下画面が表示されます。
![]()
- 外部キーを沢山つけてる場合は、Migrationの順番が重要なので試す場合は「OFF」がオススメです。
- 「 Laravel( CRUD BUILD) 」 ボタンをクリックでファイルが生成されます!!
【 ダウンロードファイルを確認しましょう 】
【移行方法】
Macの場合: 「Optionキー + ドラッグ&ドロップ」 → 「結合」でフォルダの上書きではなく、結合になります。
Winの場合:「ドラッグ&ドロップ」で結合できます。ダウンロードした各フォルダを上記の方法で移動しましょう!
【 LaravelのHOMEページを表示しましょう 】
私の環境では「http://localhost/ 」
【 「 コマンド 」 を3つ順番に打ちましょう! 】
<<重要>>
DBや.env設定など最低限の設定を完了しておくこと!!php artisan migrate composer update //テストデータいらない場合は打たなくてもOK php artisan db:seed以上3つです。
【 完成: ブラウザで確認しましょう! 】
http://localhost/ (URLは開発環境によって異なります) で確認しましょう!
あなたが作ったテーブルのCRUD画面へ遷移できますよ!!作成されたindex/show/editの画面
基本的な処理が最初から出来てます!捗りそうですね!
Validationがスゴイ! → テーブル設計に合わせて生成
例でざっくりテーブルをER図で書いてみました。
以下"t_gsusers"テーブルを中心に見ていきます。
生成されたコントローラーのcreate(),edit()にはこういったvalidationが挿入されます。
TGsusersController$this->validate($request, [ "name" => "required|max:128", //string('name',128) "lid" => "required|max:128", //string('lid',128) "lpw" => "required|max:128", //string('lpw',128) "m_department_id" => "required|integer", //integer('m_department_id') "m_position_id" => "required|integer", //integer('m_position_id') "m_prefectures_id" => "required|integer", //integer('m_prefectures_id') "m_kanri_id" => "required|integer", //integer('m_kanri_id') "m_lifeflg_id" => "required|integer", //integer('m_lifeflg_id') "m_test_id" => "required|integer", //integer('m_test_id') ]);{{old('name')}} → 入力項目を補完
validationで未入力等ではじかれた場合、入力した文字を消さずに表示します。
date,datetime型 → 入力フォー厶type="date"
- 画面表示側もtype='date'
- validation側もdateチェックが入ります。
ER図外部キー設定→JOIN自動生成 QueryBuilderをController側にコメントで用意
イチからQueryBuilderを使用して、JOINを書くのは大変です。ベースになる部分は生成してくれます。使用する場合はコメントになってるのでコメントを外し、上記に書かれてるコードのEloquentModelの1行をコメントして使います。
※機械的に作成してるため、より良い記述に変更は必要になります。動作した後はコードを見ると良いでしょう
- Routing
- Controller
- Views
- Model 後は、自分で変更をいれていくだけですね。
日本発 Laravelツール
日本から海外でも使われるプロダクトになることを願っております。
Twitter: LaravelDB.com
以上
- 投稿日:2020-02-04T14:19:10+09:00
最新版『 Laravel DB.com 』コード書かずに超スピード開発~(DEMO動画あり)~
Laravel DB.com ってなに?
「LaravelDB.com」は、ER図(テーブルの設計)を作成することで、”Migration” OR ”CRUD(テストデータ)”のコードまで生成できるサービス(ツール)です。
DEMO動画(操作方法)
【YouTubeリンク>> https://t.co/jGVarMoXse 】
— LaravelDB.com (@ErdLaravel)の操作方法
「 Laravel DB.com 」 、利用方法まとめ
2020年2月2日リニューアルしたので、使用方法をまとめたいと思います。
注意)以前に見たことがある人は「PWA」でChcheAPIが効いてて古い画面のままかもしれません。
その場合には、[開発者ツール]→[Application]→[Clear Storage]でキャッシュを削除してください。確認したLaravel動作環境
- 開発環境: Laravel5.5 ~ 6.x / PHP7.2.1 / MySQL5.6.38
- 確認環境: Mac: Chromeブラウザ
メリット
- インストール不要
- テーブル設計をクラウドに保存可能 (他アプリの設計に使い回せる!)
- Migration自動生成
- CRUD自動生成
- テストデータ(Faker)自動生成
- Validation自動生成(テーブル設計時のカラム型を参照して作成してる。※Enum型は自分でCODE修正必要かも)
- Webサービスだから、どのPCでも同じデータを活用して作成できる
さあ、 LaravelDB.com はじめて行きましょう!
【 CRUD: 新しいプロジェクトを用意 】
最低限Laravelがインストールした状態を準備します!
1. この時点ではまだLOGINは作らないように!
2. .env と MySQLのDB準備はしておきましょう。
【 LaravelDB.comへ ログイン 】
Googleアカウントのみログイン可能です。(画面右上 OR ページ下部の中央)
LaravelDB.com
【 LaravelDB.comの POINT!! 】
ER図を作成していきますが重要事項があります!
<<重要>>
主キーのAutoIncrementは どのテーブルも『 id 』 と固定すること!!
LaravelのEloquentModel を便利に使うためには、どのテーブルも 『 id 』 と固定するのが吉です。
理由は「 モデル名::find($id); 」と便利に使う場合は 主キーのAutoIncrement 『 id 』 名にしておかないと動作しないからです( Laravelを勉強してる人はなんとなく知ってることでしょうか )。
※ laravelDB.comのCRUDでも生成されるファイルは「 ○○○::find($id); 」を使ってるためそ!
※ laravelに最初から入っているテーブル「user」「password_resets」は作成しないこと(migrate時に上書きしてしまいます)
※慣れれば簡単ですが、少しだけ慣れる時間は必要ですね。
【 CRUDを生成してみる! 】
- まずは、ER図を簡単に作ってみましょう!! その後に右メニュー「 ER図のLoad/Save 」をクリックすると以下画面が表示されます。
![]()
- 外部キーを沢山つけてる場合は、Migrationの順番が重要なので試す場合は「OFF」がオススメです。
- 「 Laravel( CRUD BUILD) 」 ボタンをクリックでファイルが生成されます!!
【 ダウンロードファイルを確認しましょう 】
【移行方法】
Macの場合: 「Optionキー + ドラッグ&ドロップ」 → 「結合」でフォルダの上書きではなく、結合になります。
Winの場合:「ドラッグ&ドロップ」で結合できます。ダウンロードした各フォルダを上記の方法で移動しましょう!
【 LaravelのHOMEページを表示しましょう 】
私の環境では「http://localhost/ 」
【 「 コマンド 」 を3つ順番に打ちましょう! 】
<<重要>>
DBや.env設定など最低限の設定を完了しておくこと!!php artisan migrate composer update //テストデータいらない場合は打たなくてもOK php artisan db:seed以上3つです。
【 完成: ブラウザで確認しましょう! 】
http://localhost/ (URLは開発環境によって異なります) で確認しましょう!
あなたが作ったテーブルのCRUD画面へ遷移できますよ!!作成されたindex/show/editの画面
基本的な処理が最初から出来てます!捗りそうですね!
Validationがスゴイ! → テーブル設計に合わせて生成
例でざっくりテーブルをER図で書いてみました。
以下"t_gsusers"テーブルを中心に見ていきます。
生成されたコントローラーのcreate(),edit()にはこういったvalidationが挿入されます。
TGsusersController$this->validate($request, [ "name" => "required|max:128", //string('name',128) "lid" => "required|max:128", //string('lid',128) "lpw" => "required|max:128", //string('lpw',128) "m_department_id" => "required|integer", //integer('m_department_id') "m_position_id" => "required|integer", //integer('m_position_id') "m_prefectures_id" => "required|integer", //integer('m_prefectures_id') "m_kanri_id" => "required|integer", //integer('m_kanri_id') "m_lifeflg_id" => "required|integer", //integer('m_lifeflg_id') "m_test_id" => "required|integer", //integer('m_test_id') ]);{{old('name')}} → 入力項目を補完
validationで未入力等ではじかれた場合、入力した文字を消さずに表示します。
date,datetime型 → 入力フォー厶type="date"
- 画面表示側もtype='date'
- validation側もdateチェックが入ります。
ER図外部キー設定→JOIN自動生成 QueryBuilderをController側にコメントで用意
イチからQueryBuilderを使用して、JOINを書くのは大変です。ベースになる部分は生成してくれます。使用する場合はコメントになってるのでコメントを外し、上記に書かれてるコードのEloquentModelの1行をコメントして使います。
※機械的に作成してるため、より良い記述に変更は必要になります。動作した後はコードを見ると良いでしょう
- Routing
- Controller
- Views
- Model 後は、自分で変更をいれていくだけですね。
日本発 Laravelツール
日本から海外でも使われるプロダクトになることを願っております。
Twitter: LaravelDB.com
以上
- 投稿日:2020-02-04T14:19:10+09:00
コード書かずに超スピード開発~(DEMO動画あり)~最新版『 Laravel DB.com 』
Laravel DB.com ってなに?
コード書かずに超スピード開発って書いてますが、基本ベースのCRUD生成までがコード書かないってことですね。そこからの細かい仕様に合わせる部分は自身で修正する必要はあリます。ベースを作るまでは自動って感じでしょうか。
「LaravelDB.com」は、ER図(テーブルの設計)を作成することで、”Migration” OR ”CRUD(テストデータ)”のコードまで生成できるサービス(ツール)です。
DEMO動画(操作方法)
【YouTubeリンク>> https://t.co/jGVarMoXse 】
— LaravelDB.com (@ErdLaravel)の操作方法注意)以前に見たことがある人は「PWA」でChcheAPIが効いてて古い画面のままかもしれません。
その場合には、[開発者ツール]→[Application]→[Clear Storage]でキャッシュを削除してください。「 LaravelDB.com 」の特徴
- Webサービスだから「 家・会社のパソコン 」でも同じデータを活用できる
- テーブル設計をクラウドに保存可能 ( 他アプリの設計に使い回せる! )
- Migration自動生成 / CRUD自動生成 / テストデータ(Faker)自動生成
- Validation(テーブル設計時のカラム型を参照して自動生成 ※Enum型は自分でCODE修正必要かも...)
- 確認環境: Mac: Chromeブラウザ
- 開発環境: Laravel5.5 ~ 6.x / PHP7.2.1 / MySQL5.6.38
【 CRUD: 新しいプロジェクトを用意 】
最低限Laravelがインストールした状態(Welcomeページ表示)を準備します!
1. この時点ではまだLOGINは作らないように!
2. .env と MySQLのDB準備はしておきましょう。
【 LaravelDB.comへ ログイン 】
Googleアカウントのみログイン可能です。(画面右上 OR ページ下部の中央)
LaravelDB.com
【 LaravelDB.comの POINT!! 】
ER図を作成していきますが重要事項があります!
<<重要>>
主キーのAutoIncrementは どのテーブルも『 id 』 と固定すること!!
LaravelのEloquentModel を便利に使うためには、どのテーブルも 『 id 』 と固定するのが吉です。
理由は「 モデル名::find($id); 」と便利に使う場合は 主キーのAutoIncrement 『 id 』 名にしておかないと動作しないからです( Laravelを勉強してる人はなんとなく知ってることでしょうか )。
※ laravelDB.comのCRUDでも生成されるファイルは「 ○○○::find($id); 」を使ってるためそ!
※ laravelに最初から入っているテーブル「user」「password_resets」は作成しないこと(migrate時に上書きしてしまいます)
※慣れれば簡単ですが、少しだけ慣れる時間は必要ですね。
【 CRUDを生成してみる! 】
- まずは、ER図を簡単に作ってみましょう!! その後に右メニュー「 ER図のLoad/Save 」をクリックすると以下画面が表示されます。
![]()
- 外部キーを沢山つけてる場合は、Migrationの順番が重要なので試す場合は「OFF」がオススメです。
- 「 Laravel( CRUD BUILD) 」 ボタンをクリックでファイルが生成されます!!
【 ダウンロードファイルを確認しましょう 】
【移行方法】
Macの場合: 「Optionキー + ドラッグ&ドロップ」 → 「結合」でフォルダの上書きではなく、結合になります。
Winの場合:「ドラッグ&ドロップ」で結合できます。ダウンロードした各フォルダを上記の方法で移動しましょう!
【 LaravelのHOMEページを表示しましょう 】
私の環境では「http://localhost/ 」
【 「 コマンド 」 を3つ順番に打ちましょう! 】
<<重要>>
DBや.env設定など最低限の設定を完了しておくこと!!php artisan migrate composer update //テストデータいらない場合は打たなくてもOK php artisan db:seed3つ目のコマンドはテストデータ登録の実行コマンド
Faker注意点有り!
テストデータ登録「 database/seeds/以下 」Fakerファイルの中はこんな感じです。
自動で文字が入ってるので、精度はまあこんなもんですかね。※文字列、数値、日付は自動で判定して入れてくれてるようです。
※ Fakerで使えるタイプ一覧、 自身で変更して使いたい場合はこちらを参考にすることをオススメします。「address」や「url」を使う場合はカラムサイズに気をつけましょう!
【 完成: ブラウザで確認しましょう! 】
http://localhost/ (URLは開発環境によって異なります) で確認しましょう!
あなたが作ったテーブルのCRUD画面へ遷移できますよ!!作成されたindex/show/editの画面
基本的な処理が最初から出来てます!捗りそうですね!
Validationがスゴイ! → テーブル設計に合わせて生成
例でざっくりテーブルをER図で書いてみました。
以下"t_gsusers"テーブルを中心に見ていきます。
生成されたコントローラーのcreate(),edit()にはこういったvalidationが挿入されます。
TGsusersController$this->validate($request, [ "name" => "required|max:128", //string('name',128) "lid" => "required|max:128", //string('lid',128) "lpw" => "required|max:128", //string('lpw',128) "m_department_id" => "required|integer", //integer('m_department_id') "m_position_id" => "required|integer", //integer('m_position_id') "m_prefectures_id" => "required|integer", //integer('m_prefectures_id') "m_kanri_id" => "required|integer", //integer('m_kanri_id') "m_lifeflg_id" => "required|integer", //integer('m_lifeflg_id') "m_test_id" => "required|integer", //integer('m_test_id') ]);{{old('name')}} → 入力項目を補完
validationで未入力等ではじかれた場合、入力した文字を消さずに表示します。
date,datetime型 → 入力フォー厶type="date"
- 画面表示側もtype='date'
- validation側もdateチェックが入ります。
ER図外部キー設定→JOIN自動生成 QueryBuilderをController側にコメントで用意
イチからQueryBuilderを使用して、JOINを書くのは大変です。ベースになる部分は生成してくれます。使用する場合はコメントになってるのでコメントを外し、上記に書かれてるコードのEloquentModelの1行をコメントして使います。
※機械的に作成してるため、より良い記述に変更は必要になります。動作した後はコードを見ましょう!
最低限のコードは出来上がってます!あとは、仕様に合わせて修正・追加していきましょう!!!
- Routing
- Controller
- Views
- Model
後は、自分で変更をいれていくだけですね。Laravel専用ツール!!
日本から世界へ!世界で使われるプロダクトになることを願っております。
是非、応援の程よろしくお願いいたしますm(_ _)mTwitter: LaravelDB.com
LaravelDB.com解説ページ一覧
LaravelDB.com 対応カラム一覧
https://qiita.com/daisu_yamazaki/items/92dc3cc599a264c3fb0f
LaravelDB.com テーブル命名ルール
https://qiita.com/daisu_yamazaki/items/1cb5987cc6d1008def82
LaravelDb.com integerの注意点
https://qiita.com/daisu_yamazaki/items/f2e6d58cfa20fa81fd54
LaravelDB.com Faker(テストデータ投入)
https://qiita.com/daisu_yamazaki/items/57669e8fa2c256d85c95
以上
- 投稿日:2020-02-04T11:59:11+09:00
Laravel で Cloud KMS を使って暗号化/復号化をしてみた。
こんにちわ! @ktoshi です!
以前、Cloud KMSをGoで使う 記事を書きましたが、今回は PHP(というよりLaravel)で使ってみようと思います。
実際はGoより先にこっちで使い始めてたのですが、諸々あり後発の記事になりました。
そんなわけではじめましょー。Cloud KMS
暗号鍵をセキュアに管理できるGCPのサービスです。
自動ローテーションやIAMでの権限管理でセキュアなアクセスを実現できる上、APIを利用した暗号化/復号化を行えるので非常に柔軟な利用が可能です。
類似したサービスはAWSの「KMS」や Azureの「Key Vault」などがあります。Key Ring と Key
Cloud KMS では Key を Key Ring で管理しています。
関係性は字のごとく、鍵と鍵束ですね。
ある程度同じ用途で使用される鍵については同じ Key Ring でまとめておくとよいでしょう。
実際に暗号化などで利用されるのは Key となります。目的
Cloud KMS に暗号鍵を用意し、Laravel + Cloud KMS で文字列を暗号化/復号化する。
実践
では実際に暗号化/復号化を行いましょう。
大きく分けて作業は3つです。
- 環境準備
- 暗号鍵の準備
- 暗号化
- 復号化
※ 下記手順の[PROJECT_NAME]、[KEY_RING_NAME]、[KEY_NAME] は適宜置き換えてください。
前提条件
前提として下記が導入されていることとします。
なお、バージョンは私の環境でのものです。
- PHP: 7.3.10
- composer: v1.9.0
- Laravel: 6.1
- gcloud: 268.0.0
- terraform: v0.12.16
環境準備
PHPでCloudKMSを扱うためのライブラリをインストールします。
composer でインストールをします。composer require google/cloud-kms暗号鍵の準備
暗号をするにはまず暗号鍵を準備しないことには始まりません。
私は terraform を利用しましたが、 gcloud コマンドでも簡単に作成できます。
terraform や gcloud のインストールについては話がそれるので割愛します。terraform の場合
# 鍵束の作成 resource "google_kms_key_ring" "sample_key_ring" { name = "[KEY_RING_NAME]" location = "global" } # 鍵の作成 resource "google_kms_crypto_key" "sample_crypto_key" { name = "[KEY_NAME]" key_ring = google_kms_key_ring.sample_key_ring.self_link }gcloud コマンドの場合
# 鍵束の作成 $ gcloud kms keyrings create [KEY_RING_NAME] --location global --project [PROJECT_NAME] # 鍵の作成 $ gcloud kms keys create [KEY_NAME] --location global --keyring [KEY_RING_NAME] --purpose encryption --project [PROJECT_NAME]これで暗号鍵の準備は完了です。
暗号化
では、実際に Laravel で暗号化を行います。
今回は処理を行っている箇所のみ抜粋しています。use Google\Cloud\Kms\V1\KeyManagementServiceClient; public function encryption(String $str) { $kms = new KeyManagementServiceClient(); $projectId = '<PROJECT_NAME>'; $location = '<PROJECT_LOCATION>'; $keyRingId = '<KEY_RING_NAME>'; $keyId = '<KEY_NAME>'; $cryptoKeyName = $kms->cryptoKeyName($projectId, $location, $keyRingId, $keyId); return $kms->encrypt($cryptoKeyName, $str)->getCiphertext() }これで暗号化した文字列を返すことができます。
なお、Cloud KMS では本気で暗号化されるため、帰ってきた文字列が UTF-8 ではなくなり、
DBなどに直接突っ込むことができません。
DBへ突っ込むときなどは base64_encode を使ってエンコードしておくと良いです。復号化
暗号化処理はできたので、次に復元しましょう。
やってることはほぼ、暗号化と一緒です。use Google\Cloud\Kms\V1\KeyManagementServiceClient; public function decryption(String $str) { $kms = new KeyManagementServiceClient(); $projectId = '<PROJECT_NAME>'; $location = '<PROJECT_LOCATION>'; $keyRingId = '<KEY_RING_NAME>'; $keyId = '<KEY_NAME>'; $cryptoKeyName = $kms->cryptoKeyName($projectId, $location, $keyRingId, $keyId); return $kms->decrypt($cryptoKeyName, $str)->getPlaintext() }これで復号化された文字列を取得できます。
なお、暗号化の際に Base64 でエンコードしている場合はデコードしてから渡しましょう
私はこれを忘れてめちゃくちゃ時間食いました。。。まとめ
前回、Goで行った処理と同様のものをPHPで行いました。
結局やることとしては、暗号化鍵を取得して、それを使って暗号化/復号化をするだけですね。
前の記事でも記載しましたが、今後セキュアでかつ可逆暗号化が必要となるケースがあると思います。
そんなときにぜひこの記事が参考になればと思います。
それでは、みなさまも引き続き安全で快適な暗号化ライフを。
- 投稿日:2020-02-04T11:19:03+09:00
ルートモデルバインディングってどうやって解決しているのか気になったのでコードの奥地にでかけてみた
ルートモデルバインディングまでの道のり
laravelでweb.phpが読み込まれるまでの道のりを読んでおくとさらにわかりやすいかもしれません。
Let's read
ルートが決定されるまで
Illuminate/Routing/Router::dispatchToRoute()から始めていきます。Illuminate/Routing/Routerpublic function dispatchToRoute(Request $request) { return $this->runRoute($request, $this->findRoute($request)); }
runRoute()の引数に渡されるfindRoute()を見ていきます。Illuminate/Routing/Routerprotected function findRoute($request) { $this->current = $route = $this->routes->match($request); $this->container->instance(Route::class, $route); return $route; }
$this->routesはroutes/web.phpなどのルートファイルを読み込んで、その1つ1つのルートをIlluminate/Routing/Routeとして保持しているIlluminate/Routing/RouteCollectionです。
Illuminate/Routing/RouteCollection::match()にリクエストを渡して呼び出しています。Illuminate/Routing/RouteCollectionpublic function match(Request $request) { $routes = $this->get($request->getMethod()); $route = $this->matchAgainstRoutes($routes, $request); if (! is_null($route)) { return $route->bind($request); } //... }
$request->getMethod()でリクエストされたメソッドを取得して、$this->get()ですべてのルートから取得したメソッドと同じルートを抜き出します。
$this->matchAgatinstRoutes()で1つのルートに絞っていきます。Illuminate/Routing/RouteCollection.phpprotected function matchAgainstRoutes(array $routes, $request, $includingMethod = true) { [$fallbacks, $routes] = collect($routes)->partition(function ($route) { return $route->isFallback; }); return $routes->merge($fallbacks)->first(function ($value) use ($request, $includingMethod) { return $value->matches($request, $includingMethod); }); }routeのプロパティの
isFallbackでroutesを振り分けています。(初期値はfalse)
isFallbackは後退という意味でマッチングの優先順位を下げています。(ルーティングは上に書かれたものから優先される)コレクションクラスの
merge()で$routesに$fallbacksを結合します。これで$routesに入っていた値が優先されます。コレクションクラスの
first()の引数に渡したコールバックでルートを探していきます。Illuminate/Routing/Route.phppublic function matches(Request $request, $includingMethod = true) { $this->compileRoute(); foreach ($this->getValidators() as $validator) { if (! $includingMethod && $validator instanceof MethodValidator) { continue; } if (! $validator->matches($this, $request)) { return false; } } return true; }
$this->compileRoute()はIlluminate/Routing/RouteCompiler::compile()の結果を$this->compiledに格納します。Illuminate/Routing/RouteCompiler.phppublic function compile() { $optionals = $this->getOptionalParameters(); $uri = preg_replace('/\{(\w+?)\?\}/', '{$1}', $this->route->uri()); return ( new SymfonyRoute($uri, $optionals, $this->route->wheres, ['utf8' => true], $this->route->getDomain() ?: '') )->compile(); }
getOptionalParameters()はOptionalParameterを取得します。
preg_replace()ではOptionalParameterから'?'を取り除きます。例) {hoge?} -> {hoge}
SymfonyRouteからはより細かい話になってくるのでざっくり解説します。(というか読めない...)
vendor/symfony/routing/RouteCompiler::compilePattern()でurlのpathからどのような正規表現で値を抜き出すかを決めてるっぽいです。コメントで「「{}」で囲まれたすべての変数に一致し....」とあるのでおそらくそうだと思います。(laravelも「{}」の中身を抜き出すので)
// Match all variables enclosed in "{}" and iterate over them. But we only want to match the innermost variable // in case of nested "{}", e.g. {foo{bar}}. This in ensured because \w does not match "{" or "}" itself.
vendor/symfony/routing/CompiledRouteのregexプロパティにこの正規表現をセットしています。最終的に
vendor/symfony/routing/CompiledRouteが$this->compiledにセットされます。foreachでrouteを1つずつバリデーションにかけていきます。バリデーションの内容は
getValidators()で取得します。public static function getValidators() { return static::$validators = [ new UriValidator, new MethodValidator, new SchemeValidator, new HostValidator, ]; }それぞれのクラスの
match()を使ってマッチするuriを探していきます。すべてのバリデーションを通過したrouteが返されます。これが
Illuminate/Routing/RouteCollection::matchAgainstRoutes()の返り値になります。Illuminate/Routing/RouteCollection.php// 再掲 public function match(Request $request) { $routes = $this->get($request->getMethod()); $route = $this->matchAgainstRoutes($routes, $request); if (! is_null($route)) { return $route->bind($request); } }マッチするrouteが見つかると
Illuminate/Routing/Route::bind()にリクエストが渡されて呼び出されます。Illuminate/Routing/Route.phppublic function bind(Request $request) { $this->compileRoute(); $this->parameters = (new RouteParameterBinder($this)) ->parameters($request); $this->originalParameters = $this->parameters; return $this; }
$this->compileRoute()は先程と同じなので割愛します。
parameters()でuriから値をキャプチャしていきます。Illuminate/Routing/RouteParameterBinder.phppublic function parameters($request) { $parameters = $this->bindPathParameters($request); //... return $this->replaceDefaults($parameters); } protected function bindPathParameters($request) { $path = '/'.ltrim($request->decodedPath(), '/'); preg_match($this->route->compiled->getRegex(), $path, $matches); return $this->matchToKeys(array_slice($matches, 1)); }
bindPathParameters()が実際の処理をしていきます。
decodePath()でURLエンコードされたuriをデコードします。
preg_match()で$pathからvendor/symfony/routing/CompiledRouteのregexプロパティにセットした正規表現を使って値を取得していきます。
「{}」で囲ったところに値するものを取得します。
$this->matchToKeys()で整形をします。例 web.phpで定義したルート: '/{hoge}/{fuga}' 実際のパス: '/1/4' 整形された配列: ['hoge' => '1', 'fuga' => '4']整形された配列を
Illuminate/Routing/Routeのparametersプロパティに格納します。
Illuminate/Routing/Router::findRoute()に戻ります。Illuminate/Routing/Router.phpprotected function findRoute($request) { $this->current = $route = $this->routes->match($request); $this->container->instance(Route::class, $route); return $route; }マッチしたrouteを変数に格納し、コンテナにバインドしています。
runRoute()に戻ります。Illuminate/Routing/Router.phppublic function dispatchToRoute(Request $request) { return $this->runRoute($request, $this->findRoute($request)); } protected function runRoute(Request $request, Route $route) { $request->setRouteResolver(function () use ($route) { return $route; }); $this->events->dispatch(new RouteMatched($route, $request)); return $this->prepareResponse($request, $this->runRouteWithinStack($route, $request) ); }
setRouteResolver()はプロパティである$this->routeResolverに引数のクロージャをセットしています。ルートがマッチしましたよ〜というイベントを発火させて、
$this->runRouteWithinStack()に参ります。Illuminate/Routing/Router.phpprotected function runRouteWithinStack(Route $route, Request $request) { $shouldSkipMiddleware = $this->container->bound('middleware.disable') && $this->container->make('middleware.disable') === true; $middleware = $shouldSkipMiddleware ? [] : $this->gatherRouteMiddleware($route); return (new Pipeline($this->container)) ->send($request) ->through($middleware) ->then(function ($request) use ($route) { return $this->prepareResponse( $request, $route->run() ); }); }ここでは決定したルートのアクションを呼ぶ前にミドルウェアを集めて実行しています。(LaravelのPipeline::thenでmiddlewareを処理している流れをまとめてみた)
ミドルウェアは
app/Http/Kernel.phpに定義されているルートミドルウェアが使われています。
その中にルートモデルバインディングを行っている`\Illuminate\Routing\Middleware\SubstituteBindings::class'があります。protected $routeMiddleware = [ 'auth' => \App\Http\Middleware\Authenticate::class, 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, 'can' => \Illuminate\Auth\Middleware\Authorize::class, 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, ];ルートモデルバインディング
ミドルウェアは
handle()が呼ばれます。Illuminate/Routing/Middleware/SubstituteBindings.phppublic function handle($request, Closure $next) { $this->router->substituteBindings($route = $request->route()); $this->router->substituteImplicitBindings($route); return $next($request); }明示的な結合
substituteBindings()は明示的な結合と呼ばれるものを解決しています。app/Providers/RouteServiceProvider.php// こんな感じ public function boot() { parent::boot(); Route::model('hoge', App\User::class); }
Illuminate/Routing/Routeのparametersプロパティに該当のものがあれば解決してくれます。
$parameters = ['hoge' => '2']ならApp\User::classのprimaryKeyが'2'のものをとってきてくれる。暗黙的な結合
substituteImplicitBindings()は明示的な結合のように書かなくても、ある一定の法則で書けば勝手に解決してくれますよ〜のやつです。Illuminate/Routing/Router.phppublic function substituteImplicitBindings($route) { ImplicitRouteBinding::resolveForRoute($this->container, $route); }Illuminate/Routing/ImplicitRouteBinding.phppublic static function resolveForRoute($container, $route) { $parameters = $route->parameters(); foreach ($route->signatureParameters(UrlRoutable::class) as $parameter) { if (! $parameterName = static::getParameterName($parameter->name, $parameters)) { continue; } $parameterValue = $parameters[$parameterName]; if ($parameterValue instanceof UrlRoutable) { continue; } $instance = $container->make($parameter->getClass()->name); if (! $model = $instance->resolveRouteBinding($parameterValue)) { throw (new ModelNotFoundException)->setModel(get_class($instance), [$parameterValue]); } $route->setParameter($parameterName, $model); } }
$router->parameters()でuriから抜き出してparametersプロパティにセットしておいた配列を取得します。例 web.phpで定義したルート: '/{hoge}/{fuga}' 実際のパス: '/1/4' 整形された配列: ['hoge' => '1', 'fuga' => '4']
signatureParameters()を見ていきます。Illuminate/Routing/Route.phppublic function signatureParameters($subClass = null) { return RouteSignatureParameters::fromAction($this->action, $subClass); }
fromAction()に$this->actionを渡して呼び出しています。
$this->actionにはマッチしたルートのコントローラーとメソッドが以下の形で保持されています。
$this->action = ['uses' => 'HogeController@method', 'controller' => 'HogeController@method']Illuminate/Routing/RouteSignatureParameters.phppublic static function fromAction(array $action, $subClass = null) { $parameters = is_string($action['uses']) ? static::fromClassMethodString($action['uses']) : (new ReflectionFunction($action['uses']))->getParameters(); return is_null($subClass) ? $parameters : array_filter($parameters, function ($p) use ($subClass) { return $p->getClass() && $p->getClass()->isSubclassOf($subClass); }); }今回は
$action['uses']は文字列なのでfromClassMethodString()を見ていきます。Illuminate/Routing/RouteSignatureParameters.phpprotected static function fromClassMethodString($uses) { [$class, $method] = Str::parseCallback($uses); if (! method_exists($class, $method) && is_callable($class, $method)) { return []; } return (new ReflectionMethod($class, $method))->getParameters(); }1行目で「@」でコントローラークラスとメソッドに分けます。
ReflectionMethodクラスをつかってメソッドの引数に指定されている名前を取得して配列で返します。先程取得した配列をフィルターにかけていきます。
$p->getClass()でタイプヒンティングされているかを検証し、$p->getClass()->isSubclassOfでIlluminate/Contracts/Routing/UrlRoutableを実装しているかを検証しています。
Illuminate/Contracts/Routing/UrlRoutableはIlluminate/Database/Eloquent/Modelが実装しているのでEloquentは対象になります。コントローラーのparameter解析が終わったのでforeachに戻ります。
Illuminate/Routing/ImplicitRouteBinding.phppublic static function resolveForRoute($container, $route) { $parameters = $route->parameters(); foreach ($route->signatureParameters(UrlRoutable::class) as $parameter) { if (! $parameterName = static::getParameterName($parameter->name, $parameters)) { continue; } $parameterValue = $parameters[$parameterName]; if ($parameterValue instanceof UrlRoutable) { continue; } $instance = $container->make($parameter->getClass()->name); if (! $model = $instance->resolveRouteBinding($parameterValue)) { throw (new ModelNotFoundException)->setModel(get_class($instance), [$parameterValue]); } $route->setParameter($parameterName, $model); } }
getParameterNameでuriから取得した名前とメソッドの引数名が一致しているものを探します。(キャメルケースとスネークケースの差はここで吸収)以下の例で
$parameterValueの取得内容を見てみます。例 web.phpで定義したルート: '/{hoge}/{fuga}' 実際のパス: '/1/4' 整形された配列: ['hoge' => '1', 'fuga' => '4']$parameterValue /* 1 */ = $parameters[$parameterName /* hoge */];
$instance = $container->make($parameter->getClass()->name);でタイプヒンティングの名前からモデルインスタンスを生成します。
resolveRouteBinding()で$parameterValueを使ってwhereしてデータを引っ張ってきます。Illuminate/Database/Eloquent/Model.phppublic function resolveRouteBinding($value) { return $this->where($this->getRouteKeyName(), $value)->first(); }
$route->setParameter()でparametersプロパティを上書きします。これでルートモデルバインディングは解決です。
実際にコントローラーに引数を渡す
Illuminate/Routing/ControllerDispatcher::dispatch()の中の$controller->{$method}(...array_values($parameters))でparametersの値を渡しています。Illuminate/Routing/ControllerDispatcher.phppublic function dispatch(Route $route, $controller, $method) { $parameters = $this->resolveClassMethodDependencies( $route->parametersWithoutNulls(), $controller, $method ); if (method_exists($controller, 'callAction')) { return $controller->callAction($method, $parameters); } return $controller->{$method}(...array_values($parameters)); }これにて終了です。symfonyのところはわかりませんでしたが大体の流れはつかめたと思います。やったね!
- 投稿日:2020-02-04T00:51:43+09:00
Laravelでチェックボックスの値を取得しold関数でチェックを保持
概要
Laravel初心者(というかPHP自体の初学者ですが。。)が実務でアンケートフォームの作成を任されたので、自分用にメモを残します。
ルーティング
web.php<?php Route::get('/', 'FormController@form')->name('form'); Route::post('/confirm', 'FormController@confirm')->name('confirm'); Route::post('/complete', 'FormController@complete')->name('complete');モデル
Form.php<?php namespace App; use Illuminate\Database\Eloquent\Model; class Form extends Model { protected $fillable = [ 'checkbox', ]; }ビュー:入力画面
form.blade.php@extends('layouts.default') @section('content') <form method="post" action="{{ route('confirm') }}"> @csrf <div class="form-group"> <div class="form-check"> <input id="check1" type="checkbox" name="checkbox[]" value="選択肢1"{{ is_array(old("checkbox")) && in_array("選択肢1", old("checkbox"), true)? ' checked' : '' }}> <label class="form-check-label" for="check1">選択肢1</label> </div> <div class="form-check"> <input id="check2" type="checkbox" name="checkbox[]" value="選択肢2"{{ is_array(old("checkbox")) && in_array("選択肢2", old("checkbox"), true)? ' checked' : '' }}> <label class="form-check-label" for="check2">選択肢2</label> </div> <div class="form-check"> <input id="check3" type="checkbox" name="checkbox[]" value="選択肢3"{{ is_array(old("checkbox")) && in_array("選択肢3", old("checkbox"), true)? ' checked' : '' }}> <label class="form-check-label" for="check3">選択肢3</label> </div> @if ($errors->has('checkbox')) <span class="error">{{$errors->first('checkbox')}}</span> @endif </div> <button type="submit" class="btn btn-lg btn-primary">入力内容の確認</button> </form> @endsectionビュー:確認画面
confirm.balde.php@extends('layouts.default') @section('content') <form method="post" action="{{ route('complete') }}"> @csrf <div class="form-group"> <p class="confirm-input">{!! nl2br(e($checkbox)) !!}</p> </div> @foreach($inputs->getAttributes() as $key => $value) @if(isset($value)) @if(is_array($value)) @foreach($value as $subValue) <input name="{{ $key }}[]" type="hidden" value="{{ $subValue }}"> @endforeach else <input name="{{ $key }}" type="hidden" value="{{ $value }}"> @endif @endif @endforeach <button type="submit" name="action" value="back" class="btn btn-lg btn-info">戻る</button> <button type="submit" name="action" value="submit" class="btn btn-lg btn-primary">送信</button> </form> @endsectionコントローラー
FormController.php<?php namespace App\Http\Controllers; use App\Form; use App\Http\Controllers\Controller; use Illuminate\Http\Request; use Illuminate\Support\Facades\Validator; class FormController extends Controller { public function form() { return view('form'); } public function confirm(Request $request) { //バリデーションを実行(結果に問題があれば処理を中断してエラーを返す) $rules = [ 'checkbox' => 'required', ]; $messages = [ 'checkbox.required' => 'チェックを入れてください', ]; $validator = Validator::make($request->all(), $rules, $messages); if ($validator->fails()) { return redirect(url("/")) ->withErrors($validator) ->withInput(); } //フォームから受け取ったすべてのinputの値を取得 $inputs = new Form($request->all()); // 配列から文字列に $checkbox = ''; if (isset($request->checkbox)) { $checkbox = implode("\n",$request->checkbox); } //入力内容確認ページのviewに変数を渡して表示 return view('confirm', compact('inputs', 'checkbox')); } public function complete(Request $request) { $input = $request->except('action'); if ($request->action === 'back') { return redirect()->action('FormController@form')->withInput($input); } // チェックボックス(配列)を改行コード区切りの文字列に if (isset($request->checkbox)) { $request->merge(['checkbox' => implode("\n", $request->checkbox)]); } // データを保存 Form::create($request->all()); // 二重送信防止 $request->session()->regenerateToken(); return view('complete'); } }


























