作りたいものがありすぎる

40歳を過ぎてプログラミングを始めた人の顛末とこれからなど

Laravel本番環境が local 設定で動いていて焦った話

本番環境での composer コマンドに注意

10分程本番環境を止めてしまった。とはいえ普段はSlackの通知ばかりでサイト本体は人がほとんど見てないのでたぶんセーフだ。(本当か?)

まず結論

本番環境の composer install では 以下のオプションを付けないと、 dev環境の dusk 等も本番にインストールされてしまうケースがあるかもしれない。最悪動作しなくなるので注意。(envの設定によっては不要かもしれないが未検証)

Exception: It is unsafe to run Dusk in production. · Issue #289 · laravel/dusk · GitHub

composer install --no-dev

そもそも論

これまで本番環境の .env の APP_ENV の設定値が localのままだった、これのせいで composer installでlocal環境での phpunitや諸々が本番環境にも入ってしまっていたようだ。

従ってまず .envの 値を以下の様に書き換えた

本番before

APP_ENV=local

本番after

APP_ENV=production

ここが最初から productionに設定してあれば、そもそも今回の問題は起こっていないかもしれない。

.env を書き換えた後にconfig:clearの際に出たerror

$ php artisan config:clear

In DuskServiceProvider.php line 43:
                                           
  It is unsafe to run Dusk in production.  
                                           

これ、たしかローカル環境でも散々悩んだ DuskがLaravel5.6 ではにっちもさっちも行かなくなった際のerrorで、vendor の dusk内のメソッドで返り値の型宣言しないとerrorになる関係の奴だったと思う。

で、この後composer updateをやったら以下の様な error が出た

#色々あって最後の方
  - Installing composer/semver (1.5.1): Downloading (100%)         
  - Installing composer/ca-bundle (1.2.6): Downloading (100%)         
  - Installing composer/composer (1.10.1): Downloading (100%)         
  - Installing barryvdh/reflection-docblock (v2.0.6): Downloading (100%)         
  - Installing barryvdh/laravel-ide-helper (v2.6.7): Downloading (100%)         
symfony/event-dispatcher-contracts suggests installing psr/event-dispatcher
symfony/service-contracts suggests installing symfony/service-implementation
barryvdh/reflection-docblock suggests installing dflydev/markdown (~1.0)
Package facebook/webdriver is abandoned, you should avoid using it. Use php-webdriver/webdriver instead.
Writing lock file
Generating optimized autoload files
Carbon 1 is deprecated, see how to migrate to Carbon 2.
https://carbon.nesbot.com/docs/#api-carbon-2
    You can run './vendor/bin/upgrade-carbon' to get help in updating carbon and other frameworks and libraries that depend on it.
> Illuminate\Foundation\ComposerScripts::postAutoloadDump
> @php artisan package:discover

In DuskServiceProvider.php line 43:
                                           
  It is unsafe to run Dusk in production.  
                                           

Script @php artisan package:discover handling the post-autoload-dump event returned with error code 1

dump-autoload でも同じこと言われる これでも本番は動かない(5分位経って焦っている)

$ composer dump-autoload
Generating optimized autoload files
Carbon 1 is deprecated, see how to migrate to Carbon 2.
https://carbon.nesbot.com/docs/#api-carbon-2
    You can run './vendor/bin/upgrade-carbon' to get help in updating carbon and other frameworks and libraries that depend on it.
> Illuminate\Foundation\ComposerScripts::postAutoloadDump
> @php artisan package:discover

In DuskServiceProvider.php line 43:
                                           
  It is unsafe to run Dusk in production.  
                                           

Script @php artisan package:discover handling the post-autoload-dump event returned with error code 1

そこでerror文言でググり冒頭にも張った以下のページに辿り着く

Exception: It is unsafe to run Dusk in production. · Issue #289 · laravel/dusk · GitHub

記載されたコマンド打って本番環境に不要な Dusk含む余計なものをアンインストールできた。

$ composer install --no-dev
Loading composer repositories with package information
Installing dependencies from lock file
Package operations: 0 installs, 0 updates, 34 removals

# 色々削除される(割愛)
  - Removing webmozart/assert (1.7.0)
  - Removing phpunit/phpunit (7.5.20)
  - Removing laravel/dusk (v4.0.5)

Generating optimized autoload files
Carbon 1 is deprecated, see how to migrate to Carbon 2.
https://carbon.nesbot.com/docs/#api-carbon-2
    You can run './vendor/bin/upgrade-carbon' to get help in updating carbon and other frameworks and libraries that depend on it.
> Illuminate\Foundation\ComposerScripts::postAutoloadDump
> @php artisan package:discover
Discovered Package: barryvdh/laravel-cors
Discovered Package: barryvdh/laravel-debugbar
Discovered Package: barryvdh/laravel-ide-helper
Discovered Package: fideloper/proxy
Discovered Package: laravel/tinker
Package manifest generated successfully.

むしろ今までlocal環境の設定のままで良く動いていたな、と思う。 万一間違ってtestコマンド打っていたらDBが死んでいた。 ちなみに上に出てる Carbon の新しい奴がどうした、とかいう奴は、前に古いバージョンじゃないと駄目な依存パッケージがあったかもで、そのままにしている。

Laravel Eloquentで取得したデータの状態を意識してなかった件

今までDBファサードでデータの取得をしてましたが、最近はすっかりEloquentでデータを取ったり入れたりするようになりました。が、チェーンメソッドをごりごり書く際に上手くデータが取れない時があって、なんで?ってなる事がしばしばでしたが、データの状態をちゃんと意識しないで感覚と経験則と実装時のTry&Error でなんとかしちゃってました。 でも、この辺をちゃんと意識すべきだなと。

経験のある人から見ると「今それ?」みたいな話かもしれませんが、つまりこれです。

まずお馴染みのModelがあったとします。

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    protected $table = 'users';

    // リレーションとか scope とかのメソッドが続く

}

それを use App\User; して使って他のクラス内で使える様にする訳ですが、 ごく単純に使うならまずこうですよね。

$user = User::find(1);

DBなら、user tableの主キーとなる id=1 のレコード1件を抽出します。 あとこんな風にも使います。

$user = User::find(1)->get();

とにかく欲しい。これでデータ取得、みたいな時ですね。

また、ちょっと違った条件で呼び出したい際はこんな風に呼んだりもします。

User::where('community_id', 1);

DBで考えると users table の community_id カラムの値が 1の奴だけ、 つまりコミュニティ1に属するユーザーだけを複数あれば取得、みたいな感じですね。

当然こいつもgetして使う訳です。

User::where('community_id', 1)->get();

で、個人的によく使うのがこのパターンで、複雑な階層下のtableなんかを呼ぶ際は、外部キーをベースに値を呼び出して使う。なんていうのがある訳です。

User::where('community_id', 1)
  ->LeftJoin('comments', 'comments.user_id', '=', 'users.id')
  ->where('comments.active', 1)
  ->get();

こんな風にアクティブな該当ユーザーのコメントのみを出す、みたいなニュアンスでクエリビルダゴリゴリ書いて使う感じですね。(リレーションメソッド使えって話もありますが)

で、こんな感じでいろんな形式でデータを取得するわけですが、なんか上手くいかないなー。って場合があって、その原因がわかりました。

$user = User::find(1);
dd(get_class($user));

ってクラス名を出力したら、最初に紹介した奴、それぞれ違うんですね。

$user = User::find(1);
dd(get_class($user));

これは App\User class と出力されます。

つまり、これはまだ只のModelクラスのインスタンスにすぎません。

次にこれ、一件やっている事は上と同じですが、インスタンスされるクラスが異なるんですね。

$user = User::where('id', 1);
dd(get_class($user));

