nanisore oishisou

プログラマ、ララ・ベル子さん改めArm4さんのゆるふわ奮闘記。

Laravelでカスタムフォーム使ってViewを綺麗に保とうず

どうも。
お久しぶり、あなたのベル子です。

ベル子の雑談ファンのみんな安心してくれたまえ。
やたら長くなったから巻末に付いておるぞよ。

本題

みなさんLaravel Collective使ってますか?

laravelcollective.com

「何それ、美味しそう」マンの諸君への解説

説明しよう。
Laravel CollectiveとはLaravelのアップデートでコアのフレームワークからは削除されてしまったLaravelの機能を、ライブラリとして組み込むことができるライブラリである。

大きく分けて5種類の機能があり、それぞれcomposerでinstallする必要がある。

Annotations

関連するメタデータを付与してやることで、イベントをリッスンしたり、ルーティングしたりできるという機能。
Laravel5で追加されるはずの機能だったようですが、最終的なリリース版からは切り離されてしまったようです。

Forms & HTML

bladeテンプレートで使用できるフォームとhtmlのヘルパーを集めたパッケージ。
Laravel5でForm、Htmlファサードコンポーネントはコアから削除されてしまいました。

元々、私はフロントエンジニアなんで、このFormコンポーネントのコードが逆に長くて読みづらくてうざいので、後述のカスタムマクロするときくらいしか使っていなかったんですが、Input::old()をいちいち書くのがうざいということもあり、最近はナチュラルに受け止められるようになりました。

www.dn-web64.com

SSH

Laravelコード内でSSHで何かやるときに使う機能だったけど、今はそういうのはEnvoy使えよってことで削除されたらしいです。

IronMQ Laravel Queue Driver

ちょっと前にキューのチュートリアルを書きましたが、Laravel 5.2からフレームワーク側で用意してくれているキュードライバーの設定からIronMQが削除されたので、IronMQ使いたい人はこれを突っ込めよって話です。

Command Bus

5.1で消えたようです。
コントローラーのアクション内での様々な処理をタスクとして分けてコマンドを実行するというような実装パターンの時に、タスクをカプセル化する便利なメソッドだそうで、
どういうときに使うかというと、こないだ書いたようなキューに押し込む必要があるようなメール送信とか、そういうのをハンドリングしたいときに使うものだったようなのですが、
あんまり使ったことある人いないかもしれませんが、Laravelには5.1からJobという概念が出てきました。

このCommand Busで作るコマンドのクラスを作成するときmake:commandというコマンドを叩いてapp/Commandsの配下にできるということだったんですけど、これLaravel 4.2使ってる人だと分かると思うんですけど、コンソールコマンドと混同してしまいますよね。

たぶん分かりづらいねってことになって名前とディレクトリを切り離したんだと思います。

だいたい使うことがあるのはForms & HTML

前置きが長くなりましたが、だいたい使うことがあるのはフォームヘルパーです。

先述したとおり、バリデーションでリダイレクトバックした際に前にインプットしてあったデータを再度セットしてやるInput::old()、今のバージョンだとold()をフォームに書くのがうざいという理由で使っている人が多いんじゃないでしょうか。
あとselectが1行で書けるというのもありますね。

正直、引数がバラバラで覚えづらいので、Emmetで一瞬で書けるフォームパーツをいちいちドキュメントを見ながら書くというのが非常につらいので、それ以外はあまりメリットは感じられないです。

加えて、フォーム系はフロント側でdata属性を追加しないといけないことが多くて、こういうことをフォームヘルパーでやろうとするとカスタムで作らないといけませんでした。
最新版では引数で渡せるようになったようですが。
古い案件はつらいですね。

ただ、このカスタムでform&htmlパーツが作れるというのが、このライブラリの最強にして最大の魅力

なんです!!

↓インストール方法はこちらの分かりやすい記事を参照してください。

qiita.com

例えば、時刻を入力させるのに

  • 時間と分はそれぞれ別のプルダウン形式で入力
  • 時間は1~24までの1時間刻みで。
  • 分は00から55までの5分刻みで。

のようなフォームパーツがアプリケーションのいたるところに登場するシステムを想像してみてください。

これを自力でViewに書いていくとどうなるか。

<div class="row">
    <div class="col-md-3 form-inline">
        <select class="form-control" id="test_time_h" name="test_table[test_time_h]">
            <option value="01">01</option>
            <option value="02">02</option>
            <option value="03">03</option>
            <option value="04">04</option>
            <option value="05">05</option>
            <option value="06">06</option>
            <option value="07">07</option>
            <option value="08">08</option>
            <option value="09">09</option>
            <option value="10">10</option>
            <option value="11">11</option>
            <option value="12">12</option>
            <option value="13">13</option>
            <option value="14">14</option>
            <option value="15" selected="selected">15</option>
            <option value="16">16</option>
            <option value="17">17</option>
            <option value="18">18</option>
            <option value="19">19</option>
            <option value="20">20</option>
            <option value="21">21</option>
            <option value="22">22</option>
            <option value="23">23</option>
            <option value="24">24</option>
        </select>
        <span></span>
    </div>
    <div class="col-md-3 form-inline">
        <select class="form-control"  id="test_time_i" name="test_table[test_time_i]">
            <option value="00">00</option>
            <option value="05">05</option>
            <option value="10">10</option>
            <option value="15">15</option>
            <option value="20">20</option>
            <option value="25">25</option>
            <option value="30">30</option>
            <option value="35">35</option>
            <option value="40" selected="selected">40</option>
            <option value="45">45</option>
            <option value="50">50</option>
            <option value="55">55</option>
        </select>
        <span></span>
    </div>
</div>

はい。こんなコードを自力で書くのなんて、ぞっとしますね。
中間部分はループさせられるとしても、こんなん大量にあるのにいちいち全部同じように書くなんて想像するだけで、ぞっとします。

それをカスタムフォームヘルパーを使えばあら不思議!!

{!! Form::selectTime('test_table', 'test_time', $row->test_time, ['class' => 'form-control']) !!}

たったの1行で書けてしまうんですね。

