20191218のlaravelに関する記事は18件です。

画像の動的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で続きを読む

Laravel Socialite 独自ドライバーの作り方・改

前に書いたけどQiita版は後から修正しないのでまた書く。

環境

  • Socialite 4.3.1
  • Laravel 6.x
  • PHP 7.4

SocialiteProvidersは非公式

Laravelのドキュメントに書かれているけどSocialiteProvidersはコミュニティによる非公式なもの。サードパーティパッケージ。
https://socialiteproviders.netlify.com/

最近見かけたfreeeのPHP SDK
https://github.com/freee/freee-accounting-sdk-php
これもSocialiteProviders使ってる。Socialiteは問題なく動くとは思うけどその後Auth周りの色々やっている部分がなんでこんなことしてるのかさっぱり分からない。(たぶんDB使わないようにしてるだけ)

こんなことしなくても公式のSocialiteを拡張する形で作れば簡単。

Laravelプロジェクト内に作る

一番シンプルに既存のプロジェクト内に作る方法。

app/Socialite/FooProvider.phpを作る。

namespace App\Socialite;

use Laravel\Socialite\Two\AbstractProvider;
use Laravel\Socialite\Two\ProviderInterface;
use Laravel\Socialite\Two\User;

class FooProvider extends AbstractProvider implements ProviderInterface
{
    //
}

中身は公式からコピペして書き換えればいい。URLとmapUserToObject()が主。
https://github.com/laravel/socialite/tree/4.0/src/Two

次にAppServiceProvider@boot()

use Illuminate\Support\ServiceProvider;
use Laravel\Socialite\Facades\Socialite;
use App\Socialite\FooProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot()
    {
        Socialite::extend(
            'foo',
            function ($app) {
                return Socialite::buildProvider(FooProvider::class, config('services.foo'));
            }
        );
    }
}

PHP7.4なら1行で書けるかも。

Socialite::extend('foo', fn($app) => Socialite::buildProvider(FooProvider::class, config('services.foo')));

後はconfig/services.php.envで設定。

    'foo' => [
        'client_id'     => env('FOO_ID'),
        'client_secret' => env('FOO_SECRET'),
        'redirect'      => env('FOO_REDIRECT'),
    ],

これでSocialite::driver('foo')で使えるようになる。

作ったのはFooProviderの1ファイルだけ。
SocialiteProvidersのなぜかイベント使ってるような箇所は不要。

composerパッケージに分離

最低限必要なファイルは3つ。

  • FooProvider.php
  • FooServiceProvider.php
  • composer.json

FooServiceProviderが前は少し間違ってた。複数の独自ドライバーを同時に使った時のみ発生するので気付きにくかった。
LaravelやSocialiteのバージョンの影響も受けるけど現時点のLaravel6とSocialite4.3.1ならこれでいい。

namespace My\Socialite\Foo;

use Illuminate\Support\ServiceProvider;
use Laravel\Socialite\Facades\Socialite;

class FooServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap the service provider.
     *
     * @return void
     */
    public function boot()
    {
        Socialite::extend(
            'foo',
            function ($app) {
                $config = $app['config']['services.foo'];

                return Socialite::buildProvider(FooProvider::class, $config);
            }
        );
    }
}

テストは公式を参考に真似すればいい。
https://github.com/laravel/socialite/blob/4.0/tests/OAuthTwoTest.php

  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

laravel-admin で多対多リレーションの項目を multipleSelect() で AND 検索する方法