Illuminate\Database\Eloquent\Builder class が出力されます

dd($user)って出力すると量が多くてヤバイ奴です。

Laravelやってる人は経験あると思います。dd() の際うっかりすげえ量のオブジェクトが出力されてマシンがうなってなかなか画面表示されない。って奴ですね。

しかし、これらに ->get() を付与するとさらにクラスが変わります。

$usr = User::find(1)->get();
dd(get_class($user));

Illuminate\Database\Eloquent\Collection class と出力

つまりコレクションクラス(配列っぽく便利に使えるアレ)のインスタンスとして出力されます。

$usr = User::where('id', 1)->get();
dd(get_class($user));

これも同様、 Illuminate\Database\Eloquent\Collection class と出力

コレクションクラスのインスタンスとして出力されます。

つまり ->get() を付けたら実体化してコレクションとして加工や出力に便利に使える状態に変換される訳ですね。

ところがややこしいのはこれ、 ->first() です

ひとまずこの条件で絞り込んで、最初の1件だけを取得する。なんていうときに使います。私は曖昧なデータの取れ方をするのが嫌であまり使わないですが。

$user = User::where('created_at', '>', Carbon::toDay())->first();
dd(get_class($user));

これ App\User class と出力されます。

コレクションでもなくまだピュアなモデルなのかよ!って感じです。

でもよく考えたら、 find(1) と同じ扱いですね。

でもこれらはとても、混乱しがちで間違いがちなんですよね。

first() もごりごりクエリビルダ書いた最後に付けることが良く多くて、取れるデータとして確定するニュアンスなので。 ->get() と同じ認識でいましたが、インスタンスが異なる為、本質も異なるんですね。

なので、余談ですが first() の後には ->get() が付けられて、コレクションインスタンスに変換が出来ます。使い道が今一つわかりませんが。

$user = User::where('created_at', '>', Carbon::toDay())->first()->get();
dd(get_class($user));

Illuminate\Database\Eloquent\Collection と出力

あとこれ、当たり前ですが最後に ->toArray() を付けるといずれも完全な配列となります。

User::find(1)->toArray();
User::find(1)->get()->toArray();

普通の配列データになる

私はこれで必要なIDの配列を取得して、別のModelを呼ぶ際のwhereInの条件にして使うことが多いです。TOP階層から5階層配下(鬼のようなDB構造...)のtableのリレーションでの値を出す必要があったりして、そんな場合に使います。

また、実際のアプリケーションで複雑な検索条件に対応する際の方法として、一度、単純なクエリ find() や where() でデータを呼び出して、get() を繋げないて$queryみたいな変数に入れ、それをさらに様々な処理を経由して条件追加をチェーンメソッドで繋げて、最後に ->get() みたいな使い方をします。

以下のような感じ。

// よく使う例

public function getUser(Request $request)

    $query = User::find($request->id);

    if (!is_null($request->date)) {
      $query->where('date', $request->date);
    }

    if (!is_null($request->name)) {
      $query->where('name', 'like', "%{$request->name}%");
    }

    return $query;
}

で、この関数の呼び出し元で

public function index(Request $request)

    // 上の関数を呼ぶ
    $query = this->getUser($request);

    // この辺に $query->を使ってさらに色々処理を繋げたり加工したり

    // で、最後にこれをしたりしなかったりして
    $user = query->get();

    // 出力
    return view('index', [
        'user' => $user
    ]);
}

みたいな感じ。

ですが、その際にこの辺のデータも持ち方の違いをきちんと意識してないと、なにがなんだかわかんなくなる。という事がありますので、現在のデータの状態を意識しつつ、クエリビルダを繋げる必要があるな。と感じた次第。

でも、この辺、有志のリファレンスサイトEloquent:利用の開始 5.8 Laravel見るとちゃんと書いてあるんですよね。

データの取得の方法論ばかり意識していて、これまではどのクラスのインスタンスの状態であるか?というのを意識してなかった。そういえばdd()で出す際になんか違うな―。位の認識しかしてないという…。

余談ですが、ここに意識が向かったきっかけですが、最近チームで実装をするようになり、後から私のコードを読む人の為に、わかりやすいコードを意識するようになり、メソッドにきちんとアノテーションを書くことも始めました。(最近かよ!)

とはいえ、けっこう俺流で書いてしまっていたのですが、これを書くことで起こった良い変化として、引数にはどんな型のデータが必要で、返り値のデータの型は何なのか?というのを意識的に統一して書くようになりました。

その際の返り値に、じゃあクラスのインスタンスとしては何を書くべきなの?って時に、あれ?ってなってこの記事に至ります。

しかし、返り値については、これまでは単純な配列や数値等、あと、Modelで書いたら、全部内容同じでしょ、位にしか認識してなかったので、今後は返り値が何のClassのインスタンスなのかをきちんと意識して実装できるようにします。

40歳を過ぎてプログラミングを始めたが50歳を前にして業務委託プログラマーとして半年が経過した

近況です。

タイトルの通りで、なんとかWeb系のプログラマーとして仕事を続けることが出来てます。

一昨年末に

sakamata.hateblo.jp

半年前に

sakamata.hateblo.jp

を書いて、その後どうなったみたいな概要を書きます。

俺この案件で死亡フラグたったかも。という位、幸運が舞い込んだ

5月から7月にかけて週3日の業務委託のプログラマーとしてなんとかお仕事をさせていただいたのですが、色々あって2か月程の契約で満了となった後に、次のお仕事を探していただいて、見つかったのがなんとびっくり、電車不要、徒歩20分での勤務先という嘘みたいに近所の案件のお話を頂き飛びつきました。

しかも、業務の内容も自分がいま伸ばしたい方向性にマッチした新規開発案件、季節の移ろいを感じながら毎朝公園内を徒歩通勤していると「俺、この案件が終わったら結婚するんだ」みたいな気分になってきます。

という事でとにかく7月からはそれなりに頑張りました。

今のテーマは設計、とにかく設計ですね。クラス階層毎のメソッドの役割の分割を言葉に落とし込むレベルできちんと分ける事、後時間が無くて書けないテストをきちんと書く事、エラーハンドルをしっかり行う事。

いつももっとうまくやりたいけど、時間が無くて次に行かないと、という焦りを持ちながら開発しているので、実装の速度を上げていきたいです。とはいえ、要件や仕様が複雑な部分ではどうしても最初に書く内容が、俗人化してファットなメソッドを書いてしまうのがなんとももどかしいです。リファクタリングをせず、最初から奇麗に引数を渡して複雑な処理を細分化できるレベルに到達するにははまだまだですね。

自分には少々荷が重い部分もありましたが今の所何とかなってます。でももっと経験を積んだ人のコードや言葉が欲しい毎日です。

自作アプリも地味に機能追加と改善を続ける

その一方で、滞在者確認アプリの方も時間を見つけてはコツコツ開発を続けて、いろんなものが追加されました。

例えばこんな奴、音声を聞いてやってください。

他にも訪問者の来訪記録を過去に遡って閲覧できるlog機能を追加しました。

f:id:sakamata:20200105155109p:plain
LOG機能の画面

これにより、コミュニティのオーナーさんが来訪者の傾向を調べたり、初訪問者の記録を取る事で、端末とユーザーの紐づけのヒントにして、楽な登録を行える事を狙っています。

また、既に作成していた訪問予告機能が近々アップデートされます。従来の24時間以内の訪問宣言だけでなく、来月までの未来の一定期間「きょう、あした、明後日、今週、土日、来週、今月、来月」をユルく宣言出来るようになります。

f:id:sakamata:20200105155952p:plain
予定機能のアップデート画面

今月行くかも。とか誰かが宣言したとします。それを見た誰かが「来週行くかも」と宣言することで、予告の日程を徐々に狭めて、じゃあ来週の金曜日夕方あたり、で、なんとなく人があつまるかなー、みたいなものを狙ってます。

