- 投稿日:2019-12-18T19:55:25+09:00
画像の動的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/cacheDynamicImage.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')); } }
- 投稿日:2019-12-18T19:38:23+09:00
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
- 投稿日:2019-12-18T18:04:10+09:00
【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);
- 投稿日:2019-12-18T15:51:16+09:00
laravel-admin で多対多リレーションの項目を multipleSelect() で AND 検索する方法
処理の流れは以下のとおりです。
- grid画面でmultipleSelect()を使用して一つの項目で複数選択できるようにします
- multipleSelect()で取得した配列で中間テーブルを検索します
- 中間テーブルで多対多で紐づくテーブルのIDを検索し、全てヒットするもう一つの外部キーをgroupbyとhavingを使用して特定します
- $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()); });
- 投稿日:2019-12-18T15:23:16+09:00
【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/
こういうコンポーネントの仕様と、サンプルのドキュメントを生成してくれるツールです。 こういうの。
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', }で私の環境だといけました。
ポイントとしては、
webpackConfig
はlaravel 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:buildstyleguideというディレクトリに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
- 投稿日:2019-12-18T15:18:06+09:00
一人で新規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アプリ自体は比較的に簡単に作れたのですが、なんか遅いんです。
地図をドラッグしたあとのアイドル状態時にデータベースからデータを取得して地図に表示する仕組みなのですが、
なんか遅い。調べてみると、
- WebサーバーがDBサーバーからデータを取得するのに時間がかかっている
- ブラウザが取得するデータの通信量が大きい
というダブルパンチで遅くなっていることがわかりました。
まず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
型のid
、Int
型のwage
、String
型の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の方が私は好きです。
- 投稿日:2019-12-18T13:53:05+09:00
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
- 投稿日:2019-12-18T12:16:57+09:00
WebでLaravel のサンプルプログラムを動かせる Tinkerwell
Tinkerwellとは
- Web上で実行できる Laravel対応のコードスペニット
- Laravel のコードをブラウザで動かして コードをURLで共有できます。
- 勉強やコードレビューなどで、実際に動くものを見せたりするのに便利そうです。
実行画面
- 右上の Save を押すと共有URLが 発行されます。
あれこれ
- 初回開くと Loadingにめっちゃ時間かかります。今後の改善されることを期待。
- embed 対応してくれるとめっちゃ良いですね!
デスクトップ版(有料)があるようです。 未調査です。
Laravel の Tinker を調べていたら、たまたま発掘したので、ご紹介でした。
?Say hello to Tinkerwell Web!
— Marcel Pociot ? (@marcelpociot) December 13, 2019
It is Tinkerwell right in your browser - packed with code snippets from the @laravelphp documentation.
Learning Laravel never was easier.
Tinker with code, save snippets and share them with others.https://t.co/gH9sPj4T98 pic.twitter.com/iJNy319ACD
- 投稿日:2019-12-18T10:27:13+09:00
初心者のためのセルフレビューチェック項目〜もうクソコードのレビューはさせない〜
この記事について
新卒1年目のわたしがコードレビューで指摘された箇所を参考に、初学者がよく指摘されることとその対処法をまとめました。
どの言語にも共通することと、PHP / Laravel 限定のもの別にまとめてあります。
また、それぞれのミスについて、指摘されたくない度
を星5つ満点で評価してみました 笑
レビューされるとき、レビューするときにこの記事に書いてあることが参考になればいいなと思います。注意
- あくまでも弊社基準のレビューです
- VSCode 使っている人向けかもしれません
言語に関係ないイージーなミス
■ スペルミス
指摘されたくない度★★★★★
指摘されたら恥ずかしいやつ。[対処法]
- VSCode 拡張機能 Code Spell Checker
- スペルミスしてる単語を下画像のように波線でハイライトしてくれる
- 不安だったらググる癖をつける
■ インデントずれ
指摘されたくない度★★★★★
GitHub 上でレビューするとなかなか気付き辛い。[対処法]
- VSCode 拡張機能 indent-rainbow
- 下画像のような感じでインデントに色付けをしてくれる。また、ズレがある部分を赤く表示してくれる
■ 余分なスペースが入ってしまう
指摘されたくない度★★★★★
最初の頃、誰もがやってしまうミス。[対処法]
- VSCode の setting.json に以下を追加
setting.json"files.trimTrailingWhitespace": true,■ ファイル末尾改行がない
指摘されたくない度★★★★★
[対処法]
- VSCode の setting.json に以下を追加
setting.json"files.insertFinalNewline": trueレビュワーとして気付くには
- GitHub の FilesChanged でみると、末尾改行がないファイルは下記画像のようなマークがあるのですぐに気付ける
■ デバッグ、コメントアウトの消し忘れ
指摘されたくない度★★★★★
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 されるべき。[対処法]
- 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 は発行されたクエリの表示もできるので、すごく便利。■ 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 で書くとなにがいいの?
- PHPMD の CyclomaticComplexity(関数の複雑性) の発生を防ぐことができる。
● 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もやっているのでぜひ覗いて行ってください〜
- 投稿日:2019-12-18T10:27:13+09:00
初心者のためのセルフレビューチェック項目〜クソコードのレビューはさせない〜
この記事について
新卒1年目のわたしがコードレビューで指摘された箇所を参考に、初学者がよく指摘されることとその対処法をまとめました。
どの言語にも共通することと、PHP / Laravel 限定のもの別にまとめてあります。
また、それぞれのミスについて、指摘されたくない度
を星5つ満点で評価してみました 笑
レビューされるとき、レビューするときにこの記事に書いてあることが参考になればいいなと思います。注意
- あくまでも弊社基準のレビューです
- VSCode 使っている人向けかもしれません
言語に関係ないイージーなミス
■ スペルミス
指摘されたくない度★★★★★
指摘されたら恥ずかしいやつ。[対処法]
- VSCode 拡張機能 Code Spell Checker
- スペルミスしてる単語を下画像のように波線でハイライトしてくれる
- 不安だったらググる癖をつける
■ インデントずれ
指摘されたくない度★★★★★
GitHub 上でレビューするとなかなか気付き辛い。[対処法]
- VSCode 拡張機能 indent-rainbow
- 下画像のような感じでインデントに色付けをしてくれる。また、ズレがある部分を赤く表示してくれる
■ 余分なスペースが入ってしまう
指摘されたくない度★★★★★
最初の頃、誰もがやってしまうミス。[対処法]
- VSCode の setting.json に以下を追加
setting.json"files.trimTrailingWhitespace": true,■ ファイル末尾改行がない
指摘されたくない度★★★★★
[対処法]
- VSCode の setting.json に以下を追加
setting.json"files.insertFinalNewline": trueレビュワーとして気付くには
- GitHub の FilesChanged でみると、末尾改行がないファイルは下記画像のようなマークがあるのですぐに気付ける
■ デバッグ、コメントアウトの消し忘れ
指摘されたくない度★★★★★
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 されるべき。[対処法]
- 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 は発行されたクエリの表示もできるので、すごく便利。■ 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 で書くとなにがいいの?
- PHPMD の CyclomaticComplexity(関数の複雑性) の発生を防ぐことができる。
● 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もやっているのでぜひ覗いて行ってください〜
- 投稿日:2019-12-18T10:07:38+09:00
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を更新したときは、キャッシュクリアを忘れずにしようと思います
- 投稿日:2019-12-18T08:57:59+09:00
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を繋ぎ込んでいきます。この全体図の赤色部分になります。
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
を用意。
methods
のgetTask()
で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
データを用意するところは先ほどと同じです。
methods
のsubmit()
メソッドで、
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-model
でtask
データとバインディングすることで、
フォームにデータが入力されたら
<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-model
でtask
データとバインディングして、
formのv-on:submit.prevent="submit"
でsumit
メソッドを呼んでいます。commit:タスク編集ページAPI繋ぎ込み
これでタスク編集ページは完成。
タスク削除API繋ぎ込み
最後に、タスク一覧ページのDeleteボタンとタスク削除APIを繋ぎ込んでいきます。
まずは
<script>
。resources/js/components/TaskListComponent.vuemethods: { 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の認証処理などがあるといいです。今回のチュートリアルで
ざっくりと全体イメージをまずはつかんで、
今後上記のような詳細な処理を少しずつ追加していくといいかと思います。
- 投稿日:2019-12-18T08:57:41+09:00
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を実装していきます。今回は一番シンプルな形で進めるので、
各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.phpSchema::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.phpclass Task extends Model { + protected $fillable = [ + 'title', + 'content', + 'person_in_charge', + ]; }
commit:タスクモデル作成
seeder作成
次に、テストデータを自動生成するための
seederを作成します。まずは下記コマンドでseederファイルを生成。
php artisan make:seeder TasksTableSeeder生成されたseederファイルのrunメソッドを
このように修正します。database/seeds/TasksTableSeeder.phppublic 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.phppublic 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.phpRoute::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.phpRoute::get('/tasks', 'TaskController@index'); + Route::post('/tasks', 'TaskController@store'); Route::get('/tasks/{task}', 'TaskController@show');
※ルーティングの定義順を間違えると正しく動かないので、この通りに記述してください
コントローラにstoreメソッド追加。
app/Http/Controllers/TaskController.phpuse 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.phpRoute::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.phpRoute::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結合編
- 投稿日:2019-12-18T08:57:19+09:00
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によるフロントエンド実装のみを行います。作るページ(コンポーネント)は全部で4つです。
- タスク一覧
- タスク詳細
- タスク登録
- タスク編集
最初に各ページの完成状態の画像を確認します。
前にインストールした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が表示されると思います。これが正しく表示されていれば、
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.jsimport 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.jsimport 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.jsimport 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編
- 投稿日:2019-12-18T08:56:52+09:00
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の実装を
一通り実践することができます。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のウェルカムページが表示されれば
正常に動作しています。laravel/uiインストール
次に、laravel/uiというパッケージをcomposerでインストールします。
これは、
Laravelでフロントエンド開発をするための
ベースを簡単に提供してくれるツールです。
下記コマンドを実行。composer require laravel/uicommit: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-routercommit: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フロントエンド編
- 投稿日:2019-12-18T06:37:09+09:00
【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); } }これは…さすがにちょっと書きたくないですよね。
モデルクラスにルール定義を委譲
ルールをコントローラに直書きすると使い回しが効かないので,モデルに定義してみましょう。以下のようなフローに則って,バリデーションを分割します。
- フォーマットだけで判定できる静的バリーデーション を実行
- モデルに値を
fill()
する- 現在の状態や他のフィールドと比較を行うインスタンスバリデーション を実行
$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.php
のattributes.<フィールド名>
に翻訳された:attribute
相当の値を定義validation.php
のvalues.<フィールド名>.<値>
に翻訳された: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リレーション初心者向け!外部キーがデフォルトでないパターン!!! でした。勢いだけで内容が頭に入ってこない記事だったので少しマークダウンの整形をお手伝いさせていただきました(笑)
- 投稿日:2019-12-18T02:31:56+09:00
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最高って感じですね
- 投稿日:2019-12-18T00:57:11+09:00
Nuxt.jsでvue-carouselで商品画像一覧をカスタマイズして表示する
概要
商品画像を画像と商品名の組み合わせのコンポーネントを作成し、カルーセルスライダーで表示を行う。
商品は4つずつ並べて横にスライドさせていけるイメージです。画面イメージ
参考
nuxt.jsにvue-carouselを導入してスライダーを作成
実装手順
vue-carouselのインストール
npm install -S vue-carouselNuxtのプラグインで
vue-carousel.js
を作成し、下記を実装するvue-carousel.jsimport Vue from 'vue' import VueCarousel from 'vue-carousel' Vue.use(VueCarousel)nuxt.config.jsplugins: [ { src: '~/plugins/vue-carousel', ssr: false } ],コンポーネント設計
今回は、4枚ごとの商品画像を表示するようのコンポーネントとして実装していおきます。
実装
実際のコンポーネントの実装
<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があればデザインのカスタマイズもしつつ簡単に実装可能でした
あくまでメモ用なのでソースが汚かったりおかしかったりするかもしれませんが
良かったら参考にしてください。