- 投稿日:2019-12-18T23:55:07+09:00
phpからPDOでmysql接続できなかった話
phpでPODを使ってmysqlに接続すると、
Uncaught PDOException: SQLSTATE[HY000] [1045] Access denied for user
このエラーが出てきて、ググった方法を試しても全く解決されなかった。
ユーザー名もパスワードも合ってるし、環境設定もできてるし、php.iniの設定もできてるし、ユーザー権限もALLになってるし、、、、
そこでもう一回上記の確認をしてみると、、、、、あ!!!!!!!
ユーザー権限のdb名の部分が間違っていた、、、、
db名を今使ってる名前にして、楽々接続完了!!!db周りの環境設定とかエラーとかすごい苦手だったけど、今回のことで少し詳しなったのでmysqlインストール後の設定方法に関して一応整理しておく。
①mysqlインストール
②php.iniでpdo部分のコメントアウト
③Apacheの再起動
④.bashrcにPATHを通す
⑤mysqlを起動後にユーザー権限を付与とりあえず、dbのエラー発生したら、①PATHが通っているのか(.bashrc)②ユーザー権限が合っているのかを真っ先に確認すればいいんだね。
- 投稿日:2019-12-18T23:12:08+09:00
$_POST は万能じゃなかった!(困って学ぶ日々の備忘録)
痛い目に合いつつ学んだ内容をただ残していくだけの記事ですが、調べて日本語の記事があんまりなかったのでまずは簡単な内容でも助かる方がいるかも!と思い、公開。
タイトル通りですが、GET/POST で送られてくるデータを受け取る時は何もかも
$_POST
や$_GET
で受け取ってしまえばいい、わけではないのです。
長い前書きは抜きにして、今日は「PHPphp://input
と$_GET / $_POST
の違い、使い分け」について最近ちょっと困った結果学んだ情報を軽く整理してみました。
$_POST
は万能じゃない受け取れる情報の量の許容範囲が
$_GET
よりあるからといって便利に思われがちですが、そうではにないことを最近実感した。
なんと・・・受信する殆どのデータが問題なのに、たま~にだけなぜか文字列が変なところで途切れたりして、構造が壊れ化けてしまうという・・・ちょっと調べたら:
- XMLの受信
- JQueryからのAjaxリクエストには向いていないという。
今まであんまり意識せずにフレームワークのInputクラス(つまり、さらなる$_POSTラッパー)を使ったりしていましたが、本来は用途に応じて使い分ける必要があるとわかりました。$_POSTを使って良いケース
以下のコンテンツタイプのみ、phpのラッパーである $_POST は良い仕事をするという大前提があるようです。これは「すべてのユーザーエージェントによってサポート対象であるコンテンツタイプだから」であって、それらのみが保証されているということのようです。
- application/x-www-form-urlencoded
- multipart/form-data
HTMLフォームで送信できる内容に限って使うイメージを持てば安全かなと思います。
ちなみに、私の場合はヘッダーが「application/x-www-form-urlencoded」で送られてきていた xml も化けてしまったので、ヘッダーのみで安心するのもちょっと厳しい気がします。$_POSTを避けたほうがいいケース
- JSON を送信するとき(コンテンツタイプが application/json )
- xml を送信するとき(コンテンツタイプが application/xml )
- YAML を送信するとき(コンテンツタイプが application/yaml )
上記(というか、使って良いケース以外!)の場合 $_POST に格納されたデータが壊れるケースが多いようです。壊れからも様々で、最初はただ「うまくURLデコードできないかな?」と思ってしまうレベルだったりします。
他順なデータでテストしても問題ないのに、ちょっと特殊な文字が含まれ送られてきた分だけ文字化けして、結局形式が壊れて例えば XML としてパースできないデータになってしまう場合がります。$_POST が使えない場合は?
php://input
から生のデータを引っ張り出す。
ただし、この場合は $_POST の恩恵(データをURLデコードしてくれちゃったり、配列にしてくれるあたり)が受けない分、全部自分で実装しないといけない。
送られてくるデータは、ただの(おそらくURLエンコードまたはRAW URLエンコードされた) string になるので、ある意味「自由に何でもできる状態」ではあります。今回参考になった記事
https://stackoverflow.com/questions/8893574/php-php-input-vs-post
- 投稿日:2019-12-18T21:03:22+09:00
CakePHPのドキュメント翻訳は英語学習とフレームワークのキャッチアップにつながる
この記事は、PHP Advent Calendar 2019の18日目の記事です。
TL;DR
- CakePHPなどフレームワークの多くには、コミュニティでメンテされている日本語翻訳ドキュメントが存在する
- ドキュメントの変更を追って自ら翻訳対応していくことで、英語力とフレームワーク自身の理解が進む
CakePHP Book
つい最近、メジャーバージョンアップ(4)がリリースされたことで話題(!?)のCakePHPには、開発者向けのドキュメントとしてCakePHP Bookというものが公開されています。
https://book.cakephp.org/4/en/index.html
CakePHPを触ったことがある方なら一度は訪れたことがあるサイトかと思います。CakePHPはOSSですので、もちろんこのサイト自体も、GitHubに公開されています。
https://github.com/cakephp/docs
こちらのサイトは、様々な言語で翻訳されており、特に日本語ドキュメントのメンテナンスも盛んに行われています。
筆者自身も昨年からちらほらと翻訳やもとの英語ドキュメントを修正したりしています。
https://github.com/cakephp/docs/pulls?q=is%3Apr+author%3Ahgsgtk+is%3Aclosed
山があるのは、マイナーバージョンアップ時の移行ガイドを翻訳した際のものです。
3.7 Migration Guide: https://github.com/cakephp/docs/pull/5950
3.8 Migration Guide: https://github.com/cakephp/docs/pull/6128
3.9 Migration Guide: https://github.com/cakephp/docs/pull/6228CakePHP Bookの翻訳を継続してやってみると色々メリットがあったので紹介して、あわゆくば、読んでいただいた方々をこちらの沼に誘えればと思います。
ドキュメント翻訳のメリット
仕様の変化をおえる
これは、特に移行ガイドを翻訳する際の気づきですが、ただ流し読みするのではなく、一文づつ翻訳していくため、どのような変更が行われたのかを知ることに繋がります。また、どう訳していいかわからない場面において、一度その機能の仕様自体を調べるので、普段業務で触れない部分についての理解を深めることが出来ます。
さらに、継続してバージョンアップの移行ガイドをやっていくと、ちょっとした傾向みたいな部分も見えてきます。とくにCakePHP3.6以降は、CakePHP4に向けた非推奨機能の追加が多かったのですが、なんとなくの情報として得るのと、実体験としてドキュメントメンテナンスを通じて知るのでは大きな違いがあります。
対応する技術的な英語文を知れる
シンプルですが、英語文を訳して読んでいく際に、普段、日本語で見聞きする技術的単語や文章がどういうふうに表現されるのかを知る切っ掛けに繋がります。「英語がなければなるべく翻訳されたやつがいい」という方でも、強制的に英語に立ち向かう土俵に立つので、強制的な英語学習となります。
コミュニティのコアな方々と触れ合える
ドキュメントをメンテナンスするような方は、そのフレームワークにおいて、非常に熱量の高い方々が多いです。そのような方々とGitHub上で触れ合えることは刺激になります。また、そこでやっておくとリアルであった際にすでにGitHub上でコミュニケーションを取っているので話の種に繋がります。コミュ力がなくても事前に知ってもらえるのはメリットですね。
ドキュメントを翻訳したいぞと思ったら
「CakePHP Cookbook を直す方法(表示確認してからプルリクエストを出すまで)」という記事にて、 @tenkoma さんが環境構築の仕方から丁寧に解説してくださっています(さすが丁寧のてんこまさん!)。CakePHP Bookは、PHPコミュニティで広く知られている上に、修正ポイントが多く、コントリビュートチャンスは非常に多くなっています。ぜひ、チャレンジしてみてください!
- 投稿日:2019-12-18T20:13:16+09:00
PHPでお問い合わせフォームを作る
PHPでお問い合わせフォームを作ってみよう
最近、新人君たちにプログラミングを教える機会が多くなってきました。
教えていく中で「自分の教え方って大丈夫なんだろうか?」と最近不安になっている次第。
という事で簡単なお問い合わせフォームを作る事で、人への教え方を再考するために「お問い合わせフォーム」を題材にちょっとずつ難しい事を実装していきます。
そして、最終的には実戦で使えるようなフォームを作れるようになります。フォームの全体の流れ
- 入力画面
- 確認画面
- 完了画面
てな感じで
じゃあ、もうちょっと細かくみてみましょう
入力画面
うん、これがないと始まらないですね。
今回はお決まりの以下の入力項目にしてみます
項目1 お名前
項目2 メールアドレス
項目3 お問い合わせ内容お、お、ぉぅ 最低限の設定やね。。今後拡張していく予定なので我慢してください。。。
確認画面
そう、ここで入力した内容を表示して「送っていいですか?」って確認させます。
完了画面
要するに「ありがとうございました」ページです
ここでメール送信とかデータベースに接続とかいろいろあるけど、とりあえずはメール送信しときましょう細かいとこ
とりあえずPHPで動く事を目標にするのでCSS等は後回しにします
入力画面
取り敢えず最低限のこのhtmlで
index.html<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> </head> <body> <form action="./confirm.php" method="post"> <div id="contents"> <input type="text" name="name"> </br> <input type="text" name="mail"> </br> <textarea name="toiawase"></textarea> </br> </br> <input type="submit" value="確認"> </div> </form> </body> </html>味気の無い画面ですね。
name="name" のテキストボックスが名前を入れるところです
name="mail" がメールアドレス、テキストエリアに問い合わせ内容ですね
そして formタグで 確認ボタンを押した後のリクエスト先が指定されています
action=./confirm.php"
です。この記述で、ボタンを押したらconfirm.phpにリクエストを投げます。
そしてmethod="post
の記述でリクエスト時はPOSTでデータを投げる、と指定しています。確認画面
入力画面で確認ボタンをクリックすると form タグの指定通り confirm.php にリクエストが投げられます。
confirm.php<?php $name = $_POST['name']; $mail = $_POST['mail']; $toiawase = $_POST['toiawase']; echo " <!DOCTYPE html> <html> <head> <meta charset='UTF-8'> <title>Insert title here</title> </head> <body> お名前:$name </br> メールアドレス:$mail </br> お問い合わせ内容 </br> $toiawase </br> </br> <form action='complete.php' method='post'> <input type='hidden' name='name' value='$name'> <input type='hidden' name='mail' value='$mail'> <input type='hidden' name='toiawase' value='$toiawase'> <input type='button' onclick='history.back()' value='戻る''> <input type='submit' value='確認'> </form> </body> </html>";はい、必要最低限の記述です。
index.html
で入力された内容が表示されると思います。
ここ(confirm.php)で、本当はやっておかなきゃいけない事、やっておいた方が良い事、やらない方が良い事を書いておきますやっておかなきゃいけない事
postで投げられたデータのチェック(nullチェック、文字数チェック、その他もろもろチェック)やっておいた方が良い事
セッションへの値保存、テンプレートの使用やらない方が良い事
hidden タグの使用これらのやり方、対応方法は後々の記事で書いていきます。
ここまでで データの入力、データの送信、データの受信、データの表示まで出来ました。
この画面で確認がOKであれば 次へボタン をクリックして完了画面に移ります確認画面
complete.php<?php $name = $_POST['name']; $mail = $_POST['mail']; $toiawase = $_POST['toiawase']; $mailTO = "test@test.com"; // メールの送信先 $mailHeader = "From: from@from.com"; // メールの送信者 $mailSubject = "お問い合わせありがとうございます"; // メールの件名 $mailBody = ' $name 様\r\n お問い合わせありがとうございます\r\n \r\n ご返信まで~~~~'; mail($mailTO, $mailSubject, $mailBody, $mailHeader); echo " <!DOCTYPE html> <html> <head> <meta charset='UTF-8'> <title>Insert title here</title> </head> <body> お問い合わせありがとうございます。 </body> </html> ";はい、これでデータ(名前、メールアドレス、問い合わせ内容)を入力し、データを送信
confirm.php で受け取ったデータの表示、データの送信
complete.php でデータ受け取り、メール送信、完了画面の表示
ここまで出来ました。最後に
今回書いた内容は本当に必要最低限の流れですが、次回からこのフォームに肉付けしていこうと思います。
- 投稿日: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-18T18:57:08+09:00
php 連想配列の特定のキーの値を取得する
$targetValueArray = array_column($originArray, ‘id’); //keyがidの値を配列として取得取得した配列の重複を削除したい場合、array_unique
$uniqueArray = array_unique($targetValueArray); //重複削除された際にキー(連番)が飛び飛びになってしまうため、添番を振り直す $uniqueArray = array_value($uniqueArray);
- 投稿日: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-18T17:30:35+09:00
Magento2+Vagrant+xdebug remote debug
Xdebug
Xdebug's (remote) debugger allows you to examine data structure, The Xdebug extension provides debugging
and profiling capabilities for PHP scripts.
Xdebug supports PHP 5.4, 5.5, 5.6, 7.0, 7.1, 7.2, and 7.3.
You can debug your PHP coding using built functions likeprint_r()
orvar_dump()
or using log file however this is not enough and it will slow you down during developing.
by using Magento2+xdebug it will speed you up during developing and it will make your life easierInstalling Xdebug
Preconditions
- you have Magento2+Vagrant environment
※ If you don't haveMagento2+Vagrant
environment you can follow this tutorial magento2:開発環境構築 Vagrant- PHP version is 7.1~
- you already install PHPSTORM
install Xdebug (inside Vagrant)
inside-vagrantsudo pecl install xdebugOn my server, I got the following output.
inside-vagrant-outputBuild process completed successfully Installing '/usr/lib/php/20151012/xdebug.so' install ok: channel://pecl.php.net/xdebug-2.6.1 configuration option "php_ini" is not set to php.ini location You should add "zend_extension=/usr/lib/php/20151012/xdebug.so" to php.iniNext, we need to enable the xdebug extension by modifying the php.ini configuration file
/etc/php/7.1/fpm/php.ini
or/etc/php/7.1/fpm/conf.d/20-xdebug.ini
inside-vagrantsudo vim /etc/php/7.1/fpm/php.ini
/etc/php/7.1/fpm/php.inizend_extension=/usr/lib/php/20151012/xdebug.so xdebug.remote_host=192.168.33.10 xdebug.remote_enable=1 xdebug.remote_port=9900 xdebug.idekey="PHPSTORM" xdebug.remote_log="/tmp/xdebug.log"xdebug.remote_host You can set it to a specific domain or a IP address. im my case I set
192.168.33.10
because it is my vagrant private network IP
xdebug.remote_port You can set it to a specific port by default it will be 9000 In case I set to9000
since my 9000 port is listen to something else.
xdebug.idekey since I use PHPSTORM ID I setted toPHPSTORM
xdebug.remote_log it is used as filename to a file to which all remote debugger communications are logged.after setting you to need to restart your web server.
in case you Apacheinside-vagrantsudo /etc/init.d/apache2 restart
in case you Nginx
inside-vagrantsudo /etc/init.d/nginx restart
To verify if xdebug successfully enable. You can add
phpinfo()
into[magento-folder]/pub/index.php
in my case, it was/var/www/html/pub/index.php
you should get output like this.
Configuring XDebug client
In my case, I will Configure PHPStorm IDEA
open your project in phpstorm
you will set the port as you set it inphp.ini
ofxdebug.remote_port
phpstormPHPSTORM -> Preference -> Languages & Frameworks -> PHP -> DebugPHPstorm configuration
Deployment configure
phpstormphpstorm -> Preference -> Build, Execution, Deployment -> Deploymentadd a new remote server by click on [+] icon
host You can set it to a specific domain or an IP address
username remote server username since we use vagrant username will bevagrant
password remote server password since we use vagrant password will bevagrant
root path vagrant Magento folder absolute location/var/www/html/
Servers configure
phpstormPHPSTORM -> Preference -> Languages & Frameworks -> PHP -> Serversadd a new server by click on [+] icon
host You can set it to a specific domain or an IP address
Use path mapping check the checkbox
under project files set the vagrant Magento folder absolute location/var/www/html/
Run/debug configure
phpstormPHPSTORM ->Toolbar ->Edit Configurations
- add a new debug by click on [+] icon
- select PHP Remote Debug
Server selected Server we already create it on the last step.
IDE key you will set the IDE key as you set it inphp.ini
ofxdebug.idekey
Browser Configuration
- Install xdebug chrome extension Xdebug helper
after install extension set IDE key
- on
chrome -> toolbar-> xdebug chrome extension icon
right click
2.selectPhpstorm
set IDE key you will set the IDE key as you set it inphp.ini
ofxdebug.idekey
3.click onsave
buttonenable xdebug chrome extension
- on
chrome -> toolbar-> xdebug chrome extension icon
left clickdebug Time!
phpstormPHPSTORM -> toolbar
On the PhpStorm toolbar, toggle the Start Listening for PHP Debug Connections button to start listening for incoming PHP debug connections, or choose Run | Start Listening for PHP Debug Connections from the main menu.
Set a breakpoint in your code. Breakpoints can be set in the PHP context inside we set on Magento
pub/index.php
3.on browser refresh website it automatically redirects to PHPstorm
https://gyazo.com/c11de6afa9e6b666a5ff3ef7b983a37d
- 投稿日:2019-12-18T14:47:29+09:00
フルスタックエンジニアへの道(CakePHP/React)
はじめに
こんにちは、 @IZUMIRU0313 です。
ランサーズ Advent Calendar 2019 23日目の記事です。法人向けの社外人材活用サービス「Lancers Enterprise」のフルスタックエンジニアです。
まだよわよわなので恐縮ですが、api blueprintでAPI仕様書、CakePHPでAPI、ReactでUIを実装しています?想定する読者は、サーバーサイドエンジニアでフロントエンド(React)も学習していこうとしている方です。
エンジニア経歴
学生時代は、主にRails、Swift、AWS(EC2、S3)、Heroku、WordPressを利用して、サービス開発やインターンに取り組んでいました。特に以下2つのサービスは、すべての設計および開発をやっていたため、努力は報われると今日でも思える貴重な経験になっています。
フロントは、SassとjQueryが多少書けるレベルでした?ランサーズには、SREとしてジョインしました。当時、ターミナルはgitと多少のコマンドを知っているレベルであり、@yakitori009さんに、何から何まで教えていただきながら取り組んでいました?♂️
LPICでインプットしながら取り組んでいたため、座学と実務の両輪が上手く回せていました。
- 踏み台サーバーの移行
- Let's Encryptワイルドカード証明書の導入
- AutoScaling
- AutoScaling中ではデプロイ不可
- docker-compose対応
- MySQLコンテナ、WordPressコンテナの構築
- MySQLのバージョンアップ5.6->5.7
- LambdaでGitHubとChatworkの連携
- LambdaでAthenaのload partitionを自動実行
その後、サーバーサイドエンジニアとしてCakePHPでプラットフォームの開発をすることにしました。まともにチーム開発とCakePHPを書くのは初めてだったため、@waldo0515さんや@numanomanuさん、井上さんにシステム設計からプロジェクトマネジメント、コーディングに渡るまで大変お世話になりました?♂️
インプットは、オブジェクト指向やドメイン駆動設計、クリーンアーキテクチャ、リーダブルコード等に努めました。
- ランサーランク制度
- アナリティクス
- 顔写真判定
- ヤフースコア
- Linkedinログイン
- 提案見積書
- 業種
- 提案追加オプション
- Freelance Basics移行
- Lancers Pro
- BigQuery/Redashで全社データ出し・モニタリング構築
JavaScriptの習得
正直まだまだ未熟であり器用貧乏になる可能性も大いにあるのですが、自分が目指したいエンジニア像のために本格的にJavaScriptに力を入れることにしました?
まずは、半年後業務でReactを書けるレベルになることを目標に、GASでの個人開発から始めました。SREの際にLambdaでnode.jsを書いていたこともあり、特に詰まることなく開発できました。
インプットは、改訂新版JavaScript本格入門を読んでいました。
- [GAS/Twilio]広瀬すずさんがSlackのリマインダー機能でモーニングコールしてくれるサービス
- [GAS/GoogleCloudNaturalLanguageAPI]宇垣美里さんが時には厳しく、時には優しくしてくれるアメムチbot
- IZUMIRU/kenkahadamewan
- [Nuxt/WebSpeechAPI]騒がしい居酒屋でもワンタップで店員さんを呼ぶサービス「親指ですみません」
Reactの習得
ES6のお作法や非同期通信の変遷等も一通り理解することができたので、本格的にReactの学習を始めました。
元々副業や個人開発でVueやNuxtを触る機会があったのですが、個人的にはReactの方が学習ハードルが高かった印象です。
特にJSX(TSX)、TypeScript、Redux、redux-sagaは業務で開発するまで理解できませんでした。半年ほど、@intrudercl14さんと@takepo0928さんにキャリアやJavaScript、Reactのアドバイスをいただき、なんとか「Lancers Enterprise」の開発にジョインすることができました?♂️
- チュートリアル
- React.Component
- 一人React.js Advent Calendar 2014
- りあクト!
- React開発 現場の教科書
- WEB+DB PRESS Vol.112
- Atomic Design
- Atomic Design by Brad Frost
- Redux. From twitter hype to production
特にりあクト!は、対話形式で先輩エンジニアが後輩エンジニアに教えるというストーリーなので、非常に読みやすくオススメです。
またVue、React、React(Redux)で同じアプリケーションを実装することは、共通点と相違点を把握でき学習促進に繋がったのでオススメです。
Reactの学習と合わせて、APIの学習にも努めました。APIは学生時代のサービスでRailsでAPIを生やし、Swiftでキャッチするという経験等はありましたが、なんちゃってAPIレベルだったので1から学習しました。
- Web API: The Good Parts
- IZUMIRU/youtube-manager-nuxt
- IZUMIRU/youtube-manager-go
- GraphQLはRESTの置き換えではない
- 「GraphQL」徹底入門 ─ RESTとの比較、API・フロント双方の実装から学ぶ
- vvakame/graphql-with-go-book
- IZUMIRU/laravel-vue-with-graphql
- IZUMIRU/amplify-react
- よくわかるgRPC
- スターティングgRPC
- IZUMIRU/hello-grpc
展望
ReactやTypeScriptの学習は継続していますが、ReactNativeやFlutterの学習もし始めたため、@sayanetさんと@terukuraさんとともに「Lancers Enterprise」をより良くした後は、アプリの改善にコミットできたらと考えています。
またモチベーション高く学習するには、自分の性格を理解することが大事だなと非常に思いました。家だと怠惰なので仕事終わり必ずカフェに行く、まずは簡単なアプリケーションを開発した後に体系だった書籍で質を上げていく等。長くなりそうなので、個人開発のすゝめ的な記事は別途書けたら良いなと思います。
QiitaいいねやTwitterフォローは励みになります?
- 投稿日:2019-12-18T14:41:38+09:00
オリジナルWebサービス -Lunches- (フルスクラッチ開発)
はじめに
フロントエンジニアを目指してプログラミング学習をしている小林と申します。
本記事ではオリジナルWebサービス「Lunches」の概要や制作過程について説明します。URL
リンク:Lunches
目的
- フルスクラッチ開発でPHP、SQLの言語理解を深める
- Webサービスの基本的な構成、動作を把握する
スペック
プログラミング言語:HTML5/ CSS3 / Javascript / PHP
データベース言語:MySQL
開発環境:macOS Catalina 10.15.1
バージョン管理:SourceTree
本番環境:さくらサーバー
機能
ユーザー管理機能
・ユーザー登録
・ログイン
・プロフィール編集
・ユーザー削除機能
・退会イベント機能(メインサービス)
・イベント投稿
・イベント詳細
・イベント一覧(ページネーション)
・カテゴリー検索、日付検索サービス概要
「Lunches」はランチタイムに恋愛やビジネス、友達作りなどもイベントを気軽に開催することができる
Webサービスです。
インターネットを通じて人との交流を活性化させることを目的として制作しました。開発手順
1.ワイヤーフレーム作成
7つブラウザ画面をノートに手書きでワイヤーフレームを作成しました。
2.テーブル設計
実装させたい機能から必要な情報を洗い出し、それに応じてテーブルを作成しました。
作成したテーブルは以下の3つです。3.画面モック作成
ワイヤーフレームを元にHTML・CSSでコーディングを行い画面モックを作成しました。
セキュリティ
バリデーションチェック
・未入力チェック
・Email型式チェック(正規表現)
・Email重複チェック
・最大、最小文字数チェック
・半角英数字チェック
・同値チェック例外処理
DBへ接続する際にはエラーで接続できない可能性を考慮して「try」「catch」で例外処理を行っています。
セッションIDの再生成
セッションハイジャックによって第三者による乗っ取りを防ぐためにsession_regenerate_idを使用
この関数をコールすることで現在のセッションデータを保持したまま、セッションIDを新しくすることができる。
パスワードハッシュ
DB側でユーザーのパスワードが漏れないようにパスワードをpassword_hashでセキュリティを高めています。ログイン時にはpassword_verifyを使用してハッシュ化されたパスワードと照合しています。
このとき第一引数である$passにはフォームからpostされたパスワード
そして、第二引数にはDBから配列形式で取り出した情報を$resultに詰め
array_shiftを使って先頭のパスワードを取り出しています。SQLインジェクション対策
DB接続時にはプレースホルダーを使用し、SQL文を作成。
値をバインドすることでSQLインジェクション対策を行っています。
Lunchesの使い方
①イベントの登録・ プロフィール編集
イベント投稿、プロフィール編集の画像登録ではjQueryを使用し、ドラック&ドロップでファイルをinputすることができます。②イベント一覧・検索機能
イベント一覧ページではGETパラメータを使用して、ページネーションと検索機能を実装しています。
検索機能はカテゴリー検索、投稿日時のソート順検索ができます。今後の課題
・スマートフォンにも対応したレスポンシブデザイン
・オブジェクト指向に基づく、保守性の高いコード設計
・FLOCSSをベースとしたCSS設計主に以上の3点です。
特に様々なデバイスからアクセスされるユーザーを想定したレスポンシブデザインでの設計は
現在のWebサービスでは必要不可欠な物だと思いました。
- 投稿日: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-18T03:23:42+09:00
Microsoft製OSS、daprでRoadrunner + PHPのアプリケーションを動かす
弁護士ドットコム Advent Calendar 2019- Qiitaの19日目の記事です。
先日Microsoftが公開したdaprというOSSの上でPHPを動かせるか検証した内容になっています。
ことはじめ
2019年10月、Microsoftはdapr(Distributed Application Runtime) というOSSを公開しました。現状まだα版なのでプロダクションには載せられないですが、メッセージングを中心としたマイクロサービス開発を容易にすると書かれています。pub/subを中心にしたアーキテクチャの実現、というのは面白そうなプロダクトだと思ったのでこの機会に触ってサンプルを作ってみました。
(その他、daprの詳細はこちらのQiitaの記事やこちらの記事を見るのが良いかもしれません。)
どんなの作ったの
このような構成のサンプルをPHPで作りました。
コードはこちら想定したユースケース(適当)としては、Twitterみたいなフォローフォロワーの概念があったとして、フォロワーが増えた時に"followerAdded"みたいなイベントを発火させて、emailとpush通知のマイクロサービスを叩くようなフローです。(適当)
なんでPHPなの
dapr自体、まだ公開されて間も無いため、日本語はおろか英語でもなかなか情報がなく、チュートリアルもnode,go,pythonはあるものの、PHPはなかったためです。PHPでも動くといいなーと思ったので今回調べてみました。(動きました)
daprのしくみ
daprはpub/subを実現するために、daprdというプロキシを提供しています。アプリケーションはこのプロキシを介してWebサーバーを動作させる必要があります。
ローカル環境(ホストマシン上)で直接動かしたり、kubernetes上でサイドカーとして動かす例がチュートリアルに載っています。ホストマシンの環境を汚さずに簡単に使いたいのであれば、プロキシを同じコンテナで動作させることも可能でした。(やろうと思えば)
今回はdocker-composeで簡単に構築したいので、プロキシを同じDockerイメージに含めて動作させます。コンテナはこのようなイメージです。
Dockerfileはこんな感じになります。
FROM php:7.3-cli-alpine AS base-image RUN apk --update --no-cache add wget bash # install dapr-cli RUN wget -q https://raw.githubusercontent.com/dapr/cli/master/install/install.sh -O - | /bin/bash # daprdは`dapr-cli init`を叩くと落ちてくるが、同時にredisとplacementという別のコンテナを立ち上げてしまう。 # イメージの構築で叩けないので、直接curlでバイナリを落とすことにする。 # ↓このへんのソースを読むとdaprdのパッケージが置いてあるURLがわかる。 # ref. https://github.com/dapr/cli/blob/d585612185a4a525c05fb62b86e288ccad510006/pkg/standalone/standalone.go#L265 RUN curl -Lo /tmp/daprd.tar.gz \ https://github.com/dapr/dapr/releases/download/v0.3.0/daprd_linux_amd64.tar.gz \ && tar xzvf /tmp/daprd.tar.gz -C /usr/local/bin ...略 FROM base-image AS app # 本来は自分でdaprdとアプリケーションのプロセスを立ち上げる必要があるが、 # dapr-cliのコマンドを使って簡単な方法で起動させる EXPOSE 8080 ENTRYPOINT [\ "dapr",\ "run",\ "--app-port",\ "8080",\ "sh",\ "invoke-service.sh"\ ] CMD [""]実際にアプリケーション全体を動作させるにあたり、各サービスのコンテナで動作させるプロキシの他にもいくつかコンテナが必要です。placementと名付けられているコンテナ、メッセージブローカーの役割を負うミドルウェア、エンドポイントとなるコンテナです。結果、イメージとしてこのような構成になります。
placement
- あまり仕組みが理解できていないのですが、proxyが起動した際にコンテナのIPがログに表示されるのでサービスディスカバリーやロードバランサー的な役割に見えます。
メッセージブローカー
- dapr-cliを使った場合、デフォルトではredisが起動するようになっています。 チュートリアルをみた感じだと、kafkaなども使えるようです。
エンドポイント
- daprdがそのままエンドポイント用のプロセスとしても動作します。"/v.1.0/publish/xxxEvent"のようなパスでPOSTするとイベント(トピック)が発火します。トピックのサブスクライブ方法は後述します。
- 今回は取り上げないですが、サブスクライバ側にはapp-idというものが付与できるため、"/v.1.0/invoke/app-id/method/xxxhogefuga"というような呼び出し方でGETやPOSTを使った同期通信(たぶん)もできるようでした。
placementとメッセージブローカーにあたるコンテナはdapr-cliを使って
dapr init
と叩いても起動できます。今回のようにdocker-composeを使う場合には自前でplacementとredisなど用意する必要があります。
(dapr initで立ち上げたコンテナ情報をdocker-compose.ymlに書くだけなので用意は簡単でした。)ネットワークは謎です。各daprdとplacement, message brokerが具体的にどういった流れでネットワーク越しに連携するのかまでは調べられませんでした。(ドキュメントとかに載っているのかな??)
(余談)RoadRunnerってなに
Golang製のPHPのプロセスマネージャで、Nginxとかに頼らずWebサーバーを立てられるようになります。(ざっくり)
- 簡単に入れられる(独自調べ)
- 速いらしい
- production readyとのこと
PHPでつくる部分(api-facade)
クライアントからまず通信を待ち受けるapi-facadeの部分になります。
RoadRunnerを使った場合、同じプロセスで複数のリクエストを処理するため、これを踏まえた実装が必要です。今回サンプルコードを1枚のPHPに収めるべくSlim4を使うことにしており、繋ぎ込む必要があります。繋ぎ込むといっても特に難しいことはなく、RoadRunner側で用意しているHttpClientが返すRequestクラスはPsr\Http\Message\ServerRequestInterfaceを継承しているため簡単にSlim4へ渡すことができます。下記のようなコードで繋ぎ込みは完了です。
api-facade/api-facade.php$app = AppFactory::create(); ...中略 $psr7 = new RoadRunner\PSR7Client($worker); while ($req = $psr7->acceptRequest()) { try { // RoadRunnerのリクエストはSlim4へそのまま渡すことができる $resp = $app->handle($req); $psr7->respond($resp);イベントのパブリッシュはdaprのエンドポイントへPOSTします。
サンプルでは簡易的に下記の関数を定義してあります。
今回はGuzzleを使いました。api-facade/api-facade.phpfunction publishTopic(string $topic, array $message) { (new Client)->post( "dapr-endpoint:3500/v1.0/publish/${topic}", [ 'form_params' => $message ] ); }POSTだけ待ち受けるfollowersというリソースを用意します。
特に処理はしません。followerが増えた"てい"です。api-facade/api-facade.php$app->post('/followers', function (ServerRequestInterface $request, ResponseInterface $response, $args) { // add follower process // ... // ... publishTopic('followerAdded', []); $response->getBody()->write(''); return $response; });PHPでつくる部分(email-notification-service)
daprdは同じコンテナ内で起動しているアプリケーションを検知した際に特定のパスでGETで通信を行います。このタイミングでサブスクライブするトピックなどを、サービス側からdapr側にレスポンスを返して伝えることになります。
- /dapr/config
- とりあえず200でレスポンス返してあげれば良さそうでした。(記載が見つけられなかった)
- /dapr/subscribe
- jsonでサブスクライブするトピックのリストを返す必要があります。
- ここではfollowerAddedをリストに加えてレスポンスを返しています。
email-notification-service.php$app->get('/dapr/config', function (ServerRequestInterface $request, ResponseInterface $response, $args) { return $response; }); $app->get('/dapr/subscribe', function (ServerRequestInterface $request, ResponseInterface $response, $args) { $subscribeTopics = [ 'followerAdded' ]; $response->getBody()->write(json_encode($subscribeTopics, JSON_THROW_ON_ERROR, 512)); return $response; });'followerAdded'をサブスクライブしているので、'/followerAdded'のパスでPOSTを待ち受けています。
アクセスログでpub/subできているか確認するので、実装は特にしていません。email-notification-service.php$app->post('/followerAdded', function (ServerRequestInterface $request, ResponseInterface $response, $args) { $response->getBody()->write(''); return $response; });PHPでつくる部分(push-notification-service)
email-notification-serviceと同じような記述です。
$app->get('/dapr/config', function (ServerRequestInterface $request, ResponseInterface $response, $args) { return $response; }); $app->get('/dapr/subscribe', function (ServerRequestInterface $request, ResponseInterface $response, $args) { $subscribeTopics = [ 'followerAdded' ]; $response->getBody()->write(json_encode($subscribeTopics, JSON_THROW_ON_ERROR, 512)); return $response; });$app->post('/followerAdded', function (ServerRequestInterface $request, ResponseInterface $response, $args) { $response->getBody()->write(''); return $response; });つ・な・げ・て・み・た・い
composerとphpのアプリはコンテナを分けてあるので、動かす場合は最初にsetup用で用意したdocker-composeを動かします。
(依存してるライブラリなどをapi-facadeとサービス側に落とします)
その後、dokcer-compose up
で色々立ち上がるかと思います。docker-compose -f docker-compose-setup.yml up docker-compose up
最後に下記のような感じでapi-facadeを叩くと、サブスクライブしているサービスに通信が飛んでいることがアクセスログからわかるかと思います。
curl -X POST localhost:8080/followers
まとめ
ロードマップにはこんなこと書いてるので、PHPもSDK待ちかなーとか、かと思っていましたが、daprとのインターフェースはhttpかgRPCのようなので、PHPでも実装できました。
今回取り上げませんでしたが、pub/subだけでなく同期通信もでき(チュートリアルでは一番最初にやります)、インターフェースもhttpかgRPCでdaprが間を取り持ってくれるので、pub/subやるために特定のミドルウェアと直接お喋りする必要がなくなるのは良い印象です。
α版なので実際にゴリゴリ使うには早いですし、流行るかも不明ですが、面白そうなのでしばらく追ってみたいなと思いました。
- 投稿日: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:31:00+09:00
[Re:ゼロから始めるVue生活] Vueで検索/ソートさせてみた(php連携)
Vueでリスト検索とソート機能を作ってみたので
さらに応用して、phpの配列データからVueに流し込んで処理した機能を作ってみました。ロジック
php配列データ
↓
JSONデータに変換
↓
JSONデータをJSの配列に格納
↓
JSの配列をvueのdataに格納
これでvueで取り扱えるデータになりました。computed(算出プロパティ)
matched:
フォーム入力の数値 <= budgetの数値のリストのリストを表示
sorted:
ボタンのオンオフで昇順・降順ソートlimited:
limit数分表示できる<?php $list = [ ['id' => '1', 'name' => '商品A', 'price' => '500'], ['id' => '2', 'name' => '商品B', 'price' => '300'], ['id' => '3', 'name' => '商品C', 'price' => '2000'], ['id' => '4', 'name' => '商品D', 'price' => '5000'], ['id' => '5', 'name' => '商品E', 'price' => '1500'], ['id' => '6', 'name' => '商品F', 'price' => '250'], ['id' => '7', 'name' => '商品G', 'price' => '100'], ['id' => '8', 'name' => '商品H', 'price' => '750'], ]; $list_json = json_encode($list); ?> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html lang="ja"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Vue App</title> </head> <body> <div id="app"> <input v-model.number="budget">円以下 <p>{{ matched.length }}件表示中</p> <button v-on:click="order=!order">価格 ▼</button> <div v-for="item in limited" v-bind:key="item.id"> {{ item.name }}: {{ item.price }}円 </div> </div> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script src="https://cdn.jsdelivr.net/npm/lodash@4.17.5/lodash.min.js"></script> <script> let list = JSON.parse('<?php echo $list_json; ?>'); const app = new Vue({ el: '#app', data: { // 検索初期値 budget: '', // 検索数 limit: 10000000000000, // 検索リスト list: list, // ソート初期値 order: false, }, computed: { matched: function() { return this.list.filter(function(el) { return el.price <= this.budget }, this) }, sorted: function() { return _.orderBy(this.matched, 'price', this.order ? 'desc' : 'asc') }, limited: function() { return this.sorted.slice(0, this.limit) } } }); </script> </body> </html>phpを使用せずVueだけで作ったコードもありますので
こちらを参照してください。
[Re:ゼロから始めるVue生活]メモ