他にも今のアプリで追加したい要件や直したい箇所はいくらでもあるので、地味に改善は続けて行くつもりです。ゆくゆくはスマホのネイティブアプリにしたいですね。

滞在者確認アプリを使ってもらう場所が増えました

秋葉原に出来たプログラマーの為の学習スペースプログラマー自習室|ギークライブラリー秋葉原に滞在者確認アプリ、Livelynkを導入していただきました。

f:id:sakamata:20191109164058j:plain

家ではなかなか集中して勉強できないけど、どこか静かな場所でまずはしっかり勉強したい!そして交流もしたい。という人の為かな?とにかく新しい試みに次々チャレンジしているオーナーさんで、プログラマーにとって、とても面白くて魅力的な空間を提供しています。

f:id:sakamata:20191109164200j:plain f:id:sakamata:20191109164031j:plain

興味のある方は是非一度訪問してみることをおすすめします。 駅から近いが、迷路の様な場所にあり、絶対迷う事請け合いですが、それを込みで楽しんでいただけると良いかと思います。

ひとまず今はそんなところです。

WordPress『更新に失敗しました。 エラーメッセージ: 返答が正しい JSON レスポンスではありません。』でハマりClassic Editorで解消した

WordPress 5.3.2 にアップデートした後、記事の編集を使用とするとタイトルの様なエラーが画面上部に表示され、更新が出来ない状態となる、結論からいうとプラグインで Classic Editor を入れて、この古い方のエディタを有効にしたら無事動いた。 Classic Editor – WordPress プラグイン | WordPress.org 日本語

原因究明までの道のり

エラー文『エラーメッセージ: 返答が正しい JSON レスポンスではありません。』でググった所 php.ini や Apache nginx の設定の関連で画像の最大サイズ超過でも同じエラーが出るらしいが、画像の無い記事を更新したり、新規作成でタイトルのみを作成しても同様の現象が出る為、どうも原因が異なる事が推察される。

またブラウザ依存の問題を疑い、chromeの該当URLのキャッシュを削除したが駄目だった。また、ブラウザをedgeでやっても同じ現象が確認されたので、ブラウザ問題でない事が推察される。

ところが、固定ページ一覧から「クイック編集」でタイトルなどの変更は受け付ける。ということで、DBやブラウザ等のシステム側に依存した現象でない事が確認できた。

検索を続け以下の記事に行き当たる 「更新に失敗しました」と表示され、更新できません。 | WordPress.org 日本語

こちらも同様の症状に悩んでおりましたが、アプデ内容にエディタの変更が含まれていたこと、いつもエディタから保存すると再現し、逆にタイトル変更やトップページのヘッダなどは保存できることからもしかしたらと思い、プラグインでクラシックエディタに戻したら無事更新できるようになりました。 どうやら新しいエディタに原因があるみたいですね。 こちらで上記の方法で何とか解決出来たので報告とご参考までに。

まさにこれだった。 という事で上記のプラグインWordPressの管理画面の「プラグイン」>「新規追加」でインストールして有効化、懐かしい画面で記事の編集を行った所、無事記事が更新できた。

トラブル満載だな、WordPress

【輪読会資料】PHPフレームワーク Laravel Webアプリケーション開発 8章 コンソールアプリケーション 前半資料

以下の記事は2019年9月12日、コワーキングスペース秋葉原Weeybleにて行われる [秋葉原] Laravel Webアプリケーション開発 輪読&勉強会 コンソールアプリケーションの輪読会資料の一部となります。
今回は 4章前半 8-1 『Commandの基礎』, 8-2『Commandの実装』部分の記事をアップします。

また、元になっている書籍は以下となります。

PHPフレームワーク Laravel Webアプリケーション開発 バージョン5.5 LTS対応

PHPフレームワーク Laravel Webアプリケーション開発 バージョン5.5 LTS対応

また書籍のコードが記載されているGitHubレポジトリは以下となります。とても助かります!
GitHub - laravel-socym/chapter08

  • 8-1 Commandの基礎

    • 8-1-1 クロージャによるCommandの基礎
    • 8-1-2 クラスによるCommandの作成
    • 8-1-3 Commandへの入力
    • 8-1-4 Commandからの入力
    • 8-1-5 Commandの実行
  • 8-2 Commandの実装

    • 8-2-1 サンプル実装の仕様
    • 8-1-2 Commandの生成
    • 8-1-3 ユースケースクラスとサービスクラスの分離
    • 8-2-4 ユースケースクラスのひな形を作成する
    • 8-2-5 サービスクラスの実装
    • 8-2-6 ユースケースクラスの実装
    • 8-2-7 Commandクラスの仕上げ


8-1 Commandの基礎

LaravelにはCommandというコンポーネントがあり、これを使えばコンソールアプリケーションに必要な機能を簡単に実装できる
Commandの作り方には2種類ある

  • クロージャ
    • 簡単な処理を書くのに向いている
  • クラス
    • 複雑な処理にも対応可能


8-1-1 クロージャによるCommandの基礎

こんな風にオリジナルのコマンドを作る事ができる ここでは単純な文字列を表示するCommandをクロージャで作る

実行してみる

php artisan hello:closure
Hello Closure command!

routes/console.phpクロージャで書いて定義する

<?php

Artisan::command('hello:closure', function () {
    $this->comment('Hallo Closure Command!');  // Command本体 文字列出力
})->describe('サンプルコマンド(クロージャ実装)'); //コマンドの説明

routes/console.php にコマンドを書くとlistに自動登録される、以下のコマンドで登録が確認できる

実行してみる

vagrant@homestead:~/study_command$ php artisan list
Laravel Framework 6.0.1

Usage:
  command [options] [arguments]

Options:
  -h, --help            Display this help message
  -q, --quiet           Do not output any message
  -V, --version         Display this application version
      --ansi            Force ANSI output
      --no-ansi         Disable ANSI output
  -n, --no-interaction  Do not ask any interactive question
      --env[=ENV]       The environment the command should run under
  -v|vv|vvv, --verbose  Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug

Available commands:
  clear-compiled       Remove the compiled class file
  down                 Put the application into maintenance mode
  env                  Display the current framework environment

  ### 省略 ###

 flare
  flare:test           Send a test notification to Flare
 hello
  hello:closure        サンプルコマンド(クロージャ実装)
 key
  key:generate         Set the application key

  ### 省略 ###


8-1-2 クラスによるCommandの作成

以下の様なコマンドでClassのひな形を作れる

php artisan make:command HelloCommand
Console command created successfully.

ちなみに、どうも同時にどこかに自動で登録をしているようで ファイル名を間違えて自分で手直しすると、 list に出てこなかったり、実行できなかったり登録する?みたいなコマンドが出たりしたが、正体不明であった)

作成されたファイル app/Console/Commands/HelloCommand.php の中身

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;

class HelloCommand extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'command:name';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Command description';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        //
    }
}

上記を以下の様に書き換えると、クロージャ同様のコマンドが作れる

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;

class HelloCommand extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    // コマンドを指定する
    protected $signature = 'hello:class';

    /**
     * The console command description.
     *
     * @var string
     */
    // コマンドの説明を指定
    protected $description = 'サンプルコマンド(クラスで実装)';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    // 実行されるコマンドの定義
    public function handle()
    {
        $this->comment('Hello class command!');
    }
}

実行コマンドと結果

実行してみる

php artisan hello:class 
Hello class command!

リストにも登録されているのが確認できる

vagrant@homestead:~/study_command$ php artisan list

### 省略 ###

