20200204のlaravelに関する記事は13件です。

【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に戻せます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【黒歴史】未経験半年で作成した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関係

ER図はこんな感じです。
スクリーンショット 2020-01-06 1.44.55.png

マイグレーションファイルは下記のようになっています。

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_idhourがなぜかstring(varchar)だったりします。

インデックスと制約についてですが、特に考量していませんでした。
存在は知っていましたが付け方を知らなかった気がします。

修正後

スクリーンショット 2020-02-03 1.14.00.png

tweetsテーブルをpostsテーブルに変更しました。

また、活動内容を投稿するたびにactivitiesの持っていた合計時間格納用カラムに加算してましたが、postsからSQLで集計するようにしたため削除。
継続日数もロジックでなんとかなりそうだったので削除しました。

usersはidをtwitterのユーザーidにし、他のカラム名も変更しています。

その他、外部キーも設定し、その他検索で使うと思われるものにindexを貼っています(この小規模ので必要かはわかりませんが)。

2.ルーティング

web.php
Route::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');
});

図で表すとこのようになります。
スクリーンショット 2020-01-06 3.20.47.png

何がだめだったか

もっとまとめられました。

修正後

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::prefixgroupに変更
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を使い切りだせそうなものは切り出しました
スクリーンショット 2020-02-03 20.44.23.png

5.Log

これは未だにどうするのが良いのか、はっきりと分かってはいません
もともとは、エラー箇所にLogを書き込む処理おいているだけでした。

今回は
Eloquentは作成や更新などイベントが起こるとそれをフックに、そのタイミングで処理ができるので、そこにLog処理を置きました。

下記のようにすると、Userが作成されたタイミングでLogが書かれます

php artisan make:observer UserObserver --model=User
UserObserver.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.最適化

一番最初、完成した後やったことは
.envAPP_ENVproductionにし、APP_DEBUGfalseにしただけでした。

なにがだめだったか

公式にかかれている最適化処理があるのに関わらず、行っていませんでした。

オートローダー最適化

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)のカラムへ保存していました。
投稿から取得して計算するように変更したところ、コード量がごっそり減りました。

Before
SC000019.JPG

After
SC000020.JPG

おわりに

半年前のコードが完全に他人のもので、想像以上に時間がかかりました。
頭の中で考えてとりあえず作るということのデメリットが享受できました。
今後は事前の設計をしっかりして、物を作りたいです。

また半年したらこのコードもだめだなと思えるような成長を遂げていたいものです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

未経験半年で作成した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関係

ER図はこんな感じです。
スクリーンショット 2020-01-06 1.44.55.png

マイグレーションファイルは下記のようになっています。

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_idhourがなぜかstring(varchar)だったりします。

インデックスと制約についてですが、特に考量していませんでした。
存在は知っていましたが付け方を知らなかった気がします。

修正後

スクリーンショット 2020-02-03 1.14.00.png

tweetsテーブルをpostsテーブルに変更しました。

また、活動内容を投稿するたびにactivitiesの持っていた合計時間格納用カラムに加算してましたが、postsからSQLで集計するようにしたため削除。
継続日数もロジックでなんとかなりそうだったので削除しました。

usersはidをtwitterのユーザーidにし、他のカラム名も変更しています。

その他、外部キーも設定し、その他検索で使うと思われるものにindexを貼っています(この小規模ので必要かはわかりませんが)。

2.ルーティング

web.php
Route::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');
});

図で表すとこのようになります。
スクリーンショット 2020-01-06 3.20.47.png

何がだめだったか

もっとまとめられました。

修正後

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::prefixgroupに変更
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を使い切りだせそうなものは切り出しました
スクリーンショット 2020-02-03 20.44.23.png

5.Log

これは未だにどうするのが良いのか、はっきりと分かってはいません
もともとは、エラー箇所にLogを書き込む処理おいているだけでした。

今回は
Eloquentは作成や更新などイベントが起こるとそれをフックに、そのタイミングで処理ができるので、そこにLog処理を置きました。

下記のようにすると、Userが作成されたタイミングでLogが書かれます

php artisan make:observer UserObserver --model=User
UserObserver.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.最適化

一番最初、完成した後やったことは
.envAPP_ENVproductionにし、APP_DEBUGfalseにしただけでした。

なにがだめだったか

公式にかかれている最適化処理があるのに関わらず、行っていませんでした。

オートローダー最適化

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)のカラムへ保存していました。
投稿から取得して計算するように変更したところ、コード量がごっそり減りました。

Before
SC000019.JPG

After
SC000020.JPG

おわりに