▼カスタムフォームヘルパーの実装方法の参考ページ qiita.com

カスタムフォームヘルパーの実装方法は↑こちらの方の方法とほぼ同様に実装してますが、私は以下のようにapp直下に入れてます。

<?php

namespace App\Providers;

use Form;
use Illuminate\Support\ServiceProvider;

class FormMacroServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap the application services.
     *
     * @return void
     */
    public function boot()
    {

        require app_path().'/macros.php';

    }

    /**
     * Register the application services.
     *
     * @return void
     */
    public function register()
    {
        //
    }
}

Laravel 4.2ではデフォでフォームヘルパー使えるのでライブラリのインストールは必要なくて、カスタムしたかったらapp/start/global.phpで以下のようにmacroを書いたphpファイルをrequireしてやればいいです。

require app_path().'/macros.php';

別にフォームだけじゃなくてhtmlもオレオレパーツ作り放題

システムを通して毎回出てくる系のhtmlを作っておくと、これまた1行で書けてすごい便利です。
業務システムとか、この特殊ラベルどのページにも出てきますやんみたいなのありがちですよね。(アイコン付きの出荷済み、発送済みとかそいういうやつ)

あれも引数にkey渡すだけで全てよしなに表示してくれるパーツが作れます。

アイコン変えたかったら1箇所変えるだけ。

超絶DRY
超絶DRY

大事なことだから2回言いました。

雑談コーナー

最近、イラストを公式Twitterにアップしている関係で、ベル子さんの趣味は絵を描くことなのかしらと思ってる人が多いと思うんですが、私の最大の趣味はプロフィールの趣味のところにもあるとおり、小説の執筆です。

「野いちご」「魔法のiらんど」といった携帯小説時代から書いていまして、小さな賞とかをもらったこともあるんですが、まあ、ずっと書いてる割にあまり自慢できる成績は残せていません。

好きな作家が乙一東野圭吾江戸川乱歩のようなあまり陽気じゃない路線なので、周りの人に明るく「これ読んでみてー♡」と言えないのが上手にならない最大の要因なんですが、初期の頃の作品と比べるとかなり文章がマシになってきました。
が、ストーリーは結構、初期のもののほうが面白いので、リライトしようかなーと、最近、思ってます。

文章を書くことを勧めてくれたのは小学校の5年のときの担任の先生で、すごく嫌っていた先生なんですが、「あなたは文学的な才能を持っているので、文章を書いたほうがいいと思います」と、とにかく私を理解して肯定しようと努力してくれた人です。

今の私はついつい彼と同じことやってしまうんですけど、ありがたいけど、うざったいんですよね。気を付けないと。

でも、お陰様で翻訳やちょっとした記事を書くといった文章を書く仕事をすることもできましたし、絵以外の趣味にも出会えました。

サンキュー、高橋!
(↑先生を面と向かって呼び捨てで呼んでたけど、今思うとどんだけエキセントリックなんだよって感じ)

最後まで読み切った、そこの君、明日幸運が訪れます!
ありがとう★

9Sと2Bを描いてみた

f:id:arm4:20171127104146p:plain

どーん。
ここのところ、通勤中やら、寝る前やらに描いていたニーアオートマタの9Sと2Bのイラストが仕上がったのでブログで紹介したいと思いますよ。

小さくなるとナインズの目があんまり見えなくて怖いですね。
イラストって大きさによって結構印象が変わるから、拡大したり縮小したりしながら描いたほうがいいかも。

あと、最近思ったのは左右逆にしながら描いていくとバランス取りやすいですね。何ていうか効き顔っていうんですが、描きやすいほうの顔の向きとかあるんで。

↓描き初めの頃のナインズ。角度変えただけでも結構印象違う。

f:id:arm4:20171127104949p:plain

ついでに服の描き込みをする前のものも貼っておきます。 ちょっと2Bの顔を最後に変えたので違う顔してますね。

f:id:arm4:20171127105134p:plain

こんな感じで技術だけじゃなくてイラストもこれからこちらのブログで紹介していこうかと思いますー。

普段はファンアートよりオリジナルが多いよー。
さらに言うと女の子ばっかり描くよー。
よろしくね★

gitのブランチ名を入れ替えたい

たとえば、ちょっと大きな修正を行っているfeatureブランチが存在するとする。
デザインリニューアルとかそんな感じの。

ブランチ一覧

  • new_design
  • staging
  • develop
  • master

new_designでは現行バージョンから廃止される機能などもあるので、基本的にdevelopからコードを取得してマージしていない。

現行でも新デザインでも必要なbug_fixはそれぞれ適宜に取り込んでいる。

そんなこんなで、晴れて、新デザインをリリースすることになった。

基本的に違うバージョンなのでnew_designをdevelopにマージすることは できない。

ということはnew_designから新たにmasterを作ってデプロイというような流れになると思う。

今後はnew_designで開発を続けて、現状のdevelopブランチは更新されない。

new_designで開発を続けて、master02にマージしてデプロイ?

ブランチ一覧?

  • new_design
  • master02
  • staging02
  • staging
  • develop
  • master

この経緯を知らない新人PGが入ってきて環境を作ってコードを修正してと言われたとする。

(?????
よく分からないけど、とりあえずdevelopからブランチ作って作業すればいいのかな。。。よく分からないけど。。)

たとえnew_designからブランチを作ってと指示されていたとしても、上のようなブランチ一覧を見たPGは混乱してしまうと思う。

こんなとき、どうするか。

そうだ、リネームしよう。

現状にそっていないネーミングの関数
現状にそっていないネーミングのクラス

PGなら分かると思うが、これらはバグの温床である。

現状にそっていないネーミングのブランチ

これも事故の元となるので、面倒がらずにちゃんとリネームしてあげよう。

重大な変更作業なので、作業の前にはバックアップを取ることを忘れずに。

事前準備

まずは作業前にmaster以外の自分のローカルブランチを削除しておく。

develop → develop_old

# ローカルにリネームした新しいブランチを作成
git branch develop_old origin/develop
# チェックアウト
git checkout develop_old
# 新しいブランチとしてリモートにpushし、上流ブランチに設定
git push -u origin develop_old
# 古いほうのリモートブランチを削除
git push origin :develop