flare
  flare:test           Send a test notification to Flare
 hello
  hello:class          サンプルコマンド(クラスで実装)
  hello:closure        サンプルコマンド(クロージャ実装)
 key
  key:generate         Set the application key

### 省略 ###


8-1-3 Commandへの入力

vagrant@homestead:~/study_command$ php artisan hello:option --param
Hello Option param  => 有
vagrant@homestead:~/study_command$ php artisan hello:option
Hello Option param  => 無

コマンドには引数を入れて処理をさせることが出来る
実際にこんなコマンドを叩いた経験があるかもしれないが、

php artisan make:migration [マイグレーションファイル名] --table=[テーブル名]

後ろのファイル名(必須)と テーブル名(オプション) が引数となっている


コマンド引数 (必須)

$signature に引数を { } で指定できる

<?php

protected $signature = 'hello:class {name}';

handleメソッド内で argument() を使って呼べる

<?php

    public function handle()
    {
        $param = $this->argument('name');
        $this->comment('Hello class command! Param => ' . ($param));
    }

コマンド実行結果

やってみる際にコードのコメントを変更

vagrant@homestead:~/study_command$ php artisan hello:class taro
Hello class command! Param => taro

その他引数の指定方法

コマンド引数 内容
{name} 引数を文字列として取得 省略するとエラー
{name?} 引数を文字列として取得 省略可能
{name=default} 引数を文字列として取得 デフォルト値指定
{name*} 引数を配列として取得 省略するとエラー
{name : description} : コロン以降に説明を記述可能 : 前後にスペース必須

オプション引数 (任意)

あっても無くても良いオプションの引数を指定できる
以下は引数を論理値として使える例で、引数の文字列は指定したものを決め打ちしないと駄目

<?php
protected $signature = 'hello:option {--param}';

option()メソッドで利用できる

<?php

    public function handle()
    {
        $param = $this->option('param');
        $this->comment('Hello Option param  => ' . ($param ? '有' : '無'));
    }

やってみる際にコードのコメントを変更

引数ありで実行 指定すると true となる

vagrant@homestead:~/study_command$ php artisan hello:option --param
Hello Option param  => 有

引数無しで実行 指定なしは false となる

vagrant@homestead:~/study_command$ php artisan hello:option
Hello Option param  => 無

オプション引数の指定方法には以下のようなものがある

コマンド引数 内容
{--switch} 引数を論地値として取得 省略するとエラー
{--switch=} 引数を文字列として取得 省略可能
{--switch=default} 引数を文字列として取得 デフォルト値指定
{--switch=*} 引数を配列として取得 省略するとエラー
{--switch : description} :コロン以降に説明を記述可能 : 前後にスペース必須


8-1-4 Commandからの入力

出力の際の文字色や表示レベルをカスタマイズできる

<?php

    public function handle()
    {
        // 文字色,背景色の設定
        $this->line('line');
        $this->info('info');
        $this->comment('comment');
        $this->question('question');
        $this->error('error');
        $this->warn('warn');
        $this->table(['h1', 'h2'], [[1, 2]]);

        // 出力レベルの設定
        $this->info('quiet', OutputInterface::VERBOSITY_QUIET);
        $this->info('normal', OutputInterface::VERBOSITY_NORMAL);
        $this->info('verbose', OutputInterface::VERBOSITY_VERBOSE);
        $this->info('very_verbose', OutputInterface::VERBOSITY_VERY_VERBOSE);
        $this->info('debug', OutputInterface::VERBOSITY_DEBUG);

    }

出力レベルはLaravelのLog機能同様に通常で出力、詳細まで出す、デバッグで出す、みたいなレベル付けで出力ができる

実行例

それぞれ出力されるものが異なってくる

php artisan output:test --quiet
php artisan output:test
php artisan output:test -v
php artisan output:test -vv
php artisan output:test -vvv


8-1-5 Commandの実行

コンソールのコマンドで実行させるだけでなく、プログラムを直接書いてCommandの実行も可能
ここでは例としてroutes/web.php に書いて Log::debug(); で出力してみる

<?php

Route::get('/no_args', function () {
    Artisan::call('output:test');
});

// php artisan hello:option --param=1 --param=2

Route::get('/with_args', function () {
    Artisan::call('hello:option', [
        'arg' => 'value',
        '--switch' => 'false',
    ]);
});
Artisan::call('コマンド', ['引数指定']);

Artisanというファサードで call メソッドを呼ぶ、
第一引数は呼びたいコマンドを指定
第二引数にコールバック関数を入れる 引数を指定可能、上の例では第二引数を配列にして2つの引数を送っている

ブラウザで設定したurlにアクセスすると

http://study_command.test/no_args

<?php

    public function handle()
    {
        $param = $this->option('param');
        $this->comment('Hello Option param  => ' . ($param ? '有' : '無'));

        log::debug(print_r('command HelloOptionParamCommand run!!',1));
    }

handle() に書いたlogが出力されているのが確認できる

laravel.log

[2019-09-08 11:43:58] local.DEBUG: command output:test run!!  

ちなみに handle() の中にcallメソッドで別のコマンドを呼ぶ処理を書けばCommandから別のCommandを実行することができる

<?php
    public function handle()
    {
        $this->call('hello:param');
    }


8-2 Commandの実装

ここでは実際のコマンドラインアプリケーションの実装方法の解説をExportOrdersCommandで説明する

8-2-1 サンプル実装の仕様

ここではコマンド実行でDBの値をTSV形式で出力できる機能を実装する

TSVとは何? Weblio辞書

フルスペル:Tab Separated Values 読み方:ティーエスブイ 別名:タブ区切り TSVとは、文字や文字列の間にタブ記号を挿入して区切りを設けること、あるいは、そのようにして各データを区切って管理するファイル形式のことである。

書籍にはDB定義のカラム仕様や出力されるデータの形式が詳細に記載されているがここでは割愛する。

<?php
        // ひとまずseederファイルの一部でDB構造はなんとなく把握してください。
        Schema::create('orders', function (Blueprint $table) {
            $table->increments('id');
            $table->string('order_code', 32);
            $table->dateTime('order_date');
            $table->string('customer_name', 100);
            $table->string('customer_email', 255);
            $table->string('destination_name', 100);
            $table->string('destination_zip', 10);
            $table->string('destination_prefecture', 10);
            $table->string('destination_address', 100);
            $table->string('destination_tel', 20);
            $table->integer('total_quantity');
            $table->integer('total_price');
            $table->timestamps();

            $table->unique('order_code');
            $table->index('order_date');
        });

        Schema::create('order_details', function (Blueprint $table) {
            $table->string('order_code', 32);
            $table->integer('detail_no');
            $table->string('item_name', 100);
            $table->integer('item_price');
            $table->integer('quantity');
            $table->integer('subtotal_price');

            $table->primary(['order_code', 'detail_no']);

            $table->index('order_code');
            /** @noinspection PhpUndefinedMethodInspection */
            $table->foreign('order_code')->references('order_code')->on('orders');
        });

登録されるコマンドは以下の様なものとなる

vagrant@homestead:~/study_command$ php artisan list
Laravel Framework 6.0.1
  ### 省略 ###

app
  app:export-orders    購入情報を出力する

  ### 省略 ###

出力される内容は以下の様なタブ区切りでカラム名とレコードを出力する

 php artisan app:export-orders
購入コード      購入日時        明細番号        商品名  商品価格        購入点数小計金額        合計点数        合計金額        購入者氏名      購入者メールアドレス    送付先氏名      送付先郵便番号  送付先都道府県  送付先住所      送付先電話番号
1111-1111-1111-1111     2019-09-08 00:00:00     1       商品1   1000    1      1000     大阪 太郎       osaka@example.com       送付先 太郎     1234567 大阪府 送付先住所1     06-0000-0000


8-2-2 Commandの生成