半年前のコードが完全に他人のもので、想像以上に時間がかかりました。
頭の中で考えてとりあえず作るということのデメリットが享受できました。
今後は事前の設計をしっかりして、物を作りたいです。

また半年したらこのコードもだめだなと思えるような成長を遂げていたいものです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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/
image.png

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.exe

Shared Drivesの設定

チェックを入れて「Apply」をクリック。
Dockerを再起動してちゃんと設定できているか確認します。
image.png

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.yml
version: '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/Dockerfile
FROM 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 443
nginx/nginx.conf
user 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.conf
server {

  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.zip

php7.3/DockerFileの編集

入手したファイル名に合わせて13~25行目を変更してください

php7.3/tnsnames.oraの編集

ご自分の環境に合わせて記述してください

php7.3/Dockerfile
FROM 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/composer

Laravel用に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 -d

LaravelのインストールとOCI8設定

1.php-fpmに入ります

docker-compose exec php-fpm bash

2.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=oracle

6..env内のORACLEの設定

.env
DB_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

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

laravelでoracleと接続する方法
【Laravel】Laradockで複数プロジェクトを動かす手順

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

laravel + vue.jsでSPAの開発をしていてデバックがしたい!!!

結論
SPAで変数とかの中身を見るには
laravel debugarとclock workの両方をcomposerでインストールする必要あり。

STEP1 laravel debugarのインストール

これはいつも通りですね
こちらの↓READ MEを参考に開発環境にインストール
https://github.com/barryvdh/laravel-debugbar

composer require barryvdh/laravel-debugbar --dev

STEP2 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ヘルパメソッドが使えるようになります!
使い方は以下のように変数の中身を確認したり、メソッドを組み合わせて使うことができる!

image.png

これも詳しい使い方は公式ドキュメント↓参照
https://underground.works/clockwork/logging?#content

すると、、、
image.png

めちゃエラー出てるの恥ずかしいけどこんな感じで出ます!!!!!

デバックできずに困ってた人は参考にしてみてください~

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

laravel + vue.jsでSPAの開発をしていてデバッグがしたい!!!

SPAだとlaravelのエラー内容や変数とかの中身をブラウザに返すことができないけどデバッグがしたい!人用

結論
そのためには、、、
laravel debugbarとclock workの両方をcomposerでインストールする必要あり。

STEP1 laravel debugbarのインストール

これはいつも通りですね
こちらの↓READ MEを参考に開発環境にインストール
https://github.com/barryvdh/laravel-debugbar

composer require barryvdh/laravel-debugbar --dev

STEP2 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ヘルパメソッドが使えるようになります!
使い方は以下のように変数の中身を確認したり、メソッドを組み合わせて使うことができる!

image.png

これも詳しい使い方は公式ドキュメント↓参照
https://underground.works/clockwork/logging?#content

また.envに以下をの設定を追加するとlaravel dubagbarが画面に表示されなくなります!

DEBUGBAR_ENABLED=false

すると、、、
image.png

めちゃエラー出てるの恥ずかしいけどこんな感じで出ます!!!!!

デバッグできずに困ってた人は参考にしてみてください~

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

『 Laravel DB.com 』コード書かずにCRUD作成【最新版 2020/02/01 〜利用方法まとめ〜】

「 Laravel DB.com 」 、利用方法まとめ

2020年2月1日リニューアルしたので、使用方法をまとめたいと思います。
en.jpg

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)

【 CRUD: 新しいプロジェクトを用意 】

最低限Laravelがインストールした状態を準備します!
1. この時点ではまだLOGINは作らないように!
2. .env と MySQLのDB準備はしておきましょう。
image.png

【 LaravelDB.comへ ログイン 】

Googleアカウントのみログイン可能です。(画面右上 OR ページ下部の中央)
LaravelDB.com
en.jpg

【 LaravelDB.comの POINT!! 】

ER図を作成していきますが重要事項があります!

<<重要>>
主キーのAutoIncrementは どのテーブルも『 id 』 と固定すること!!

image.png

LaravelのEloquentModel を便利に使うためには、どのテーブルも 『 id 』 と固定するのが吉です。
理由は「 モデル名::find($id); 」と便利に使う場合は 主キーのAutoIncrement 『 id 』 名にしておかないと動作しないからです( Laravelを勉強してる人はなんとなく知ってることでしょうか )。
※ laravelDB.comのCRUDでも生成されるファイルは「 ○○○::find($id); 」を使ってるためそ!
※ laravelに最初から入っているテーブル「user」「password_resets」は作成しないこと(migrate時に上書きしてしまいます)
※慣れれば簡単ですが、少しだけ慣れる時間は必要ですね。