new_design → develop

# ローカルにリネームした新しいブランチを作成
git branch develop origin/new_design
# チェックアウト
git checkout develop
# 新しいブランチとしてリモートにpushし、上流ブランチに設定
git push -u origin develop
# 古いほうのリモートブランチを削除
git push origin :new_design

masterとstagingについても同様の作業をする

ブランチ一覧

  • develop_old
  • master_old
  • staging_old
  • staging
  • develop
  • master

oldではなくバージョン番号にしてもいいと思う。

チームメンバーのローカルでもリネームしてもらう

# ローカルのリモートブランチ情報を最新の状態にフェッチする
git fetch -p
# 状態を確認
git branch -avv
# ローカルのリモートブランチ情報がリモートのブランチ一覧と合致していることを確認
# masterブランチにチェックアウト
git checkout master
# ローカルの作業ブランチを削除
git branch -d new_design
git branch -d develop
# ローカルの作業ブランチを新規作成
git branch develop origin/develop
git branch develop_old origin/develop_old
# 状態を確認
git branch -avv
# 上流ブランチにローカルブランチ名と同じ名前のリモートブランチが設定されていることを確認

ブランチ名の入れ替えは、結構思い切った作業だと思うけど、
それはリファクタリングと同じで
何か起こると怖いから汚いコードを放置しているのと同じ。

放置された汚いコードの上から、さらにコードを付け加えたり削除したりを繰り返すと、いつか誰も読めないグチャグチャなものになり、
もっとも恐れていることが起きる。

いろんな経緯があって、たくさんブランチができてる、怖いから全部残している、その気持ち、非常によく分かります。

でも、家の大掃除だと思って、思い切ってgitのブランチ名を適切な状態に整理しましょう!

Laravel5.4でイベント&キューを使ってメールをキュー送信する手順(3)

Laravel5.4でイベント&キューを使ってメールをキュー送信する手順もとうとう最終回を迎えます!!

上級Laravelerの人はこの回から読んでも大丈夫です。
初級Laravelerの人は過去記事から読んでみてください。

arm4.hatenablog.com

arm4.hatenablog.com

というわけで恒例の今北さん用3行を

  • Laravel新規プロジェクトを作ってダミーデータを挿入した
  • 投稿一覧ページと詳細ページを作っていいねボタンを設置した
  • いいね押したら投稿者にメールでお知らせが行くようにした

今回やること

  • ボタンの種類を増やして種類ごとにメール送信の挙動を変える
  • イベント&リスナーでメール送信を実装
  • リスナーをキュー化して非同期でメールを送信

イベントリスナーって何?

いわゆるオブザーバーパターンと呼ばれるコードの実装方法で、JavaScriptでDOM操作するときにはおなじみの方法です。

ボタンがクリックされたら"click"というイベントが発火して、そのイベントが発火したら特定の処理が走るようにするという実装方法です。

イベント&リスナーとか使ったことない機能で怖いと思うサーバサイドエンジニア諸君もいるかもしれませんが、JavaScriptで普段やってることを、PHPでもやる。ただそれだけのことです。

フロントエンドから来たエンジニアにとっては、むしろ分かりやすくてコードも綺麗になるので、とてもオススメです。

↓これより下は手順だから、ですますやめます。

実装方法を考える

まずはどんなイベントが必要か考える。

今回は「お知らせの種類」ごとにイベントを作成することにした。

  • ユーザー全員に知らせるお知らせイベント
  • 本人だけに知らせるお知らせイベント

今回は処理にフォーカスしたイベントを考えてみたが、イベントには複数のリスナークラス(処理)を紐付けられるので、もっと大きく捉えて「何をした」というイベントを発火させて、その後の処理をリスナーとして登録するという実装方法もあるだろう。

例えば、「いいね」ボタンを押したら、「全員にお知らせメールを送り、アプリの通知機能でお知らせする」のように複数の処理が紐付くような場合は、「○○ボタンが押された」のようなイベントを作成するといいかもしれない。

ルートを追加する

まずは「やだねする」「どうでもいい」「君の名は?」の3種類を押したとき用のルートを追加しよう。

routes/web.php

<?php
.
.
.

Route::post('/send-dislike/{post}', 'HomeController@sendDislike')->name('send.dislike');
Route::post('/send-whatever/{post}', 'HomeController@sendWhatever')->name('send.whatever');
Route::post('/send-yourname/{post}', 'HomeController@sendYourname')->name('send.yourname');

コントローラーを修正

まずはイベント&リスナーを使わず、「いいねする」と同じように本人にだけメールが送信されるところまで作っていこう。

<?php
.
.
.

    public function sendDislike(Request $request, Post $post)
    {
        $triggered_user = Auth::user();

        $data = [
            'type' => 'やだね',
        ];

        // やだねのときは本人だけに知らせる
        Mail::to($post->user)
                ->send(new SendMail($post, $triggered_user, $data));

        return response()->json(['status' => 0]);
    }
    public function sendWhatever(Request $request, Post $post)
    {
        $triggered_user = Auth::user();

        $data = [
            'type' => 'どうでもいい',
        ];

        // どうでもいいときは誰にも知らせない
        
        return response()->json(['status' => 0]);
    }
    public function sendYourname(Request $request, Post $post)
    {
        $triggered_user = Auth::user();

        $data = [
            'type' => '君の名は?',
        ];

        // 君の名は?のときは本人だけに知らせる
        Mail::to($post->user)
                ->send(new SendMail($post, $triggered_user, $data));

        return response()->json(['status' => 0]);
    }

同じことをしてる部分をまとめる

ボタンを押した時に何のボタンかdataに入れてpostしているので、それを使ってデータセット部分を1つのメソッドで済ませるようにリファクタリングしてみる。

<?php
.
.
.
class HomeController extends Controller
{
    private $types = [
        'like'     => 'いいね',
        'dislike'  => 'やだね',
        'whatever' => 'どうでもいい',
        'yourname' => '君の名は?',
    ];