8-1章でのコマンドに習いファイルを作成し、ひな形として動くコマンドファイルを作成する

php arrisan make:command ExportOrdersCommand

app\Console\Commands\ExportOrdersCommand.php

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;

class ExportOrdersCommand extends Command
{
    // コメントは省略
    protected $signature = 'app:export-orders';

    protected $description = '購入情報を出力する';

    public function __construct()
    {
        parent::__construct();
    }

    public function handle()
    {
        $this->info('hello');
    }
}

実行してみてhelloを確認

vagrant@homestead:~/study_command$ php artisan app:export-orders
hello


8-2-3 ユースケースクラスとサービスクラスの分離

ここでは設計と実装について説明しているがこのUML図とコメントでおおよそを把握してください。

f:id:sakamata:20190911082822p:plain

処理毎にクラスを分離して役割を明確にして、定められた役割のみを担うように実装をする。
これにより処理の再利用性が高まる。 8-3 では実際に ExportOrdersServiceクラスを再利用する。

また、テストを容易にでくるメリットもあり、図の真ん中の ExportOrderUseCaseも単体でテスト可能となる。


8-2-4 ユースケースクラスのひな形を作成する

まずごく簡単な処理の流れがつかめる実装を行う Commandクラス(1階層目)に ExportOrdersUseCase を注入して run メソッドに引数を入れて走らせる処理を書く

<?php

namespace App\Console\Commands;

use App\UseCases\ExportOrdersUseCase;
use Carbon\Carbon;
use Illuminate\Console\Command;

class ExportOrdersCommand extends Command
{
    protected $signature = 'app:export-orders';

    /** @var TemplateExportOrdersUseCase */
    private $useCase;

    protected $description = '購入情報を出力する';

    public function __construct(ExportOrdersUseCase $useCase)
    {
        parent::__construct();
        $this->useCase = $useCase;
    }

    public function handle()
    {
        $tsv = $this->useCase->run(Carbon::today());
        echo $tsv;
    }
}

ExportOrdersUseCase クラス(2階層目)には runメソッドを書いて渡されたCarbonオブジェクトで Y-m-d形式の表示を行う仮実装を行う。

<?php

declare(strict_types=1);

namespace App\UseCases;

use App\Services\ExportOrdersService;
use Carbon\Carbon;

final class TemplateExportOrdersUseCase
{
    /**
     * @param Carbon $targetDate
     * @return string
     */
    public function run(Carbon $targetDate): string
    {
        return $targetDate->format('Y-m-d') . 'の購入情報';
    }
}

コマンド実行で 本日 + 文字列 が出力されるようになる。

php artisan app:export-orders
2019-09-09の購入情報


8-2-5 サービスクラスの実装

次に3階層目(Service)の処理を書く

<?php

declare(strict_types=1);

namespace App\Services;

use Carbon\Carbon;
use Generator;
use Illuminate\Database\Connection;

final class ExportOrdersService
{
    /** @var Connection */
    private $connection;

    public function __construct(Connection $connection)
    {
        $this->connection = $connection;
    }

    /**
     * 対象日の購入情報を取得
     *
     * @param Carbon $date
     * @return Generator
     */
    public function findOrders(Carbon $date): Generator
    {
        return $this->connection
            ->table('orders')
            ->join('order_details', 'orders.order_code', '=', 'order_details.order_code')
            ->select([
                'orders.order_code',
                'orders.order_date',
                'orders.total_price',
                'orders.total_quantity',
                'orders.customer_name',
                'orders.customer_email',
                'orders.destination_name',
                'orders.destination_zip',
                'orders.destination_prefecture',
                'orders.destination_address',
                'orders.destination_tel',
                'order_details.*',
            ])
            ->where('order_date', '>=', $date->toDateString())
            ->where('order_date', '<', $date->copy()->addDay()->toDateString())
            ->orderBy('order_date')
            ->orderBy('orders.id')
            ->orderBy('order_details.detail_no')
            ->cursor();  // ここがポイント! cursor は呼び出し時点でレコードの読み込みはしない

            // https://localdisk.hatenablog.com/entry/2016/07/20/173208
            // cursor メソッドを使っています。これは 5.2.33 から追加されました*1。
            // 内部でジェネレータを使っているのでメモリ不足で死ななくて最高です。
    }
}

クエリビルダで単純に必要なデータを取得しているのみ。 引数にCoerbonの日付を入れてクエリ条件にしている。

ポイントは最後の cursor() メソッドでこれは呼び出し時点でクエリを実行しないので、メモリ的に厳しい処理にも対応できるらしい。

コードの詳細な説明は書籍を参照のこと。


8-2-6 ユースケースクラスの実装

次に2階層目(UseCase)クラスを実装する。

<?php

declare(strict_types=1);

namespace App\UseCases;

use App\Services\ExportOrdersService;
use Carbon\Carbon;

final class ExportOrdersUseCase
{
    /** @var ExportOrdersService */
    private $service;

    public function __construct(ExportOrdersService $service)
    {
        $this->service = $service;
    }

    /**
     * @param Carbon $targetDate
     * @return string
     */
    public function run(Carbon $targetDate): string
    {
        // (1) データベースから購入情報を取得
        $orders = $this->service->findOrders($targetDate);

        // (2) TSV ファイル用コレクションを生成
        $tsv = collect();
        // (3) タイトル行を追加
        $tsv->push($this->title());

        // (4) 購入情報を追加
        foreach ($orders as $order) {
            $tsv->push([
                $order->order_code,
                $order->order_date,
                $order->detail_no,
                $order->item_name,
                $order->item_price,
                $order->quantity,
                $order->subtotal_price,
                $order->customer_name,
                $order->customer_email,
                $order->destination_name,
                $order->destination_zip,
                $order->destination_prefecture,
                $order->destination_address,
                $order->destination_tel,
            ]);
        }

        // (5) 各要素を TSV 形式に変換
        return $tsv->map(function (array $values) {
                return implode("\t", $values);
            })->implode("\n") . "\n";
    }

    private function title(): array
    {
        return [
            '購入コード',
            '購入日時',
            '明細番号',
            '商品名',
            '商品価格',
            '購入点数',
            '小計金額',
            '合計点数',
            '合計金額',
            '購入者氏名',
            '購入者メールアドレス',
            '送付先氏名',
            '送付先郵便番号',
            '送付先都道府県',
            '送付先住所',
            '送付先電話番号',
        ];
    }
}

書籍にはないが、そろそろSeederを追加してダミーデータを入れておく ファイルを書くより書籍用のGithubリポジトリのコードを流用するのが良い。

php artisan migrate
php artisan db:seed

seeder に購入日を本日に指定したレコードを挿入しておく( Carbon::now()でよろしくやる )

作ったコマンド実行をすると、見出しとレコードがタブスペース繋がりで出力される

php artisan app:export-orders

購入コード      購入日時        明細番号        商品名  商品価格        購入点数小計金額        合計点数        合計金額        購入者氏名      購入者メールアドレス    送付先氏名      送付先郵便番号  送付先都道府県  送付先住所      送付先電話番号
1111-1111-1111-1111     2019-09-08 00:00:00     1       商品1   1000    1      1000     大阪 太郎       osaka@example.com       送付先 太郎     1234567 大阪府 送付先住所1     06-0000-0000

データが無い場合は見出しのみ出力される


8-2-7 Commandクラスの仕上げ

日付を引数で指定して渡してやるとその日に購入されたレコードが表示されるようになる

<?php

### 省略 ###

    protected $signature = 'app:export-orders {date}';

  ### 省略 ###

    public function handle()
    {
        // 日付のパラメーターを取得してUseCaseに処理を渡す
        $date = $this->argument('date');
        $tagetDate = Carbon::createFromFormat('Ymd', $date);
        $tsv = $this->useCase->run($tagetDate);

        echo $tsv;
    }