【 CRUDを生成してみる! 】

  1. まずは、ER図を簡単に作ってみましょう!! その後に右メニュー「 ER図のLoad/Save 」をクリックすると以下画面が表示されます。
  2. 外部キーを沢山つけてる場合は、Migrationの順番が重要なので試す場合は「OFF」がオススメです。
  3. 「 Laravel( CRUD BUILD) 」 ボタンをクリックでファイルが生成されます!!

【 ダウンロードファイルを確認しましょう 】

  1. Zip圧縮ファイルがダウンロードされるので、Zipを展開(解凍)して中を見てみましょう!。CRUDに必要なファイルが一式入っています。
  2. 下図:左側(CRUDファイル)、右図laravelのフォルダ

【移行方法】
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画面へ遷移できますよ!!

welcome.blade.php

作成されたindex/show/editの画面
基本的な処理が最初から出来てます!捗りそうですね!
image.png

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


以上

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

最新版『 Laravel DB.com 』コード書かずにCRUD作成~利用方法まとめ~

「 Laravel DB.com 」 、利用方法まとめ

2020年2月2日リニューアルしたので、使用方法をまとめたいと思います。
en.jpg

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)

【 CRUD: 新しいプロジェクトを用意 】

最低限Laravelがインストールした状態を準備します!
1. この時点ではまだLOGINは作らないように!
2. .env と MySQLのDB準備はしておきましょう。
image.png

【 LaravelDB.comへ ログイン 】

Googleアカウントのみログイン可能です。(画面右上 OR ページ下部の中央)
LaravelDB.com
en.jpg

【 LaravelDB.comの POINT!! 】

ER図を作成していきますが重要事項があります!

<<重要>>
主キーのAutoIncrementは どのテーブルも『 id 』 と固定すること!!

image.png

LaravelのEloquentModel を便利に使うためには、どのテーブルも 『 id 』 と固定するのが吉です。
理由は「 モデル名::find($id); 」と便利に使う場合は 主キーのAutoIncrement 『 id 』 名にしておかないと動作しないからです( Laravelを勉強してる人はなんとなく知ってることでしょうか )。
※ laravelDB.comのCRUDでも生成されるファイルは「 ○○○::find($id); 」を使ってるためそ!
※ laravelに最初から入っているテーブル「user」「password_resets」は作成しないこと(migrate時に上書きしてしまいます)
※慣れれば簡単ですが、少しだけ慣れる時間は必要ですね。


【 CRUDを生成してみる! 】

  1. まずは、ER図を簡単に作ってみましょう!! その後に右メニュー「 ER図のLoad/Save 」をクリックすると以下画面が表示されます。
  2. 外部キーを沢山つけてる場合は、Migrationの順番が重要なので試す場合は「OFF」がオススメです。
  3. 「 Laravel( CRUD BUILD) 」 ボタンをクリックでファイルが生成されます!!

【 ダウンロードファイルを確認しましょう 】

  1. Zip圧縮ファイルがダウンロードされるので、Zipを展開(解凍)して中を見てみましょう!。CRUDに必要なファイルが一式入っています。
  2. 下図:左側(CRUDファイル)、右図laravelのフォルダ

【移行方法】
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画面へ遷移できますよ!!

welcome.blade.php

作成されたindex/show/editの画面
基本的な処理が最初から出来てます!捗りそうですね!
image.png

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


以上

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

最新版『 Laravel DB.com 』コード書かずに超スピード開発~(DEMO動画あり)~

Laravel DB.com ってなに?

「LaravelDB.com」は、ER図(テーブルの設計)を作成することで、”Migration” OR ”CRUD(テストデータ)”のコードまで生成できるサービス(ツール)です。

DEMO動画(操作方法)

en.jpg


「 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準備はしておきましょう。
image.png

【 LaravelDB.comへ ログイン 】

Googleアカウントのみログイン可能です。(画面右上 OR ページ下部の中央)
LaravelDB.com
en.jpg

【 LaravelDB.comの POINT!! 】

ER図を作成していきますが重要事項があります!

<<重要>>
主キーのAutoIncrementは どのテーブルも『 id 』 と固定すること!!

image.png

LaravelのEloquentModel を便利に使うためには、どのテーブルも 『 id 』 と固定するのが吉です。
理由は「 モデル名::find($id); 」と便利に使う場合は 主キーのAutoIncrement 『 id 』 名にしておかないと動作しないからです( Laravelを勉強してる人はなんとなく知ってることでしょうか )。
※ laravelDB.comのCRUDでも生成されるファイルは「 ○○○::find($id); 」を使ってるためそ!
※ laravelに最初から入っているテーブル「user」「password_resets」は作成しないこと(migrate時に上書きしてしまいます)
※慣れれば簡単ですが、少しだけ慣れる時間は必要ですね。