処理の流れは以下のとおりです。

  1. grid画面でmultipleSelect()を使用して一つの項目で複数選択できるようにします
  2. multipleSelect()で取得した配列で中間テーブルを検索します
  3. 中間テーブルで多対多で紐づくテーブルのIDを検索し、全てヒットするもう一つの外部キーをgroupbyとhavingを使用して特定します
  4. $query->whereIn に特定した外部キーの配列を渡してフィルタします
  • ActivationKeyCreateHistoryProduct.php (中間テーブル)
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class ActivationKeyCreateHistoryProduct extends Model
{
    protected $table = 'activation_key_create_history_product';

    public static function ActivationKeyCreateHistoryProducts($productIds)
    {
        return ActivationKeyCreateHistoryProduct::whereIn('product_id', $productIds)
            ->groupBy('activation_key_create_history_id')
            ->havingRaw('count(distinct product_id) = ' . count($productIds))
            ->get('activation_key_create_history_id');
    }
}
  • ActivationKeyCreateHistoryController.php (フィルタ部分)
        $grid->filter(function ($filter) {
            $filter->where(function ($query) {
                $aKCreateHistoryProducts = ActivationKeyCreateHistoryProduct::ActivationKeyCreateHistoryProducts($this->input);
                $aKCreateHistoriesIds = [];
                foreach ($aKCreateHistoryProducts as $item) {
                    $aKCreateHistoriesIds[] = $item['activation_key_create_history_id'];
                }

                $query->whereIn('id', $aKCreateHistoriesIds);

            }, 'Products')->multipleSelect(Product::pluck('name', 'id')->toArray());
        });
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【vue】Vue Styleguidistの使い方を説明① 〜Laravel + vue環境でVue Styleguidistを動かす〜

※元々ブログに書いていたのですが、qiitaに転載しました
https://www.moyashidaisuke.com/entry/vue-styleguidist-install

概要

vueのstyleguild「Vue Styleguidist」をLaravel + vue環境で使い始めてみました。
思ったより手間取ってしまったので、設定ファイルや、私の環境で発生したエラーの対応等を残しておきます。

↑のカメレオンはVue Styleguidistのロゴ。きもかわいい。

※Laravel環境じゃなくても多分参考になると思います。

Vue Styleguidistとは

とりあえず動作サンプル見た方がわかりやすいのでいきなりですがリンクを。

https://vue-styleguidist.github.io/basic/

こういうコンポーネントの仕様と、サンプルのドキュメントを生成してくれるツールです。 こういうの。

20191005220952.png

GitHubのStar数はこの記事を書いてる時点で1419なので、デファクトになってる感はまだ無いですが、競合のvueseよりはstar数多いのでこちらを採用しました。
https://github.com/vue-contrib/vuese

導入手順(理想形

特にはまらないですんなりいくパターン。

https://vue-styleguidist.github.io/docs/GettingStarted.html#_1-install

前提

  • Laravel + vue環境導入済み
  • npmじゃなくてyarn
  • Vue CLIは使ってない

公式手順だとnpmですが、私の環境ではyarnを使っているのでyarnの手順を紹介します。

インストール

$ yarn add -D vue-styleguidist

〜色々インストールされる。省略〜

Done in 296.61s.

style guildの設定

公式手順だとリンクが2つ貼ってあります。

https://vue-styleguidist.github.io/docs/Components.html#finding-components
https://vue-styleguidist.github.io/docs/Webpack.html

いきなりぶん投げられてわかんないですが、プロジェクトルートに

styleguide.config.jsというファイルを作成してください。

中身は

const VueLoaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
  webpackConfig: {
    module: {
      rules: [
        // Vue loader
        {
          test: /\.vue$/,
          exclude: /node_modules/,
          loader: 'vue-loader'
        },
      ]
    },
    plugins: [
      new VueLoaderPlugin()
    ]
  },
  // vueファイルへのpathを指定
  components: 'resources/js/components/**/[A-Z]*.vue',

}

で私の環境だといけました。

ポイントとしては、webpackConfiglaravel mixの設定を流用しても全く動かないです。(あれはmixで動的にconfigを生成したりしてるので)

また、vue-loaderの設定もちゃんと書いてあげないと動かないです。デフォルトで呼んでくれたりはしないようです。

あと、vueのファイルはLaravelだと普通resources/js以下で作成してる事が多いと思いますが、適時調整してください。

