20191218のPHPに関する記事は23件です。

phpからPDOでmysql接続できなかった話

phpでPODを使ってmysqlに接続すると、
Uncaught PDOException: SQLSTATE[HY000] [1045] Access denied for user
このエラーが出てきて、ググった方法を試しても全く解決されなかった。
ユーザー名もパスワードも合ってるし、環境設定もできてるし、php.iniの設定もできてるし、ユーザー権限もALLになってるし、、、、
そこでもう一回上記の確認をしてみると、、、、、

あ!!!!!!!

ユーザー権限のdb名の部分が間違っていた、、、、
db名を今使ってる名前にして、楽々接続完了!!!

db周りの環境設定とかエラーとかすごい苦手だったけど、今回のことで少し詳しなったのでmysqlインストール後の設定方法に関して一応整理しておく。

①mysqlインストール
②php.iniでpdo部分のコメントアウト
③Apacheの再起動
④.bashrcにPATHを通す
⑤mysqlを起動後にユーザー権限を付与

とりあえず、dbのエラー発生したら、①PATHが通っているのか(.bashrc)②ユーザー権限が合っているのかを真っ先に確認すればいいんだね。

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

$_POST は万能じゃなかった!(困って学ぶ日々の備忘録)

痛い目に合いつつ学んだ内容をただ残していくだけの記事ですが、調べて日本語の記事があんまりなかったのでまずは簡単な内容でも助かる方がいるかも!と思い、公開。

タイトル通りですが、GET/POST で送られてくるデータを受け取る時は何もかも $_POST$_GET で受け取ってしまえばいい、わけではないのです。
長い前書きは抜きにして、今日は「PHP php://input$_GET / $_POST の違い、使い分け」について最近ちょっと困った結果学んだ情報を軽く整理してみました。

$_POST は万能じゃない

受け取れる情報の量の許容範囲が $_GET よりあるからといって便利に思われがちですが、そうではにないことを最近実感した。
なんと・・・受信する殆どのデータが問題なのに、たま~にだけなぜか文字列が変なところで途切れたりして、構造が壊れ化けてしまうという・・・

ちょっと調べたら:
- XMLの受信
- JQueryからのAjaxリクエスト

には向いていないという。
今まであんまり意識せずにフレームワークのInputクラス(つまり、さらなる$_POSTラッパー)を使ったりしていましたが、本来は用途に応じて使い分ける必要があるとわかりました。

$_POSTを使って良いケース

以下のコンテンツタイプのみ、phpのラッパーである $_POST は良い仕事をするという大前提があるようです。これは「すべてのユーザーエージェントによってサポート対象であるコンテンツタイプだから」であって、それらのみが保証されているということのようです。

  • application/x-www-form-urlencoded
  • multipart/form-data

HTMLフォームで送信できる内容に限って使うイメージを持てば安全かなと思います。
ちなみに、私の場合はヘッダーが「application/x-www-form-urlencoded」で送られてきていた xml も化けてしまったので、ヘッダーのみで安心するのもちょっと厳しい気がします。

$_POSTを避けたほうがいいケース

  • JSON を送信するとき(コンテンツタイプが application/json )
  • xml を送信するとき(コンテンツタイプが application/xml )
  • YAML を送信するとき(コンテンツタイプが application/yaml )

上記(というか、使って良いケース以外!)の場合 $_POST に格納されたデータが壊れるケースが多いようです。壊れからも様々で、最初はただ「うまくURLデコードできないかな?」と思ってしまうレベルだったりします。
他順なデータでテストしても問題ないのに、ちょっと特殊な文字が含まれ送られてきた分だけ文字化けして、結局形式が壊れて例えば XML としてパースできないデータになってしまう場合がります。

$_POST が使えない場合は?

php://input から生のデータを引っ張り出す。
ただし、この場合は $_POST の恩恵(データをURLデコードしてくれちゃったり、配列にしてくれるあたり)が受けない分、全部自分で実装しないといけない。
送られてくるデータは、ただの(おそらくURLエンコードまたはRAW URLエンコードされた) string になるので、ある意味「自由に何でもできる状態」ではあります。

今回参考になった記事

https://stackoverflow.com/questions/8893574/php-php-input-vs-post

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

CakePHPのドキュメント翻訳は英語学習とフレームワークのキャッチアップにつながる

この記事は、PHP Advent Calendar 2019の18日目の記事です。

TL;DR

  • CakePHPなどフレームワークの多くには、コミュニティでメンテされている日本語翻訳ドキュメントが存在する
  • ドキュメントの変更を追って自ら翻訳対応していくことで、英語力とフレームワーク自身の理解が進む

CakePHP Book

つい最近、メジャーバージョンアップ(4)がリリースされたことで話題(!?)のCakePHPには、開発者向けのドキュメントとしてCakePHP Bookというものが公開されています。

https://book.cakephp.org/4/en/index.html

CakePHPを触ったことがある方なら一度は訪れたことがあるサイトかと思います。CakePHPはOSSですので、もちろんこのサイト自体も、GitHubに公開されています。

https://github.com/cakephp/docs

こちらのサイトは、様々な言語で翻訳されており、特に日本語ドキュメントのメンテナンスも盛んに行われています。

筆者自身も昨年からちらほらと翻訳やもとの英語ドキュメントを修正したりしています。

Screen Shot 2019-12-18 at 20.48.47.png

https://github.com/cakephp/docs/pulls?q=is%3Apr+author%3Ahgsgtk+is%3Aclosed

山があるのは、マイナーバージョンアップ時の移行ガイドを翻訳した際のものです。

3.7 Migration Guide: https://github.com/cakephp/docs/pull/5950
3.8 Migration Guide: https://github.com/cakephp/docs/pull/6128
3.9 Migration Guide: https://github.com/cakephp/docs/pull/6228

CakePHP Bookの翻訳を継続してやってみると色々メリットがあったので紹介して、あわゆくば、読んでいただいた方々をこちらの沼に誘えればと思います。

ドキュメント翻訳のメリット

仕様の変化をおえる

これは、特に移行ガイドを翻訳する際の気づきですが、ただ流し読みするのではなく、一文づつ翻訳していくため、どのような変更が行われたのかを知ることに繋がります。また、どう訳していいかわからない場面において、一度その機能の仕様自体を調べるので、普段業務で触れない部分についての理解を深めることが出来ます。

さらに、継続してバージョンアップの移行ガイドをやっていくと、ちょっとした傾向みたいな部分も見えてきます。とくにCakePHP3.6以降は、CakePHP4に向けた非推奨機能の追加が多かったのですが、なんとなくの情報として得るのと、実体験としてドキュメントメンテナンスを通じて知るのでは大きな違いがあります。

対応する技術的な英語文を知れる

シンプルですが、英語文を訳して読んでいく際に、普段、日本語で見聞きする技術的単語や文章がどういうふうに表現されるのかを知る切っ掛けに繋がります。「英語がなければなるべく翻訳されたやつがいい」という方でも、強制的に英語に立ち向かう土俵に立つので、強制的な英語学習となります。

コミュニティのコアな方々と触れ合える

ドキュメントをメンテナンスするような方は、そのフレームワークにおいて、非常に熱量の高い方々が多いです。そのような方々とGitHub上で触れ合えることは刺激になります。また、そこでやっておくとリアルであった際にすでにGitHub上でコミュニケーションを取っているので話の種に繋がります。コミュ力がなくても事前に知ってもらえるのはメリットですね。

ドキュメントを翻訳したいぞと思ったら

CakePHP Cookbook を直す方法(表示確認してからプルリクエストを出すまで)」という記事にて、 @tenkoma さんが環境構築の仕方から丁寧に解説してくださっています(さすが丁寧のてんこまさん!)。CakePHP Bookは、PHPコミュニティで広く知られている上に、修正ポイントが多く、コントリビュートチャンスは非常に多くなっています。ぜひ、チャレンジしてみてください!

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

PHPでお問い合わせフォームを作る

PHPでお問い合わせフォームを作ってみよう

最近、新人君たちにプログラミングを教える機会が多くなってきました。
教えていく中で「自分の教え方って大丈夫なんだろうか?」と最近不安になっている次第。
という事で簡単なお問い合わせフォームを作る事で、人への教え方を再考するために「お問い合わせフォーム」を題材にちょっとずつ難しい事を実装していきます。
そして、最終的には実戦で使えるようなフォームを作れるようになります。

フォームの全体の流れ

  1. 入力画面
  2. 確認画面
  3. 完了画面

てな感じで

じゃあ、もうちょっと細かくみてみましょう

入力画面

うん、これがないと始まらないですね。
今回はお決まりの以下の入力項目にしてみます
項目1 お名前
項目2 メールアドレス
項目3 お問い合わせ内容

お、お、ぉぅ 最低限の設定やね。。今後拡張していく予定なので我慢してください。。。

確認画面

そう、ここで入力した内容を表示して「送っていいですか?」って確認させます。

完了画面

要するに「ありがとうございました」ページです
ここでメール送信とかデータベースに接続とかいろいろあるけど、とりあえずはメール送信しときましょう

細かいとこ

とりあえずPHPで動く事を目標にするのでCSS等は後回しにします

入力画面

取り敢えず最低限のこのhtmlで

index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<form action="./confirm.php" method="post">
    <div id="contents">
        <input type="text" name="name">
        </br>
        <input type="text" name="mail">
        </br>
        <textarea name="toiawase"></textarea>
        </br>
        </br>
        <input type="submit" value="確認">
    </div>
</form>
</body>
</html>

味気の無い画面ですね。
name="name" のテキストボックスが名前を入れるところです
name="mail" がメールアドレス、テキストエリアに問い合わせ内容ですね
そして formタグで 確認ボタンを押した後のリクエスト先が指定されています
action=./confirm.php" です。この記述で、ボタンを押したらconfirm.phpにリクエストを投げます。
そして method="post の記述でリクエスト時はPOSTでデータを投げる、と指定しています。

確認画面

入力画面で確認ボタンをクリックすると form タグの指定通り confirm.php にリクエストが投げられます。

confirm.php
<?php

    $name = $_POST['name'];
    $mail = $_POST['mail'];
    $toiawase = $_POST['toiawase'];

echo "
<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8'>
<title>Insert title here</title>
</head>
<body>
お名前:$name
</br>
メールアドレス:$mail
</br>
お問い合わせ内容
</br>
$toiawase
</br>
</br>
<form action='complete.php' method='post'>
<input type='hidden' name='name' value='$name'>
<input type='hidden' name='mail' value='$mail'>
<input type='hidden' name='toiawase' value='$toiawase'>
<input type='button' onclick='history.back()' value='戻る''>
<input type='submit' value='確認'>
</form>
</body>
</html>";

はい、必要最低限の記述です。
index.html で入力された内容が表示されると思います。
ここ(confirm.php)で、本当はやっておかなきゃいけない事、やっておいた方が良い事、やらない方が良い事を書いておきます

やっておかなきゃいけない事
postで投げられたデータのチェック(nullチェック、文字数チェック、その他もろもろチェック)

やっておいた方が良い事
セッションへの値保存、テンプレートの使用

やらない方が良い事
hidden タグの使用

これらのやり方、対応方法は後々の記事で書いていきます。
ここまでで データの入力、データの送信、データの受信、データの表示まで出来ました。
この画面で確認がOKであれば 次へボタン をクリックして完了画面に移ります

確認画面

complete.php
<?php

    $name = $_POST['name'];
    $mail = $_POST['mail'];
    $toiawase = $_POST['toiawase'];

    $mailTO = "test@test.com"; // メールの送信先
    $mailHeader = "From: from@from.com"; // メールの送信者
    $mailSubject = "お問い合わせありがとうございます"; // メールの件名
    $mailBody = '
    $name 様\r\n
    お問い合わせありがとうございます\r\n
    \r\n
    ご返信まで~~~~';

    mail($mailTO, $mailSubject, $mailBody, $mailHeader);


echo "
<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8'>
<title>Insert title here</title>
</head>
<body>
お問い合わせありがとうございます。
</body>
</html>
";

はい、これでデータ(名前、メールアドレス、問い合わせ内容)を入力し、データを送信
confirm.php で受け取ったデータの表示、データの送信
complete.php でデータ受け取り、メール送信、完了画面の表示
ここまで出来ました。

最後に

今回書いた内容は本当に必要最低限の流れですが、次回からこのフォームに肉付けしていこうと思います。

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

画像の動的resize on Laravel

<img src="/path/to/img.jpg" >

って書いてて、あなんかこの画像やっぱ重いなぁと思ったら

<img src="{{ resize('/path/to/img.jpg' ,200, 200) }}" >
<!-- /storage/cache/aerwsvv4w4cey75hf.jpg のように置換されます-->

って書き換えるだけで

  • サムネイル生成(初回アクセス時のみ)
  • 2回目以後のアクセスは、キャッシュ画像に直接アクセス
  • サムネイルは、画像の更新や指定サイズの変更タイミングで自動更新

される実装です。

command
$ composer require intervention/image
$ php artisan storage:link
$ mkdir storage/app/public/cache
DynamicImage.php
<?php

namespace App\Services;

use Intervention\Image\Image;

/**
 * Class DynamicThumb
 * @package App\Services
 */
class DynamicImage
{
    const CACHE_DIR = 'cache';
    const IMG_QUALITY = 80;

    /**
     * resize処理
     * @param string $imagePath
     * @param array $query
     * @return string|null
     */
    public static function resize(string $imagePath, array $query): string
    {
        return self::execute($imagePath, __METHOD__, $query);
    }

    /**
     * crop処理
     * @param string $imagePath
     * @param array $query
     * @return string|null
     */
    public static function crop(string $imagePath, array $query): string
    {
        return self::execute($imagePath, __METHOD__, $query);
    }

    /**
     * fit処理
     * @param string $imagePath
     * @param array $query
     * @return string|null
     */
    public static function fit(string $imagePath, array $query): string
    {
        return self::execute($imagePath, __METHOD__, $query);
    }

    /**
     * キャッシュがなければ生成して publicパスを返す
     * @param string $imagePath
     * @param string $action
     * @param array $query
     * @return string|null
     */
    private static function execute(string $imagePath, string $action, array $query): ?string
    {
        if (!self::cacheExists($imagePath, $query)) {

            $image = \Image::make(public_path($imagePath));

            $action = $action . 'Image';
            $image = $action($image, $query);

            $image->save(self::getFullPathOfCacheImage($imagePath, $query), self::IMG_QUALITY);
        }

        return self::getPublicPathOfCacheImage($imagePath, $query);
    }

    /**
     * キャッシュファイル存在するか?
     * @param string $imagePath
     * @param array $query
     * @return bool
     */
    private static function cacheExists(string $imagePath, array $query): bool
    {
        return file_exists(self::getFullPathOfCacheImage($imagePath, $query));
    }

    /**
     * キャッシュディレクトリのpublicパス
     * @return string
     */
    private static function getPublicPathOfCacheDir(): string
    {
        return '/storage/' . self::CACHE_DIR . '/';
    }

    /**
     * キャッシュディレクトリのフルパス
     * @return string
     */
    private static function getFullPathOfCacheDir(): string
    {
        return storage_path('app/public/' . self::CACHE_DIR) . '/';
    }

    /**
     * キャッシュ画像のbaseName
     * @param string $imagePath
     * @param array $query
     * @return string
     */
    private static function getBaseNameOfCacheImage(string $imagePath, array $query): string
    {
        return md5(http_build_query([
            'path' => $imagePath,
            'query' => $query,
            'modified' => filemtime(public_path($imagePath))
        ])) . '.jpg';
    }

    /**
     * キャッシュ画像のpublicパス
     * @param string $imagePath
     * @param array $query
     * @return string
     */
    private static function getPublicPathOfCacheImage(string $imagePath, array $query): string
    {
        return self::getPublicPathOfCacheDir() . self::getBaseNameOfCacheImage($imagePath, $query);
    }

    /**
     * キャッシュ画像のフルパス
     * @param string $imagePath
     * @param array $query
     * @return string
     */
    private static function getFullPathOfCacheImage(string $imagePath, array $query): string
    {
        return self::getFullPathOfCacheDir() . self::getBaseNameOfCacheImage($imagePath, $query);
    }

    /**
     * $width x $height に収まるよう縮小
     * @param Image $image
     * @param array $query
     * @return Image
     */
    private static function resizeImage(Image $image, array $query): Image
    {
        $width = $height = null;
        extract($query);

        if ($image->width() < $image->height()) {
            $image->resize($width, null, function ($constraint) {
                $constraint->aspectRatio();
                $constraint->upsize();
            });
        } else {
            $image->resize(null, $height, function ($constraint) {
                $constraint->aspectRatio();
                $constraint->upsize();
            });
        }

        return $image;
    }

    /**
     * $position(top, center ,bottom)を起点に、$width x $height に収まるようトリミング
     * @param Image $image
     * @param array $query
     * @return Image
     */
    private static function fitImage(Image $image, array $query): Image
    {
        $width = $height = $position = null;
        extract($query);

        $image->fit($width, $height, function ($constraint) {
            $constraint->upsize();
        }, $position ?? 'center');

        return $image;
    }

    /**
     * $x x $yを起点に $width x $height で切り出す
     * @param Image $image
     * @param array $query
     * @return Image
     */
    private static function cropImage(Image $image, array $query): Image
    {
        $width = $height = $x = $y = null;
        extract($query);

        if ($x) {
            $image->crop($width, $height, $x, $y);
        } else {
            $image->crop($width, $height);
        }

        return $image;
    }
}
helpers.php
<?php

use App\Services\DynamicImage;


if(!function_exists('resize')){

    function resize(string $basePath, int $width, int $height): string
    {
        return DynamicImage::resize($basePath, compact('width', 'height'));
    }
}

if(!function_exists('crop')){

    function crop(string $basePath, int $width, int $height, ?int $x = null, ?int $y = null): string
    {
        return DynamicImage::crop($basePath, compact('width', 'height', 'x', 'y'));
    }
}

if(!function_exists('fit')){

    function fit(string $basePath, int $width, int $height, ?string $position = null): string
    {
        return DynamicImage::fit($basePath, compact('width', 'height', 'position'));
    }
}



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

php 連想配列の特定のキーの値を取得する

array_columnを使う。

$targetValueArray = array_column($originArray, ‘id’); //keyがidの値を配列として取得

取得した配列の重複を削除したい場合、array_unique

$uniqueArray = array_unique($targetValueArray);

//重複削除された際にキー(連番)が飛び飛びになってしまうため、添番を振り直す
$uniqueArray = array_value($uniqueArray);
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Laravel】PUT、DELETEリクエスト(疑似フォームメソッドについて)