### 省略 ###

出力結果

vagrant@homestead:~/study_command$ php artisan app:export-orders 20180629
購入コード      購入日時        明細番号        商品名  商品価格        購入点数        小計金額        合計点数        合計金額        購入者氏名      購入者メールアドレス    送付先氏名      送付先郵便番号  送付先都道府県  送付先住所  送付先電話番号
1111-1111-1111-1112     2018-06-29 23:59:59     1       商品1   1000    2       2000    神戸 花子       kobe@example.com        送付先 太郎     1234567 兵庫県  送付先住所2     078-0000-0000
1111-1111-1111-1112     2018-06-29 23:59:59     2       商品2   500     1       500     神戸 花子       kobe@example.com        送付先 太郎     1234567 兵庫県  送付先住所2     078-0000-0000
vagrant@homestead:~/study_command$ php artisan app:export-orders 20190908
購入コード      購入日時        明細番号        商品名  商品価格        購入点数        小計金額        合計点数        合計金額        購入者氏名      購入者メールアドレス    送付先氏名      送付先郵便番号  送付先都道府県  送付先住所  送付先電話番号
1111-1111-1111-1111     2019-09-08 00:00:00     1       商品1   1000    1       1000    大阪 太郎       osaka@example.com       送付先 太郎     1234567 大阪府  送付先住所1     06-0000-0000

さらに必須引数に日時 オプションに出力先を指定すれば、その場所にファイルが出力される

<?php
    // オプションの引数を追加
    protected $signature = 'app:export-orders {date} {--output=}';

    public function handle()
    {
        $date = $this->argument('date');
        $tagetDate = Carbon::createFromFormat('Ymd', $date);
        $tsv = $this->useCase->run($tagetDate);

        // オプションの引数を取得できれば指定されたpathに出力
        $outputFilePath = $this->option('output');
        if (is_null($outputFilePath)) {
            echo $tsv;
            return;
        }

        file_put_contents($outputFilePath, $tsv);
    }

出力pathとファイル名をオプションで指定すればファイルとして出力できる。

php artisan app:export-orders 20190908 --output tmp/orders.tsv

【輪読会資料】PHPフレームワーク Laravel Webアプリケーション開発 4章 後半資料 レスポンス ミドルウェア

以下の記事は2019年8月8日、コワーキングスペース秋葉原Weeybleにて行われる [秋葉原] Laravel Webアプリケーション開発 輪読&勉強会 HTTPリクエストとレスポンスの輪読会資料の一部となります。
今回は 4章後半 4-3『レスポンス』, 4-4『ミドルウェア』部分の記事をアップします。

また、元になっている書籍は以下となります。

PHPフレームワーク Laravel Webアプリケーション開発 バージョン5.5 LTS対応

PHPフレームワーク Laravel Webアプリケーション開発 バージョン5.5 LTS対応

また書籍のコードが記載されているGitHubレポジトリは以下となります。とても助かります!
GitHub - laravel-socym/chapter04-2: 4章 リクエスト・レスポンス

4-3 レスポンス

4-3-1 さまざまなレスポンス

Responseファサードの実態は Illuminate\Contracts\Routing\ResponseFactoryクラスである。
ファクトリークラスなので呼び出す生成メソッドで実際に生成されているResponseクラスは異なる。
アプリケーションからユーザーに返却するデータの種類でうまく使い分けを。

ここではデータタイプごとの返却方法を紹介する

文字列返却

シンプルなパターン、ダイレクトに下記例 hello world みたいに与えるか
Response メソッドの第三引数に 'content-type' => 'text/plain' を指定する

chapter04-2/PlainTextAction.php at master · laravel-socym/chapter04-2 · GitHub

上記サイトよりコード引用

<?php

declare(strict_types=1);
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\Response;

// 同一クラス名となりますので、このサンプルではファサードを別名としています
use Illuminate\Support\Facades\Response as LaravelResponse;
use function response;

class PlainTextAction
{
    public function __invoke(Request $request): Response
    {
       // 同一クラス名となりますので、このサンプルではファサードを別名としています
        $response = LaravelResponse::make('hello world');
        // ヘルパー関数を利用する場合
        $response = response('hello world');
        // content-typeを変更
        $response = response('hello world', Response::HTTP_OK, [
            'content-type' => 'text/plain'
        ]);
        return $response;
    }
}

テンプレート出力

標準のBladeテンプレート view を使う場合等、 View ファサードを使う。responseヘルパーメソッド等、まあ普通に勉強していると最初の当たり前として出る奴。

chapter04-2/BladeAction.php at master · laravel-socym/chapter04-2 · GitHub

<?php
$response = Response::view('view.file');

// 上記のメソッドと同じ結果が得られます
$response = view('view.file');

// ステータスコードを変更し、ビューを出力します。
$response = response(view('view.file'), Response::HTTP_ACCEPTED);

JSON出力

chapter04-2/JsonAction.php at master · laravel-socym/chapter04-2 · GitHub

<?php
$response = Response::make('hello world');

// ヘルパー関数を利用する場合
$response = response('hello world');

// content-typeを変更
$response = response('hello world', Response::HTTP_OK, [
    'content-type' => 'text/plain'
]);

任意のメディアタイプ

return response()->json(['message' => 'laravel'], Response::HTTP_OK, [
    'content-type' => 'application/vnd.laravel-api+json'
]);

また、jsonp を使いたい場合は 上記のメソッド名の json を jsonp に変えるだけ

ダウンロードレスポンス

ダウンローダー等DLをレスポンスさせたい場合は downloadメソッドを使う

chapter04-2/DownloadAction.php at master · laravel-socym/chapter04-2 · GitHub

<?php
$response = Response::download(storage_path('app/cover_min.png'));

// ヘルパー関数を利用する場合
$response = response()->download(storage_path('app/cover_min.png'));

// 第二引数、第三引数に任意の指定を行うパターン
$response = response()->download(storage_path('app/cover_min.png'), 'image.png', [
    'content-type' => 'image/png',
]);

return $response;

リダイレクトレスポンス

リダイレクト先を指定する奴

HTTPパラメーターを渡したい場合は withInputメソッド
リダイレクト→エラーメッセージ等の場合は with メソッドを使う

<?php
// どれも同じ結果なのでお好みで
$response = Response::redirectTo('/');
$response = response()->redirectTo('/');
$response = ridirect('/')

// リダイレクト時に動作を行う例
$response = ridirect('/')
  ->withInput($request->all())
  ->with('error', 'Validation error!!');

return $response

Server-Sent Events 実装

Server-Sent Events(SSE)はhtml5からの機能、サーバー側からのプッシュデータ通信を利用できるがWebSocketと異なり、双方向通信は出来ない。

chapter04-2/StreamAction.php at master · laravel-socym/chapter04-2 · GitHub

書籍だと以下の様に response()->stream( ... ) メソッドを使っているが、 現在のLaravelでは? response()->StreamedResponse( ... ) が使える様だ

LaravelでSSE(Server Sent Events)を利用してサーバから通知する | Minory

また、上記の記事でSSEに加えてクライアント側から非同期通信でステータス情報を送る事で、疑似的な双方向通信を行う事ができるそうです。

<?php
namespace App\Http\Controllers;
use Illuminate\Http\Response;
use Symfony\Component\HttpFoundation\StreamedResponse;

class StreamAction
{
    /**
     * @return StreamedResponse
     */
    public function __invoke(): StreamedResponse
    {
        return response()->stream(function () {
            while (true) {
                echo 'data: ' . rand(1, 100) . "\n\n";
                ob_flush();
                flush();
                usleep(200000);
            }
        }, Response::HTTP_OK, [
            'content-type'      => 'text/event-stream',
            'X-Accel-Buffering' => 'no',
            'Cache-Control'     => 'no-cache',
        ]);
    }
}