    private $triggered_user = null;
    private $data = null;
.
.
.
    private function setAlertData($request) {
        $this->triggered_user = Auth::user();

        $this->data = [
            'type' => $this->types[$request->type],
        ];
    }

    public function sendLike(Request $request, Post $post)
    {
        $this->setAlertData($request);

        // いいねのときはユーザー全員に知らせる
        Mail::to($post->user)
                ->send(new SendMail($post, $this->triggered_user, $this->data));

        return response()->json(['status' => 0]);
    }

    public function sendDislike(Request $request, Post $post)
    {
        $this->setAlertData($request);

        // やだねのときは本人だけに知らせる
        Mail::to($post->user)
                ->send(new SendMail($post, $this->triggered_user, $this->data));

        return response()->json(['status' => 0]);
    }
    public function sendWhatever(Request $request, Post $post)
    {
        $this->setAlertData($request);

        // どうでもいいときは誰にも知らせない
        
        return response()->json(['status' => 0]);
    }
    public function sendYourname(Request $request, Post $post)
    {
        $this->setAlertData($request);

        // 君の名は?のときは本人だけに知らせる
        Mail::to($post->user)
                ->send(new SendMail($post, $this->triggered_user, $this->data));

        return response()->json(['status' => 0]);
    }

EventServiceProviderにイベントを記載

直書きしていたメール送信部分をイベントにしていく。

プロジェクト作成時から用意されているEventServiceProvider.phpの$listen部分に連想配列の形式で、イベント(key) => リスナー(value)のように記載してイベントとリスナーを登録する。

まだapp/Eventsやapp/Listenersというディレクトリが無くても構わない。ここにクラス名を記載してgenerateコマンドを実行すると、ディレクトリとファイルが作成される。

イベント、リスナーの命名規則はないが、イベント名は「何が起こった」、リスナー名は「何をする」のように付けると分かりやすい。

つなげて読むと、「何が起きたら、何をする」と読めるからだ。

app/Providers/EventServiceProvider.php

<?php
.
.
.
    protected $listen = [
        'App\Events\PersonalAlertCreated' => [
            'App\Listeners\SendPersonalAlert',
        ],
        'App\Events\PublicAlertCreated' => [
            'App\Listeners\SendPublicAlert',
        ],
    ];

イベント&リスナーファイルを作成

generateコマンドを実行する

php artisan event:generate

イベントを発火させる

コントローラーを修正し、メール送信している箇所にイベントを仕込んでいく。

  • 全員に知らせる ( PublicAlertCreated )
  • 本人だけに知らせる ( PersonalAlertCreated )

app/Http/Controllers/HomeController.php

<?php
.
.
.
use App\Events\PublicAlertCreated;
use App\Events\PersonalAlertCreated;
.
.
.
    public function sendLike(Request $request, Post $post)
    {
        $this->setAlertData($request);

        // いいねのときはユーザー全員に知らせる
        event(new PublicAlertCreated($post, $this->triggered_user, $this->data));

        return response()->json(['status' => 0]);
    }

    public function sendDislike(Request $request, Post $post)
    {
        $this->setAlertData($request);

        // やだねのときは本人だけに知らせる
        event(new PersonalAlertCreated($post, $this->triggered_user, $this->data));

        return response()->json(['status' => 0]);
    }

    public function sendWhatever(Request $request, Post $post)
    {
        $this->setAlertData($request);

        // どうでもいいときは誰にも知らせない
        
        return response()->json(['status' => 0]);
    }

    public function sendYourname(Request $request, Post $post)
    {
        $this->setAlertData($request);

        // 君の名は?のときは本人だけに知らせる
        event(new PersonalAlertCreated($post, $this->triggered_user, $this->data));

        return response()->json(['status' => 0]);
    }

イベントファイルを設定

イベントファイルはデータを受け取ってセットするだけのクラスなのでロジックを書くことはない。

コンストラクタの中でイベントで使用するデータをセットする。

PersonalAlertCreatedの記述もまったく同じなので、ここでは省略するが、ファイルは2つともちゃんと設定すること。

app/Events/PublicAlertCreated.php

<?php
.
.
.
use App\Post;
use App\User;
.
.
.
class PublicAlertCreated
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

     public $post;
     public $triggered_user;
     public $data;