package.jsonにコマンド追加

これは公式そのままで大丈夫です。

{
  "scripts": {
+    "styleguide": "vue-styleguidist server",
+    "styleguide:build": "vue-styleguidist build"
  }
}

実行

hot reload版

yarn run styleguide

サーバが立ち上がってlocalhost:6060でつなげるようになります。vagrantやDocker等の仮想環境を使っている方はポートの設定をしてください。

私はdocker-composeを使っていたので

      ports:
        - 6060:6060 # styleguide

を追加しました。

htmlとjs吐き出す版

yarn run styleguide:build

styleguideというディレクトリにhtmlとjsが吐き出されますので、htmlを開けばOKです。

エラー色々

styleguide.config.jsの設定系

componentsへのpathがおかしい

画面を開くとこれが表示されるパターン。

Welcome to Vue Styleguidist!
We couldn’t find any components using these patterns:

src/{components,Components}/**/*.vue
Create styleguide.config.js file in your project root directory like this:

module.exports = {
  components: 'src/components/**/*.vue'
};
Read more in the locating components guide.

componentsの設定を自分の環境に合わせればOK。

Failed to compile

Failed to compile
./resources/js/components/XXXXX.vue 1:0
Module parse failed: Unexpected token (1:0)
You may need an appropriate loader to handle this file type.

vue-loaderの設定をいれてあげればOK。

vueとvue-template-compilerのバージョン違い

Failed to compile
./resources/js/components/XXXXXX.vue (./node_modules/vue-styleguidist/loaders/vuedoc-loader.js!./resources/js/components/XXXXXX.vue)
Error: 

Vue packages version mismatch:

- vue@2.6.8 (/var/www/node_modules/vue/dist/vue.runtime.common.js)
- vue-template-compiler@2.6.10 (/var/www/node_modules/vue-docgen-api/node_modules/vue-template-compiler/package.json)

This may cause things to work incorrectly. Make sure to use the same version for both.
If you are using vue-loader@>=10.0, simply update vue-template-compiler.
If you are using vue-loader@<10.0 or vueify, re-installing vue-loader/vueify should bump vue-template-compiler to the latest.

バージョン合わせないとダメらしい。
yarn.lockを確認すると

vue-template-compiler@^2.0.0:
  version "2.6.10"
  resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.10.tgz#323b4f3495f04faa3503337a82f5d6507799c9cc"
  integrity sha512-jVZkw4/I/HT5ZMvRnhv78okGusqe0+qH2A0Em0Cp8aq78+NK9TII263CDVz2QXZsIT+yyV/gZc/j/vlwa+Epyg==
  dependencies:
    de-indent "^1.0.2"
    he "^1.1.0"

となっており、^2.0.0 2以上を使ってね、の指定で2.6.10をinstallしちゃってる。

というわけで、無理やり2.6.8を入れてみる。

$ yarn add vue-template-compiler@2.6.8
yarn add v1.13.0

info Direct dependencies
└─ vue-template-compiler@2.6.8
info All dependencies
└─ vue-template-compiler@2.6.8
Done in 223.61s.

これで2.6.8入ったと思いきや、2.6.10も入ったままなのでダメ。

yarn.lockはこんな状態

vue-template-compiler@2.6.8:
  version "2.6.8"
  resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.8.tgz#750802604595134775b9c53141b9850b35255e1c"
  integrity sha512-SwWKANE5ee+oJg+dEJmsdxsxWYICPsNwk68+1AFjOS8l0O/Yz2845afuJtFqf3UjS/vXG7ECsPeHHEAD65Cjng==
  dependencies:
    de-indent "^1.0.2"
    he "^1.1.0"