【 CRUDを生成してみる! 】

  1. まずは、ER図を簡単に作ってみましょう!! その後に右メニュー「 ER図のLoad/Save 」をクリックすると以下画面が表示されます。
  2. 外部キーを沢山つけてる場合は、Migrationの順番が重要なので試す場合は「OFF」がオススメです。
  3. 「 Laravel( CRUD BUILD) 」 ボタンをクリックでファイルが生成されます!!

【 ダウンロードファイルを確認しましょう 】

  1. Zip圧縮ファイルがダウンロードされるので、Zipを展開(解凍)して中を見てみましょう!。CRUDに必要なファイルが一式入っています。
  2. 下図:左側(CRUDファイル)、右図laravelのフォルダ

【移行方法】
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画面へ遷移できますよ!!

welcome.blade.php

作成されたindex/show/editの画面
基本的な処理が最初から出来てます!捗りそうですね!
image.png

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


以上

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

コード書かずに超スピード開発~(DEMO動画あり)~最新版『 Laravel DB.com 』

Laravel DB.com ってなに?

コード書かずに超スピード開発って書いてますが、基本ベースのCRUD生成までがコード書かないってことですね。そこからの細かい仕様に合わせる部分は自身で修正する必要はあリます。ベースを作るまでは自動って感じでしょうか。

「LaravelDB.com」は、ER図(テーブルの設計)を作成することで、”Migration” OR ”CRUD(テストデータ)”のコードまで生成できるサービス(ツール)です。

DEMO動画(操作方法)

en.jpg

注意)以前に見たことがある人は「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準備はしておきましょう。
image.png

【 LaravelDB.comへ ログイン 】

Googleアカウントのみログイン可能です。(画面右上 OR ページ下部の中央)
LaravelDB.com
en.jpg

【 LaravelDB.comの POINT!! 】

ER図を作成していきますが重要事項があります!

<<重要>>
主キーのAutoIncrementは どのテーブルも『 id 』 と固定すること!!

image.png

LaravelのEloquentModel を便利に使うためには、どのテーブルも 『 id 』 と固定するのが吉です。
理由は「 モデル名::find($id); 」と便利に使う場合は 主キーのAutoIncrement 『 id 』 名にしておかないと動作しないからです( Laravelを勉強してる人はなんとなく知ってることでしょうか )。
※ laravelDB.comのCRUDでも生成されるファイルは「 ○○○::find($id); 」を使ってるためそ!
※ laravelに最初から入っているテーブル「user」「password_resets」は作成しないこと(migrate時に上書きしてしまいます)
※慣れれば簡単ですが、少しだけ慣れる時間は必要ですね。


【 CRUDを生成してみる! 】

  1. まずは、ER図を簡単に作ってみましょう!! その後に右メニュー「 ER図のLoad/Save 」をクリックすると以下画面が表示されます。
  2. 外部キーを沢山つけてる場合は、Migrationの順番が重要なので試す場合は「OFF」がオススメです。
  3. 「 Laravel( CRUD BUILD) 」 ボタンをクリックでファイルが生成されます!!

【 ダウンロードファイルを確認しましょう 】

  1. Zip圧縮ファイルがダウンロードされるので、Zipを展開(解凍)して中を見てみましょう!。CRUDに必要なファイルが一式入っています。
  2. 下図:左側(CRUDファイル)、右図laravelのフォルダ

【移行方法】
Macの場合: 「Optionキー + ドラッグ&ドロップ」 → 「結合」でフォルダの上書きではなく、結合になります。
Winの場合:「ドラッグ&ドロップ」で結合できます。

ダウンロードした各フォルダを上記の方法で移動しましょう!

【 LaravelのHOMEページを表示しましょう 】

私の環境では「http://localhost/

【 「 コマンド 」 を3つ順番に打ちましょう! 】

<<重要>>
 DBや.env設定など最低限の設定を完了しておくこと!!

php artisan migrate

composer update

//テストデータいらない場合は打たなくてもOK
php artisan db:seed

3つ目のコマンドはテストデータ登録の実行コマンド

Faker注意点有り!

テストデータ登録「 database/seeds/以下 」Fakerファイルの中はこんな感じです。
TUsersTableSeeder_php_—_test3.jpg
自動で文字が入ってるので、精度はまあこんなもんですかね。※文字列、数値、日付は自動で判定して入れてくれてるようです。
Fakerで使えるタイプ一覧、 自身で変更して使いたい場合はこちらを参考にすることをオススメします。

「address」や「url」を使う場合はカラムサイズに気をつけましょう!