4-3-2 リソースクラスを組み合わせた REST API アプリケーション

HATEOASとは

Hypermedia as the Engine of Application State の略。

APIのレスポンスにも別リソースのリンクを埋め込み、リンクを辿るだけで別アプリケーションの呼び出しを可能にすること。らしい。

ブログで考えると、記事の出力をするAPIにユーザープロフィールや個別のコメントへ投稿者のプロフィールの情報へのURLリンクを埋め込む事で、通常、URLのpathやページの仕様や変化した場合、完全分離されているクライアント(フロント側)ではそれを知る術はないが、リンクが埋め込まれている事で、これらの問題を解決できる。という仕組みと思想的なもののようです。

この考えに対応しているJSONフォーマットとして、[JSON API], [HAL4], [JSON-LD] 等がある

Web APIにはJSONベースのフォーマットを使おう - Qiita

以下は HAL を採用した例となる

引用元
The Hypertext Application Language

{
    "_links": {
        "self": { "href": "/orders" },
        "curies": [{ "name": "ea", "href": "http://example.com/docs/rels/{rel}", "templated": true }],
        "next": { "href": "/orders?page=2" },
        "ea:find": {
            "href": "/orders{?id}",
            "templated": true
        },
        "ea:admin": [{
            "href": "/admins/2",
            "title": "Fred"
        }, {
            "href": "/admins/5",
            "title": "Kate"
        }]
    },
    "currentlyProcessing": 14,
    "shippedToday": 20,
    "_embedded": {
        "ea:order": [{
            "_links": {
                "self": { "href": "/orders/123" },
                "ea:basket": { "href": "/baskets/98712" },
                "ea:customer": { "href": "/customers/7809" }
            },
            "total": 30.00,
            "currency": "USD",
            "status": "shipped"
        }, {
            "_links": {
                "self": { "href": "/orders/124" },
                "ea:basket": { "href": "/baskets/97213" },
                "ea:customer": { "href": "/customers/12369" }
            },
            "total": 20.00,
            "currency": "USD",
            "status": "processing"
        }]
    }
}

どのようなJSONフォーマットを採用するかは決まっておらす、仕様は様々だが、
Laravel ではこれらのサポート機能として API Resource 機能が提供されている

REST & Hypermedia APIs - Alpha Hydrae

hal+json.png

リソースクラスの基本

resource ってのを make できるらしい
以下のコマンドでapp/Http/Resource ディレクトリ配下にファイルが作成される

php arrisan make:resource UserResource
php arrisan make:resource CommentResource
php arrisan make:resource CommentResourceCollection
php arrisan make:resource ArticleResource

ここではeroquent を使っているが、そうじゃなくてもOKだそうです。
作成した4つのファイルを使って、上の例にある、HATEOASのJSON を出力する例を以下に示します。

chapter04-2/ArticlePayloadAction.php at master · laravel-socym/chapter04-2 · GitHub

chapter04-2/ArticleResource.php at master · laravel-socym/chapter04-2 · GitHub

chapter04-2/UserResource.php at master · laravel-socym/chapter04-2 · GitHub

chapter04-2/CommentResource.php at master · laravel-socym/chapter04-2 · GitHub

chapter04-2/CommentResourceCollection.php at master · laravel-socym/chapter04-2 · GitHub

ルーター登録

<?php
Route::get('/payload', 'ArticlePayLoadAction');

4-4 ミドルウェア

Laravelにおける、ミドルウェアはコントローラークラスの前後に位置し主にHTTPリクエストのフィルタリングやHTTPレスポンスの変更を行う

4-4-1 ミドルウェアの基本

Laravelのミドルウェアは以下の3種類がある

この流れはフィルタリングなどで使用

HTTPリクエスト --->  ミドルウェア ---> コントローラー

この流れはレスポンス内容の変更や新たなレスポンスの生成が可能

HTTPリクエスト <---  ミドルウェア <--- コントローラー

4-4-2 デフォルトで用意されているミドルウェア

app/Http/kernel.php の App\Http\Kernel クラスで定義されている

グローバルミドルウェア

ルーターに登録されたコントローラークラスが捜査する前に実行される

ルートミドルウェア

デフォルトでwebミドルウェアグループに記述されている

名前付きミドルウェア

ルーターへの登録またはコントローラークラスのコンストラクタなどで任意の名前を指定して利用する

Laravel 標準では多数のミドルウェアが登録されてすでに動作しているが、不要なミドルウェアを削除するとパフォーマンス向上が見込めるケースがあるので必要に応じて使うとよいらしい。

4-4-3 独自ミドルウェアの実装

アプリ固有のミドルウェアを利用するには専用ミドルウェアを実装する必要がある。

ここではリクエストヘッダとレスポンスヘッダにログを書き出すミドルウェア実装を例に解説する。

ミドルウェアクラスの生成

実装例として HeaderDumperクラスを作成する

ミドルウェア作成コマンド

php aritsan male:middleware HeaderDumper

すると app/Http/Middelware ディレクトリ配下に HeaderDumper.php が作られる

リクエストヘッダのログ出力

このクラスでリクエストヘッダをログに書き出す処理の実装例が以下
引用元 : chapter04-2/HeaderDumper.php at master · laravel-socym/chapter04-2 · GitHub

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Response;

class HeaderDumper
{
    private $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    public function handle($request, Closure $next)
    {
        // リクエストheader を log 出力する
        if ($request instanceof Request) {
            $this->logger->info('request', [
                'header' => strval($request->headers)
            ]);
        }
        // 上記同様の結果となるヘルパ関数を使った例
        // info('request', ['header' => strval($request->headers)]);

        /** @var Response $response */
        $response = $next($request);

        // レスポンスheaderをログ出力する
        if ($response instanceof Response) {
            $this->logger->info('response', [
                'header' => strval($response->headers)
            ]);
        }
        return $response;
    }
}

ヘルパ関数 info

レスポンスヘッダのログ出力

上のコードにある後半部分がレスポンスヘッダをログ出力する部分、以下にも記述 レスポンスヘッダを取得するには、ミドルウェアクラス内の $next($request) の戻り値を取得する。

<?php
        /** @var Response $response */
        $response = $next($request);

        // レスポンスheaderをログ出力する
        if ($response instanceof Response) {
            $this->logger->info('response', [
                'header' => strval($response->headers)
            ]);
        }
        return $response;

ミドルウェアの登録

ミドルウェアを作ったら App\Http\Kernel クラスに登録が必要

<?php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{

    protected $middleware = [
        \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
        \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
        \App\Http\Middleware\TrimStrings::class,
        \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
        \App\Http\Middleware\TrustProxies::class,
        \App\Http\Middleware\UrlPath::class,
        \App\Http\Middleware\CssFileDate::class,
        // ここに登録しないと動きません seeder の run() とかと同じイメージですね
        \App\Http\Middleware\HeaderDumper::class, // laravel本 4-4 サンプル
    ];

    protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            // 省略
        ],

        'api' => [
            'throttle:60,1',
            'bindings',
        ],
    ];

    protected $routeMiddleware = [
        // routes/web routes/api で使えるアレはココで定義している
        'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
        // 省略
    ];

}

routeMiddeware は routes/web.php や routes/api.php のルーティングで使えるアレを登録できる

<?php
Route::get('admin/profile', function () {
    // $routeMiddleware の auth は下のこれっすね
})->middleware('auth');

おまけ オレオレミドルウェア

以下を参考に作ったものです

キャッシュを有効にしつつ、cssやjsファイルの変更を確実に反映させる – doop