vue-template-compiler@^2.0.0:
  version "2.6.10"
  resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.10.tgz#323b4f3495f04faa3503337a82f5d6507799c9cc"
  integrity sha512-jVZkw4/I/HT5ZMvRnhv78okGusqe0+qH2A0Em0Cp8aq78+NK9TII263CDVz2QXZsIT+yyV/gZc/j/vlwa+Epyg==
  dependencies:
    de-indent "^1.0.2"
    he "^1.1.0"

installしたりremoveしてもダメだったので、最終手段でyarn.lockを直接書き換える。

vue-template-compiler@2.6.8:
  version "2.6.8"
  resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.8.tgz#750802604595134775b9c53141b9850b35255e1c"
  integrity sha512-SwWKANE5ee+oJg+dEJmsdxsxWYICPsNwk68+1AFjOS8l0O/Yz2845afuJtFqf3UjS/vXG7ECsPeHHEAD65Cjng==
  dependencies:
    de-indent "^1.0.2"
    he "^1.1.0"

vue-template-compiler@^2.0.0:
  version "2.6.8"
  resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.8.tgz#750802604595134775b9c53141b9850b35255e1c"
  integrity sha512-SwWKANE5ee+oJg+dEJmsdxsxWYICPsNwk68+1AFjOS8l0O/Yz2845afuJtFqf3UjS/vXG7ECsPeHHEAD65Cjng==
  dependencies:
    de-indent "^1.0.2"
    he "^1.1.0"
$ yarn install

これで動きました。

issueはちょいちょい見かけるのだけど、ちゃんとした対応方法は不明。誰か知ってたら教えて下さい。

https://github.com/vuejs/vue/issues/3941

実際のドキュメントを生成したり、パラメータの解説したりの予定。

第2段

https://www.moyashidaisuke.com/entry/vue-styleguild-sections

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

一人で新規Webサービスを作ろうとしたら、Amplifyにたどり着いた

こんにちは。
フリーランスエンジニアのtelumoです。

はじめに

この記事では、

  • Amplifyを使うまでの経緯
  • 実際に使ってみた所感

について書きたいと思います。
具体的なAmplifyの使い方は他の記事や公式に任せたいと思います。
この記事では、私の目線でAmplifyを語っていますので、この記事を読めば体系的にAmplifyが理解できるとかは期待しないでください。。。(すみません)

Amplifyがバックエンドの選択肢の一つになる、そんなきっかけになれば幸いです。

Amplifyを使うまでの経緯

前提

今年、友人の紹介である建設会社でWebサービスを開発する依頼を受けました。
そのWebサービスは求人サイトで、サービスの特徴としては、地図から求人が探せるというものです。
大島てるの求人サイト版のようなイメージです。

得意なフレームワークでWebアプリを構築

はじめ私はDjangoでアプリを構築していました。
(一人プロジェクトだったので技術は自由に選べました)

Djangoは、

  • モデルを記述するだけでDBマイグレーションをやってくれる
  • ユーザーのクラスを継承するだけで複数ロールのユーザーを作成して、それぞれに認可を与えられる
  • テンプレートエンジンを利用してフロントが比較的楽にかける

といった特徴があります。
これらは、Djangoの特権でもなんでもなく、他のフルスタックフレームワークにも存在する便利機能です。

私はPythonが得意で、DjangoでのWebアプリ構築経験があったので、
「まずはDjangoで」
的なノリでアプリを構築しました。

プロトタイプが完成

地図を使ったアプリは作ったことがなかったので、最初の段階では実際にアプリを作りながら技術検証をしていました。
インフラ(AWSを利用)はpulumiでさくっと実装しました。

余談ですが、pulumiのawsxを利用すると、AWSのベストプラクティスに則ったインフラが数行のコードで書けるのでおすすめです。

そして、最初のプロトタイプが完成しました。

DjangoでWebアプリ自体は比較的に簡単に作れたのですが、なんか遅いんです。
地図をドラッグしたあとのアイドル状態時にデータベースからデータを取得して地図に表示する仕組みなのですが、
なんか遅い。