    /**
     * Create a new message instance.
     *
     * @return void
     */
    public function __construct(Post $post, User $triggered_user, $data)
    {
        $this->post = $post;
        $this->triggered_user = $triggered_user;
        $this->data = $data;
    }

リスナーファイルを設定

handleの部分に処理を書いていく。

app/Listeners/SendPublicAlert.php

<?php
.
.
.
use App\User;
use App\Mail\SendMail;
use Illuminate\Support\Facades\Mail;
use App\Events\PublicAlertCreated;
.
.
.
    public function handle(PublicAlertCreated $event)
    {
        $post = $event->post;
        $triggered_user = $event->triggered_user;
        $data = $event->data;

        // クリックした人以外の全員に送る
        $users = User::whereNotIn('id', [$triggered_user->id])->get();

        foreach ($users as $user) {
            Mail::to($user)
                    ->send(new SendMail($post, $triggered_user, $data));
        }
    }

app/Listeners/SendPersonalAlert.php

<?php
.
.
.
use App\User;
use App\Mail\SendMail;
use Illuminate\Support\Facades\Mail;
use App\Events\PersonalAlertCreated;
.
.
.
    public function handle(PersonalAlertCreated $event)
    {
        $post = $event->post;
        $triggered_user = $event->triggered_user;
        $data = $event->data;

        Mail::to($post->user)
                ->send(new SendMail($post, $triggered_user, $data));
    }

ここまで設定したら、「いいねする」ボタンを押してみよう。

自分以外の全員あてのメール(およそ40通)が送られてきていたら、イベントリスナーでメール送信の実装はできている。

大量の処理を追加してみよう

ここで、publicAlertのほうに大量にデータを突っ込む処理を入れないといけないことになったとする。

app/Listeners/SendPublicAlert.php

<?php
.
.
.
use App\User;
use App\Mail\SendMail;
use Illuminate\Support\Facades\Mail;
use App\Events\PublicAlertCreated;
.
.
.
    public function handle(PublicAlertCreated $event)
    {
        $post = $event->post;
        $triggered_user = $event->triggered_user;
        $data = $event->data;

        // クリックした人以外の全員に送る
        $users = User::whereNotIn('id', [$triggered_user->id])->get();

        // 大量の処理を追加!!
        for ($i=0;$i<5000;$i++) {
            $users = User::whereNotIn('id', [$triggered_user->id])->get();
        }

        // みんなに知られてしまうアラート
        foreach ($users as $user) {
            Mail::to($user)
                    ->send(new SendMail($post, $triggered_user, $data));
        }
    }

これで「いいねする」ボタンを押してみた人は気づいたと思うが、40通のメールの送信処理&5000回のクエリ取得が終わってからレスポンスが返ってくるので、「メール完了 メールを送りました!」の表示が表示されるのはボタンを押してからしばらく経ってからになる。

本番のアプリケーションでは二重投稿を防止する処理などが入っていて、レスポンスが返ってくるまでユーザーがボタンを押せないこともあるだろう。

送信完了のお知らせ表示が表示される時間が、送信ユーザー数に依存するというのはユーザーにとって分かりづらいため、送信できていないと勘違いして連投してしまう人もいるかもしれない。

多くのユーザーにメールを送信したり、多くの通知用データを挿入したりする処理を挟んでしまうと、その処理が完了してレスポンスが返ってくるまでに時間がかかる。

こんなときは、処理の受付だけ済ませて、あとの処理はバックグラウンドでやってもらいたいと思う。

誰も32個の小包の発送手続きが完了する様を20分立ったまま眺めていたくはない。
32個の小包を誰かに渡して、「住所は貼っといたから、あとはよろしく」と言いたいのだ。

そこでキューの登場

この「あとは、そっちでよろしく」方式のことを、キューと呼ぶ。待ち行列という意味だ。

ユーザーは自分の代わりに、小人たちを列に並ばせて小包を32個発送させられる。この小人のことをワークやジョブと呼ぶ。

キューを使うには、小人を並ばせるためのテーブルが必要だ。

Laravelではジョブを管理するDriverは以下の中から選べる。

  • sync
  • database
  • Amazon SQS
  • Beanstalkd
  • Redis

今回はアプリで使ってるDBにキュー用テーブルを作成して実装するdatabaseを選択することにする。

キューの設定をする

.env

QUEUE_DRIVER=database

config/queue.php

ひとまずqueue.phpはデフォルトのままでOK。

ジョブを発行するためのテーブルを作成

以下のコマンドを実行するとジョブテーブルのmigrationが出来る。

failed-tableのほうはその名のとおり、エラーを吐いたジョブを記録しておくテーブルだ。こちらは、作らなくてもキュー化はできる。

php artisan queue:table
php artisan queue:failed-table

マイグレーションファイルが出来たのでmigrateを実行しよう。

php artisan migrate

jobsとfailed_jobsというテーブルがDBに作成された。

メール送信部分をキュー化

テーブルができたら、SendPersonalAlert.phpとSendPublicAlert.phpのMailのsendをqueueとするだけ。

SendPublicAlert

            Mail::to($user)
                    ->queue(new SendMail($post, $triggered_user, $data));

SendPersonalAlert