css ファイルって更新してもブラウザ側でキャッシュが読み込まれてしまうケースがあって、開発中に更新してもデザイン変わらず「うぎゃー」ってなることがある。しかし css 読み込みの際にクエリパラメーターを付けて、毎回値を変えれば再読み込みされます。

<link href="http://app_whois.test/css/livelynk.css?q=ここにランダムな値" rel="stylesheet">

しかし、適当なランダム値にすると毎回css呼ばれるのも本番では遅くなって嫌なので、cssファイルの更新日時をクエリのパラメーターにして読み込んでやれば、cssを更新した時だけ、再読み込みされる!というちょっとうれしい奴です。

<link href="http://app_whois.test/css/livelynk.css?q=20190628113106" rel="stylesheet">
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\View;

class CssFileDate
{
    public function handle($request, Closure $next)
    {
        // ファイルpathから更新日を出力、pathにパラメータを入れcssや画像のキャッシュクリアに利用
        $path = public_path();
        $filename = $path . '/css/livelynk.css';
        if (file_exists($filename)) {
            $file_date = date('YmdHis', filemtime($filename));
        } else {
            $file_date =  str_random(6);
        }
        // file_date という変数が view のどこでも使える様になる 
        View::share('file_date', $file_date);
        return $next($request);
    }
}

bladeテンプレート側ではこんな風に使えます

<link href="{{ asset('css/livelynk.css') }}?q={{$file_date}}" rel="stylesheet">

Laravelの単機能を作るまでの大まかな流れ

設計やら命名規則やら一緒くたになってごちゃごちゃとしてます。
また、俺ルールと一般的なルールと、厳密なルールがごっちゃにかいてあります。
が、自分なりに他の人と共通作業を行う際のコンセンサスを取るメモみたいなものをアップしておきます。何かの参考になれば幸い。

(アプリケーションはAPIとし、ごく基本的な設定関連はほぼ終わっている状態と想定)

ルーティング定義をする

routes/api.php

ルートプレフィックスで上層のpathをまとめる
基本アルファベット順で並べる
特別な処理は上か下の行にまとめる

同じControllerの処理はまずCRUDの順で並べ、次にその他の処理を並べる。

基本はControllerで書いたメソッドの順と揃っているのが望ましい

しかし、これだと1行で行けるらしい、他のメソッド入れた際はどうするんだろう?

Route::resource('users', 'UserController');

Controller 作成

コマンド例 : php artisan make:controller HogeFugaController --resource

( --resources オプション CRUDメソッドを自動生成してくれる)

ディレクトリ/命名例 : app/Http/Controllers/HogeFugaController.php

命名規則:

  • 単数形,table名か機能名の UperCamelCase
  • 最後に Controller を書く

アノテーションAPI仕様を書く
requestを受けてserviceに送り返り値をもらう処理を書く
retrun [ ] で想定するAPIの返り値を仮作成
フロント側に要件での最低限の出力を可能とする

unit testファイルを作る

ひとまず pathを指定して
->assertStatus(200) のテストを書く

テストを実施する(時間があればこの辺詳細書きたい)

migration ファイル作成

コマンド例 :
php artisan make:migration create_users_statuses_table

ディレクトリ例 :
app/Http/Controllers/HogeFugaController.php

命名規則 :

  • スネークケース
  • table(複数形)名に続けて create|edit|delete のいずれかを書く
  • 最後に tableを書く

1テーブルの作成・編集・削除につき1ファイルを生成する
migration実施とphpMyAdminでの仕様との差を確認

  • comment->('仮カラム、仮型') 等と通常のコメントで仮である事を明文化しておく
  • コメントアウトをして作らないでおく

factory 作成

migrationファイルと合わせて factoryを作る。

コマンド例 :
php artisan make:factory HogeFugaFactory
ディレクトリ/命名例 :
database/factories/HogeFugaFactory.php

faker を使ったり、Corbon や決め打ちの値で1レコード文の標準的なダミーレコード作成を定義する

seeder ファイル作成

コマンド例 : php artisan make:seeder UserStatusTableSeeder
ディレクトリ/命名例 : database/seeds/UserStatusTableSeeder.php

ダミーレコードを作る為の定義をするファイル
up メソッド内で未定義のカラムがある場合は faker で定義された値でレコードが生成される

upメソッド内の以下の違いに注意、作成時と編集時はメソッド名が異なる
Schema::create( ... )
Schema::table( ... )

忘れがち!
database/seeds/DatabaseSeeder.php

$this->call(UserStatusTableSeeder::class);

を書かないとseedingは実行されない

また、Seeder書いたら以下のコマンドもやっておく  

composer dump-autoload

少なくともレコード1件目はid系カラムのリレーションが成立する現実的値で具体的なデータを入れておく
2件目以降必要な現実的なデータがあれば、個別に生成できるようにしておく

以降のデータは他のtableの連携等含め、リレーションされるidは固定値、その他の値はある程度ランダムで生成されるレコード数を現実的な運用時のページング等の移動が行える程度には入れておく

seeder実施、各カラムに正しくfakeの値が入っているか確認

モデル作成と設定

コマンド例 :
php artisan make:model Models/HogeFuga

ディレクトリ/命名例 :
app/Models/HogeFuga.php

※コマンド時はフォルダpathの指定を間違えないこと

命名規則:

  • 単数形,table名か機能名の UperCamelCase
  • tableの場合は対となるControllerを持つ
  • 最後に Model は書かない

hidden 設定
filiter 設定
table 名指定
リレーション設定

ここにクエリビルダはEloquentの処理は書かない

あとでやる 例外として scpope 等の共通のSQLフィルタ処理のメソッド等を書く

Service 層の作成

app/Http/Services/HogeFugaService.php
※ artisanコマンドでは作れない、コピペで作る

命名規則:

  • Controllerと対にする
  • 単数形,table名か機能名の UperCamelCase
  • 最後に Service を書く
    • Controller が HogeFugaController なら
    • Service は HogeFUgaService となる

必要なら Controllerから受けたrequestを受ける
1行で済むクエリビルダやEloquentならここに書く

必要なビジネスロジック処理を書く
複数行に渡るDB処理は、 Repositoryに渡して返り値をもらう処理を書く 引数を渡してRepository処理に反映させても良い

必要であれば他のtable層のRepository層を呼び出しても良いがなるべくしない

Repository 層の作成

app/Http/Repositorys/HogeFugaRepository.php
※ artisanコマンドでは作れない、コピペで作る

命名規則:

  • 単数形,table名か機能名の UperCamelCase
  • 最後に Repository を書く
  • Serviceと対にする
    • Controller が HogeFugaController
    • Service が HogeFUgaService なら
    • Repository は HogeFugaRepository となる

use App/Models/Hoge
use DB
を書く

DBから値を拾う処理のみを書く
簡単な if分岐やswitch 等はOK
メソッドにどのServiceのメソッドからの呼び出しかコメントを書く

引数を渡して処理を変える等しても良いが、詳細やルールをコメントする

Requests (フォームリクエスト)によるバリデートの作成

コマンド例 : php artisan make:request StoreBlogPost

ディレクトリ/命名例 : app/Htp/Requests/HogeFugaCreareUpdate.php

命名規則:

  • 単数形,table名か機能名の UperCamelCase Controller と同じ
  • 続けて対となるControllerのメソッド名
  • create, update 等、複数のメソッドで共通化できるものは両方の名前を続けて書く

authorrize() は、ほぼ true となる

rules() にバリデート条件を書く、仕様書に合わせて書く
特殊なバリデートが必要な箇所はオリジナルバリデートを作成する

本来はこのへんでしっかり test を書いて検証をしたい所だが、ひとまず、値を入れてみて手動のpost,get,put,delete,のtestをする


ひとまずこんな感じ。