調べてみると、

  1. WebサーバーがDBサーバーからデータを取得するのに時間がかかっている
  2. ブラウザが取得するデータの通信量が大きい

というダブルパンチで遅くなっていることがわかりました。

まず1に関しては、WebサーバーとDBの最適化が必要だなと感じました。
プロトタイプなので、どちらも小さなスペックのものを利用していたので遅いのは当然なのですが、新規サービスでどれだけのスペックのものを用意すればいいのかが測りづらい、、、
当然ですが、サービスが利用されなくてもサーバー台はかかる。。。そもそも、一人プロジェクトでインフラに時間をかけたくないな。。。
みたいなことを考えていました。
スペックの問題以外にも、クエリが最適化されているかという点も自信がありませんでした。

次に2に関してですが、APIのレスポンスの実装がおかしいことがわかりました。
テストデータが数百件のうちは、いいんですが、数万件になった時に、取得するデータがたくさんあると、(当たり前ですが)表示も遅くなります。
だからAPIのレスポンスのパラメーターを適切に調節してあげればいいだけです。
しかし、それと同時に
もしかして、APIの修正が入ったらいちいちAPI側とブラウザ側書き直さなきゃいけない!?
みたいなことを考えていました。

WebサーバーをLambdaにする

これを解決するために、色々と試行錯誤しました。
後から考えると、この試行錯誤している時の私の失敗は、
Djangoメインで考えていたことです。
そもそも、Webアプリケーションを作るとなったらWebサーバー + RDBという構成は一般的で、
Djangoでなくても、そういう構成で利用できるフレームワークをまず考えてしまう人は多いと(勝手に)予想しています。

試行錯誤のうちの一つは、WebサーバーをLambdaに置き換えるというものです。
「Webサーバーがサーバーだからスペックやコストを気にする必要があるんだ。だったらサーバーレスにすればいいじゃない。」
という単純な思考です。

LambdaにDjangoを乗せるには、zappaを使えば簡単ですね。
ただ、ここで有名な問題にぶち当たります。
それは、「Lambda + RDS相性良くない問題」です。

ただ、Aurora ServerlessのData APIを利用すれば、その問題を解決できると思ってました。
確かにそれでLambdaからRDSは問題なく利用できます。
しかし、それだとDjangoの良さを殺すことになります。何せ、Data APIを呼び出すためのコードを別に書く必要があるのですから。

それに、「Webサーバー + RDB」構成において、RDB(RDS)が占めるコストは少なくありません。
新規サービスでリクエスト数もわからないのに常時稼働のRDBを保持するのは賢明ではないと思いました。

他にも、コンテナサービスであるFargateやk8sを考えましたが、学習コストが高いのと、RDBの問題で二の足を踏んでました。

Amplifyを思い出す

そんなこんなしている時にふとAmplifyを思い出しました。
AWS Dev Day 2018の「Dev Day Challenge」に参加した際に、チャットアプリを作りAmplifyを使ったのです。

Dav Dayのときは、開発時間もあっという間だったので詳しく理解することはできませんでしたが、
「そういえば、AWSソリューションアーキテクトの人がAmplifyをめちゃくちゃ推してたな」
という印象が強く残っていたので、調べてみることにしました。

すると、以下の点で今回の案件にぴったりなバックエンドだと感じました。

  • サーバーレスでリクエスト課金なので、リクエスト数に最適なインフラを構築する必要がない(そもそもBaaSなのでインフラを意識する必要ない)
  • GraphQLがデフォルトで使え、必要なデータをフロントが選択できる(APIを実装する必要がない)
  • ユーザーの種類に応じて権限を細かく設定できる(Cognitoとの連携がやりやすい)

ということで、色々ありましたがAmplifyを使ってみることにしました。
Amplifyを使うということは、Djangoはもう必要がないということです。

フロントはNuxt.jsでやりました。
地図の実装が重要なアプリケーションなので、HTMLを返Djangoのようなフレームワークは合っていなかったのかなと思います。