Laravelのhttpメソッドではgetリクエストとpostリクエストしか対応していない。
公式ドキュメント(https://readouble.com/laravel/5.6/ja/routing.html)

PUTやDELETEを使う際には、formで一旦POSTメソッドを指定して、hiddenで_methodプロパティ(隠しメソッド)を投げる。(疑似フォームメソッド)

laravelのBladeではディレクティブが使える

<form action="/foo/bar" method="POST">
    @csrf
    @method("PUT")
</form>

要するにこういうこと

<form action="/foo/bar" method="POST">
    <input type="hidden" name="_method" value="PUT">
    <input type="hidden" name="_token" value="{{ csrf_token() }}">
</form>

ルートの定義はputでOK

Route::put($uri, $callback);
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Magento2+Vagrant+xdebug remote debug

Xdebug

Xdebug's (remote) debugger allows you to examine data structure, The Xdebug extension provides debugging
and profiling capabilities for PHP scripts.
Xdebug supports PHP 5.4, 5.5, 5.6, 7.0, 7.1, 7.2, and 7.3.
You can debug your PHP coding using built functions like print_r() or var_dump() or using log file however this is not enough and it will slow you down during developing.
by using Magento2+xdebug it will speed you up during developing and it will make your life easier

Installing Xdebug

Preconditions

  1. you have Magento2+Vagrant environment
    ※ If you don't have Magento2+Vagrant environment you can follow this tutorial magento2:開発環境構築 Vagrant
  2. PHP version is 7.1~
  3. you already install PHPSTORM

install Xdebug (inside Vagrant)

inside-vagrant
sudo pecl install xdebug

On my server, I got the following output.

inside-vagrant-output
Build process completed successfully
Installing '/usr/lib/php/20151012/xdebug.so'
install ok: channel://pecl.php.net/xdebug-2.6.1
configuration option "php_ini" is not set to php.ini location
You should add "zend_extension=/usr/lib/php/20151012/xdebug.so" to php.ini

Next, we need to enable the xdebug extension by modifying the php.ini configuration file
/etc/php/7.1/fpm/php.ini or /etc/php/7.1/fpm/conf.d/20-xdebug.ini

inside-vagrant
sudo vim /etc/php/7.1/fpm/php.ini
/etc/php/7.1/fpm/php.ini
zend_extension=/usr/lib/php/20151012/xdebug.so
xdebug.remote_host=192.168.33.10
xdebug.remote_enable=1
xdebug.remote_port=9900
xdebug.idekey="PHPSTORM"
xdebug.remote_log="/tmp/xdebug.log"

xdebug.remote_host You can set it to a specific domain or a IP address. im my case I set 192.168.33.10 because it is my vagrant private network IP
xdebug.remote_port You can set it to a specific port by default it will be 9000 In case I set to 9000 since my 9000 port is listen to something else.
xdebug.idekey since I use PHPSTORM ID I setted to PHPSTORM
xdebug.remote_log it is used as filename to a file to which all remote debugger communications are logged.

after setting you to need to restart your web server.
in case you Apache

inside-vagrant
sudo /etc/init.d/apache2 restart

in case you Nginx

inside-vagrant
sudo /etc/init.d/nginx restart

To verify if xdebug successfully enable. You can add phpinfo() into [magento-folder]/pub/index.php
in my case, it was /var/www/html/pub/index.php
you should get output like this.
xdebug-enabled-successfully.png

Configuring XDebug client

In my case, I will Configure PHPStorm IDEA
open your project in phpstorm
you will set the port as you set it in php.ini of xdebug.remote_port

phpstorm
PHPSTORM -> Preference -> Languages & Frameworks -> PHP -> Debug

phpstorm-preferance-xdebug.png

PHPstorm configuration

Deployment configure

phpstorm
phpstorm -> Preference -> Build, Execution, Deployment -> Deployment

add a new remote server by click on [+] icon

phpstorm-preferences-Deployment.png

host You can set it to a specific domain or an IP address
username remote server username since we use vagrant username will be vagrant
password remote server password since we use vagrant password will be vagrant
root path vagrant Magento folder absolute location /var/www/html/

Servers configure

phpstorm
PHPSTORM -> Preference -> Languages & Frameworks -> PHP -> Servers

add a new server by click on [+] icon

phpstorm-preferences-Servers.png

host You can set it to a specific domain or an IP address
Use path mapping check the checkbox
under project files set the vagrant Magento folder absolute location /var/www/html/

Run/debug configure

phpstorm
PHPSTORM ->Toolbar ->Edit Configurations
  1. add a new debug by click on [+] icon
  2. select PHP Remote Debug

phpstorm-run:debug-edit.png

phpstorm-run:debug.png
Server selected Server we already create it on the last step.
IDE key you will set the IDE key as you set it in php.ini of xdebug.idekey

Browser Configuration

  1. Install xdebug chrome extension Xdebug helper
  2. after install extension set IDE key

    1. on chrome -> toolbar-> xdebug chrome extension icon right click

    cheome-Xdebug-helper-options.png
    2.select Phpstorm set IDE key you will set the IDE key as you set it in php.ini of xdebug.idekey
    chrome-Xdebug-helper-idekey.png
    3.click on save button

  3. enable xdebug chrome extension

    1. on chrome -> toolbar-> xdebug chrome extension icon left click

    cheome-Xdebug-helper-enable.png
    2. click on debug

debug Time!

phpstorm
PHPSTORM -> toolbar 
  1. On the PhpStorm toolbar, toggle the Start Listening for PHP Debug Connections button to start listening for incoming PHP debug connections, or choose Run | Start Listening for PHP Debug Connections from the main menu.
    phpstorm-toolbar-listen.png

  2. Set a breakpoint in your code. Breakpoints can be set in the PHP context inside we set on Magento pub/index.php
    phpstorm-breakpoint.png
    3.on browser refresh website it automatically redirects to PHPstorm
    https://gyazo.com/c11de6afa9e6b666a5ff3ef7b983a37d

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

フルスタックエンジニアへの道(CakePHP/React)

はじめに

こんにちは、 @IZUMIRU0313 です。
ランサーズ Advent Calendar 2019 23日目の記事です。

法人向けの社外人材活用サービス「Lancers Enterprise」のフルスタックエンジニアです。
まだよわよわなので恐縮ですが、api blueprintでAPI仕様書、CakePHPでAPI、ReactでUIを実装しています?

想定する読者は、サーバーサイドエンジニアでフロントエンド(React)も学習していこうとしている方です。

Lancers Enterprise

エンジニア経歴

学生時代は、主にRails、Swift、AWS(EC2、S3)、Heroku、WordPressを利用して、サービス開発やインターンに取り組んでいました。特に以下2つのサービスは、すべての設計および開発をやっていたため、努力は報われると今日でも思える貴重な経験になっています。
フロントは、SassとjQueryが多少書けるレベルでした?

学生時代

ランサーズには、SREとしてジョインしました。当時、ターミナルはgitと多少のコマンドを知っているレベルであり、@yakitori009さんに、何から何まで教えていただきながら取り組んでいました?‍♂️
LPICでインプットしながら取り組んでいたため、座学と実務の両輪が上手く回せていました。

  • 踏み台サーバーの移行
  • Let's Encryptワイルドカード証明書の導入
  • AutoScaling
  • AutoScaling中ではデプロイ不可
  • docker-compose対応
  • MySQLコンテナ、WordPressコンテナの構築
  • MySQLのバージョンアップ5.6->5.7
  • LambdaでGitHubとChatworkの連携
  • LambdaでAthenaのload partitionを自動実行

その後、サーバーサイドエンジニアとしてCakePHPでプラットフォームの開発をすることにしました。まともにチーム開発とCakePHPを書くのは初めてだったため、@waldo0515さんや@numanomanuさん、井上さんにシステム設計からプロジェクトマネジメント、コーディングに渡るまで大変お世話になりました?‍♂️
インプットは、オブジェクト指向やドメイン駆動設計、クリーンアーキテクチャ、リーダブルコード等に努めました。

JavaScriptの習得

正直まだまだ未熟であり器用貧乏になる可能性も大いにあるのですが、自分が目指したいエンジニア像のために本格的にJavaScriptに力を入れることにしました?
まずは、半年後業務でReactを書けるレベルになることを目標に、GASでの個人開発から始めました。SREの際にLambdaでnode.jsを書いていたこともあり、特に詰まることなく開発できました。
インプットは、改訂新版JavaScript本格入門を読んでいました。

Reactの習得

ES6のお作法や非同期通信の変遷等も一通り理解することができたので、本格的にReactの学習を始めました。
元々副業や個人開発でVueやNuxtを触る機会があったのですが、個人的にはReactの方が学習ハードルが高かった印象です。
特にJSX(TSX)、TypeScript、Redux、redux-sagaは業務で開発するまで理解できませんでした。

半年ほど、@intrudercl14さんと@takepo0928さんにキャリアやJavaScript、Reactのアドバイスをいただき、なんとか「Lancers Enterprise」の開発にジョインすることができました?‍♂️

特にりあクト!は、対話形式で先輩エンジニアが後輩エンジニアに教えるというストーリーなので、非常に読みやすくオススメです。

りあクト!

またVue、React、React(Redux)で同じアプリケーションを実装することは、共通点と相違点を把握でき学習促進に繋がったのでオススメです。

comparison-vue-react-redux

Reactの学習と合わせて、APIの学習にも努めました。APIは学生時代のサービスでRailsでAPIを生やし、Swiftでキャッチするという経験等はありましたが、なんちゃってAPIレベルだったので1から学習しました。

展望

ReactやTypeScriptの学習は継続していますが、ReactNativeやFlutterの学習もし始めたため、@sayanetさんと@terukuraさんとともに「Lancers Enterprise」をより良くした後は、アプリの改善にコミットできたらと考えています。

またモチベーション高く学習するには、自分の性格を理解することが大事だなと非常に思いました。家だと怠惰なので仕事終わり必ずカフェに行く、まずは簡単なアプリケーションを開発した後に体系だった書籍で質を上げていく等。長くなりそうなので、個人開発のすゝめ的な記事は別途書けたら良いなと思います。

QiitaいいねやTwitterフォローは励みになります?

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

オリジナルWebサービス -Lunches- (フルスクラッチ開発)

はじめに

フロントエンジニアを目指してプログラミング学習をしている小林と申します。
本記事ではオリジナルWebサービス「Lunches」の概要や制作過程について説明します。

スクリーンショット 2019-11-08 午後5.15.02.png

URL

リンク:Lunches

目的

  • フルスクラッチ開発でPHP、SQLの言語理解を深める
  • Webサービスの基本的な構成、動作を把握する

スペック

プログラミング言語:HTML5/ CSS3 / Javascript / PHP

データベース言語:MySQL

開発環境:macOS Catalina 10.15.1

バージョン管理:SourceTree

本番環境:さくらサーバー

機能

ユーザー管理機能
 ・ユーザー登録
 ・ログイン
 ・プロフィール編集
 ・ユーザー削除機能
 ・退会

イベント機能(メインサービス)
 ・イベント投稿
 ・イベント詳細
 ・イベント一覧(ページネーション)
 ・カテゴリー検索、日付検索

サービス概要

「Lunches」はランチタイムに恋愛やビジネス、友達作りなどもイベントを気軽に開催することができる 
Webサービスです。
インターネットを通じて人との交流を活性化させることを目的として制作しました。

開発手順

1.ワイヤーフレーム作成

7つブラウザ画面をノートに手書きでワイヤーフレームを作成しました。

2.テーブル設計

実装させたい機能から必要な情報を洗い出し、それに応じてテーブルを作成しました。
作成したテーブルは以下の3つです。

users
users.png

event
event.png

category
category.png

3.画面モック作成

ワイヤーフレームを元にHTML・CSSでコーディングを行い画面モックを作成しました。

セキュリティ

バリデーションチェック

 ・未入力チェック
 ・Email型式チェック(正規表現)
 ・Email重複チェック
 ・最大、最小文字数チェック
 ・半角英数字チェック
 ・同値チェック

*コード一部抜粋
スクリーンショット 2019-11-27 22.18.15.png

例外処理

DBへ接続する際にはエラーで接続できない可能性を考慮して「try」「catch」で例外処理を行っています。
スクリーンショット 2019-11-29 21.11.58.png

セッションIDの再生成

セッションハイジャックによって第三者による乗っ取りを防ぐためにsession_regenerate_idを使用
この関数をコールすることで現在のセッションデータを保持したまま、セッションIDを新しくすることができる。
session.png

パスワードハッシュ

スクリーンショット 2019-11-29 22.20.27.png
DB側でユーザーのパスワードが漏れないようにパスワードをpassword_hashでセキュリティを高めています。

スクリーンショット 2019-12-04 19.54.15.png

ログイン時にはpassword_verifyを使用してハッシュ化されたパスワードと照合しています。
このとき第一引数である$passにはフォームからpostされたパスワード
そして、第二引数にはDBから配列形式で取り出した情報を$resultに詰め
array_shiftを使って先頭のパスワードを取り出しています。

SQLインジェクション対策

DB接続時にはプレースホルダーを使用し、SQL文を作成。
値をバインドすることでSQLインジェクション対策を行っています。
スクリーンショット 2019-12-04 21.30.56.png

Lunchesの使い方

①イベントの登録・ プロフィール編集

スクリーンショット 2019-12-18 14.18.27.png
イベント投稿、プロフィール編集の画像登録ではjQueryを使用し、ドラック&ドロップでファイルをinputすることができます。

②イベント一覧・検索機能

スクリーンショット 2019-12-18 14.25.58.png
イベント一覧ページではGETパラメータを使用して、ページネーションと検索機能を実装しています。
検索機能はカテゴリー検索、投稿日時のソート順検索ができます。

今後の課題

・スマートフォンにも対応したレスポンシブデザイン
・オブジェクト指向に基づく、保守性の高いコード設計
・FLOCSSをベースとしたCSS設計

主に以上の3点です。
特に様々なデバイスからアクセスされるユーザーを想定したレスポンシブデザインでの設計は
現在のWebサービスでは必要不可欠な物だと思いました。

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

Laravel6.xでパスワードリセットメールをカスタマイズする

やりたいこと

  • Laravelのauthで導入されるログイン機能は便利だけど、パスワードリセットやメール認証のメールが英語なので、日本語にしたい。
  • 言語ファイルいじるだけでも日本語化は出来るが、できれば好きなデザイン・内容にしたい。

環境

  • Laravel 6.x (執筆時点では6.5.2)
$ php artisan --version
Laravel Framework 6.5.2
  • 事前に認証機能の導入を済ませている。
    • 具体的に言うと下記コマンドを実行済みで、プロジェクトも作ってある。
$ composer require laravel/ui --dev
$ php artisan ui vue --auth

大雑把な流れ

  • Userクラスの通知関数をオーバーライド
  • そこで用いるためのNotificationを作成、コーディング
  • メール文面を作成

手順(パスワードリセットの場合)

通知クラスの作成

artisanを利用してまずは空で作る。中身は後の手順で書く。

$ php artisan make:notification PasswordResetNotification
Notification created successfully.

これで app/Notifications/PasswordResetNotification.php が作成される。

Userクラスの通知関数をオーバーライド

既存のUserモデルクラスに対して、以下のオーバーライド処理を追記する。

app/User.php
    /**
     * Override to send for password reset notification.
     *
     * @param [type] $token
     * @return void
     */
    public function sendPasswordResetNotification($token)
    {
        $this->notify(new PasswordResetNotification($token));
    }

通知クラスの中身をコーディング

1からNotificationクラスを書いても良いけど、折角なのでデフォルトで導入されて使われている ResetPassword クラスをパクr……継承して使わせてもらう。
まずは全文がこちら。

app/Notifications/PasswordResetNotification.php
<?php

namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Auth\Notifications\ResetPassword;

class PasswordResetNotification extends ResetPassword
{
    use Queueable;

    /**
     * Get the mail representation of the notification.
     *
     * @param  mixed  $notifiable
     * @return \Illuminate\Notifications\Messages\MailMessage
     */
    public function toMail($notifiable)
    {
        if (static::$toMailCallback) {
            return call_user_func(static::$toMailCallback, $notifiable, $this->token);
        }

        return (new MailMessage)
                    ->subject('パスワードリセット通知')
                    ->view('emails.password_reset', [
                        'reset_url' => url(config('app.url').route('password.reset', ['token' => $this->token, 'email' => $notifiable->getEmailForPasswordReset()], false))
                    ]);
    }
}

順に解説する。
make:notification 時でデフォルトで書かれている内容は割愛するが、以下の関数は不要なので削除してしまう(オーバーライドの必要性がない)。

  • __construct()
  • via()
  • toArray()

次に use について。

use Illuminate\Auth\Notifications\ResetPassword;

こちらを追加しておく。
今回のカスタマイズをしない場合に使用されるのがこのクラスなので、つまりこれをありがたく流用させてもらうことで、がっつりと手間を省いてしまう。
なので extends の部分も、

class PasswordResetNotification extends ResetPassword

としておく。
もとは extends Notification だがここを流用するクラスに変えておく。

あとはメール送信部分である toMail() だけコーディングしてあげれば良い。

    /**
     * Get the mail representation of the notification.
     *
     * @param  mixed  $notifiable
     * @return \Illuminate\Notifications\Messages\MailMessage
     */
    public function toMail($notifiable)
    {
        if (static::$toMailCallback) {
            return call_user_func(static::$toMailCallback, $notifiable, $this->token);
        }

        return (new MailMessage)
                    ->subject('パスワードリセット通知')
                    ->view('emails.password_reset', [
                        'reset_url' => url(config('app.url').route('password.reset', ['token' => $this->token, 'email' => $notifiable->getEmailForPasswordReset()], false))
                    ]);
    }

if文部分は流用元そのままなので、そのまま利用。

件名を ->subject() の引数に入れておき、メールで送りたいViewを ->view() で指定してあげればOK。
ここでは 'emails.password_reset' を指定しているので、 resources/views/emails/password_reset.blade.php がViewとして扱われてメールで送られていく、ということになる。

view() 指定で送るようにすれば、Bladeテンプレートが利用できるのでとても便利。
第2引数にはArrayで変数を送ることもできる。上記の例でリンクURLとして使用するための url(config('app.url').~~ としている部分は、流用元そのままなので深く考えずにコピペで良いと思う。

メール文面の作成

あとは普段通りにViewを作るだけ!

手順(メール認証の場合)

基本的にはパスワード認証と同じ。以下の点が異なるだけ。

  • 継承元クラスは VerifyEmail になる。
    • use Illuminate\Auth\Notifications\VerifyEmail; になる。
  • toMail() のコードは以下のような感じ。
    public function toMail($notifiable)
    {
        $verificationUrl = $this->verificationUrl($notifiable);

        if (static::$toMailCallback) {
            return call_user_func(static::$toMailCallback, $notifiable, $verificationUrl);
        }

        return (new MailMessage)
                    ->subject('メール認証')
                    ->view('emails.email_verify', [
                        'verify_url' => $verificationUrl
                    ]);
    }
  • view側で verify_url の変数参照する際は {{ $verify_url }} ではなく {!! $verify_url !!} とする。URL内にアンパサンドが入ってくるので。

参考:Framework側のコード

困ったらFramework側のコードを見てしまうと参考になるかも。
このあたり。

  • vendor\laravel\framework\src\Illuminate\Auth\Notifications
    • ResetPassword.php
    • VerifyEmail.php
  • vendor\laravel\framework\src\Illuminate\Notifications\resources\views
    • email.blade.php

単に日本語化するだけであれば、今回のような手間をかけなくても、上記のファイルを見ながら resources\lang\ja.json にゴリゴリと対訳を書いてしまうだけでも良い。

参考URL

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

WebでLaravel のサンプルプログラムを動かせる Tinkerwell

Tinkerwellとは

  • Web上で実行できる Laravel対応のコードスペニット
  • Laravel のコードをブラウザで動かして コードをURLで共有できます。
  • 勉強やコードレビューなどで、実際に動くものを見せたりするのに便利そうです。

実行画面

image.png

あれこれ

  • 初回開くと Loadingにめっちゃ時間かかります。今後の改善されることを期待。
  • embed 対応してくれるとめっちゃ良いですね!
  • デスクトップ版(有料)があるようです。 未調査です。

  • Laravel の Tinker を調べていたら、たまたま発掘したので、ご紹介でした。

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

初心者のためのセルフレビューチェック項目〜もうクソコードのレビューはさせない〜

この記事について

新卒1年目のわたしがコードレビューで指摘された箇所を参考に、初学者がよく指摘されることとその対処法をまとめました。
どの言語にも共通することと、PHP / Laravel 限定のもの別にまとめてあります。
また、それぞれのミスについて、指摘されたくない度を星5つ満点で評価してみました 笑
レビューされるとき、レビューするときにこの記事に書いてあることが参考になればいいなと思います。

注意

  • あくまでも弊社基準のレビューです
  • VSCode 使っている人向けかもしれません

言語に関係ないイージーなミス

■ スペルミス

指摘されたくない度★★★★★
指摘されたら恥ずかしいやつ。

[対処法]

  • VSCode 拡張機能 Code Spell Checker
    • スペルミスしてる単語を下画像のように波線でハイライトしてくれる

スクリーンショット 2019-12-18 0.53.33.png

  • 不安だったらググる癖をつける

■ インデントずれ

指摘されたくない度★★★★★
GitHub 上でレビューするとなかなか気付き辛い。

[対処法]

  • VSCode 拡張機能 indent-rainbow
    • 下画像のような感じでインデントに色付けをしてくれる。また、ズレがある部分を赤く表示してくれる

スクリーンショット 2019-12-15 22.15.12.png

■ 余分なスペースが入ってしまう

指摘されたくない度★★★★★
最初の頃、誰もがやってしまうミス。

[対処法]

  • VSCode の setting.json に以下を追加
setting.json
"files.trimTrailingWhitespace": true,

■ ファイル末尾改行がない

指摘されたくない度★★★★★

[対処法]

  • VSCode の setting.json に以下を追加
setting.json
"files.insertFinalNewline": true

レビュワーとして気付くには

  • GitHub の FilesChanged でみると、末尾改行がないファイルは下記画像のようなマークがあるのですぐに気付ける

スクリーンショット 2019-12-15 22.46.11.png

■ デバッグ、コメントアウトの消し忘れ

指摘されたくない度★★★★★
dump() など、デバッグコードを消さずに残してしまう。

[対処法]

レビュー出す前に確認をする。

  • add する前にエディタ上で確認
  • push 後に Github 上で確認

■ 使っていないメソッドの消し忘れ

指摘されたくない度★★★★☆

試しに書いてみたけど結局使わなかったメソッドを消し忘れてしまう。

[対処法]

レビュー出す前に確認をする。

  • add する前にエディタ上で確認
  • push 後に Github 上で確認

■ 厳密等価演算子 === を使う

指摘されたくない度★★★★☆

[対処法]

基本的にゆるい等価比較 == は使わないって決めてれば良さそう

■ ダサいメソッド名と変数名

指摘されたくない度★★★★☆

● 長すぎる

例えば、Images ディレクトリ内のファイルで「この写真はjpeg形式か」みたいなメソッドを作るとき。メソッド名はisJpegImage()にしなくても isJpeg() で十分伝わる

● 正しい命名規則でない

指摘されたくない度★★★★☆
推奨されているケースで書く

~PHP の場合~

  • メソッド:キャメルケース
  • 変数:キャメルケース
  • 定数:大文字のスネークケース(コンスタントケース)
● 複数形であるべきところが単数形

配列につける変数名は複数形が良い。

$numbers = [1, 2, 3];

// ループ処理も複数形が好ましい
foreach($values as $value) {
  // 処理
}

但し、可算名詞と不可算名詞に気をつける。
例)data の複数形は datas ではない

● beforeXXX と afterXXX に注意

例えば、beforePostDate() というメソッドがあるなら、return されるものは PostDate を含まない日付が好ましい。afterPostDate() というメソッドでも、PostDate を含まない日付が return されるべき。

参考: beforeの日付は当日を含むか含まないか

[対処法]

  • codic で検索してみる
    • 例えば日本語で「有効かどうか」と入力すると、is_valid とカラム名っぽいものが表示される。状況に合わせてキャメルケースにしたりする。
  • 戻り値が bool 型のメソッドは hasXXX isXXX とかにするといい感じになる。
    • 但し、is + 現在形の動詞(isShow など)は英語的におかしいので避けるべき。

PHP / Laravel でありがちなミス

■ 使っていない use の消し忘れ

指摘されたくない度★★★★☆
Laravel あるあるレビューだと思う。
使ってないものがあってもエラー出ない上、なぜか気付かれにくいので、ずっと残り続けることがあって厄介。

[対処法]

  • VSCode 拡張機能 phpcs
    • これを入れると、保存時に自動で未使用の use を削除してくれます。但し、修正したくない箇所も勝手に修正されてしまうことがあるので、各自設定が必要
    • こちらの記事→ VScodeでPHP CodeSnifferの設定をしたい時の手順 に設定方法が書いてあった  

■ N+1 が解決されていない

指摘されたくない度★★★★☆

[対処法]

Laravel の Eagerロード で解決。
参考: https://readouble.com/laravel/6.x/ja/eloquent-relationships.html#eager-loading

そもそも N+1 になってることに気付くには?

Laravel Debugbar を導入すると、N+1 があると、画像のように N+1 Queries タブに数字が表示される。さらに、You should add 'with(partners)' のように追加するべきメソッドも表示してくれる。
Laravel Debuger は発行されたクエリの表示もできるので、すごく便利。

スクリーンショット 2019-12-16 15.35.08.png

■ if文じゃなくてもかける

指摘されたくない度★★★☆☆

● メソッドチェーンで頑張れるとき

下記コードの①②は同じ結果になる。

sample.php
// ①
if ($this->isFemale) {
    return $Girls
        ->where(function ($query) {
            $query->....;

// ②
// when が使える!!
return $Girls
    ->when($this->isFemale, function ($query) {
        $query->where(function ($query) {
            $query->....;

  • when で書くとなにがいいの?
● optional と 三項演算子をうまく使う

下記コードの①②は同じ結果になる。

sample-blade.php
// ①
@if (is_null($comment->created_at))
<span>なし</span>
@else
<span>{{ $comment->created_at->format('Y/m/d') }}</span>
@endif

// ②
// `optional()` と三項演算子でスマートに!!
<span>{{ optional($comment->created_at)->format('Y/m/d') ?? 'なし' }}</span>

  • optional ヘルパについて
    • これを使えば、is_null での判定も不要になり、かなり綺麗に書ける
  • 三項演算子は if 文で書くには冗長すぎるようなときに使うといいかもしれない。
@unless が使える

下記コードの①②は同じ結果になる。

sample-blade.php
// ①
@if (!user->isFemale())
<p>男だよ</p>
@endif

// ②
// unless が使える
@unless (user->isFemale())
<p>男だよ</p>
@endunless

  • unless で書けば、! で条件を判定させる必要がないから、よりわかりやすい。

■ 文字列演算子 . の代わりに sprintf を使う

指摘されたくない度★★★☆☆
文字列演算子で結合すると、複雑なものだと読み辛くなる。
sprintf で書くのがいいかもしれない。

$time = '今朝'
$food = 'ホットケーキ'
$num = 2

// 読みづらい
echo $time . '、' . $food . 'を' . $num . '枚食べました。';

// 読みやすく、修正もしやすい
echo sprintf('%s、%sを%d枚食べました。', $time, $food, $num);

■ メソッドに切り出す

指摘されたくない度★★★☆☆

Post クラスは status カラムを持っていて、status が 1 のとき、編集可能だとする。

class Post
{
    //
}

$post = new Post();

if ($post->status === 1) {
    // 処理
}

上記の例では、クラス外でその Post が編集可能かを判断するとき、$post->status === 1 としなければならない。よって、初めてこのコードを読む人は status が 1 とはどんな状態の Post なのか、確認する必要がある。

[対処法]

Post クラスに isEditable() というメソッドを追加した。
さらに、1 はどのステータスなのか分かりづらいから、EDITABLE 定数に入れた。
これで別クラスで Post が編集可能かを判定するときも、可読性は高く保てる。

class Post
{
    private const EDITABLE = 1;

    public function isEditable(): bool
    {
        return $this->status === self::EDITABLE;
    }
}


$post = new Post();

if ($post->isEditable()) {
    // 処理
}

whereKey() whereNotKey() が使える

指摘されたくない度★★★☆☆

whereKey() を使えば、where や whereIn メソッドの第1引数でプライマリーキーの指定が不要になる。

public function getPost()
{
    return Post::where('id', $hoge);
}

// whereKey() でよりシンプルに
public function getPost()
{
    return Post::whereKey($hoge);
}

whereNotKey()whereKey() の逆。
参考:https://readouble.com/laravel/5.4/ja/upgrade.html

■ $loop を使う

指摘されたくない度★★★☆☆
foreach() でいちばん最初のループだけある処理をしたい、というとき $loop を使うと綺麗に書ける。

@foreach ($posts as $key => $value)
@if ($key === 0)
// 処理
@endif

@endforeach

// $loop を使ってシンプルに
@foreach ($posts as $post)
@if ($loop->first)
// 処理
@endif

@endforeach

$loop のプロパティは他にもある。
参考;Laravel:$loopによるループ変数の使用例

まとめ

プルリクを作り、レビュー依頼をする前に、自分の書いたコードを確認し直す習慣をつけていきましょう。

以下に、どんな言語でも使えそうなチェックリストを置いておくので、コピペして使ってみてください!

- [ ] スペルミスがないか
- [ ] インデントずれがないか
- [ ] 余分なスペースがないか
- [ ] ファイル末尾改行
- [ ] デバッグ、コメントアウトの消し忘れ
- [ ] 使っていないメソッドの消し忘れ
- [ ] 厳密等価演算子 ===
- [ ] メソッド、変数などの命名

最後に

弊社の宣伝をします。

千株式会社

私が新卒で入社した会社です。
千株式会社 では幼稚園・保育園向けインターネット写真サービス「はいチーズ!」を提供しています。
新卒、中途共に絶賛採用中です!

週1回のリモートワーク、フレックス制度(コアタイムが12:00-15:00)など、柔軟な働き方ができる会社です。(朝の6時に出社すれば、なんと15時に退勤できてしまいます...!)
モダン(Laravel + Docker + CircleCI + AWS)で自由な環境で働いてみたい方におすすめの会社です。
もっと詳しく知りたい方は こちら

アドベントカレンダー 千 Advent Calendar 2019もやっているのでぜひ覗いて行ってください〜

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

初心者のためのセルフレビューチェック項目〜クソコードのレビューはさせない〜

この記事について

新卒1年目のわたしがコードレビューで指摘された箇所を参考に、初学者がよく指摘されることとその対処法をまとめました。
どの言語にも共通することと、PHP / Laravel 限定のもの別にまとめてあります。
また、それぞれのミスについて、指摘されたくない度を星5つ満点で評価してみました 笑
レビューされるとき、レビューするときにこの記事に書いてあることが参考になればいいなと思います。

注意

  • あくまでも弊社基準のレビューです
  • VSCode 使っている人向けかもしれません

言語に関係ないイージーなミス

■ スペルミス

指摘されたくない度★★★★★
指摘されたら恥ずかしいやつ。

[対処法]

  • VSCode 拡張機能 Code Spell Checker
    • スペルミスしてる単語を下画像のように波線でハイライトしてくれる

スクリーンショット 2019-12-18 0.53.33.png

  • 不安だったらググる癖をつける

■ インデントずれ

指摘されたくない度★★★★★
GitHub 上でレビューするとなかなか気付き辛い。

[対処法]

  • VSCode 拡張機能 indent-rainbow
    • 下画像のような感じでインデントに色付けをしてくれる。また、ズレがある部分を赤く表示してくれる

スクリーンショット 2019-12-15 22.15.12.png

■ 余分なスペースが入ってしまう

指摘されたくない度★★★★★
最初の頃、誰もがやってしまうミス。

[対処法]

  • VSCode の setting.json に以下を追加
setting.json
"files.trimTrailingWhitespace": true,

■ ファイル末尾改行がない

指摘されたくない度★★★★★

[対処法]

  • VSCode の setting.json に以下を追加
setting.json
"files.insertFinalNewline": true

レビュワーとして気付くには

  • GitHub の FilesChanged でみると、末尾改行がないファイルは下記画像のようなマークがあるのですぐに気付ける

スクリーンショット 2019-12-15 22.46.11.png

■ デバッグ、コメントアウトの消し忘れ

指摘されたくない度★★★★★
dump() など、デバッグコードを消さずに残してしまう。

[対処法]

レビュー出す前に確認をする。

  • add する前にエディタ上で確認
  • push 後に Github 上で確認

■ 使っていないメソッドの消し忘れ

指摘されたくない度★★★★☆

試しに書いてみたけど結局使わなかったメソッドを消し忘れてしまう。

[対処法]

レビュー出す前に確認をする。

  • add する前にエディタ上で確認
  • push 後に Github 上で確認

■ 厳密等価演算子 === を使う

指摘されたくない度★★★★☆

[対処法]

基本的にゆるい等価比較 == は使わないって決めてれば良さそう

■ ダサいメソッド名と変数名

指摘されたくない度★★★★☆

● 長すぎる

例えば、Images ディレクトリ内のファイルで「この写真はjpeg形式か」みたいなメソッドを作るとき。メソッド名はisJpegImage()にしなくても isJpeg() で十分伝わる

● 正しい命名規則でない

指摘されたくない度★★★★☆
推奨されているケースで書く

~PHP の場合~

  • メソッド:キャメルケース
  • 変数:キャメルケース
  • 定数:大文字のスネークケース(コンスタントケース)
● 複数形であるべきところが単数形

配列につける変数名は複数形が良い。

$numbers = [1, 2, 3];

// ループ処理も複数形が好ましい
foreach($values as $value) {
  // 処理
}

但し、可算名詞と不可算名詞に気をつける。
例)data の複数形は datas ではない

● beforeXXX と afterXXX に注意

例えば、beforePostDate() というメソッドがあるなら、return されるものは PostDate を含まない日付が好ましい。afterPostDate() というメソッドでも、PostDate を含まない日付が return されるべき。

参考: beforeの日付は当日を含むか含まないか

[対処法]

  • codic で検索してみる
    • 例えば日本語で「有効かどうか」と入力すると、is_valid とカラム名っぽいものが表示される。状況に合わせてキャメルケースにしたりする。
  • 戻り値が bool 型のメソッドは hasXXX isXXX とかにするといい感じになる。
    • 但し、is + 現在形の動詞(isShow など)は英語的におかしいので避けるべき。

PHP / Laravel でありがちなミス

■ 使っていない use の消し忘れ

指摘されたくない度★★★★☆
Laravel あるあるレビューだと思う。
使ってないものがあってもエラー出ない上、なぜか気付かれにくいので、ずっと残り続けることがあって厄介。

[対処法]

  • VSCode 拡張機能 phpcs
    • これを入れると、保存時に自動で未使用の use を削除してくれます。但し、修正したくない箇所も勝手に修正されてしまうことがあるので、各自設定が必要
    • こちらの記事→ VScodeでPHP CodeSnifferの設定をしたい時の手順 に設定方法が書いてあった  

■ N+1 が解決されていない

指摘されたくない度★★★★☆

[対処法]

Laravel の Eagerロード で解決。
参考: https://readouble.com/laravel/6.x/ja/eloquent-relationships.html#eager-loading

そもそも N+1 になってることに気付くには?

Laravel Debugbar を導入すると、N+1 があると、画像のように N+1 Queries タブに数字が表示される。さらに、You should add 'with(partners)' のように追加するべきメソッドも表示してくれる。
Laravel Debuger は発行されたクエリの表示もできるので、すごく便利。

スクリーンショット 2019-12-16 15.35.08.png

■ if文じゃなくてもかける

指摘されたくない度★★★☆☆

● メソッドチェーンで頑張れるとき

下記コードの①②は同じ結果になる。

sample.php
// ①
if ($this->isFemale) {
    return $Girls
        ->where(function ($query) {
            $query->....;

// ②
// when が使える!!
return $Girls
    ->when($this->isFemale, function ($query) {
        $query->where(function ($query) {
            $query->....;

  • when で書くとなにがいいの?
● optional と 三項演算子をうまく使う

下記コードの①②は同じ結果になる。

sample-blade.php
// ①
@if (is_null($comment->created_at))
<span>なし</span>
@else
<span>{{ $comment->created_at->format('Y/m/d') }}</span>
@endif

// ②
// `optional()` と三項演算子でスマートに!!
<span>{{ optional($comment->created_at)->format('Y/m/d') ?? 'なし' }}</span>

  • optional ヘルパについて
    • これを使えば、is_null での判定も不要になり、かなり綺麗に書ける
  • 三項演算子は if 文で書くには冗長すぎるようなときに使うといいかもしれない。
@unless が使える

下記コードの①②は同じ結果になる。

sample-blade.php
// ①
@if (!user->isFemale())
<p>男だよ</p>
@endif

// ②
// unless が使える
@unless (user->isFemale())
<p>男だよ</p>
@endunless

  • unless で書けば、! で条件を判定させる必要がないから、よりわかりやすい。

■ 文字列演算子 . の代わりに sprintf を使う

指摘されたくない度★★★☆☆
文字列演算子で結合すると、複雑なものだと読み辛くなる。
sprintf で書くのがいいかもしれない。

$time = '今朝'
$food = 'ホットケーキ'
$num = 2

// 読みづらい
echo $time . '、' . $food . 'を' . $num . '枚食べました。';

// 読みやすく、修正もしやすい
echo sprintf('%s、%sを%d枚食べました。', $time, $food, $num);

■ メソッドに切り出す

指摘されたくない度★★★☆☆

Post クラスは status カラムを持っていて、status が 1 のとき、編集可能だとする。

class Post
{
    //
}

$post = new Post();

if ($post->status === 1) {
    // 処理
}

上記の例では、クラス外でその Post が編集可能かを判断するとき、$post->status === 1 としなければならない。よって、初めてこのコードを読む人は status が 1 とはどんな状態の Post なのか、確認する必要がある。

[対処法]

Post クラスに isEditable() というメソッドを追加した。
さらに、1 はどのステータスなのか分かりづらいから、EDITABLE 定数に入れた。
これで別クラスで Post が編集可能かを判定するときも、可読性は高く保てる。

class Post
{
    private const EDITABLE = 1;

    public function isEditable(): bool
    {
        return $this->status === self::EDITABLE;
    }
}


$post = new Post();

if ($post->isEditable()) {
    // 処理
}

whereKey() whereNotKey() が使える

指摘されたくない度★★★☆☆

whereKey() を使えば、where や whereIn メソッドの第1引数でプライマリーキーの指定が不要になる。

public function getPost()
{
    return Post::where('id', $hoge);
}

// whereKey() でよりシンプルに
public function getPost()
{
    return Post::whereKey($hoge);
}

whereNotKey()whereKey() の逆。
参考:https://readouble.com/laravel/5.4/ja/upgrade.html

■ $loop を使う

指摘されたくない度★★★☆☆
foreach() でいちばん最初のループだけある処理をしたい、というとき $loop を使うと綺麗に書ける。

@foreach ($posts as $key => $value)
@if ($key === 0)
// 処理
@endif

@endforeach

// $loop を使ってシンプルに
@foreach ($posts as $post)
@if ($loop->first)
// 処理
@endif

@endforeach

$loop のプロパティは他にもある。
参考;Laravel:$loopによるループ変数の使用例

まとめ

プルリクを作り、レビュー依頼をする前に、自分の書いたコードを確認し直す習慣をつけていきましょう。

以下に、どんな言語でも使えそうなチェックリストを置いておくので、コピペして使ってみてください!

- [ ] スペルミスがないか
- [ ] インデントずれがないか
- [ ] 余分なスペースがないか
- [ ] ファイル末尾改行
- [ ] デバッグ、コメントアウトの消し忘れ
- [ ] 使っていないメソッドの消し忘れ
- [ ] 厳密等価演算子 ===
- [ ] メソッド、変数などの命名

最後に

弊社の宣伝をします。

千株式会社

私が新卒で入社した会社です。
千株式会社 では幼稚園・保育園向けインターネット写真サービス「はいチーズ!」を提供しています。
新卒、中途共に絶賛採用中です!

週1回のリモートワーク、フレックス制度(コアタイムが12:00-15:00)など、柔軟な働き方ができる会社です。(朝の6時に出社すれば、なんと15時に退勤できてしまいます...!)
モダン(Laravel + Docker + CircleCI + AWS)で自由な環境で働いてみたい方におすすめの会社です。
もっと詳しく知りたい方は こちら

アドベントカレンダー 千 Advent Calendar 2019もやっているのでぜひ覗いて行ってください〜

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

Laravelにて「.env.testing」がうまく読み込まれないときの対処法

はじめに

マイグレーションファイルを追加して、以下のコマンドを実行したのですが、「Nothing to migrate.」となり、マイグレーションが実行されなかったので、対処法をまとめました

$ php artisan migrate --env=testing
Nothing to migrate.

Laravelのバージョン

$ php artisan --version
Laravel Framework 5.8.29

対処法

以下のコマンドを実行して、キャッシュをクリアすることで、マイグレーションが実行されました

$ php artisan config:clear
Configuration cache cleared!

$ php artisan migrate --env=testing
Migrating: 2019_12_17_150625_create_hoges_table
Migrated:  2019_12_17_150625_create_hoges_table (0.02 seconds)

終わりに

.env.testingを更新したときは、キャッシュクリアを忘れずにしようと思います

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

Vue.js + LaravelでシンプルなSPA構築チュートリアル:VueとAPI結合編

はじめに

Vue.jsとLaravelによるSPA実装のチュートリアル記事です。

本記事は、4本の連載記事の4本目です。

Vue.js + LaravelでシンプルなSPA構築チュートリアル:概要編
Vue.js + LaravelでシンプルなSPA構築チュートリアル:Vueフロントエンド編
Vue.js + LaravelでシンプルなSPA構築チュートリアル:LaravelAPI編
Vue.js + LaravelでシンプルなSPA構築チュートリアル:VueとAPI結合編
↑↑今ここ↑↑

前回まで

Vue.jsでフロントエンド実装と、
LaravelのAPI実装が完了しました。

APIにつないでない状態の
・タスク一覧
・タスク詳細
・タスク登録
・タスク編集
のページと、
・タスク一覧取得API
・タスク詳細取得API
・タスク登録API
・タスク更新API
・タスク削除API
が完成している状態です。

今回はこの静的ページと
APIを繋ぎ込んでいきます。

この全体図の赤色部分になります。

Untitled Diagram.png

axios

今回、フロントページから
AjaxでAPIにリクエストを送信して
データの取得や更新を行います。

Ajax通信を簡単に実装するため、
今回はaxiosというパッケージを利用します。
https://qiita.com/ksh-fthr/items/2daaaf3a15c4c11956e9

特に難しいところはありませんが、
axiosの使い方を簡単に把握しておきましょう。

laravel/uiでベースを構築したので、
自分でインストールや設定作業などしなくても
最初からaxiosが利用できる状態です。

タスク一覧取得API繋ぎ込み

早速、タスク一覧ページとタスク一覧取得APIを繋ぎ込んでみましょう。

まずは<script>に必要なデータ、メソッドを定義します。

resources/js/components/TaskListComponent.vue
<script>
-    export default {}
+    export default {
+        data: function () {
+            return {
+                tasks: []
+            }
+        },
+        methods: {
+            getTasks() {
+                axios.get('/api/tasks')
+                    .then((res) => {
+                        this.tasks = res.data;
+                    });
+            }
+        },
+        mounted() {
+            this.getTasks();
+        }
+    }

</script>

まず data には空配列の tasks を用意します。

そして、 methods にある getTasks() メソッドで、
タスク一覧取得APIにリクエストして
そのレスポンスを先ほどの tasks の中に入れています。
(このメソッドで先ほど話したaxiosを利用してリクエストしています)

そして、画面描画時にこの getTasks() メソッドが実行されるように、
mounted() でメソッドを呼び出しています。

これで<script>側は完了です。

次に<templete>側も修正します。

resources/js/components/TaskListComponent.vue
- <tr>
-     <th scope="row">1</th>
-     <td>Title1</td>
-     <td>Content1</td>
-     <td>Ichiro</td>
-     <td>
-         <button class="btn btn-primary">Show</button>
-     </td>
-     <td>
-         <button class="btn btn-success">Edit</button>
-     </td>
-     <td>
-         <button class="btn btn-danger">Delete</button>
-     </td>
- </tr>
- <tr>
-     <th scope="row">2</th>
-     <td>Title2</td>
-     <td>Content2</td>
-     <td>Jiro</td>
-     <td>
-         <button class="btn btn-primary">Show</button>
-     </td>
-     <td>
-         <button class="btn btn-success">Edit</button>
-     </td>
-     <td>
-         <button class="btn btn-danger">Delete</button>
-     </td>
- </tr>
- <tr>
-     <th scope="row">3</th>
-     <td>Title3</td>
-     <td>Content3</td>
-     <td>Saburo</td>
-     <td>
-         <button class="btn btn-primary">Show</button>
-     </td>
-     <td>
-         <button class="btn btn-success">Edit</button>
-     </td>
-     <td>
-         <button class="btn btn-danger">Delete</button>
-     </td>
- </tr>


+ <tr v-for="task in tasks">
+     <th scope="row">{{ task.id }}</th>
+     <td>{{ task.title }}</td>
+     <td>{{ task.content }}</td>
+     <td>{{ task.person_in_charge }}</td>
+     <td>
+         <router-link v-bind:to="{name: 'task.show', params: {taskId: task.id }}">
+             <button class="btn btn-primary">Show</button>
+         </router-link>
+     </td>
+     <td>
+         <router-link v-bind:to="{name: 'task.edit', params: {taskId: task.id }}">
+             <button class="btn btn-success">Edit</button>
+         </router-link>
+     </td>
+     <td>
+         <button class="btn btn-danger">Delete</button>
+     </td>
+ </tr>

まずはべた書きで表示していた
3行のデータを削除します。

そして、先ほど定義したtasksデータをv-forで表示します。
<tr v-for="task in tasks">

ID、Title、Content、Person In Chargeの
各カラムは {{ task.title }} のようにデータを動的に表示させます。

- <td>Title1</td>
+ <td>{{ task.title }}</td>

また、「Show」「Edit」ボタンの
リンクURLのパラメータもべた書きしていたので、
ちゃんと動的にidを設定します。

- <router-link v-bind:to="{name: 'task.show', params: {taskId: 3}}">
+ <router-link v-bind:to="{name: 'task.show', params: {taskId: task.id }}">

これで、
APIからデータを取得し
それをv-forで画面に一覧表示できるようになりました。

commit:タスク一覧ページAPI繋ぎ込み

タスク一覧ページ完成です。

タスク詳細取得API繋ぎ込み

次に、タスク詳細ページとタスク詳細取得APIを繋ぎ込んでいきます。

まずは<script>

resources/js/components/TaskShowComponent.vue
<script>
    export default {
        props: {
            taskId: String
        },
+        data: function () {
+            return {
+                task: {}
+            }
+        },
+        methods: {
+            getTask() {
+                axios.get('/api/tasks/' + this.taskId)
+                    .then((res) => {
+                        this.task = res.data;
+                    });
+            }
+        },
+        mounted() {
+            this.getTask();
+        }
    }

</script>

一覧ページと同じように、
data に空のtaskを用意。
methodsgetTask() でAPIからタスクデータを取得。
mounted() で画面描画時にメソッド呼び出し。
としています。

次に<templete>側。

resources/js/components/TaskShowComponent.vue
<div class="form-group row border-bottom">
    <label for="id" class="col-sm-3 col-form-label">ID</label>
    <input type="text" class="col-sm-9 form-control-plaintext" readonly id="id"
-           v-bind:value="taskId">
+           v-model="task.id">

</div>

<div class="form-group row border-bottom">
    <label for="title" class="col-sm-3 col-form-label">Title</label>
    <input type="text" class="col-sm-9 form-control-plaintext" readonly id="title"
-           value="title title">
+           v-model="task.title">

</div>

<div class="form-group row border-bottom">
    <label for="content" class="col-sm-3 col-form-label">Content</label>
    <input type="text" class="col-sm-9 form-control-plaintext" readonly id="content"
-           value="content content">
+           v-model="task.content">

</div>

<div class="form-group row border-bottom">
    <label for="person-in-charge" class="col-sm-3 col-form-label">Person In Charge</label>
    <input type="text" class="col-sm-9 form-control-plaintext" readonly id="person-in-charge"
-           value="Ichiro">
+           v-model="task.person_in_charge">

</div>

各データをv-modelで表示するようにしました。

これでAPI取得したデータをタスク詳細ページに表示できました。

commit:タスク詳細ページAPI繋ぎ込み

タスク詳細ページ完成です。

タスク登録API繋ぎ込み

次に、タスク登録ページとタスク登録APIを繋ぎ込んでいきます。

まずは<script>

resources/js/components/TaskCreateComponent.vue
<script>
-    export default {}
+    export default {
+        data: function () {
+            return {
+                task: {}
+            }
+        },
+        methods: {
+            submit() {
+                axios.post('/api/tasks', this.task)
+                    .then((res) => {
+                        this.$router.push({name: 'task.list'});
+                    });
+            }
+        }
+    }

</script>

空のtaskデータを用意するところは先ほどと同じです。

methodssubmit() メソッドで、
taskデータをタスク登録APIにPOST送信する処理を書いています。

また、APIによるデータ登録完了後、
this.$router.push({name: 'task.list'}); でタスク一覧ページにリダイレクトしています。

 
 
次に<templete>側。

resources/js/components/TaskCreateComponent.vue
- <form>
+ <form v-on:submit.prevent="submit">
    <div class="form-group row">
        <label for="title" class="col-sm-3 col-form-label">Title</label>
-        <input type="text" class="col-sm-9 form-control" id="title">
+        <input type="text" class="col-sm-9 form-control" id="title" v-model="task.title">
    </div>
    <div class="form-group row">
        <label for="content" class="col-sm-3 col-form-label">Content</label>
-        <input type="text" class="col-sm-9 form-control" id="content">
+        <input type="text" class="col-sm-9 form-control" id="content" v-model="task.content">
    </div>
    <div class="form-group row">
        <label for="person-in-charge" class="col-sm-3 col-form-label">Person In Charge</label>
-        <input type="text" class="col-sm-9 form-control" id="person-in-charge">
+        <input type="text" class="col-sm-9 form-control" id="person-in-charge" v-model="task.person_in_charge">
    </div>
    <button type="submit" class="btn btn-primary">Submit</button>

</form>

各フォームはv-modeltaskデータとバインディングすることで、
フォームにデータが入力されたら
<scripts>側のtaskデータも更新されるようになっています。

そして、
<form v-on:submit.prevent="submit">
で、フォーム送信時に先ほど定義したsubmitメソッドを呼び出すようにしています。

これで、入力内容が反映されたtaskデータを
submitメソッドでAPI送信できる状態になっています。

commit:タスク登録ページAPI繋ぎ込み

これでタスク登録ページ完成です。

タスク更新API繋ぎ込み

次に、タスク編集ページとタスク更新APIを繋ぎ込んでいきます。

まずは<script>

resources/js/components/TaskEditComponent.vue
<script>
    export default {
        props: {
            taskId: String
        },
+        data: function () {
+            return {
+                task: {}
+            }
+        },
+        methods: {
+            getTask() {
+                axios.get('/api/tasks/' + this.taskId)
+                    .then((res) => {
+                        this.task = res.data;
+                    });
+            },
+            submit() {
+                axios.put('/api/tasks/' + this.taskId, this.task)
+                    .then((res) => {
+                        this.$router.push({name: 'task.list'})
+                    });
+            }
+        },
+        mounted() {
+            this.getTask();
+        }
    }

</script>

タスク詳細ページとタスク登録ページでやったことを
両方やっているだけです。

空のtaskデータを用意し、
getTask() メソッドでAPIから取得したデータをセットする。

submit メソッドでは、
タスク更新APIにputリクエストを送信しています。

 
 
次に<template>

resources/js/components/TaskEditComponent.vue
- <form>
+ <form v-on:submit.prevent="submit">
    <div class="form-group row">
        <label for="id" class="col-sm-3 col-form-label">ID</label>
-        <input type="text" class="col-sm-9 form-control-plaintext" readonly id="id" v-bind:value="taskId">
+        <input type="text" class="col-sm-9 form-control-plaintext" readonly id="id" v-model="task.id">
    </div>
    <div class="form-group row">
        <label for="title" class="col-sm-3 col-form-label">Title</label>
-        <input type="text" class="col-sm-9 form-control" id="title">
+        <input type="text" class="col-sm-9 form-control" id="title" v-model="task.title">
    </div>
    <div class="form-group row">
        <label for="content" class="col-sm-3 col-form-label">Content</label>
-        <input type="text" class="col-sm-9 form-control" id="content">
+        <input type="text" class="col-sm-9 form-control" id="content" v-model="task.content">
    </div>
    <div class="form-group row">
        <label for="person-in-charge" class="col-sm-3 col-form-label">Person In Charge</label>
-        <input type="text" class="col-sm-9 form-control" id="person-in-charge">
+        <input type="text" class="col-sm-9 form-control" id="person-in-charge" v-model="task.person_in_charge">
    </div>
    <button type="submit" class="btn btn-primary">Submit</button>

</form>

これはタスク登録ページと同じです。

各フォームはv-modeltaskデータとバインディングして、
formの v-on:submit.prevent="submit"sumitメソッドを呼んでいます。

commit:タスク編集ページAPI繋ぎ込み

これでタスク編集ページは完成。

タスク削除API繋ぎ込み

最後に、タスク一覧ページのDeleteボタンとタスク削除APIを繋ぎ込んでいきます。

まずは<script>

resources/js/components/TaskListComponent.vue
methods: {
    getTasks() {
        axios.get('/api/tasks')
            .then((res) => {
                this.tasks = res.data;
            });
    },
+    deleteTask(id) {
+        axios.delete('/api/tasks/' + id)
+            .then((res) => {
+                this.getTasks();
+            });
+    }
},

deleteTask() メソッドを追加しました。
タスクIDを引数で受け取り、
タスク削除APIにリクエストを送信しています。

削除完了したら、
getTasks() メソッドを呼んで
タスク一覧を再読み込みしています。

次に<template>

resources/js/components/TaskListComponent.vue
<td>
-    <button class="btn btn-danger">Delete</button>
+    <button class="btn btn-danger" v-on:click="deleteTask(task.id)">Delete</button>

</td>

もともと設置していたDeleteボタンに
v-on:click="deleteTask(task.id)" を追加しました。

これで、このボタンをクリックしたら deleteTask() メソッドが呼ばれます。

commit:タスク一覧ページ削除API繋ぎ込み

これでタスク一覧ページの削除処理もできたので、
全ページ、全機能が完成しました。

おわりに

シンプルなCRUD機能のアプリを
Vue.jsのSPAとLaravelのAPIで構築しました。

Vue側もLaravel側もほとんど難しいところもなく、
かなり簡単に書けたと思います。

今回はできるだけ簡単に一通りの機能を作るチュートリアルとしたかったため、
本来実装すべき処理を省いた箇所が多いです。

Vue側では
Ajaxのエラーハンドリングや
API送信前のバリデーションなど
本来は実装すべきです。

Laravel側もバリデーションや
APIの認証処理などがあるといいです。

今回のチュートリアルで
ざっくりと全体イメージをまずはつかんで、
今後上記のような詳細な処理を少しずつ追加していくといいかと思います。

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

Vue.js + LaravelでシンプルなSPA構築チュートリアル:LaravelAPI編

はじめに

Vue.jsとLaravelによるSPA実装のチュートリアル記事です。

本記事は、4本の連載記事の3本目です。

Vue.js + LaravelでシンプルなSPA構築チュートリアル:概要編
Vue.js + LaravelでシンプルなSPA構築チュートリアル:Vueフロントエンド編
Vue.js + LaravelでシンプルなSPA構築チュートリアル:LaravelAPI編
↑↑今ここ↑↑
Vue.js + LaravelでシンプルなSPA構築チュートリアル:VueとAPI結合編

前回まで

前回は、Vue.jsでフロントエンドのみ実装し、
静的なSPAができました。

べた書きのサンプルデータが表示されている状態で、
・タスク一覧
・タスク詳細
・タスク登録
・タスク編集
のページが完成しています。

API実装の進め方

この全体図の緑色部分にある
5つのAPIを実装していきます。

Untitled Diagram.png

今回は一番シンプルな形で進めるので、
各APIの処理は全てコントローラ内で数行で完結します。

また、API自体の実装の前に
DBのセットアップや最低限のテストデータも準備します。

SQLiteのセットアップ

今回は作業簡略化のため
MySQLやPostgreSQLを用意せず
SQLiteを使います。

まずはSQLiteのストレージとなるファイルを用意します。

database/database.sqlite に空のファイルを作成すればOKです。

次に、.envのDB接続情報を修正します。

.env
- DB_CONNECTION=mysql
- DB_HOST=127.0.0.1
- DB_PORT=3306
- DB_DATABASE=laravel
- DB_USERNAME=root
- DB_PASSWORD=


+ DB_CONNECTION=sqlite

これでSQLiteを利用するための設定は完了です。

ただし、PHPのSQLiteドライバーが有効になっている必要がありますので
もしなっていなければ有効にしてください。
https://awesome-linus.com/2019/05/24/php-sqlite-driver-install/

migration作成

migrationでタスクテーブルを作成します。

まずは下記コマンドでmigrationファイルを生成。

php artisan make:migration create_tasks_table

生成されたmigrationのupメソッドの中をこのように書き換えます。

create_tasks_table.php
Schema::create('tasks', function (Blueprint $table) {
    $table->bigIncrements('id');
    $table->string('title', 100);
    $table->string('content', 100);
    $table->string('person_in_charge', 100);
    $table->timestamps();
});

commit:migration作成

モデル作成

次に、タスクテーブルに対応する
タスクモデルを作ります。

php artisan make:model Task

生成されたモデルファイルに、
$fillable のみ追記しておきます。

app/Task.php
  class Task extends Model
  {
+    protected $fillable = [
+        'title',
+        'content',
+        'person_in_charge',
+    ];
  }

commit:タスクモデル作成

seeder作成

次に、テストデータを自動生成するための
seederを作成します。

まずは下記コマンドでseederファイルを生成。

php artisan make:seeder TasksTableSeeder

生成されたseederファイルのrunメソッドを
このように修正します。

database/seeds/TasksTableSeeder.php
 public function run()
 {
+    for ($i = 1; $i <= 10; $i++) {
+        Task::create([
+                'title' => 'title' . $i,
+                'content' => 'content' . $i,
+                'person_in_charge' => 'person_in_charge' . $i,
+            ]
+        );
+    }
 }

また、このseederを実行するためにDatabaseSeederファイルも修正します。

database/seeds/DatabaseSeeder.php
 public function run()
 {
-    // $this->call(UsersTableSeeder::class);
+    $this->call(TasksTableSeeder::class);
 }

commit:タスクseeder作成

テーブル、テストデータ生成

テーブルとテストデータを生成する準備は整いましたので、
実際に生成しましょう。

php artisan migrate --seed

これで先ほど作成したmigrationとseederが実行され、
テーブルとテストデータが10件できてるはずです。

データがちゃんと入っているか確認した場合は
tinkerを使ってみてください。

$ php artisan tinker


>>> Task::all();

これでタスクテーブルのデータが一覧で表示されます。

タスク一覧取得API実装

それでは早速API実装を始めます。
まずはタスク一覧取得APIから。

ルーティングを追加。

routes/api.php
+ Route::get('/tasks', 'TaskController@index');

次に、タスクコントローラを作成し、
そこにindexメソッドを追加します。

まずはartisanコマンドでコントローラファイル自体を生成。

php artisan make:controller TaskController

そして、indexメソッド追加。

app/Http/Controllers/TaskController.php
+ <?php
+
+ namespace App\Http\Controllers;
+
+ use App\Task;
+
+ class TaskController extends Controller
+ {
+     public function index()
+     {
+         return Task::all();
+     }
+ }

ただTaskモデルから全件取得してreturnするだけです。

POSTMANなどで
http://localhost:8000/api/tasks
にリクエストすると
タスク一覧が取得できると思います。
routes/api.phpにルーティング定義すると、自動でパスの頭に/apiがつきます。

レスポンスはこのようなjson形式になります。

レスポンス形式
[
    {
        "id": 1,
        "title": "title1",
        "content": "content1",
        "person_in_charge": "person_in_charge1",
        "created_at": "2019-12-17 00:43:38",
        "updated_at": "2019-12-17 00:43:38"
    },
    {
        "id": 2,
        "title": "title2",
        "content": "content2",
        "person_in_charge": "person_in_charge2",
        "created_at": "2019-12-17 00:43:38",
        "updated_at": "2019-12-17 00:43:38"
    },
]

commit:タスク一覧取得API実装

タスク詳細取得API実装

次にタスク詳細取得APIです。

ルーティング追加。

routes/api.php
  Route::get('/tasks', 'TaskController@index');
+ Route::get('/tasks/{task}', 'TaskController@show');

コントローラにshowメソッドを追加。

app/Http/Controllers/TaskController.php
+ public function show(Task $task)
+ {
+     return $task;
+ }

URLパラメータで受け取ったタスクモデルを
そのままreturnするだけです。
※これでLaravelが勝手にjson形式のレスポンスを返却します

commit:タスク詳細取得API実装

タスク登録API実装

次に、タスク登録APIです。

ルーティング追加。

routes/api.php
  Route::get('/tasks', 'TaskController@index');
+ Route::post('/tasks', 'TaskController@store');
  Route::get('/tasks/{task}', 'TaskController@show');

※ルーティングの定義順を間違えると正しく動かないので、この通りに記述してください

コントローラにstoreメソッド追加。

app/Http/Controllers/TaskController.php
  use App\Task;
+ use Illuminate\Http\Request;


+ public function store(Request $request)
+ {
+     return Task::create($request->all());
+ }

リクエストで受け取ったデータをそのまま
モデルのcreateでデータ登録しているだけです。

このようなjson形式のデータを受け取ることを想定しています。

リクエスト形式
{
    "title": "new title",
    "content": "new content",
    "person_in_charge": "new person_in_charge1"
}

commit:タスク登録API実装

タスク更新API実装

次に、タスク更新APIです。

ルーティング追加。

routes/api.php
  Route::get('/tasks', 'TaskController@index');
  Route::post('/tasks', 'TaskController@store');
  Route::get('/tasks/{task}', 'TaskController@show');
+ Route::put('/tasks/{task}', 'TaskController@update');

コントローラにupdateメソッド追加。

app/Http/Controllers/TaskController.php
+ public function update(Request $request, Task $task)
+ {
+     $task->update($request->all());
+
+     return $task;
+ }

受け取るリクエストの形は、
登録APIと同じjson形式です。

URLパラメータで受け取ったTaskモデルのupdateメソッドで
そのままデータを更新するだけです。

commit:タスク更新API実装

タスク削除API実装

次はタスク削除API。

ルーティング追加。

routes/api.php
  Route::get('/tasks/{task}', 'TaskController@show');
  Route::put('/tasks/{task}', 'TaskController@update');
+ Route::delete('/tasks/{task}', 'TaskController@destroy');

コントローラにdestroyメソッド追加。

app/Http/Controllers/TaskController.php
+ public function destroy(Task $task)
+ {
+     $task->delete();
+ 
+     return $task;
+ }

URLパラメータでTaskを受け取り、
それをそのままdeleteします。

commit:タスク削除API実装

おわりに

これで今回必要なAPIはすべて実装完了です。

POSTMANなどを利用して、
各APIの動作を確認するといいと思います。

本来は、このAPIでは
バリデーションを入れたり、
検索処理を入れたりすることになるかと思います。

次回は、
フロントのVueからAjaxで
このAPIに対してリクエスト送信し、
実際にデータの表示、更新、登録、削除ができるようにします。
Vue.js + LaravelでシンプルなSPA構築チュートリアル:VueとAPI結合編

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

Vue.js + LaravelでシンプルなSPA構築チュートリアル:Vueフロントエンド編

はじめに

Vue.jsとLaravelによるSPA実装のチュートリアル記事です。

本記事は、4本の連載記事の2本目です。

Vue.js + LaravelでシンプルなSPA構築チュートリアル:概要編
Vue.js + LaravelでシンプルなSPA構築チュートリアル:Vueフロントエンド編
↑↑今ここ↑↑
Vue.js + LaravelでシンプルなSPA構築チュートリアル:LaravelAPI編
Vue.js + LaravelでシンプルなSPA構築チュートリアル:VueとAPI結合編

前回まで

前回は、環境構築と必要なパッケージのインストールを行いました。

http://localhost:8000
でLaravelのウェルカムページが表示される状態で
次に進んでください。

コンポーネントの構成

本記事では、この全体図の青色部分、
Vue.jsによるフロントエンド実装のみを行います。

Untitled Diagram.png

作るページ(コンポーネント)は全部で4つです。

  • タスク一覧
  • タスク詳細
  • タスク登録
  • タスク編集

最初に各ページの完成状態の画像を確認します。

  • タスク一覧
    list.PNG

  • タスク詳細
    show.PNG

  • タスク登録
    create.PNG

  • タスク編集
    edit.PNG

前にインストールしたlaravel/ui vueに
デフォルトで組み込まれているbootstrapを使って
最低限のシンプルなUIにしています。
※今回はbootstrapの使い方には言及しません

各ページ上部にある黒い背景色の部分はヘッダーナビで、
全ページ固定で表示されるコンポーネントです。

ヘッダーナビより下の
一覧テーブルや入力フォーム部分が
URLごとに切り替わるメインのコンポーネントになります。

それでは、各ページのメインコンポーネントに加えて
ヘッダーーコンポーネントの
計5つを実装していきます。

ベースbladeとベースルーティングを追加

このアプリでは、
初回アクセス時のみLaravel側でリクエストを受けて
ページを表示し、
それ以降はフロント側のVue Routerによってルーティングが行われます。

その最初のリクエストを受け取る
Laravel側のルーティングとbladeファイルを追加します。

routes/web.php
- Route::get('/', function () {
-     return view('welcome');
- });
+ Route::get('/{any}', function() {
+     return view('app');
+ })->where('any', '.*');
resouces/views/app.blade.php
+ <!doctype html>
+ <html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
+ <head>
+     <meta charset="utf-8">
+     <meta name="viewport" content="width=device-width, initial-scale=1">
+ 
+     <!-- CSRF Token -->
+     <meta name="csrf-token" content="{{ csrf_token() }}">
+ 
+     <title>{{ config('app.name', 'Vue Laravel SPA') }}</title>
+ 
+     <!-- Styles -->
+     <link href="{{ mix('/css/app.css') }}" rel="stylesheet">
+ </head>
+ <body>
+ <div id="app">
+ 
+ </div>
+ <!-- Scripts -->
+ <script src="{{ mix('/js/app.js') }}" defer></script>
+ </body>
+ </html>

commit:ベースのbladeとルーティング追加

これで、どのURLでアクセスしても
このapp.blade.phpが表示されるようになりました。

また、前回の記事でインストールした
Vue.jsやbootstrapも
<link href="{{ mix('/css/app.css') }}" rel="stylesheet">
<script src="{{ mix('/js/app.js') }}" defer></script>
このjs、cssファイルで読み込まれているため
利用できる状態です。

試しにデフォルトで用意されている
ExampleComponentを表示してみてください。

resouces/views/app.blade.php
 <div id="app">
+ <example-component></example-component>
 </div>

これで
http://localhost:8000
にアクセスすると、
このようにExampleComponentが表示されると思います。

example.PNG

これが正しく表示されていれば、
Vue.js、bootstrapがちゃんと使えている状態です。
(このExampleComponentはbootstrapが使われています)

ヘッダーコンポーネント実装

ベースのbladeが配置できたので、
次に全ページ共通で固定表示する
ヘッダーコンポーネントを実装します。

HeaderComponentの追加

resources/js/components/HeaderComponent.vue
+ <template>
+     <div class="container-fluid bg-dark mb-3">
+         <div class="container">
+             <nav class="navbar navbar-dark">
+                 <span class="navbar-brand mb-0 h1">Vue Laravel SPA</span>
+                 <div>
+                     <button class="btn btn-success">List</button>
+                     <button class="btn btn-success">ADD</button>
+                 </div>
+             </nav>
+         </div>
+     </div>
+ </template>
+ 
+ <script>
+     export default {}
+ </script>

classがいろいろとたくさん設定されていますが、
全部bootstrapのclassで見た目を整えているだけなので、
あまり気にしなくてOKです。
 

そのコンポーネントをVueインスタンスに登録

resources/js/app.js
+ import HeaderComponent from "./components/HeaderComponent";
//↑ファイル先頭

  Vue.component('example-component', require('./components/ExampleComponent.vue').default);
+ Vue.component('header-component', HeaderComponent);

 
 

登録したコンポーネントをベースbladeに追加

resources/views/app.blade.php
 <div id="app">
+     <header-component></header-component>
 </div>

commit:ヘッダーコンポーネント実装

この状態でページを表示してみます。
npm run dev または npm run watch でソースをビルドするのを忘れないようにしましょう

ページ上部に黒いヘッダーナビが表示されていると思います。

まだボタンのリンク先は設定されていませんが、
この後ページを追加した際にこのボタンのリンクも設定します。

タスク一覧コンポーネント実装

まずタスク一覧コンポーネントを追加します。

resources/js/components/TaskListComponent.vue
+ <template>
+     <div class="container">
+         <table class="table table-hover">
+             <thead class="thead-light">
+             <tr>
+                 <th scope="col">#</th>
+                 <th scope="col">Title</th>
+                 <th scope="col">Content</th>
+                 <th scope="col">Person In Charge</th>
+                 <th scope="col">Show</th>
+                 <th scope="col">Edit</th>
+                 <th scope="col">Delete</th>
+             </tr>
+             </thead>
+             <tbody>
+             <tr>
+                 <th scope="row">1</th>
+                 <td>Title1</td>
+                 <td>Content1</td>
+                 <td>Ichiro</td>
+                 <td>
+                     <button class="btn btn-primary">Show</button>
+                 </td>
+                 <td>
+                     <button class="btn btn-success">Edit</button>
+                 </td>
+                 <td>
+                     <button class="btn btn-danger">Delete</button>
+                 </td>
+             </tr>
+             <tr>
+                 <th scope="row">2</th>
+                 <td>Title2</td>
+                 <td>Content2</td>
+                 <td>Jiro</td>
+                 <td>
+                     <button class="btn btn-primary">Show</button>
+                 </td>
+                 <td>
+                     <button class="btn btn-success">Edit</button>
+                 </td>
+                 <td>
+                     <button class="btn btn-danger">Delete</button>
+                 </td>
+             </tr>
+             <tr>
+                 <th scope="row">3</th>
+                 <td>Title3</td>
+                 <td>Content3</td>
+                 <td>Saburo</td>
+                 <td>
+                     <button class="btn btn-primary">Show</button>
+                 </td>
+                 <td>
+                     <button class="btn btn-success">Edit</button>
+                 </td>
+                 <td>
+                     <button class="btn btn-danger">Delete</button>
+                 </td>
+             </tr>
+             </tbody>
+         </table>
+     </div>
+ </template>
+ 
+ <script>
+     export default {}
+ </script>

ID、Title、Content(内容)、Person In Charge(担当者)、各種操作ボタン
をカラムに持つテーブルです。

現時点では、サンプルとして3行ほどべた書きで
タスクを表示しています。

後々の作業でここは
LaravelAPIからデータを受け取り表示するようになります。

また、
Show、Edit、Deleteのボタンを設置していますが
いまはリンク先が設定されていません。

後々各コンポーネントを実装したらリンク先を設定していきます。
 
 
追加したタスク一覧コンポーネントを
Vue Routerに登録します。

resources/js/app.js
+ import VueRouter from 'vue-router';
  import HeaderComponent from "./components/HeaderComponent";
+ import TaskListComponent from "./components/TaskListComponent";


  window.Vue = require('vue');


+ Vue.use(VueRouter);
+ 
+ const router = new VueRouter({
+     mode: 'history',
+     routes: [
+         {
+             path: '/tasks',
+             name: 'task.list',
+             component: TaskListComponent
+         },
+     ]
+ });


  const app = new Vue({
      el: '#app',
+     router
  });

VueRouter自体の詳しい解説は省略しますが、
ポイントはここです。

routes: [
    {
        path: '/tasks',
        name: 'task.list',
        component: TaskListComponent
    },
]

ここで、
「/tasks」のURLでアクセスしたら
「TaskListComponent」を表示する。
このルーティングの名前は「task.list」である。
と設定しています。

別ページ(コンポーネント)を追加した際は、
同じようにこの routes に設定を加えていくことになります。
 
 

そして、ルーティングで紐づけられたコンポーネントを表示するために、
ベースのbladeに <router-view> を配置する必要があります。

resources/views/app.blade.php
  <div id="app">
     <header-component></header-component>


+    <router-view></router-view>
  </div>

先ほどVue Routerで設定したとおり、
URLに紐づくコンポーネントがこの
<router-view> の部分に表示されることになります。

この状態で
http://localhost:8000/tasks
にアクセスしてみましょう。
※ビルドを忘れずに

お手本で見た通りの
一覧テーブルが表示されていると思います。

ついでに、
ヘッダーコンポーネントにある
「List」ボタンのリンク先を設定しておきましょう。

resources/js/components/HeaderComponent.vue
<nav class="navbar navbar-dark">
    <span class="navbar-brand mb-0 h1">Vue Laravel SPA</span>
    <div>
        <button class="btn btn-success">List</button>
+        <router-link v-bind:to="{name: 'task.list'}">
            <button class="btn btn-success">List</button>
+        </router-link>
        <button class="btn btn-success">ADD</button>
    </div>

</nav>

このように <route-link>v-bind:to
リンク先のルーティング名を設定することで
SPAのリンクとして動作させることができます。

commit:タスク一覧コンポーネント実装

タスク詳細コンポーネント実装

次に、タスク詳細コンポーネントを追加します。

まずコンポーネントファイル作成。

resources/js/components/TaskShowComponent.vue
+ <template>
+     <div class="container">
+         <div class="row justify-content-center">
+             <div class="col-sm-6">
+                 <form>
+                     <div class="form-group row border-bottom">
+                         <label for="id" class="col-sm-3 col-form-label">ID</label>
+                         <input type="text" class="col-sm-9 form-control-plaintext" readonly id="id"
+                                v-bind:value="taskId">
+                     </div>
+                     <div class="form-group row border-bottom">
+                         <label for="title" class="col-sm-3 col-form-label">Title</label>
+                         <input type="text" class="col-sm-9 form-control-plaintext" readonly id="title"
+                                value="title title">
+                     </div>
+                     <div class="form-group row border-bottom">
+                         <label for="content" class="col-sm-3 col-form-label">Content</label>
+                         <input type="text" class="col-sm-9 form-control-plaintext" readonly id="content"
+                                value="content content">
+                     </div>
+                     <div class="form-group row border-bottom">
+                         <label for="person-in-charge" class="col-sm-3 col-form-label">Person In Charge</label>
+                         <input type="text" class="col-sm-9 form-control-plaintext" readonly id="person-in-charge"
+                                value="Ichiro">
+                     </div>
+                 </form>
+             </div>
+         </div>
+     </div>
+ </template>
+ 
+ <script>
+     export default {
+         props: {
+             taskId: String
+         }
+     }
+ </script>

taskIdをURLパラメータとして受け取って、
そのIDのみ
<input type="text" class="col-sm-9 form-control-plaintext" readonly id="id" v-bind:value="taskId">
v-bind:value="taskId" 部分で動的に表示しています。

それ以外のcontent、person-in-chargeは
まだべた書きにしているだけです。

このコンポーネントをVue Routerに登録します。

resources/js/app.js
import VueRouter from 'vue-router';
import HeaderComponent from "./components/HeaderComponent";
import TaskListComponent from "./components/TaskListComponent";
+ import TaskShowComponent from "./components/TaskShowComponent";


{
    path: '/tasks',
    name: 'task.list',
    component: TaskListComponent
},

+ {
+     path: '/tasks/:taskId',
+     name: 'task.show',
+     component: TaskShowComponent,
+     props: true
+ },

これで、/tasks/:taskId のURLでアクセスすると、
TaskShowComponentが表示されます。

:taskId の部分は、任意のタスクIDが入ります。

このURLパラメータが、
先ほどのタスク詳細コンポーネントの中で使われていた
taskId となります。

http://localhost:8000/tasks/3
のように :taskId の部分に好きな数字を入れてアクセスすると
タスク詳細コンポーネントが表示されます。

ついでにタスク一覧コンポーネントに置いていた
「Show」ボタンのリンク先を設定しておきましょう。

resources/js/components/TaskListComponent.vue

+    <router-link v-bind:to="{name: 'task.show', params: {taskId: 1}}">
        <button class="btn btn-primary">Show</button>
+    </router-link>


+    <router-link v-bind:to="{name: 'task.show', params: {taskId: 2}}">
        <button class="btn btn-primary">Show</button>
+    </router-link>


+    <router-link v-bind:to="{name: 'task.show', params: {taskId: 3}}">
        <button class="btn btn-primary">Show</button>
+    </router-link>

これで、一覧ページの「Show」ボタンをクリックすると
タスク詳細ページに遷移するようになりました。

commit:タスク詳細コンポーネント実装

タスク登録コンポーネント実装

次にタスク登録コンポーネントを実装します。

まずコンポーネントファイル作成。

resources/js/components/TaskCreateComponent.vue
+ <template>
+     <div class="container">
+         <div class="row justify-content-center">
+             <div class="col-sm-6">
+                 <form>
+                     <div class="form-group row">
+                         <label for="title" class="col-sm-3 col-form-label">Title</label>
+                         <input type="text" class="col-sm-9 form-control" id="title">
+                     </div>
+                     <div class="form-group row">
+                         <label for="content" class="col-sm-3 col-form-label">Content</label>
+                         <input type="text" class="col-sm-9 form-control" id="content">
+                     </div>
+                     <div class="form-group row">
+                         <label for="person-in-charge" class="col-sm-3 col-form-label">Person In Charge</label>
+                         <input type="text" class="col-sm-9 form-control" id="person-in-charge">
+                     </div>
+                     <button type="submit" class="btn btn-primary">Submit</button>
+                 </form>
+             </div>
+         </div>
+     </div>
+ </template>
+ 
+ <script>
+     export default {}
+ </script>

ただ空のフォームを表示しているだけです。
現時点では送信処理は書いていません。

このコンポーネントをVue Routerに登録します。

resources/js/app.js
import VueRouter from 'vue-router';
import HeaderComponent from "./components/HeaderComponent";
import TaskListComponent from "./components/TaskListComponent";
+ import TaskCreateComponent from "./components/TaskCreateComponent";
import TaskShowComponent from "./components/TaskShowComponent";


{
    path: '/tasks',
    name: 'task.list',
    component: TaskListComponent
},

+ {
+     path: '/tasks/create',
+     name: 'task.create',
+     component: TaskCreateComponent
+ },
{
    path: '/tasks/:taskId',
    name: 'task.show',
    component: TaskShowComponent,
    props: true
},

これで、
http://localhost:8000/tasks/create
でアクセスすればタスク登録ページが表示されます。

ついでにヘッダーコンポーネントに置いていた
「Add」ボタンのリンク先を設定しておきます。

resources/js/components/HeaderComponent.vue
<div>
    <router-link v-bind:to="{name: 'task.list'}">
        <button class="btn btn-success">List</button>
    </router-link>
+    <router-link v-bind:to="{name: 'task.create'}">
        <button class="btn btn-success">ADD</button>
+    </router-link>

</div>

commit:タスク登録コンポーネント実装

タスク編集コンポーネント実装

次に、タスク編集コンポーネントを実装します。

まずコンポーネントファイルを作成。

resources/js/components/TaskEditComponent.vue
+ <template>
+     <div class="container">
+         <div class="row justify-content-center">
+             <div class="col-sm-6">
+                 <form>
+                     <div class="form-group row">
+                         <label for="id" class="col-sm-3 col-form-label">ID</label>
+                         <input type="text" class="col-sm-9 form-control-plaintext" readonly id="id" v-bind:value="taskId">
+                     </div>
+                     <div class="form-group row">
+                         <label for="title" class="col-sm-3 col-form-label">Title</label>
+                         <input type="text" class="col-sm-9 form-control" id="title">
+                     </div>
+                     <div class="form-group row">
+                         <label for="content" class="col-sm-3 col-form-label">Content</label>
+                         <input type="text" class="col-sm-9 form-control" id="content">
+                     </div>
+                     <div class="form-group row">
+                         <label for="person-in-charge" class="col-sm-3 col-form-label">Person In Charge</label>
+                         <input type="text" class="col-sm-9 form-control" id="person-in-charge">
+                     </div>
+                     <button type="submit" class="btn btn-primary">Submit</button>
+                 </form>
+             </div>
+         </div>
+     </div>
+ </template>
+ 
+ <script>
+     export default {
+         props: {
+             taskId: String
+         }
+     }
+ </script>

詳細ページと同様に、
taskId をURLパラメータで受け取り、
IDの欄にデータを表示しています。

このコンポーネントをVue Routerに登録します。

resources/js/app.js
import TaskCreateComponent from "./components/TaskCreateComponent";
import TaskShowComponent from "./components/TaskShowComponent";
+ import TaskEditComponent from "./components/TaskEditComponent";


{
    path: '/tasks',
    name: 'task.list',
    component: TaskListComponent
},
{
    path: '/tasks/create',
    name: 'task.create',
    component: TaskCreateComponent
},
{
    path: '/tasks/:taskId',
    name: 'task.show',
    component: TaskShowComponent,
    props: true
},

+ {
+     path: '/tasks/:taskId/edit',
+     name: 'task.edit',
+     component: TaskEditComponent,
+     props: true
+ },

これで、
http://localhost:8000/tasks/:taskId/edit
にアクセスするとタスク編集ページが表示されます。

:taskId の部分は任意のタスクIDになります。

ついでにタスク一覧コンポーネントに置いていた
「Edit」ボタンのリンク先も設定しておきます。

resources/js/components/TaskListComponent.vue

+    <router-link v-bind:to="{name: 'task.edit', params: {taskId: 1}}">
        <button class="btn btn-success">Edit</button>
+    </router-link>


+    <router-link v-bind:to="{name: 'task.edit', params: {taskId: 2}}">
        <button class="btn btn-success">Edit</button>
+    </router-link>


+    <router-link v-bind:to="{name: 'task.edit', params: {taskId: 3}}">
        <button class="btn btn-success">Edit</button>
+    </router-link>

commit:タスク編集コンポーネント実装

おわりに

これで、
・タスク一覧ページ
・タスク詳細ページ
・タスク登録ページ
・タスク編集ページ
が実装できました。

現時点ではAPIでデータを取得する処理はできていませんが、
この状態でもVue.jsによる 静的な SPAにはなっています。

もしデータベースを利用しないような
ウェブサイトなどをVue.jsでSPAとして構築する場合は
今回解説した内容を基本として
ページの追加をしていくだけです。

それでは、次にLaravelのAPI実装に進みましょう。
Vue.js + LaravelでシンプルなSPA構築チュートリアル:LaravelAPI編

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

Vue.js + LaravelでシンプルなSPA構築チュートリアル:概要編

はじめに

Vue.jsとLaravelによるSPA実装のチュートリアル記事です。

本記事は、4本の連載記事の1本目です。

Vue.js + LaravelでシンプルなSPA構築チュートリアル:概要編
↑↑今ここ↑↑
Vue.js + LaravelでシンプルなSPA構築チュートリアル:Vueフロントエンド編
Vue.js + LaravelでシンプルなSPA構築チュートリアル:LaravelAPI編
Vue.js + LaravelでシンプルなSPA構築チュートリアル:VueとAPI結合編

Vue.js 2.5
Laravel 6.7
を利用していますが、
別のバージョンでも大枠は同じだと思うので、
チュートリアルとしては参考にしていただけると思います。

アプリ構成

タスクを
・一覧表示
・詳細表示
・登録
・更新
・削除
する機能がある
シンプルなアプリケーションです。

一番シンプルな状態でCRUDの実装を
一通り実践することができます。

Untitled Diagram.png

Vue.jsでフロントエンドを実装し、
LaravelでAPIを実装します。

各コンポーネントでは、
ajaxでLaravelのAPIにリクエストし、
データを取得、更新します。

SPAになっているので、
フロントの各コンポーネントは
ページリロードせずにVue.jsによって表示切替されます。

構築の流れ

まずこの記事で
環境構築と必要なパッケージのインストール、セットアップまで行います。

そして、
1、Vue.jsで静的なSPA実装
2、LaravelでAPI実装
3、フロントエンドとAPIの結合
という順番で実装を進めます。

上の構成図で言うと、
まず青色のVue.jsフロントエンド部分のみ実装し、
そのあと緑色のLaravelAPIを実装し、
最後に赤色のフロントエンドのAjax通信部分を実装してAPIと結合する
という流れです。

少し長くなるので、
上記の3ステップはそれぞれ別のQiita記事とします。

完成品のソースコードはGitHubに公開しています。
https://github.com/MinatoNaka/VueLaravelSpa

また、構築手順の通り1ステップごとにコミットしていますので、
コミット一覧を順に追っていくと
実装の流れが理解しやすいと思います。
https://github.com/MinatoNaka/VueLaravelSpa/commits/master

環境構築

それでは、この記事では
環境構築と必要なパッケージのインストール、セットアップを済ませます。

PHP、Composer、NPMが利用可能な環境での構築を前提としています。
(筆者はWindowsのPCにて構築しています)

Laravelプロジェクト作成

まずは、
新品のLaravelプロジェクトを作成します。
任意のディレクトリで、下記コマンドを実行。

composer create-project --prefer-dist laravel/laravel vue-laravel-spa

commit:Laravelプロジェクト作成

新品プロジェクトの状態で
一度表示確認してみます。

まずはサーバ起動

cd vue-laravel-spa

php artisan serve

このURLでアクセスします。
http://localhost:8000/

Laravelのウェルカムページが表示されれば
正常に動作しています。

キャプチャ.PNG

laravel/uiインストール

次に、laravel/uiというパッケージをcomposerでインストールします。

これは、
Laravelでフロントエンド開発をするための
ベースを簡単に提供してくれるツールです。
下記コマンドを実行。

composer require laravel/ui

commit:laravel/uiインストール

laravel/ui vueインストール

先ほどインストールしたlaravel/uiを使うと、
bootstrapやvue、reactなどさまざまな
フロントエンドのベースコードを生成できます。
Laravel 6.x JavaScriptとCSSスカフォールド

今回はvueのベースを作ります。

php artisan ui vue

このコマンドを実行すると、
package.jsonに様々なフロントエンドパッケージが追加されたり、
ベースとなるjsファイルやサンプルのVueコンポーネント、
Laravel Mixの設定ファイルなどが自動で配置されます。
commit:laravel/ui vueインストール

フロントエンドパッケージインストール

laravel/uiのvueベースをインストールした際に、
必要なフロントエンドパッケージがpackage.jsonに追記されました。
bootstrap、jquery、vueなどが追記されています。

これらのパッケージをインストールします。

npm install

このコマンドを実行したら、
/node_modules/ ディレクトリが作成され、
その配下に様々なパッケージのディレクトリ、ファイルが追加されます。

commit:フロントエンドパッケージインストール
/node_modules/ ディレクトリはgitignoreされているためコミットに含まれません

Vue Routerインストール

今回はVue.jsでSPAを作るので、
Vue Routerというパッケージを追加でインストールしておきます。

Vue Routerとは、
Vue.jsでSPAを構築するためのルーティング処理を行う
Vue公式のツールです。

npm install --save vue-router

commit:Vue Routerインストール

フロントエンドビルド実行

必要なパッケージは全てインストール完了したので、
最後にフロントエンドソースコードをビルドしてみます。

npm run dev

このコマンドを実行することで、
Laravel Mixのビルド処理が実行され、
コンパイルされたjs、cssが
/public/js public/cssに出力されます。
※Laravel Mixについては詳しく言及しません。わからない方は、こちらの記事を参照ください
Laravel Mixとは?webpackをより便利に、簡単に。Laravel以外でも使えるよ。

この後実装するHTMLファイルでは、
このコンパイルされたjs、cssを読み込むことになります。

コンパイル済みファイルはgit管理する必要がないので
gitignoreに追記しておきます。

.gitignore
+ /public/js
+ /public/css

フロントエンドビルド実行

今後jsファイルやcssファイル、vueコンポーネントを更新した際は、
毎回 npm run dev でソースをビルドしないと画面に反映されないので注意してください。

毎回ビルドを実行するのが面倒な場合は
npm run watch を実行するとウォッチモードになり
ビルド対象ファイルを更新、保存すると自動でビルドが実行されるようになるので便利です。

おわりに

これで、環境構築と必要なパッケージ類のインストールは完了です。
次は「Vue.jsで静的なSPA実装」に進みましょう。
Vue.js + LaravelでシンプルなSPA構築チュートリアル:Vueフロントエンド編

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

【Laravel】 ポリシーをバリデーションに活用する

❓背景

Laravel のバリデーションですが,標準機能としては

  • 静的に判定可能なフォーマット系のバリデーション
  • 入力値同士の大小・前後関係のバリデーション
  • データベースの重複を見るユニークバリデーション
    (PresenceVerifierInterface によってデータベースパッケージとは疎結合な形で提供される)

の,3パターンぐらいしか用意されていません。ここには重要なものが欠けています…そう,認可を使ったバリデーションが無いのです。

「もしログイン中ユーザが○○だったら,このフィールドの編集を許可する」

愚直な方法を採るならクロージャ形式で自分で書くことが考えられます。しかし,せっかくならできるだけ宣言的に書きたいところです。この記事では,認可の仕組みをバリデーションに転用する方法を考えてみます。

?認可バリデーションの導入

例えば,管理画面におけるユーザの登録・編集用のコントローラを考えてみましょう。すでに登録されている管理者が新たな管理者を登録したり,既存の管理者を編集したりするためのコントローラです。

ユーザモデルに関して,以下の3フィールドの存在を想定します。

  • role … 権限
    • admin … 全員に関する読み書きがすべてできる
    • write … 全員に関する読み取り,自分に関する書き込み,他者に関する一部の書き込みができる
    • read … 全員に関する読み取り,自分に関する書き込みができる
  • name … 氏名
  • memo … 運用上のメモ

認可処理の定義

作成に関する認可

  • role
    • 新規のユーザ登録は,自分が admin write である場合のみ行える
      (アクション自体を禁止する)
    • 自分と権限が同じか,それ以下の権限のユーザのみ発行することができる
      (アクションの内容をバリデーションする)
  • name
    • 論理的制約はなし
  • memo
    • 論理的制約はなし

認可が絡むものが2つありますが,両者はカッコ書きで書いたとおり,大きく性質が異なることに注意してください。

更新に関する認可

続いて,更新処理も同様にユースケースを想定します。

  • role
    • admin は,自分自身以外の権限を変更することができる
      (管理者不在になることを防ぐため)
    • write は,自分の権限を read降格させることのみ できる
    • read は,一切の変更ができない
  • name
    • admin は,全員の name フィールドを編集することができる
    • write read は,自分の name フィールドのみ編集できる
  • memo
    • admin write は,全員の memo フィールドを編集することができる
    • read は,自分の memo フィールドのみ編集できる

非常に複雑な要件ですが, BtoB アプリ作ってるとありそうですよね。
(実際に自分がこれに遭遇しました)

コントローラにベタ書き

まず最も愚直にコントローラにベタ書きする例を見てみましょう。

<?php

namespace App\Http\Controllers;

use App\Http\Resource\User as UserResource;
use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;

class UserController extends Controller
{
    public function store(Request $request): UserResource
    {
        if (!in_array($request->user()->role, ['admin', 'write'], true)) {
            throw new AccessDeniedHttpException('新規ユーザ発行には write 権限以上が必要です。');
        }

        $inputs = [
            'role' => $request->input('role'),
            'name' => $request->input('name'),
            'memo' => $request->input('memo'),
        ];

        $rules = [
            'role' => [
                'required',
                Rule::in(['admin', 'write', 'read']),
                function ($attribute, $value, $fail) use ($request) {
                    if ($request->user()->role !== 'admin' && $value === 'admin') {
                        $fail('admin 権限を持たないため,admin ユーザを発行できません。');
                    }
                },
            ],
            'name' => [
                'required',
                'string',
                'max:20',
            ],
            'memo' => [
                'string',
                'max:100',
            ],
        ];

        Validator::make($inputs, $rules)->validate();

        return new UserResource(User::create($inputs));
    }

    public function update(Request $request, User $user): UserResource
    {
        $inputs = [
            'role' => $request->input('role'),
            'name' => $request->input('name'),
            'memo' => $request->input('memo'),
        ];

        $rules = [
            'role' => [
                'required',
                Rule::in(['admin', 'write', 'read']),
                function ($attribute, $value, $fail) use ($request, $user) {
                    if ($request->user()->role === 'admin') {
                        if ($request->user()->is($user)) {
                            $fail('admin ユーザは,自分自身の権限を変更することはできません。');
                        }
                    } elseif ($request->user()->role === 'write') {
                        if ($request->user()->isNot($user)) {
                            $fail('write ユーザは,他者の権限を変更することはできません。');
                        } elseif ($value === 'admin') {
                            $fail('write ユーザは,自身の権限を昇格させることはできません。');
                        }
                    } else {
                        if ($request->user()->isNot($user)) {
                            $fail('read ユーザは,権限を変更することはできません。');
                        }
                    }
                },
            ],
            'name' => [
                'required',
                'string',
                'max:20',
                function ($attribute, $value, $fail) use ($request, $user) {
                    if ($request->user()->isNot($user) && $request->user()->role !== 'admin') {
                        $fail('他者の名前の編集には admin 権限が必要です。');
                    }
                },
            ],
            'memo' => [
                'string',
                'max:100',
                function ($attribute, $value, $fail) use ($request, $user) {
                    if ($request->user()->isNot($user) && !in_array($request->user()->role, ['admin', 'write'], true)) {
                        $fail('メモの編集には write 権限以上が必要です。');
                    }
                },
            ],
        ];

        Validator::make($inputs, $rules)->validate();

        $user->fill($inputs)->save();

        return new UserResource($user);
    }
}

これは…さすがにちょっと書きたくないですよね。

モデルクラスにルール定義を委譲

ルールをコントローラに直書きすると使い回しが効かないので,モデルに定義してみましょう。以下のようなフローに則って,バリデーションを分割します。

  1. フォーマットだけで判定できる静的バリーデーション を実行
  2. モデルに値を fill() する
  3. 現在の状態や他のフィールドと比較を行うインスタンスバリデーション を実行
    $this を使用可能

(これだけで1つの記事になるぐらい本当は濃い話になるのですが,ここではサラッと流します)

疑問

とはいっても,なぜ唐突にこの話がでてきたの?という疑問は沸くはずなので,軽く説明しておきます。

Q1.「静的バリデーションのみでいいのではないか?」

モデルの更新時,部分的なパラメータが送信されてきたときに,$request から取得できないフィールドと比較した相対バリデーションができないため問題があります。

$event = new Event();
$event->starts_at = '2020-01-01 00:00:00';

$inputs = [
    'ends_at' => $request->input('ends_at'),
];
$rules = [
    'ends_at' => ['reqiured', 'date', 'after:starts_at'],
];

Validator::make($inputs, $rules)->validate();

例えばこのように, starts_at が既にモデルに格納済みで,新たに ends_at のみリクエストでやってきた場合にそのままでは対応できません。送信されてきた場合とそうではない場合で処理を分岐することも可能ではありますが,コードが複雑化し,バグを生む要因になります。

そのため, $this を用いた 既に格納されている値とも比較できる バリデーションの導入には合理性があります。

Q2.「インスタンスバリデーションのみでいいのではないか?」

new \Carbon\Carbon('invalid')

のように Carbon に不正な日付時刻が入力されたとき,即座に例外がスローされるのが問題です。これはモデルで $dates $casts 等を利用して日付時刻のミューテータを定義している場合に発生する問題です。これを防ぐためには, fill() を呼ぶ前に前段でフォーマットのみのバリデーションが必要です。

Q3. それでもやっぱりモデルに書いちゃうのってどうなの?

コントローラやフォームリクエストに書くと,変更に強くなる代わりに再利用性が大きく下がる。モデルに書くと,再利用性は非常に高いが,その代償としてレールから外れたときの融通が効きづらくなってくる。一長一短だと思います。直近の業務では

  • すべてが入力されない属性の部分的な更新がある
     → モデルが有利
  • バリデーションの内容が認証ユーザの権限によって変化する
     → ややモデルが有利
  • バリデーションの内容が再利用のされるエンドポイントによって変化する
     → コントローラやフォームリクエストが有利
  • テーブルのフィールド数が約 90 個(!)ある
     → モデルが有利

という背景を考慮して,モデルを選択していました。アプリケーションの性質によってどちらが向いているか見極める必要があるでしょう。

ナイーブな実装

<?php

namespace App\Http\Controllers;

use App\Http\Resource\User as UserResource;
use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;

class UserController extends Controller
{
    public function store(Request $request): UserResource
    {
        if (!in_array($request->user()->role, ['admin', 'write'], true)) {
            throw new AccessDeniedHttpException('新規ユーザ発行には write 権限以上が必要です。');
        }

        $inputs = [
            'role' => $request->input('role'),
            'name' => $request->input('name'),
            'memo' => $request->input('memo'),
        ];

        // 静的バリデーションを実行
        Validator::make($inputs, User::staticValidationRules())->validate();

        // User インスタンスを生成して入力を埋める
        $user = new User($inputs);

        // インスタンスバリデーションを実行
        Validator::make($inputs, $user->instanceValidationRules())->validate();

        // 保存
        $user->save();

        return new UserResource($user);
    }

    public function update(Request $request, User $user): UserResource
    {
        $inputs = [
            'role' => $request->input('role'),
            'name' => $request->input('name'),
            'memo' => $request->input('memo'),
        ];

        // 静的バリデーションを実行
        Validator::make($inputs, User::staticValidationRules())->validate();

        // 入力を埋める
        $user->fill($inputs);

        // インスタンスバリデーションを実行(但し,実際に更新されるフィールドのみを対象にする)
        $dirty = $user->getDirty();
        $rules = array_intersect_key($user->instanceValidationRules(), $dirty);
        Validator::make($dirty, $rules)->validate();

        // 保存
        $user->save();

        return new UserResource($user);
    }
}
<?php

namespace App;

use Illuminate\Auth\Authenticatable;
use Illuminate\Contracts\Auth\Authenticatable as UserContract;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;

class User extends Model implements UserContract
{
    use Authenticatable;

    public static function staticValidationRules(): array
    {
        return [
            'role' => [
                'required',
                Rule::in(['admin', 'write', 'read']),
            ],
            'name' => [
                'required',
                'string',
                'max:20',
            ],
            'memo' => [
                'string',
                'max:100',
            ],
        ]);
    }

    public function instanceValidationRules(): array
    {
        return [
            'role' => [
                $this->exists
                ? function ($attribute, $value, $fail) {
                    if (Auth::user()->role === 'admin') {
                        if (Auth::user()->is($this)) {
                            $fail('admin ユーザは,自分自身の権限を変更することはできません。');
                        }
                    } elseif (Auth::user()->role === 'write') {
                        if (Auth::user()->isNot($this)) {
                            $fail('write ユーザは,他者の権限を変更することはできません。');
                        } elseif ($this->role === 'admin') {
                            $fail('write ユーザは,自身の権限を昇格させることはできません。');
                        }
                    } elseif (Auth::user()->role === 'read') {
                        if (Auth::user()->isNot($this)) {
                            $fail('read ユーザは,権限を変更することはできません。');
                        }
                    }
                }
                : function ($attribute, $value, $fail) {
                    if (Auth::user()->role !== 'admin' && $value === 'admin') {
                        $fail('admin 権限を持たないため,admin ユーザを発行できません。');
                    }
                },
            ],
            'name' => [
                $this->exists
                ? function ($attribute, $value, $fail) {
                    if (Auth::user()->isNot($this) && Auth::user()->role !== 'admin') {
                        $fail('他者の名前の編集には admin 権限が必要です。');
                    }
                }
                : function () {},
            ],
            'memo' => [
                $this->exists
                ? function ($attribute, $value, $fail) {
                    if (Auth::user()->isNot($this) && !in_array(Auth::user()->role, ['admin', 'write'], true)) {
                        $fail('メモの編集には write 権限以上が必要です。');
                    }
                }
                : function () {},
            ],
        ]);
    }
}

getDirty() の呼び出し等は隠蔽の余地があるものの,最初よりは見通しがだいぶよくなりました。もう少し整理してみましょう。コントローラは十分きれいになったので,ここからはモデルのリファクタリングに着手します。

ポリシークラスに認可処理を委譲

インスタンスバリデーションを行っているモデルのインスタンスを引数として,フィールドごとにポリシーのアビリティを定義してみましょう。以下のような命名規則に従って定義します。

{store|update}<フィールド名>of

例: storeRoleOf updateNameOf

<?php

namespace App\Policies;

use App\User;
use Illuminate\Auth\Access\HandlesAuthorization;
use Illuminate\Auth\Access\Response;

class UserPolicy
{
    use HandlesAuthorization;

    public function store(User $user): Response
    {
        return in_array($user->role, ['admin', 'write'], true)
            ? $this->allow()
            : $this->deny('新規ユーザ発行には write 権限以上が必要です。');
    }

    public function update(User $user): Response
    {
        return $this->allow();
    }

    public function storeRoleOf(User $user, User $target): Response
    {
        return $user->role !== 'admin' && $value === 'admin'
            ? $this->deny('admin を発行できるのは admin ユーザだけです。')
            : $this->store($user, $target);
    }

    public function updateRoleOf(User $user, User $target): Response
    {
        return $this->{__FUNCTION__ . 'By' . ucfirst($user->role)}($user, $target);
    }

    protected function updateRoleOfByAdmin(User $user, User $target): Response
    {
        return $user->is($target)
            ? $this->deny('admin ユーザは,自分自身の権限を変更することはできません。')
            : $this->allow();
    }

    protected function updateRoleOfByWrite(User $user, User $target): Response
    {
        if ($user->isNot($target)) {
            return $this->deny('write ユーザは,他者の権限を変更することはできません。');
        }
        if ($target->role === 'admin') {
            return $this->deny('write ユーザは,自身の権限を昇格させることはできません。');
        }
        return $this->allow();
    }

    protected function updateRoleOfByRead(User $user, User $target): Response
    {
        return $this->deny('read ユーザは,権限を変更することはできません。');
    }

    public function updateNameOf(User $user, User $target): Response
    {
        return $user->isNot($target) && $user->role !== 'admin')
            ? $this->deny('他者の名前の編集には admin 権限が必要です。')
            : $this->allow();
    }

    public function updateMemoOf(User $user, User $target): Response
    {
        return $user->isNot($target) && !in_array($user->role, ['admin', 'write'], true))
            ? $this->deny('メモの編集には write 権限以上が必要です。')
            : $this->allow();
    }
}
<?php

namespace App;

use Illuminate\Auth\Authenticatable;
use Illuminate\Contracts\Auth\Authenticatable as UserContract;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;

class User extends Model implements UserContract
{
    use Authenticatable;

    public static function staticValidationRules(): array
    {
        return [
            'role' => [
                'required',
                Rule::in(['admin', 'write', 'read']),
            ],
            'name' => [
                'required',
                'string',
                'max:20',
            ],
            'memo' => [
                'string',
                'max:100',
            ],
        ]);
    }

    public function instanceValidationRules(): array
    {
        return [
            'role' => [
                $this->exists
                ? function ($attribute, $value, $fail) {
                    $response = Gate::inspect('updateRoleOf', $this);
                    if ($response->denied()) {
                        $fail($response->message());
                    }
                }
                : function ($attribute, $value, $fail) {
                    $response = Gate::inspect('storeRoleOf', $this);
                    if ($response->denied()) {
                        $fail($response->message());
                    }
                },
            ],
            'name' => [
                $this->exists
                ? function ($attribute, $value, $fail) {
                    $response = Gate::inspect('updateNameOf', $this);
                    if ($response->denied()) {
                        $fail($response->message());
                    }
                }
                : function () {},
            ],
            'memo' => [
                $this->exists
                ? function ($attribute, $value, $fail) {
                    $response = Gate::inspect('updateMemoOf', $this);
                    if ($response->denied()) {
                        $fail($response->message());
                    }
                }
                : function () {},
            ],
        ]);
    }
}
<?php

namespace App\Http\Controllers;

use App\Http\Resource\User as UserResource;
use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;

class UserController extends Controller
{
    public function store(Request $request): UserResource
    {
        $this->authorize('store', User::class);

        $inputs = [
            'role' => $request->input('role'),
            'name' => $request->input('name'),
            'memo' => $request->input('memo'),
        ];

        // 静的バリデーションを実行
        Validator::make($inputs, User::staticValidationRules())->validate();

        // User インスタンスを生成して入力を埋める
        $user = new User($inputs);

        // インスタンスバリデーションを実行
        Validator::make($inputs, $user->instanceValidationRules())->validate();

        // 保存
        $user->save();

        return new UserResource($user);
    }

    public function update(Request $request, User $user): UserResource
    {
        $this->authorize('update', User::class);

        $inputs = [
            'role' => $request->input('role'),
            'name' => $request->input('name'),
            'memo' => $request->input('memo'),
        ];

        // 静的バリデーションを実行
        Validator::make($inputs, User::staticValidationRules())->validate();

        // 入力を埋める
        $user->fill($inputs);

        // インスタンスバリデーションを実行(但し,実際に更新されるフィールドのみを対象にする)
        $dirty = $user->getDirty();
        $rules = array_intersect_key($user->instanceValidationRules(), $dirty);
        Validator::make($dirty, $rules)->validate();

        // 保存
        $user->save();

        return new UserResource($user);
    }
}

ポリシークラスは極めて宣言的な実装になり,とてもすっきりしました。でもモデルはもう少し共通化できそうなにおいがしますね。

PolicyRule の作成

Gate::inspect() まわりの部分を共通化するための PolicyRule クラスを作成します。

  • passes() で属性名が入ってくるので,それをもとに自動でアビリティ名を推測できるようにします。
  • アビリティ引数には,デフォルトではバリデーション対象となっているモデルのインスタンスを渡すようにします。
<?php

namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Str;

class PolicyRule implements Rule
{
    protected $target;
    protected $ability;
    protected $arguments;
    protected $response;

    public function __construct(Model $target, ?string $ability = null, ?array $arguments = null)
    {
        $this->target = $target;
        $this->ability = $ability;
        $this->arguments = $arguments;
    }

    public function passes($attribute, $value): bool
    {
        $this->response = Gate::inspect(
            $this->ability ?? $this->guessAbilityName($attribute),
            $this->arguments ?? $this->target
        );

        return $this->response->allowed();
    }

    public function message(): ?string
    {
        return optional($this->response)->message();
    }

    protected function guessAbilityName(string $attribute): string
    {
        return sprintf(
            '%s%sOf',
            $this->target->exists ? 'update' : 'store',
            Str::studly($attribute)
        );
    }
}

また,「何もしない」を型で明示的に表現できるように, NoopRule クラスも一緒に作っておきましょう。

<?php

namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;

class NoopRule implements Rule
{
    public function passes($attribute, $value): bool
    {
        return true;
    }

    public function message(): ?string
    {
        return null;
    }
}

そしてこれらを簡単に利用するための,モデル用のトレイトを作成します。$this->exists による分岐はこのトレイトに任せます。

<?php

namespace App\Concerns;

use App\Rules\NoopRule;
use App\Rules\PolicyRule;
use App\Validation\Rule;

trait CreatesAuthorizationRules
{
    public function policyRule(?string $ability = null, ?array $arguments = null): PolicyRule
    {
        return new PolicyRule($this, $ability, $arguments);
    }

    public function policyRuleForStore(?string $ability = null, ?array $arguments = null): Rule
    {
        return $this->exists ? new NoopRule() : $this->policyRule($ability, $arguments);
    }

    public function policyRuleForUpdate(?string $ability = null, ?array $arguments = null): Rule
    {
        return $this->exists ? $this->policyRule($ability, $arguments) : new NoopRule();
    }
}

すると,モデルはここまでシンプルになります。

<?php

namespace App;

use Illuminate\Auth\Authenticatable;
use Illuminate\Contracts\Auth\Authenticatable as UserContract;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;

class User extends Model implements UserContract
{
    use Authenticatable;
    use Concerns\CreatesAuthorizationRules;

    public static function staticValidationRules(): array
    {
        return [
            'role' => [
                'required',
                Rule::in(['admin', 'write', 'read']),
            ],
            'name' => [
                'required',
                'string',
                'max:20',
            ],
            'memo' => [
                'string',
                'max:100',
            ],
        ]);
    }

    public function instanceValidationRules(): array
    {
        return [
            'role' => [
                $this->policyRule(),
            ],
            'name' => [
                $this->policyRuleForUpdate(),
            ],
            'memo' => [
                $this->policyRuleForUpdate(),
            ],
        ]);
    }
}

いかがでしょうか。これが求めていたゴールです。

?2段階バリデーションの抽象化

コントローラの処理もいい感じにラップするクラスを作ってあげれば,更に可読性は向上するでしょう。この部分に関しても詳細に説明すると記事が肥大化するため,簡易的な実装例のコードだけを紹介しておきます。

ModelValidator として Validator のファクトリー兼ラッパーを定義します。

<?php

namespace App\Validation;

use Illuminate\Contracts\Validation\Validator as ValidatorContract;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Validator;

class ModelValidator
{
    public $model;
    public $inputs = [];
    public $fills = [];
    public $targets = [];

    // デフォルトでは,モデルに埋めた結果,差分が発生した属性だけをインスタンスバリデーションの対象にする
    public $includeRulesForCleanAttributes = false;

    public function __construct(Model $model)
    {
        $this->model = $model;
    }

    public function setInputs(array $inputs)
    {
        $this->inputs = $inputs;

        // デフォルトでは,入力された属性のみバリデーションする
        // そのため,入力が増減する可能性のある $request->all() $request->only() は
        // 使用してはならないことに注意する。
        // 必ず1つ1つ入力を $request->input() で受け取ること。
        $this->shouldValidate(array_keys($inputs));

        // デフォルトでは,入力をすべてモデルに埋める
        $this->shouldFill($inputs);

        return $this;
    }

    public function shouldValidate(array $targets)
    {
        $this->targets = $targets;
        return $this;
    }

    public function shouldFill(array $fills)
    {
        $this->fills = $fills;
        return $this;
    }

    public function includeRulesForCleanAttributes(bool $include = true)
    {
        $this->includeRulesForCleanAttributes = $include;
        return $this;
    }

    public function validate(): void
    {
        $this->newStaticValidator()->validate();
        $this->model->fill($this->fills);
        $this->newInstanceValidator()->validate();
    }

    public function newStaticValidator(): ValidatorContract
    {
        $className = get_class($this->model);

        return $this->newValidator(
            method_exists($className, 'staticValidationRules')
            ? $className::getStaticValidationRules($this)
            : []
        );
    }

    public function newInstanceValidator(): ValidatorContract
    {
        $rules = method_exists($this->model, 'instanceValidationRules')
            ? $this->model->getInstanceValidationRules($this)
            : [];

        if (!$this->includeRulesForCleanAttributes) {
            $rules = array_intersect_key($rules, $this->model->getDirty());
        }

        return $this->newValidator($rules);
    }

    public function newValidator(array $rules): ValidatorContract
    {
        return Validator::make($this->inputs, Arr::only($rules, $this->targets));
    }
}

これを使うと,コントローラは以下のようになります。

<?php

namespace App\Http\Controllers;

use App\Http\Resource\User as UserResource;
use App\User;
use App\Validation\ModelValidator;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;

class UserController extends Controller
{
    public function store(Request $request): UserResource
    {
        $this->authorize('store', User::class);

        $inputs = [
            'role' => $request->input('role'),
            'name' => $request->input('name'),
            'memo' => $request->input('memo'),
        ];

        $user = new User();
        (new ModelValidator($user))->setInputs($inputs)->validate();
        $user->save();

        return new UserResource($user);
    }

    public function update(Request $request, User $user): UserResource
    {
        $this->authorize('update', User::class);

        $inputs = [
            'role' => $request->input('role'),
            'name' => $request->input('name'),
            'memo' => $request->input('memo'),
        ];

        (new ModelValidator($user))->setInputs($inputs)->validate();
        $user->save();

        return new UserResource($user);
    }
}

完璧ですね。ここまで来ることができれば本当のゴールでしょう。

?i18n 対応の導入

実際には,バリデーションメッセージは日本語でそのまま書かれることは少ないでしょう。ここでは resouces/lang/{ja,en}/valdation.php に翻訳を記入し,プレースホルダとして

  • :attribute … 属性名
  • :input … 入力値

を置換する処理まで導入した翻訳を実装してみましょう。最終的に,ポリシークラスで以下のように使用できることを目標とします。

public function updateNameOf(User $user, User $target): Response
{
    return $user->isNot($target) && $user->role !== 'admin')
        ? $this->deny(__('validation.insufficient_permission'))->of($target, 'role')
        : $this->allow();
}
権限不足のため,:attributeに「:input」を指定することができません。
↓
権限不足のため,ロールに「管理者」を指定することができません。

Response クラスの拡張

Validator クラスが標準で翻訳機能を有しているため,この機能を流用します。

  • Validator::makeReplacements() メソッドを使用し,翻訳ファイルの定義を使用して :attribute:value を置換します。
  • 置換結果を利用して, Response インスタンスを再生成します。
<?php

namespace App\Auth\Access;

use Illuminate\Support\Facades\Validator;
use Illuminate\Auth\Access\Response as BaseResponse;
use Illuminate\Database\Eloquent\Model;

class Response extends BaseResponse
{
    public function of(Model $model, string $attribute): Response
    {
        // ルールは不定なので,ルールごとの replacer は
        // 使用しないという意図で _ という文字列を渡す
        $rule = '_';

        $message = $this->message !== null
            ? Validator::make([$attribute => $model->$attribute], [])
                ->makeReplacements($this->message, $attribute, $rule, [])
            : null;

        return new static($this->allowed, $message, $this->code);
    }
}

そして,標準の HandlesAuthorization の代替となるヘルパートレイトを作成すれば完了です。

<?php

namespace App\Policies;

use App\Auth\Access\Response;

trait HandlesAuthorization
{
    protected function allow(?string $message = null, $code = null): Response
    {
        return Response::allow($message, $code);
    }

    protected function deny(?string $message = null, $code = null): Response
    {
        return Response::deny($message, $code);
    }
}

あとは

  • validation.php にメッセージの翻訳を定義
  • validation.phpattributes.<フィールド名> に翻訳された :attribute 相当の値を定義
  • validation.phpvalues.<フィールド名>.<値> に翻訳された :input 相当の値を定義

をやって終わりのはずなんですが…

return [
    'insufficient_permission' => '権限不足のため,:attributeに「:input」を指定することができません。',
    'attributes' => [
        'role' => 'ロール',
    ],
    'values' => [
        'role' => ['admin' => '管理者', 'write' => '書き込み', 'read' => '読み取り'],
    ],
];

Validator の継承 (バグ対応)

実は,標準の Validator では,values を考慮した :input の置換をビルトインのルールでしかやってくれません!現時点では,以下のようになってしまいます。

権限不足のため,:attributeに「:input」を指定することができません。
↓
権限不足のため,ロールに「admin」を指定することができません。

この問題を修正するプルリクエストを Laravel フレームワーク本体の 7.x ブランチ向けに提出し,既にマージされています。残念ながら破壊的変更であるため, 6.x には適用されません。

6.x ではこれを解消するために,適当なサービスプロバイダで Validator::resolver() を使用して継承した Validator を生成するようにします。

<?php

namespace App\Providers;

use App\Validation\Validator as ValidatorImpl;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Validator::resolver(function (...$args) {
            return new ValidatorImpl(...$args);
        });
    }
}
<?php

namespace App\Validation;

use Illuminate\Validation\Validator as BaseValidator;

class Validator extends BaseValidator
{
    protected function replaceInputPlaceholder($message, $attribute)
    {
        $actualValue = $this->getValue($attribute);

        if (is_scalar($actualValue) || is_null($actualValue)) {
            // 標準だと :input がそのまま表示されるので Validator::$customValues に置き換える
            $message = str_replace(':input', $this->getDisplayableValue($attribute, $actualValue), $message);
        }

        return $message;
    }
}

これで,列挙値に関してもユーザに見やすい言語で表示することができるようになります!

✨リファクタリング内容の整理

最終的なリファクタリング内容を整理してみます。

コアコンポーネントの作成

  • Gate::inspect() を判定に使用する PolicyRule を作成
    • およびそれを宣言的に無効化するための,何もバリデーションしない NoopRule を作成
    • およびそれを各モデルで使うための HasAuthorizationRules トレイトを作成
  • 認可エラーメッセージ中のプレースホルダをバリデーションの機能を流用して付与できる拡張 Response を作成
    • およびそれを各ポリシーで使うための HandlesAuthorizations トレイトを作成
  • コントローラでの2段階バリデーション呼び出しを集約する ModelValidator を作成

ユースケースごとの対応

  • ポリシークラスに {store|update}<フィールド名>of の命名規則でバリデーションに関するアビリティを定義
  • モデルに2段階バリデーションルールを定義
    • 静的バリデーションは staticValidationRules()
    • インスタンスバリデーションは instanceValidationRules() (認可バリデーションはこちらに定義)
  • コントローラでは ModelValidator からバリデーションを実行する

?最後に

バリデーションをモデルに書くか,それともコントローラかフォームリクエストに書くか。永遠の議題ですが,基本的なビジネスロジックがモデルベースになっていて,且つ DRY を優先して大きなメリットが得られるような場合には,モデルバリデーションを導入する価値はあるでしょう。

その際,今回の主題である「ポリシーのバリデーションへの活用」が権限判定の絡む複雑なバリデーションで力になってくれるはずです。また,もしモデルバリデーションを選択しなくても,ポリシーの書き方を少し変更すれば柔軟に対応することも可能ではあるでしょう。

昨日は @saya1001kirinn さんによる Laravelリレーション初心者向け!外部キーがデフォルトでないパターン!!! でした。勢いだけで内容が頭に入ってこない記事だったので少しマークダウンの整形をお手伝いさせていただきました(笑)

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

Microsoft製OSS、daprでRoadrunner + PHPのアプリケーションを動かす

弁護士ドットコム Advent Calendar 2019- Qiitaの19日目の記事です。

先日Microsoftが公開したdaprというOSSの上でPHPを動かせるか検証した内容になっています。

ことはじめ

2019年10月、Microsoftはdapr(Distributed Application Runtime) というOSSを公開しました。現状まだα版なのでプロダクションには載せられないですが、メッセージングを中心としたマイクロサービス開発を容易にすると書かれています。pub/subを中心にしたアーキテクチャの実現、というのは面白そうなプロダクトだと思ったのでこの機会に触ってサンプルを作ってみました。

(その他、daprの詳細はこちらのQiitaの記事こちらの記事を見るのが良いかもしれません。)

どんなの作ったの

このような構成のサンプルをPHPで作りました。
コードはこちら

想定したユースケース(適当)としては、Twitterみたいなフォローフォロワーの概念があったとして、フォロワーが増えた時に"followerAdded"みたいなイベントを発火させて、emailとpush通知のマイクロサービスを叩くようなフローです。(適当)

スクリーンショット 2019-12-18 0.14.32.png

なんでPHPなの

dapr自体、まだ公開されて間も無いため、日本語はおろか英語でもなかなか情報がなく、チュートリアルもnode,go,pythonはあるものの、PHPはなかったためです。PHPでも動くといいなーと思ったので今回調べてみました。(動きました)

daprのしくみ

daprはpub/subを実現するために、daprdというプロキシを提供しています。アプリケーションはこのプロキシを介してWebサーバーを動作させる必要があります。

ローカル環境(ホストマシン上)で直接動かしたり、kubernetes上でサイドカーとして動かす例がチュートリアルに載っています。ホストマシンの環境を汚さずに簡単に使いたいのであれば、プロキシを同じコンテナで動作させることも可能でした。(やろうと思えば)

今回はdocker-composeで簡単に構築したいので、プロキシを同じDockerイメージに含めて動作させます。コンテナはこのようなイメージです。

スクリーンショット 2019-12-17 22.28.23.png

Dockerfileはこんな感じになります。

FROM php:7.3-cli-alpine AS base-image

RUN apk --update --no-cache add wget bash

# install dapr-cli
RUN wget -q https://raw.githubusercontent.com/dapr/cli/master/install/install.sh -O - | /bin/bash

# daprdは`dapr-cli init`を叩くと落ちてくるが、同時にredisとplacementという別のコンテナを立ち上げてしまう。
# イメージの構築で叩けないので、直接curlでバイナリを落とすことにする。
# ↓このへんのソースを読むとdaprdのパッケージが置いてあるURLがわかる。
# ref. https://github.com/dapr/cli/blob/d585612185a4a525c05fb62b86e288ccad510006/pkg/standalone/standalone.go#L265
RUN curl -Lo /tmp/daprd.tar.gz \
      https://github.com/dapr/dapr/releases/download/v0.3.0/daprd_linux_amd64.tar.gz \
  && tar xzvf /tmp/daprd.tar.gz -C /usr/local/bin

...略

FROM base-image AS app
# 本来は自分でdaprdとアプリケーションのプロセスを立ち上げる必要があるが、
# dapr-cliのコマンドを使って簡単な方法で起動させる
EXPOSE 8080
ENTRYPOINT [\
  "dapr",\
  "run",\
  "--app-port",\
  "8080",\
  "sh",\
  "invoke-service.sh"\
]
CMD [""]

実際にアプリケーション全体を動作させるにあたり、各サービスのコンテナで動作させるプロキシの他にもいくつかコンテナが必要です。placementと名付けられているコンテナ、メッセージブローカーの役割を負うミドルウェア、エンドポイントとなるコンテナです。結果、イメージとしてこのような構成になります。

スクリーンショット 2019-12-18 0.01.10.png

  • placement

    • あまり仕組みが理解できていないのですが、proxyが起動した際にコンテナのIPがログに表示されるのでサービスディスカバリーやロードバランサー的な役割に見えます。
  • メッセージブローカー

    • dapr-cliを使った場合、デフォルトではredisが起動するようになっています。 チュートリアルをみた感じだと、kafkaなども使えるようです。
  • エンドポイント

    • daprdがそのままエンドポイント用のプロセスとしても動作します。"/v.1.0/publish/xxxEvent"のようなパスでPOSTするとイベント(トピック)が発火します。トピックのサブスクライブ方法は後述します。
    • 今回は取り上げないですが、サブスクライバ側にはapp-idというものが付与できるため、"/v.1.0/invoke/app-id/method/xxxhogefuga"というような呼び出し方でGETやPOSTを使った同期通信(たぶん)もできるようでした。

placementとメッセージブローカーにあたるコンテナはdapr-cliを使ってdapr initと叩いても起動できます。今回のようにdocker-composeを使う場合には自前でplacementとredisなど用意する必要があります。
(dapr initで立ち上げたコンテナ情報をdocker-compose.ymlに書くだけなので用意は簡単でした。)

ネットワークは謎です。各daprdとplacement, message brokerが具体的にどういった流れでネットワーク越しに連携するのかまでは調べられませんでした。(ドキュメントとかに載っているのかな??)

(余談)RoadRunnerってなに

Golang製のPHPのプロセスマネージャで、Nginxとかに頼らずWebサーバーを立てられるようになります。(ざっくり)

  • 簡単に入れられる(独自調べ)
  • 速いらしい
  • production readyとのこと

PHPでつくる部分(api-facade)

クライアントからまず通信を待ち受けるapi-facadeの部分になります。

RoadRunnerを使った場合、同じプロセスで複数のリクエストを処理するため、これを踏まえた実装が必要です。今回サンプルコードを1枚のPHPに収めるべくSlim4を使うことにしており、繋ぎ込む必要があります。繋ぎ込むといっても特に難しいことはなく、RoadRunner側で用意しているHttpClientが返すRequestクラスはPsr\Http\Message\ServerRequestInterfaceを継承しているため簡単にSlim4へ渡すことができます。下記のようなコードで繋ぎ込みは完了です。

api-facade/api-facade.php
$app = AppFactory::create();
...中略
$psr7 = new RoadRunner\PSR7Client($worker);

while ($req = $psr7->acceptRequest()) {
    try {
        // RoadRunnerのリクエストはSlim4へそのまま渡すことができる
        $resp = $app->handle($req);
        $psr7->respond($resp);

イベントのパブリッシュはdaprのエンドポイントへPOSTします。
サンプルでは簡易的に下記の関数を定義してあります。
今回はGuzzleを使いました。

api-facade/api-facade.php
function publishTopic(string $topic, array $message)
{
    (new Client)->post(
        "dapr-endpoint:3500/v1.0/publish/${topic}",
        [
            'form_params' => $message
        ]
    );
}

POSTだけ待ち受けるfollowersというリソースを用意します。
特に処理はしません。followerが増えた"てい"です。

api-facade/api-facade.php
$app->post('/followers', function (ServerRequestInterface $request, ResponseInterface $response, $args) {
    // add follower process
    // ...
    // ...

    publishTopic('followerAdded', []);

    $response->getBody()->write('');
    return $response;
});

PHPでつくる部分(email-notification-service)

daprdは同じコンテナ内で起動しているアプリケーションを検知した際に特定のパスでGETで通信を行います。このタイミングでサブスクライブするトピックなどを、サービス側からdapr側にレスポンスを返して伝えることになります。

  • /dapr/config
    • とりあえず200でレスポンス返してあげれば良さそうでした。(記載が見つけられなかった)
  • /dapr/subscribe
    • jsonでサブスクライブするトピックのリストを返す必要があります。
    • ここではfollowerAddedをリストに加えてレスポンスを返しています。
email-notification-service.php
$app->get('/dapr/config', function (ServerRequestInterface $request, ResponseInterface $response, $args) {
    return $response;
});

$app->get('/dapr/subscribe', function (ServerRequestInterface $request, ResponseInterface $response, $args) {
    $subscribeTopics = [
        'followerAdded'
    ];
    $response->getBody()->write(json_encode($subscribeTopics, JSON_THROW_ON_ERROR, 512));
    return $response;
});

'followerAdded'をサブスクライブしているので、'/followerAdded'のパスでPOSTを待ち受けています。
アクセスログでpub/subできているか確認するので、実装は特にしていません。

email-notification-service.php
$app->post('/followerAdded', function (ServerRequestInterface $request, ResponseInterface $response, $args) {
    $response->getBody()->write('');
    return $response;
});

PHPでつくる部分(push-notification-service)

email-notification-serviceと同じような記述です。

$app->get('/dapr/config', function (ServerRequestInterface $request, ResponseInterface $response, $args) {
    return $response;
});

$app->get('/dapr/subscribe', function (ServerRequestInterface $request, ResponseInterface $response, $args) {
    $subscribeTopics = [
        'followerAdded'
    ];
    $response->getBody()->write(json_encode($subscribeTopics, JSON_THROW_ON_ERROR, 512));
    return $response;
});
$app->post('/followerAdded', function (ServerRequestInterface $request, ResponseInterface $response, $args) {
    $response->getBody()->write('');
    return $response;
});

つ・な・げ・て・み・た・い

composerとphpのアプリはコンテナを分けてあるので、動かす場合は最初にsetup用で用意したdocker-composeを動かします。
(依存してるライブラリなどをapi-facadeとサービス側に落とします)
その後、dokcer-compose upで色々立ち上がるかと思います。

docker-compose -f docker-compose-setup.yml up
docker-compose up

最後に下記のような感じでapi-facadeを叩くと、サブスクライブしているサービスに通信が飛んでいることがアクセスログからわかるかと思います。

curl -X POST localhost:8080/followers

スクリーンショット 2019-12-18 2.48.39.png

まとめ

ロードマップにはこんなこと書いてるので、PHPもSDK待ちかなーとか、かと思っていましたが、daprとのインターフェースはhttpかgRPCのようなので、PHPでも実装できました。

今回取り上げませんでしたが、pub/subだけでなく同期通信もでき(チュートリアルでは一番最初にやります)、インターフェースもhttpかgRPCでdaprが間を取り持ってくれるので、pub/subやるために特定のミドルウェアと直接お喋りする必要がなくなるのは良い印象です。

α版なので実際にゴリゴリ使うには早いですし、流行るかも不明ですが、面白そうなのでしばらく追ってみたいなと思いました。

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

Laravel autoload されるからrequire_onceは不要

前置き

Laravelではrequireを書くことが殆どないので「あれ?PHPってファイルの読み込み不要なんだ」みたいな錯覚に陥ってしまいました。

// requireしなくていいじゃん!!!PHPすげー?
use Carbon\Carbon;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;

生PHPの場合

PHPはファイルを事前に読み込み(required)、別ファイルとの連携を行っています。

// ファイルの読み込み
require_once 'Sub/hello.php';
// 読み込んだファイルのHelloクラスoutputの実行
echo Hello::output();

Laravelの場合

全ファイルをrequire_onceしていると骨が折れてしまうので、
public/index.phpで事前にファイルをautoload,読み込んでいます。

require __DIR__.'/../vendor/autoload.php';

autoloadの挙動の詳細は触れませんが、これにより、Laravelではrequireを記述せずになんとなくで別ファイルの利用が可能です。

laravelっていうか、autoload最高って感じですね

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

[Re:ゼロから始めるVue生活] Vueで検索/ソートさせてみた(php連携)

Vueでリスト検索とソート機能を作ってみたので
さらに応用して、phpの配列データからVueに流し込んで処理した機能を作ってみました。

ロジック

php配列データ

JSONデータに変換

JSONデータをJSの配列に格納

JSの配列をvueのdataに格納
これでvueで取り扱えるデータになりました。

computed(算出プロパティ)
matched:
フォーム入力の数値 <= budgetの数値のリストのリストを表示
sorted:
ボタンのオンオフで昇順・降順ソート

limited:
limit数分表示できる

<?php
    $list = [
        ['id' => '1', 'name' => '商品A', 'price' => '500'],
        ['id' => '2', 'name' => '商品B', 'price' => '300'],
        ['id' => '3', 'name' => '商品C', 'price' => '2000'],
        ['id' => '4', 'name' => '商品D', 'price' => '5000'],
        ['id' => '5', 'name' => '商品E', 'price' => '1500'],
        ['id' => '6', 'name' => '商品F', 'price' => '250'],
        ['id' => '7', 'name' => '商品G', 'price' => '100'],
        ['id' => '8', 'name' => '商品H', 'price' => '750'],
    ];
    $list_json = json_encode($list);
?>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Vue App</title>
</head>
<body>
    <div id="app">
        <input v-model.number="budget">円以下
        <p>{{ matched.length }}件表示中</p>
        <button v-on:click="order=!order">価格 ▼</button>
        <div v-for="item in limited" v-bind:key="item.id">
            {{ item.name }}: {{ item.price }}円
        </div>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/lodash@4.17.5/lodash.min.js"></script>

    <script>
        let list = JSON.parse('<?php echo $list_json; ?>');
        const app = new Vue({
            el: '#app',
            data: {
                // 検索初期値
                budget: '',
                // 検索数
                limit: 10000000000000,
                // 検索リスト
                list: list,
                // ソート初期値
                order: false,
            },
            computed: {
                matched: function() {
                    return this.list.filter(function(el) {
                        return el.price <= this.budget
                    }, this)
                },
                sorted: function() {
                    return _.orderBy(this.matched, 'price', this.order ? 'desc' : 'asc')
                },
                limited: function() {
                    return this.sorted.slice(0, this.limit)
                }
            }
        });
    </script>
</body>
</html>

phpを使用せずVueだけで作ったコードもありますので
こちらを参照してください。
[Re:ゼロから始めるVue生活]メモ

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