        Mail::to($post->user)
                ->queue(new SendMail($post, $triggered_user, $data));

これでメール送信処理のジョブがテーブルの中に並ぶようになる。

あとはジョブが受付に並んだらそれを検知して実際に処理をしてくれる人が必要だ。

ワーカーを立ち上げる

ジョブを処理するプロセスを起動させる。

php artisan queue:work --tries=3

このワーカーは手動でストップするか、ターミナルを閉じるまで起動している。
バックグラウンドで永続的に起動させておきたい場合は、Supervisorなどのプロセス永続化ツールを導入しよう。

これでメール送信のほうはキュー化できた。

いったん話をメール送信だけにしたいので、大量処理の部分はコメントアウトしてからいいねボタンを押してみて欲しい。

app/Listeners/SendPublicAlert.php

<?php
.
.
.
        // 大量の処理を追加!!
        // for ($i=0;$i<5000;$i++) {
        //     $users = User::whereNotIn('id', [$triggered_user->id])->get();
        // }

Chromeのデブツールなどで観察すれば、すぐにレスポンスが返ってきていることが分かると思う。

ターミナルに処理ログが表示されていればキューで処理が実行されている。

でもまだ大量インサートの問題が残っている。
もちろん、それもキューにしたい!

リスナーをキュー化する

Laravelコマンドでgenerateすると、リスナーファイルは最初からこのShouldQueueというインターフェースを読み込んでいる。

このIlluminate\Contracts\Queue\ShouldQueueをリスナークラスに実装してあげると、キューにして処理させるべきクラスだということをLaravelに知らせることができる。

app/Listeners/SendPublicAlert.php

<?php
.
.
.
class SendPublicAlert implements ShouldQueue
{

では、さっそく大量処理部分のコメントアウトを外して、いいねボタンを押してみよう。

f:id:arm4:20170911180222p:plain

コードの変更が反映されてないと思ったら、いったんワーカーを立ち上げ直してみよう。

デーモンで立ち上げている場合は、起動時のコードがメモリにロードされていてそれで実行されてしまうためだ。

まとめ

いやーまとめるのは時間かかったけど、こうやって読み返すと、Laravelで重たい処理をキュー化するのが、いかに簡単かということが分かっていただけたことと思います!!

バッチだけじゃなくて、キューで処理というのも、いろんなことに活用できるような気がしますね。

キューにしなくても、イベント&リスナーの実装はコードが整理されて気持ちよくコーディングできるのでいいなと思いましたYO★

参考になった人は是非★をください!!

▼サンプルコードをgithubにアップしたよーー。

github.com

Laravel5.2だってPHPUnit使ってE2EテストしたいんだYO

皆さん、こんばんわ。

そういえば、ベル子じゃなくてアルマさんになったことを、すっかり忘れていました。
別に名前など、どっちでもいいのだが。

SPAを作ってると、PHPUnitでテストするのには限界がありますというか、テストしたいとこはjsで作ってるとこなんです。
そうなんです。

というわけで、「SPA Laravel テスト」みたいにググると「もしかして、Laravel Dusk?」と表示して・・・くれるまではいかないんですが、Laravelの新しいブラウザーテストパッケージであるLaravel Duskというものの存在に辿り着きます。

さすが、Laravelと思ったのも束の間、私が今作ってるアプリのLaravelのバージョンは5.2。
Laravel Duskが正式に発表されたのはLaravel5.4から。

ベイビー、これは雲行きが怪しいぜ★

開発初期の頃はもしかして5.2で作ってないかなー?と思って githubリポジトリv1.0.0のcomposer.jsonを確認してみた。
Laravel5.3に依存しているようだ。

"illuminate/support": "~5.3",

github.com

なるほどね。
使え無さそうよ。

というわけで、さっきの検索でちらほら出てきたSeleniumという、まるでどこかのブラウザーの名前を彷彿とさせるテストツールについて調べてみようと思ったわけです。

Seleniumの作者は元Googleの人で、現行バージョンのSeleniumはWebDriverという別のツールを統合して作ったものらしい。 WebDriverの作者も元Googleのエンジニアで現Facebookの人だそうです。

どうりでネーミングセンスが同じなわけだね!

と、ここまでまるでSeleniumという1つの何かのソフトウェアが存在するような口ぶりで話を進めてきたが、実は厳密に言うとSeleniumというソフトウェアは存在しない。

告白しよう。
私は「なるほど、Seleniumをインストールすればいいんですね」のような気持ちでいた。

これがSeleniumを導入したくてググってもドンピシャな答えがなかなか出てこない最大の理由である。

要するにSeleniumとは、ブラウザをコードで制御してE2Eテストを実現させるためのツール群の総称、そしてその環境自体(エコシステム)を指す名称ということです。

なので、Seleniumテスト環境を作るために導入しないとならないツールがいろいろあって、それぞれのアプリケーションの言語なんかによってもいろいろな構築方法があるのです。

でも、そんなこと言われても逆に困ります。
私はただ、早くテストがしたいだけなんだ。

そうだ、Laravel Duskと同じようにすればええやん。

中身を読んでみると、Laravel Duskは、facebook/php-webdriverのラッパーツールのようなものっぽい。

php-webdriverはWebDriverをPHPで操作するライブラリだそうな。

Laravel Duskの場合は、スタンドアロンのChromeDriverをサーバを立てずに直接操作しているようなんですが、私は普通に一般的っぽいselenium-server-standaloneを立ち上げて、それにリクエストを投げて中継してもらってWebDriverを起動するという方法で構築してみることにしました。

突然、難しくなってきて涙目の人は下のリンクが分かりやすいので、そこで勉強してきてから戻ってきてください。

app.codegrid.net

これらの環境を整えたら、あとはもうLaravelのテスト書くところにfacebook/php-webdriverのクラスを読み込んでテストコードを書くだけ。 何も難しいことはありません。

まだ研究中なので、何か認識間違いなんかもあるかもしれませんが、また分かってきたことがあったらブログにしたためようと思うよ★楽しみに待っててね。

↓導入の具体的な手順はQiitaにまとめました。

qiita.com

これを今度、社内で説明会するんだけど、うまいこと説明できるかなー。ブログにまとめてもややこしーなー。

Laravel5.4でイベント&キューを使ってメールをキュー送信する手順(2)

8月はコラボキャンプなどの準備もあって、だいぶ時間が空いてしまいました。。

気を取り直して、メールをキュー送信する手順の続きを書いていきますよ★

↓前回の記事はこちら arm4.hatenablog.com

今北さんのために(1)を3行にまとめます。

  • Laravelのアプリを作って初期設定をした
  • ユーザーがログインできるようにした
  • ダミーのユーザーとデータを作った

まとめたら、逆にちょっと悲しくなりましたが、頑張って続きを書くぞ!

ダミーの投稿記事データを作ったので投稿記事一覧ページを作成する

まずはviewを作ろう。

もう一度おさらいするが、今回つくるのはブログ記事にユーザが「いいね」を付けられるようなアプリケーションである。

記事の本文とそれに「いいね」する用の「いいね」ボタンを設置しておけばいいだろう。

今回は簡単に、ホーム画面に一覧を表示することにするので/resources/views/home.blade.phpを以下のように書き換える。

@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            @include('_alert')
        @foreach($posts as $post)
            @include('_post_panel')
        @endforeach
            <div class="text-center">{{ $posts->links() }}</div>
        </div>
    </div>
</div>
@endsection

投稿記事の部分、メール送信のお知らせメッセージを表示する部分などは詳細ページなどでも使いそうなので、パーシャルファイルにしておく。

/resources/views/_post_panel.blade.php

<div class="panel panel-default js-post" data-post_id="{{ $post->id }}">
    <div class="panel-heading">{{ $post->title }}</div>
    <div class="panel-body">
        <p class="small text-right">{{ $post->user->name }} {{ $post->updated_at->format('Y-m-d') }}</p>
        {!! nl2br(e($post->content)) !!}
    </div>
    <div class="panel-footer text-center">
        <button class="btn btn-primary js-send-stamp" data-type="like" type="button">いいねする</button>
        <button class="btn btn-danger js-send-stamp" data-type="dislike" type="button">やだねする</button>
        <button class="btn btn-info js-send-stamp" data-type="whatever" type="button">どうでもいい</button>
        <button class="btn btn-warning js-send-stamp" data-type="yourname" type="button">君の名は?</button>
    </div>
</div>

/resources/views/_alert.blade.php

<div class="alert alert-success hidden">
    <strong>メール完了</strong> メールを送りました!
</div>

今回はviewsディレクトリの直下にパーシャルファイルを突っ込んでしまったが、本番のアプリケーションでは/resources/views/partialsなどのようにパーシャルファイルだけを入れておくディレクトリを切ったほうがいいだろう。

投稿記事を1つだけ表示する詳細画面のviewも作る

/resources/views/post.blade.php

@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            @include('_alert')
            