Amplifyを使ってみた所感

Amplifyたどり着くまでの話が長くなりましたが、ここからは実際に使ってみた感じたことを書きます。

結論を先に言うと、Amplify最高でした!
これからWebアプリケーションを作る際に、まず間違いなく有力な選択肢の一つになると思います。

そういえば、Amplify自体の説明をしていませんね。
Amplifyは、AWSのBaaSです。フロントからバックエンドを簡単に構築することができます。
詳しいことは、公式や記事等を参考にしてください。

では、所感を書いていきます。

scheme.graphqlが、データモデルのドキュメントになる

Amplifyでは、バックエンドとしてGraphQLが簡単に利用できます。
データストアはDynamoDBなのですが、GraphQLを挟むことで普通のCRUDや、チャットのようなサブスクリプションが必要なアプリが楽に構築することができます。

GraphQLを利用するために、scheme.graphqlというファイルを書きます。
逆に言うと、データストア周りで書く必要があるのはscheme.graphqlだけです。

これが意味するのは、scheme.graphql自体が実質的にデータモデルのドキュメントになるということです。
これは非常に楽です。このscheme.graphqlファイルを見るだけで、

  • どんなテーブルが存在するのか
  • それぞれのテーブルにどんなカラムが存在するのか
  • 各カラムの型
  • テーブルへのアクセス権限まわり

が一目瞭然なのです。

これをみてください。

type Salary @model @auth(rules: [{allow: groups, groups: ["Admin"]}]) {
  id: ID!
  wage: Int
  currency: String
}

上記は、Amplifyの公式に書かれた例です。
GraphQLを触ったことがない人でも少なくとも以下のことが読み取れるのではないかと思います。

  • Salaryモデルは、ID型のidInt型のwageString型のcurrencyという3つのフィールドから成り立っていること。
  • Adminグループにのみ何かしらの権限が与えられていること。

@modelディレクティブで実際にDynamoDBにテーブルを作ってくれるとか、
Adminグループに与えられる権限はCRUD全ての権限であるとか、
ID型のidはAWSが自動生成してくれるとか、、、
そう言ったことはドキュメントを読んで一つ一つ理解していく必要がありますが、このようなモデルをScheme.graphqlに記述していくだけで、テーブルが簡単に作れます。しかも、(Cognitoと連携して)どんなユーザーにどんな権限が許可されているのかもすぐにわかります。

さらにAmplifyはこのScheme.graphqlを読み取って自動でquery、mutation、subscriptionのJavaScript(or TypeScript)のスクリプトを自動生成してくれます!
素晴らしい。

Djangoやその他のフレームワークでも同じようなことができるとは思います。
Djangoの場合は、モデルのクラスを実装すればいいんです。
しかしアプリごとに別々のファイルに書く必要がありますし、所詮スクリプトなので定義的ではありません。

余談ですが、宣言的なコードを見ればドキュメントを書く必要がない(少なくとも見る必要がない)と言うのはエンジニアの理想です。
関数型言語が流行ったのもそういった背景があるからだと思います。

AmplifyはRDBユーザーでも利用しやすい

AmplifyのデータストアであるDynamoDBはいわゆる「NoSQL」です。そのため、RDBを主に利用する人にとっては理解しにくいところです。

しかし、AmplifyはRDBを利用する人が理解しやすいと感じます。
RDBでは、「1対多」や「多対多」の関係を利用してテーブルを作成していきますが、同様のことがScheme.graphqlにも記述できます。
(そもそも、AWSのGraphQLサービスであるAppSyncはバックエンドにRDSも利用できるので当たり前かもしれません。)

type Post @model {
  id: ID!
  title: String!
  comments: [Comment] @connection(keyName: "byPost", fields: ["id"])
}