【 完成: ブラウザで確認しましょう! 】

http://localhost/ (URLは開発環境によって異なります) で確認しましょう!
あなたが作ったテーブルのCRUD画面へ遷移できますよ!!

welcome.blade.php

作成されたindex/show/editの画面
基本的な処理が最初から出来てます!捗りそうですね!
image.png

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(_ _)m

Twitter: 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

以上

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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で行いました。
結局やることとしては、暗号化鍵を取得して、それを使って暗号化/復号化をするだけですね。
前の記事でも記載しましたが、今後セキュアでかつ可逆暗号化が必要となるケースがあると思います。
そんなときにぜひこの記事が参考になればと思います。
それでは、みなさまも引き続き安全で快適な暗号化ライフを。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ルートモデルバインディングってどうやって解決しているのか気になったのでコードの奥地にでかけてみた

ルートモデルバインディングまでの道のり

laravelでweb.phpが読み込まれるまでの道のりを読んでおくとさらにわかりやすいかもしれません。

Let's read

ルートが決定されるまで

Illuminate/Routing/Router::dispatchToRoute()から始めていきます。

Illuminate/Routing/Router
public function dispatchToRoute(Request $request)
{
    return $this->runRoute($request, $this->findRoute($request));
}

runRoute()の引数に渡されるfindRoute()を見ていきます。

Illuminate/Routing/Router
protected function findRoute($request)
{
    $this->current = $route = $this->routes->match($request);

    $this->container->instance(Route::class, $route);

    return $route;
}

$this->routesroutes/web.phpなどのルートファイルを読み込んで、その1つ1つのルートをIlluminate/Routing/Routeとして保持しているIlluminate/Routing/RouteCollectionです。

Illuminate/Routing/RouteCollection::match()にリクエストを渡して呼び出しています。

Illuminate/Routing/RouteCollection
public 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.php
protected 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は後退という意味でマッチングの優先順位を下げています。(ルーティングは上に書かれたものから優先される)

partition

コレクションクラスのmerge()$routes$fallbacksを結合します。これで$routesに入っていた値が優先されます。

merge

コレクションクラスのfirst()の引数に渡したコールバックでルートを探していきます。

first

Illuminate/Routing/Route.php
public 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.php
public 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/CompiledRouteregexプロパティにこの正規表現をセットしています。

最終的に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.php
public 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.php
public 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/CompiledRouteregexプロパティにセットした正規表現を使って値を取得していきます。
「{}」で囲ったところに値するものを取得します。

$this->matchToKeys()で整形をします。

例
web.phpで定義したルート: '/{hoge}/{fuga}'
実際のパス: '/1/4'
整形された配列: ['hoge' => '1', 'fuga' => '4']

整形された配列をIlluminate/Routing/Routeparametersプロパティに格納します。

Illuminate/Routing/Router::findRoute()に戻ります。

Illuminate/Routing/Router.php
protected function findRoute($request)
{
    $this->current = $route = $this->routes->match($request);

    $this->container->instance(Route::class, $route);

    return $route;
}

マッチしたrouteを変数に格納し、コンテナにバインドしています。

runRoute()に戻ります。

Illuminate/Routing/Router.php
public 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.php
protected 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.php
public 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/Routeparametersプロパティに該当のものがあれば解決してくれます。

$parameters = ['hoge' => '2']ならApp\User::classのprimaryKeyが'2'のものをとってきてくれる。

暗黙的な結合

substituteImplicitBindings()は明示的な結合のように書かなくても、ある一定の法則で書けば勝手に解決してくれますよ〜のやつです。

Illuminate/Routing/Router.php
public function substituteImplicitBindings($route)
{
    ImplicitRouteBinding::resolveForRoute($this->container, $route);
}
Illuminate/Routing/ImplicitRouteBinding.php
public 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.php
public 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.php
public 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.php
protected 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クラスをつかってメソッドの引数に指定されている名前を取得して配列で返します。

ReflectionMethodの参考

先程取得した配列をフィルターにかけていきます。

$p->getClass()でタイプヒンティングされているかを検証し、$p->getClass()->isSubclassOfIlluminate/Contracts/Routing/UrlRoutableを実装しているかを検証しています。

Illuminate/Contracts/Routing/UrlRoutableIlluminate/Database/Eloquent/Modelが実装しているのでEloquentは対象になります。

コントローラーのparameter解析が終わったのでforeachに戻ります。

Illuminate/Routing/ImplicitRouteBinding.php
public 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.php
public 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.php
public 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のところはわかりませんでしたが大体の流れはつかめたと思います。やったね!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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');
    }

}


  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む