            @include('_post_panel')
        </div>
    </div>
</div>
@endsection

詳細画面のルーティング設定を追加

詳細画面用のルーティングを追加しておこう。

routing設定では、{}で囲まれた部分でパラメータを受け取ることができるのだが、この変数名をコントローラーのほうの引数のタイプヒンティングされた変数と合わせておくと
パラメータとして渡されたidに対応するModelのインスタンスをLaravelが自動的に取得してきてくれる。

もし見つからなかった場合は404を返してくる。

これがLaravelのルートモデルバインディングという機構である。

これで、いちいちコントローラーのアクションの中でModel::find($id)のように当たり前のことを何度も書かなくてよくなるので、コントローラーを綺麗に保つことができる。

とても便利!!

/routes/web.php

・
・
Route::get('/home', 'HomeController@index')->name('home');
Route::get('/posts/{post}', 'HomeController@posts')->name('posts.detail');

コントローラーの設定をする

詳細画面のアクションでは、ルートモデルバインディングを使いたいのでPostクラスをタイプヒンティングし変数をルーティングのほうと揃えておく。

/app/Http/Controllers/HomeController.php

<?php

namespace App\Http\Controllers;

use App\Post;

class HomeController extends Controller
{
・
・
・
    public function index()
    {
        $posts = Post::latest('updated_at')
                            ->paginate(5);
        return view(
                        'home',
                        compact('posts')
                    );
    }

    public function posts(Post $post)
    {
        return view(
                        'post',
                        compact('post')
                    );
    }

ここまで設定したらページがちゃんと表示されているか確認してみよう。

これで必要な画面が出来上がった。

メール送信の設定をする

fromはアプリケーションから送信される際の送信元の設定。

toのほうは通常は設定しないのだが、開発時に送信されるメールを確認したい&一意のアドレスに送信したい場合のみ、このように設定するとコードで送信先を設定しても、必ずここのtoで設定されたアドレスに送信されるので、開発時は設定しておくことをオススメする。

toにはダミーのアドレスではなく普段自分が使っている生きているアドレスを書くこと!

そうじゃないとテストのメールが送信されないのでメールが送信できているかどうか確認ができない。

config/mail.php

    'from' => [
        'address' => 'noreply@example.com',
        'name' => 'mailable_test',
    ],

    'to' => [
        'address' => 'test@example.com',
        'name' => 'お客様',
    ],

メールドライバをsendmailに設定。

.env

MAIL_DRIVER=sendmail

メール送信用のクラスを作成

いよいよ本題のメール送信部分を作っていく。

今回はLaravel5.3から実装されたMailableというメール専用クラスを作成して実装していくことにする。

ひとまず私はSendMailという名前でクラスを作ることにした。

本当のアプリケーションでは様々なメールを送信するはずなので、登録お知らせメールならRegisteredとか、出荷お知らせメールならOrderShippedなどと「何をした」のように命名するとよさげ。

php artisan make:mail SendMail

/app/Mail/というディレクトリは、プロジェクト作成時は存在しないが、コマンドを実行すると勝手に作成されて、その中にSendMail.phpというファイルが格納される。

それでは作成されたSendMailを編集していこう。

/app/Mail/SendMail.php

<?php
use App\Post;
use App\User;
・
・
・
     public $post;
     public $triggered_user;
     public $data;

    /**
     * Create a new message instance.
     *
     * @return void
     */
    public function __construct(Post $post, User $triggered_user, $data)
    {
        $this->post = $post;
        $this->triggered_user = $triggered_user;
        $this->data = $data;
    }

    /**
     * Build the message.
     *
     * @return $this
     */
    public function build()
    {
        return $this->view('emails.alert')
                    ->subject(env('APP_NAME').'からの通知')
                    ->with([
                            'to_user' => $this->to[0],
                        ]);
    }

メールテンプレート用のviewを作成

/resources/views/emails/alert.blade.php

<div>{{ $to_user['name'] }}さんへのお知らせです。</div>
<div>{{ $triggered_user->name }}さんが「{{ $post->title }}」という投稿に{{ $data['type'] }}しました!</div>
<br>
<div>▼投稿を確認する</div>
{{ route('posts.detail', ['post' => $post->id]) }}

コントローラーにいいね送信用のアクションを追加する

/app/Http/Controllers/HomeController.php

<?php
・
・
use Auth;
use App\Post;
use App\Mail\SendMail;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;

class HomeController extends Controller
{
・
・
・
    public function sendLike(Request $request, Post $post)
    {
        $triggered_user = Auth::user();

        $data = [
            'type' => 'いいね',
        ];

        Mail::to($post->user)
                ->send(new SendMail($post, $triggered_user, $data));

        return response()->json(['status' => 0]);
    }

いいね用ルーティングを追加する

/routes/web.php

<?php

Route::post('/send-like/{post}', 'HomeController@sendLike')->name('send.like');

app.jsにajaxでpostするコードを追加

今回はajaxでpostすることにする。

app.jsに以下のコードを追加しよう。

/resources/assets/js/app.js

$.ajaxSetup({
    headers: {
        'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
    }
});

$(function() {
   $(document).on('click', '.js-send-stamp', (e)=> {
        const $this = $(e.target);
        const type = $this.data('type');
        const post_id = $this.closest('.js-post').data('post_id');

        var data = {
            type,
        };

        $.ajax({
            url: '/send-' + type + '/' + post_id,
            type: 'POST',
            dataType: 'json',
            data: data,
        })
        .done(function(result) {
            $('.alert').removeClass('hidden');
            console.log("success");
        })
        .fail(function() {
            console.log("error");
        })
        .always(function() {
            console.log("complete");
        });
   })
});

f:id:arm4:20170906170139p:plain

「いいねする」ボタンをクリックして「メール完了 メールを送りました!」のお知らせが表示されていたら、toに設定したメールアドレスの受信箱を確認してみよう。

メールが届いていたら、ひとまずメールの送信まではOKだ。

いいねを拡張し、様々なパターンのお知らせを作成する

ここまでは、初心者用チュートリアルと同じようなことしか書いていないので引っかかる場所はないと思う。

今度はこのコードを拡張して「やだねする」「どうでもいい」「君の名は?」ボタンの実装を行っていく。

ここでいったん、コードを提示する前に要件を確認していこう。

君はプログラマだ。

ブログに「いいね」されると、記事を書いた本人にメールでお知らせしてくれるアプリケーションの実装は完了している。

ここで、SEにユーザーのリアクションを「いいねする」「やだねする」「どうでもいい」「君の名は?」という4種類に拡張したいと言われる。

さらに、4種類のボタンを押した際のお知らせメールの送信先を以下のようにしたいとも言われた。