type Comment @model
  @key(name: "byPost", fields: ["postID", "content"]) {
  id: ID!
  postID: ID!
  content: String!
  post: Post @connection(fields: ["postID"])
}

上記は、@connectionディレクティブを利用した1対多の関係を表したテーブルのサンプルコードです。
Postテーブルが複数のComment([Comment]のように配列で表している)を持っていることが一目瞭然です。
Post側にもComment側にも@connectionを記述することでどちらからでももう一方を取得することができます。

そして取得する時には、以下のようなGraphQLのクエリを発行すればいいのです。


query GetCommentWithPostAndComments {
  getComment( id: "a-comment-id-1" ) {
    id
    content
    post {
      id
      title
      comments {
        items {
          id
          content
        }
      }
    }
  }
}

GraphQLはフロントで取得したいカラムを選択できます。
さらにAmplifyはこのquery自体も自動で生成してくれます!
(もちろん自分でカスタマイズしたクエリを記述して利用することも可能です。)

多対多のモデルは、間にテーブルを一つ挟めば実現できます。(RDBと同じですね。)

type Post @model {
  id: ID!
  title: String!
  editors: [PostEditor] @connection(keyName: "byPost", fields: ["id"])
}

type PostEditor
  @model(queries: null)
  @key(name: "byPost", fields: ["postID", "editorID"])
  @key(name: "byEditor", fields: ["editorID", "postID"]) {
  id: ID!
  postID: ID!
  editorID: ID!
  post: Post! @connection(fields: ["postID"])
  editor: User! @connection(fields: ["editorID"])
}

type User @model {
  id: ID!
  username: String!
  posts: [PostEditor] @connection(keyName: "byEditor", fields: ["id"])
}

CI/CDもAmplify1つで。

複数環境をAmplify CLIで構築、デプロイできることは当たり前なのですが、Amplifyコンソールを利用すれば、GitHubなどのGitレポジトリと連携して環境毎のバックエンドを自動で選択してくれます。

環境 Gitレポジトリのブランチ バックエンド(Amplify)
本番環境 master prod
開発環境 dev dev

上記のように、本番環境と開発環境があって、Gitレポジトリのブランチがそれぞれmaster、devと分かれているとします。
そして、本番環境はバックエンドのprod環境を、開発環境はdev環境を利用したい。
よくありますよね。

それ、Amplifyコンソールで環境変数を設定しておけば、Amplifyが自動でやってくれます。
つまり、masterブランチに変更(プルリクからのマージ等が行われるなど)があった場合は、prodのバックエンドが利用され、
devブランチに変更があった場合は、devバックエンドが利用されます!
(もちろんバックエンドの環境は、本番、開発だけでなく自由に追加することができます。)

これは便利ですよね。
フロント側はバックエンドの指定等一切することがなく、Amplifyがやってくれるのです。

また、gitのブランチの変更を感知してデプロイされるタイミングでテストも自動でやってくれます。
現在はE2EのテストフレームワークであるCypressのみ利用できます。

おわりに

ざっくりとAmplifyを利用するに至った経緯と所感を私の視点ではありますが、書いてみました。

書き忘れましたが、最初の問題であった「レスポンスの遅さ」はAmplifyを利用することで解決していました。
もちろんサーバーを使っても、解決していたと思います。
しかし、課金コストの問題や一人でインフラを管理する時間的コストを考えると、Amplifyにして正解だと思います。

余談ですが、BaaSといえばFirebaseも有名ですが、

  • GraphQLが楽に使える
  • テーブルへのアクセス権限が同じファイルに楽にかけて管理しやすい
  • 環境を自由に分けられる

そんなAmplifyの方が私は好きです。

  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

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で続きを読む

Nuxt.jsでvue-carouselで商品画像一覧をカスタマイズして表示する

概要

商品画像を画像と商品名の組み合わせのコンポーネントを作成し、カルーセルスライダーで表示を行う。
商品は4つずつ並べて横にスライドさせていけるイメージです。

画面イメージ