  • いいねのときは登録ユーザー全員に知らせる
  • やだねのときは本人だけに知らせる
  • どうでもいいときは誰にも知らせない
  • 君の名は?のときは本人だけに知らせる

さて、どのようにコードを修正するべきだろうか。

コントローラーのアクションを4種類作って、その中に別々の処理を書く?

それとも1つのアクションでif文をつかって分岐させる?

urlを別々にしたいなら、「やだね」と「君の名は」の処理は一緒だからコントローラーにprivateメソッドを作って呼び出すようにする?

様々な実装方法があると思うが、今回はそれぞれ別アクションにして、そのアクション内でイベントを発火させて、そのイベントが発火したら実行されるリスナークラスにメール送信のコードを書いていこうと思う。

このようにすれば、今後いろいろなボタンが増えたとしても、メール送信パターンは決まっているだろうから、簡単に拡張することができる。

もし、アクションをやっぱり1つにして分岐にしようと思っても容易に対応できる。

さらに、イベントリスナーを使うメリットとしては、そのリスナーの実行コードを簡単にキュー化(非同期処理)にできるので、例えばユーザーが2000人いて、そのユーザー全員にメールを送信しなければいけない場合でも、イベント発火後にバックグラウンドで送信処理をしてくれるため、ユーザーは送信処理の終了を待たずに、次のアクションに移れるというわけだ。

ということで次回は、今回の手順の肝であるイベントを作成していく。

待て、次回!!

Laravelで入力値が可変長arrayのときにwithInputしてInput::old()で取りたい時の対処法

いつものようにB'zの曲名くらい長いタイトルですが、
そのまんまです。

ここで問題です。でーでん♪

あなたは、特定のグループ、またはグループのサブグループを複数指定して、一気に絞り込み検索できる機能を追加して欲しいと言われました。

たとえばこんな感じに

愉快な仲間たち > お絵描き部
チーム★イケメン > ボル部

さて、どんなUIでどんな入力値をpostしますか?


こういうときに、指定するか分からないセレクトをいっぱい並べるのは気持ち悪いですし、グループが100個くらいあったらどうしようって感じです。

select2を使うという方法もありますが、今回はグループのサブグループを指定する際に、親グループを選択したらparent_idが親グループのサブグループのみが表示されて、それを選択したい要件があり、select2のような並列選択では具合が悪いです。

ということで、私の場合は親グループとサブグループを選択するセレクトを2つおいて、そのセットを動的に追加するボタンを設置することにしました。

これで無限にグループを指定できます。

グループを設定して絞り込むまでは良かったんですが、 ふとwithInput()で返しているInput::old()はどのように書けばいいか迷ったので、私みたいに迷ってしまう人がいそうなので、ブログにまとめておきます。

※以下はLaravel4.2のコードです。5系の人はファサードなどを適宜修正してください。

こういう感じで

<?php

return Redirect::to('group_search')->withInput()

のようにすると、arrayで値を渡した場合はInput::old(‘groups.0.main’)という感じでピリオドで階層を掘って値が取得できます。

なので

@foreach (Input::old('groups') as $i => $group)
<select class="form-control" name="groups[{{$i}}][main]">
      <option value="">--指定なし--</option>
      <option value="1"{{{ (int)$group['main'] == 1) ? ' selected="selected"' : '' }}}>愉快な仲間たち</option>
      <option value="2"{{{ (int)$group['main'] == 2) ? ' selected="selected"' : '' }}}>チーム★イケメン</option>
</select>
<select class="form-control" name="groups[{{$i}}][sub]">
      <option value="">--指定なし--</option>
      <option value="1"{{{ (int)$group['sub'] == 1) ? ' selected="selected"' : '' }}}>お絵描き部</option>
      <option value="2"{{{ (int)$group['sub'] == 2) ? ' selected="selected"' : '' }}}>ボル部</option>
</select>
@endforeach

こういう感じで、入力値とhtmlを元に戻してやることができます。

可変長なので、foreachで0の部分を$iにして回せばいいんですね。
値はキーを指定すれば取得出来ますし、Input::old(‘groups.’.$i.‘.main’)のようにしても取得できます。

Input::old(‘groups’)がないデフォルトの入力の場合は、

<?php
    $groups[0] = [
        'main' => null,
        'sub' => null,
    ];
?>

みたいな感じで1個だけ初期のセレクトをセットしてやればよいです。

実際には、selectの部分はHTMLのmacroなどを使ったりするので、もっと簡潔で動的に書けます。

動的にセレクトを増やす部分のjsは、こんな感じです。
こちらはcloneして連番部分を振り直してあげるだけです。

    $(document).on('click', '.js-add-group-select', function(e){
        $('.js-group-select').first()
                                .clone()
                                .find('option')
                                    .attr("selected",false)
                                .end()
                                .insertAfter('.js-group-select:last');

        $('.js-group-select').each(function(i, el) {
            var inputs = ['main', 'sub'];
            var self = $(this);
            $.each(inputs, function(j, val){
                var $select = self.find('[name*="['+ val +']"]');
                var name = $select.attr('name');

                var reg = new RegExp("(groups\\[)(\\d)(\\]\\["+val+"\\])", "g");
                var replacer = '$1'+i+'$3';
                var newName = name.replace(reg, replacer);
                $select.attr('name', newName);
            });
        });
    });

最後に、これを読んでくれた君に、私が崇拝するベルばらの名言より、この言葉を贈ります。

君はWebサービスの影になれ
光ある限り存在をかたちづくる影となって無言のままそいつづけるがいい

アデュー

by ベル子