screencapture-localhost-client-products-category-1-2019-12-18-00_29_38.png

参考

nuxt.jsにvue-carouselを導入してスライダーを作成

実装手順

vue-carouselのインストール

npm install -S vue-carousel

Nuxtのプラグインでvue-carousel.jsを作成し、下記を実装する

vue-carousel.js
import Vue from 'vue'
import VueCarousel from 'vue-carousel'

Vue.use(VueCarousel)
nuxt.config.js
plugins: [
    { src: '~/plugins/vue-carousel', ssr: false }
  ],

コンポーネント設計

今回は、4枚ごとの商品画像を表示するようのコンポーネントとして実装していおきます。

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

実装

実際のコンポーネントの実装

<carousel><slide>の中に商品画像と価格と商品名をv-forで描画するようにする

:per-page="4"で4枚単位で表示するように指定しました。

中身のCSSの設定は.VueCarousel-slideの中に記載

ProductCardCarousel.vue
<template>
  <carousel :per-page="4" :pagination-enabled="false">
    <slide v-for="(prodduct_item, key) in productList" :key="key">
      <div class="product-card">
        <div v-if="prodduct_item.imgURL != ''">
          <img class="product-card-img" :src="prodduct_item.imgURL" />
        </div>
        <div v-else>
          <img
            class="product-card-img"
            :src="require('@/assets/img/NoImage.png')"
          />
        </div>
        <div class="product-card-content">
          <div class="product-card-price">
            {{ prodduct_item.price }}ポイント
          </div>
          <div class="product-card-text">{{ prodduct_item.name }}</div>
        </div>
      </div>
    </slide>
  </carousel>
</template>

<script>
import Carousel from 'vue-carousel/src/Carousel.vue'
import Slide from 'vue-carousel/src/Slide.vue'
export default {
  components: {
    Carousel,
    Slide
  },
  layout: 'client/simple',
  props: {
    productList: {
      type: Array,
      required: true,
      default: () => []
    }
  }
}
</script>

<style lang="scss" scoped>
@import '~/assets/scss/base.scss';

.VueCarousel-slide {
  padding: $space_m $space_m $space_m $space_m;
  .product-card {
    .product-card-img {
      border-radius: 50%;
      height: 80px;
    }
    .product-card-content {
      text-align: center;
      .product-card-price {
        font-size: $font-size_xs;
        font-weight: bold;
        padding: $space-s 0 0 0;
      }

      .product-card-text {
        font-size: $font-size_xs;
        padding: $space-s 0 0 0;
      }
    }
  }
}
</style>

ProductCardCarousel.vue を使用する際には下記のように実装

<template>
  <div>
    <div v-for="(category_item, key) in categoryList" :key="key">
      <div class="category-title">{{ category_item.categoryTitle }}</div>
      <product-card-carousel
        :product-list="category_item.productList"
      ></product-card-carousel>
      <product-link
        :url="category_item.categoryLink"
        :link-name="category_item.categoryName"
      ></product-link>
    </div>
  </div>
</template>

<script>
import ProductCardCarousel from '~/components/client/ProductCardCarousel'
import ProductLink from '~/components/client/ProductLink'

export default {
  components: {
    ProductCardCarousel,
    ProductLink
  },
  layout: 'client/simple',
  data() {
    return {
      categoryList: []
    }
  },
  // レンダリングの前に商品情報を取得する
  async asyncData({ app, params, store, $axios }) {
    const { data } = await $axios
      .$get(
        `/api/user/product/category/${params.id}?event_id=${
          store.getters['event_info/eventSelected'].id
        }`
      )
      .catch(errors => {})

    return {
      categoryList: data
    }
  }
}
</script>

まとめ

vue-carouselがあればデザインのカスタマイズもしつつ簡単に実装可能でした
あくまでメモ用なのでソースが汚かったりおかしかったりするかもしれませんが
良かったら参考にしてください。

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