- 投稿日:2020-07-14T18:31:53+09:00
ご主人様、小難しいDDDやクリーンアーキテクチャはお忘れになって、削除しやすいように設計するところから努力なさったらいかが?
ある日夢の中で設計に詳しい悪役令嬢が現れてこんなことを言い放ったので、考察してみましたという設定のポエムです。
問題提起
ドメイン駆動設計とか、オニオンアーキテクチャ、クリーンアーキテクチャといった考え方はもちろん重要なものの、僕は難しく考えずに「削除しやすいように機能を作る」ことができるのが第一歩として重要ではないかと思っています。
今回は、「削除しやすい設計」について持論を展開してみます。
※議論のスコープはWebサービスに限定し、例示としてPHPのフレームワークであるLaravelを用います
削除しやすいことがなぜ重要か
一度開発した機能は、それで終わりではなく、改修、改善を繰り返し、そして場合によっては仕様が廃止されることがあります。
機能の廃止に伴ってコードを削除するとき、もし既存のコードの依存関係が複雑で、簡単に削除できなかった場合は、フロントエンドのみ削除して、バックエンドのロジックを残したままにする、といった暫定対処を行うこともあるでしょう。
こういった対応で要件を満たし続けた場合、徐々に新規機能の開発や、既存機能の改善に影響を及ぼします。
既存のソースコードがどの機能で使われているのか不透明になることで、ソースコードの再利用がやりにくくなり開発工数が増加、加えて、使われていない可能性もあると開発者が認知することにより、使われていないと思って改修したら実は使われていた、という逆にハマってしまう事例も起きうるでしょう。それに、ライブラリや言語のバージョンアップといったメンテナンス工数も肥大化します。
残念なことに、一度開発した機能が廃止されるというのはWebサービスではよくあることです。
こういった背景から、改修しやすいように、改善しやすいようにという観点だけではなく、消しやすいように作るということを考えるのが重要ではないかと考えますし、実は、削除しやすいということが最も重要な設計要件といっても過言ではないのではとも思います。
削除しやすいように作ることで何が嬉しいか
前節の通り、削除しやすいようにコードを作ることで、機能の廃止時にほぼ全ての関連するコードを削除することが可能になります。
そうすれば、常にソースコードを無駄に肥大化させることを防ぎ、メンテナンスのしやすさを向上できます。
また、削除しやすいように作るということは、ソースコード間の依存関係に気を使うということになる、副次的に設計の改善効果をもたらすと思います。
例えばドメイン駆動設計に則って開発しよう、というと人によって考え方が異なるケースが多いです。削除しやすいように作ろう、と言う標語は、それに比べればかなり明確な指針と言えるのではないでしょうか。
※もちろんDDDを否定する意見では有りませんし、僕自身もDDDを愛用しています。ただ、DDDだけで全ての機能開発ができるわけではないですし、もっと明確な指針という意味でも本記事で提示する考え方を主張しています。
削除しやすい設計を実践する
弊社ではLaravelを使っているので、Laravel前提での解説となりますが、他のフレームワークにおいても応用ができると思います。
前置きが大袈裟でしたが、実践する内容は至ってシンプルなものばかりです。それでは説明を始めます。
Controllerは1クラス1メソッドにする
Controllerは1メソッドしか持たないように実装します。
Laravelですと、
__invoke
メソッドを実装したControllerにすることで、1メソッドしか持たないことを強制できます。ルーティングファイル(routes/web.php等)でもXXXController@hoge
ではなく、XXXController
だけで書けてシンプルです。一例として、ログイン処理を実行するControllerを示します。
class SignInController extends Controller { public function __invoke(LoginRequest $request, LoginUseCase $loginUseCase): UserAccountResource { // ログイン処理を行いユーザーアカウントを示すインスタンス$userを取得する。詳細は省略 // $user = $loginUseCase->execute(... return new UserAccountResource($user); } }Pros
利点としては、本来の目的通り、機能削除時にクラスごと消せばいいので大変明確です。
機能が増えてもControllerがFatになりにくい利点もあります。ログイン処理のControllerなんて基本1メソッドだから__invokeとしなくて良いのではという見解もあるかもしれませんが、例えば複数の種別のユーザーを扱っておりそれぞれログイン処理が異なる可能性などがあります。
弊社では以前UserControllerといった粒度で命名したControllerに大量のユーザー関連の処理が詰め込まれ、各アクションメソッドが何をしたいのか分からなくなることが有りましたが、その心配も不要になります。
ユースケース(またはサービスクラス)も1クラス1メソッドにする
Controllerと同様、よくあるサービスクラスも1クラス1メソッドにします。
僕は統一した決まりとして、ユースケースは
execute
メソッドだけを持っており、コンストラクタでRepository等をインジェクションしています。Prosについても同様です。UserServiceといったサービス名で作ってしまうと、あらゆる処理が詰め込まれてしまい、機能削除時に削除漏れしてしまうことが発生します。
あらゆる実装を統一したディレクトリ名の配下に置く
Laravelの場合は、
- Controller
- ユースケース(サービスクラス)
- Repository
- Eloquent Model
- メール
- イベントとイベントリスナ
- 通知
- コマンド
- テストコード
といった各機能が、基本的にはLaravelが決めたディレクトリの下に置かれることが多いと思いますが、これらのnamespaceに含まれるディレクトリ名にできるだけ単語の統一を図ります。
ある機能名が、SomeAwesomeとした場合、
App\Http\Controllers\SomeAwesome\XXXController
App\Domain\SomeAwesome
の下にユースケース、Repositoryを始めとするドメインロジックApp\Mail\SomeAwesome
の下にメールApp\Events\SomeAwesome
の下にイベント、同様にリスナも配置App\Commands\SomeAwesome
の下にコマンドといったように、Laravelの規律に従いつつ、機能別の単語に合ったnamespaceに置くようにしています。
Pros
まだ全ての機能に対しては実現できていませんが、実現できた機能は廃止するときに大変削除しやすかったです。
補足
全ての機能を
App\Domain
以下に置く方針も考えたのですが、Laravelの経験者が実装するときの学習コストが無駄に上がってしまうデメリットも鑑みて、Laravelに依存しない純粋なドメインロジックのみApp\Domain
以下に置くやり方で現状は進めています。このへんはFWとどう向き合うかによって異なります。補足2
機能名の英語ですが、ユニーク性にこだわるあまり長い名前にするとタイピングがしんどいので、多少厳密な英訳でなくてもタイピングしやすい名前にするのがおすすめです。そもそも機能名なんて割と仕様の都合で変わりますし。(経験済み)
フレームワークが提供するORMに機能に関する知識を渡さない
続いてはWebアプリケーションフレームワークが往々にして提供しているORMに関する指針です。
Laravelだと
Eloquent Model
が該当しますので、Eloquent Model
を例に話を進めます。Railsだと
Active Record
、TypeORMだとEntity
が該当すると思います。削除しやすい設計をする上で重要なのは、データベースとやり取りするためのORMに機能に関する知識を渡さないことです。
ORMのクラスは機能と独立したディレクトリに置く
往々にしてこれらのクラスは、データベースのテーブルと1:1の関係性で作られます。しかし、例えばusersテーブルは様々な機能で利用されるテーブルでしょうから、SomeAwesomeディレクトリを切ってその下に置くのは不適切と考えるほうが自然です。
そのため、
Eloquent Model
に関しては機能名のディレクトリの下に置くルールを適用しません。シンプルにApp\Models
の下に置きます。ORMに機能に関する知識をもたせるとどうなるか
例えば、Q&A機能を持ったサービスを開発しているとして、すでに質問したことのあるユーザーかどうかを判断するアクセサをモデルに実装したとします。Laravelでは
XXXAttribute
というメソッドを生やすことでアクセサを定義できます(個人的には分かりにくいのであまり好きではな略)。public function getHasQuestionsAttribute(): bool { return $this->questions->isNotEmpty(); }これだけだと非常にシンプルですが、実際にはここに「誹謗中傷の含まれた質問だったため運営から非表示対応された質問は除く」といったドメイン知識が混ざってくるとしましょう。
public function getHasQuestionsAttribute(): bool { // 雑に書いているので動くかわかりませんが、一応これが動く前提で話を進めます return $this->questions->where('is_hidden', false)->isNotEmpty(); }この書き方ですとquestionsを取得した後
Collection
のメソッドで絞っているのでパフォーマンス良くないですし、「運営が質問を非表示対応できる」機能の一部がここに現れてしまっており、今後この機能を廃止したときに削除漏れしてしまう可能性があります。このように、
Eloquent Model
を使い回す前提の機能開発は、どこに機能の実装があるか不透明になってしまい、削除しにくくなる、または削除したと思っても消しきれておらずバグを誘発する可能性があります。機能ごとにデータを取得・保持・整形するクラスを用意する
というわけで指針としては、前節における
App\Domain\SomeAwesome
といった機能別のnamespace以下に、各機能ごとのデータを取得・保持・整形するクラスを用意することになります。データの取得は機能ごとに
Repository
を作成します。同時に、データを保持するためのDTOを作成しておき、Repository
はORMを使ってデータベースから値を取得するものの、戻り値としてはDTOを返すように実装します。(※ドメイン駆動設計のアプローチではここでのDTOがEntityだったりValueObjectと呼ばれていますが、ここではDTOと便宜上呼ぶことにします)弊社で組んでいる簡単な
Repository
で例を示すと下記の感じです。<?php namespace App\Domain\SomeAwesome\Infrastructure; use App\Domain\SomeAwesome\Domain\Entity\Teacher; use App\Domain\SomeAwesome\Domain\Repository\TeacherRepositoryInterface; use App\Models\Teacher as TeacherModel; final class MySQLTeacherRepository implements TeacherRepositoryInterface { public function find(int $userId): Teacher { $teacherData = TeacherModel::findOrFail($userId); return Teacher::reconstructFromRepository( $teacherData->id, $teacherData->name ); } }
App\Models\Teacher
を使ってデータを取得しているものの、Repository
の返り値としてはTeacher
というEntity
になっていることがわかると思います。(※ここでのEntity
はDDDにおけるEntity
を指しますが、本記事では単にデータの入れ物と思っていただければ差し支えないです)機能の仕様は、
Entity
のreconstructFromRepository
メソッド内で表現したり、このRepository
で投げるSQLで表現します。いずれにせよ、Eloquent Model
自体には何もメソッドが足されていないことがポイントです。クエリを実行するための
Repository
という存在、およびデータを運ぶDTOを作成することによって、機能に依存した実装をORMから引き剥がすことができるという話でした。細かい機能でも気軽に1クラスに切り出す
最後に書くのは、細かい機能でも気軽にクラスとして切り出すというものです。
ここまでの話でも似たようなことを言ってきましたが、一応セクションとして用意しておきます。
フォーマッタの例
まずは、フォーマッタの実装について挙げます。
弊社で以前、「生徒の学年表記を短縮して表示したい」という要件が来ました。
「小学1年生、小学2年生、小学3年生」という配列を「小学1〜3年生」と表示してほしいという要件です。同じ条件が中学生、高校生にもあります。
ただ実装するだけですと、
API Resource
内でprivateなメソッドを切って変換用の関数としたり、Eloquent Model
のAttributeとしてgetFormattedGradeAttribute
などを切っても良かったでしょう。しかしここでは、専用の、
XXXXGradeFormatter
というクラスを作成することにしました。そして、そのクラスはたった1つのpublicメソッドであるformatGradesToText
を持つようにしました。final class XXXXGradeFormatter { /** * 対象学年をテキスト形式にフォーマットする * * @param MasterGrade[] $grades * @return string|null */ public static function formatGradesToText(array $grades) {Pros
こうするとテストも書きやすくなりますし、もちろん、削除したいときはクラス名でIDEを使って依存を検索すればすぐにわかります。
クラス名にXXXXとついているのは、機能の名前です。単にGrade Formatterという命名にすると、その機能以外で学年のフォーマットをしたいときに自動的に仕様が同じになります。そのときに仕様が同じとは限りませんし、そうなればリファクタしたらいい話です。最初から過度に使われることを想定するのは悪手が多いと思います。
一応、テストの例を下記に示します。学年のデータしか用意しなくていいのでテストがスリムです。
final class XXXXGradeFormatterTest extends SetUpUserDataTestCase { /** * @dataProvider dp__grade_patterns */ public function testFormatGradesToTextTest($gradeNames, $expectText) { $grades = []; //...中略 $this->assertEquals($expectText, XXXXGradeFormatter::formatGradesToText($grades)); } public function dp__grade_patterns() { return [ [ ['小学2年生'], '小学2年生', ], //...中略 [ ['小学1年生', '小学2年生', '小学4年生', '小学5年生', '小学6年生', '中学1年生', '中学2年生', '中学3年生', '高校1年生', '高校2年生', '高校浪人'], '小学1・2、4〜6年生、中学1〜3年生、高校1・2年生、高校浪人', ], [ ['小学1年生', '小学2年生', '小学3年生', '小学5年生', '小学6年生', '中学1年生', '中学3年生', '中学浪人', '高校1年生', '高校2年生', '高校3年生', 'その他'], '小学1〜3、5・6年生、中学1・3年生、中学浪人、高校1〜3年生、その他', ], ]; } }外部APIへのリクエストの例
外部のAPIへのリクエストも、リクエストごとにクラスに切り出すのがよいです。
弊社ではHeadless CMSの
microCMS
を使っており、基本はフロントエンドから呼び出すのですが、まれにバックエンドから呼ぶことがあります。外部APIへリクエストするとき、下記のようにAPIのパスを呼び出し側のクラスに直接書くのはアンチパターンです。
$response = (new Client())->get( "何かしらのAPIパス", [ 'headers' => [ 'X-API-KEY' => '何かしらのAPIキー' ], 'timeout' => 30 ] );というのも、これが複数のクラスで使われるAPIだった場合、もしAPIが廃止になったり変更になったときの影響範囲が文字列検索でしか見つけられなくなるからです。
下記のように専用のクラスに入れておくことで、IDEの依存を検索すれば見つけられるようになり、より手軽と言えます。
public function __construct( GeneralArticleMicroCmsClient $generalArticleMicroCmsClient ) { $this->generalArticleMicroCmsClient = $generalArticleMicroCmsClient; } // ...中略 $response = $this->gradeArticleMicroCmsClient->get();※本気で組むならRepositoryとしてインターフェースを作成して、裏で
microCMS
を使っていることを隠蔽したほうが良いと思います。マサカリ防止割と初心者のうちは次々とクラスを切るという発想が浮かばないものですが、切ってみるとテストも書きやすいし再利用も効く、そして削除するときも楽になるのでおすすめです。
まとめ
色々と削除しやすい設計について書いてみました。
エンジニアの学習過程という観点でも、クラスやインターフェースを気軽にサクサク作るレベルになる前にDDDやクリーンアーキテクチャを学んでも理解が追いつかないんじゃないかなと思っているフシが有り、まずは機能別にクラスを分けていこうという標語はレベル差を吸収できて便利じゃないかなあと思ってます。
クラスを次々に作っていくような方針になっているので、日に日にクラス名や変数名の命名の難しさと向き合うことになってきています。
個人的にはクラス名などの中に、主語動詞目的語をどのように入れるべきかの明確な指針が欲しいです。。。誰か知っている方がいれば教えて下さい!
よければTwitterもフォローお願いします。本記事はPHPについて書いてますが本当はフロントエンドが好きなのでフロントのツイートもしています。
- 投稿日:2020-07-14T18:31:53+09:00
ご主人様、小難しいDDDやクリーンアーキテクチャはお忘れになって、”削除しやすい設計”から始められてはいかが?
ある日夢の中で設計に詳しい悪役令嬢が現れてこんなことを言い放ったので、考察してみましたという設定のポエムです。
問題提起
ドメイン駆動設計とか、オニオンアーキテクチャ、クリーンアーキテクチャといった考え方はもちろん重要なものの、僕は難しく考えずに「削除しやすいように機能を作る」ことができるのが第一歩として重要ではないかと思っています。
今回は、「削除しやすい設計」について持論を展開してみます。
※議論のスコープはWebサービスに限定し、例示としてPHPのフレームワークであるLaravelを用います
削除しやすいことがなぜ重要か
一度開発した機能は、それで終わりではなく、改修、改善を繰り返し、そして場合によっては仕様が廃止されることがあります。
機能の廃止に伴ってコードを削除するとき、もし既存のコードの依存関係が複雑で、簡単に削除できなかった場合は、フロントエンドのみ削除して、バックエンドのロジックを残したままにする、といった暫定対処を行うこともあるでしょう。
こういった対応で要件を満たし続けた場合、徐々に新規機能の開発や、既存機能の改善に影響を及ぼします。
既存のソースコードがどの機能で使われているのか不透明になることで、ソースコードの再利用がやりにくくなり開発工数が増加、加えて、使われていない可能性もあると開発者が認知することにより、使われていないと思って改修したら実は使われていた、という逆にハマってしまう事例も起きうるでしょう。それに、ライブラリや言語のバージョンアップといったメンテナンス工数も肥大化します。
残念なことに、一度開発した機能が廃止されるというのはWebサービスではよくあることです。
こういった背景から、改修しやすいように、改善しやすいようにという観点だけではなく、消しやすいように作るということを考えるのが重要ではないかと考えますし、実は、削除しやすいということが最も重要な設計要件といっても過言ではないのではとも思います。
削除しやすいように作ることで何が嬉しいか
前節の通り、削除しやすいようにコードを作ることで、機能の廃止時にほぼ全ての関連するコードを削除することが可能になります。
そうすれば、常にソースコードを無駄に肥大化させることを防ぎ、メンテナンスのしやすさを向上できます。
また、削除しやすいように作るということは、ソースコード間の依存関係に気を使うということになる、副次的に設計の改善効果をもたらすと思います。
例えばドメイン駆動設計に則って開発しよう、というと人によって考え方が異なるケースが多いです。削除しやすいように作ろう、と言う標語は、それに比べればかなり明確な指針と言えるのではないでしょうか。
※もちろんDDDを否定する意見では有りませんし、僕自身もDDDを愛用しています。ただ、DDDだけで全ての機能開発ができるわけではないですし、もっと明確な指針という意味でも本記事で提示する考え方を主張しています。
削除しやすい設計を実践する
弊社ではLaravelを使っているので、Laravel前提での解説となりますが、他のフレームワークにおいても応用ができると思います。
前置きが大袈裟でしたが、実践する内容は至ってシンプルなものばかりです。それでは説明を始めます。
Controllerは1クラス1メソッドにする
Controllerは1メソッドしか持たないように実装します。
Laravelですと、
__invoke
メソッドを実装したControllerにすることで、1メソッドしか持たないことを強制できます。ルーティングファイル(routes/web.php等)でもXXXController@hoge
ではなく、XXXController
だけで書けてシンプルです。一例として、ログイン処理を実行するControllerを示します。
class SignInController extends Controller { public function __invoke(LoginRequest $request, LoginUseCase $loginUseCase): UserAccountResource { // ログイン処理を行いユーザーアカウントを示すインスタンス$userを取得する。詳細は省略 // $user = $loginUseCase->execute(... return new UserAccountResource($user); } }Pros
利点としては、本来の目的通り、機能削除時にクラスごと消せばいいので大変明確です。
機能が増えてもControllerがFatになりにくい利点もあります。ログイン処理のControllerなんて基本1メソッドだから__invokeとしなくて良いのではという見解もあるかもしれませんが、例えば複数の種別のユーザーを扱っておりそれぞれログイン処理が異なる可能性などがあります。
弊社では以前UserControllerといった粒度で命名したControllerに大量のユーザー関連の処理が詰め込まれ、各アクションメソッドが何をしたいのか分からなくなることが有りましたが、その心配も不要になります。
ユースケース(またはサービスクラス)も1クラス1メソッドにする
Controllerと同様、よくあるサービスクラスも1クラス1メソッドにします。
僕は統一した決まりとして、ユースケースは
execute
メソッドだけを持っており、コンストラクタでRepository等をインジェクションしています。Prosについても同様です。UserServiceといったサービス名で作ってしまうと、あらゆる処理が詰め込まれてしまい、機能削除時に削除漏れしてしまうことが発生します。
あらゆる実装を統一したディレクトリ名の配下に置く
Laravelの場合は、
- Controller
- ユースケース(サービスクラス)
- Repository
- Eloquent Model
- メール
- イベントとイベントリスナ
- 通知
- コマンド
- テストコード
といった各機能が、基本的にはLaravelが決めたディレクトリの下に置かれることが多いと思いますが、これらのnamespaceに含まれるディレクトリ名にできるだけ単語の統一を図ります。
ある機能名が、SomeAwesomeとした場合、
App\Http\Controllers\SomeAwesome\XXXController
App\Domain\SomeAwesome
の下にユースケース、Repositoryを始めとするドメインロジックApp\Mail\SomeAwesome
の下にメールApp\Events\SomeAwesome
の下にイベント、同様にリスナも配置App\Commands\SomeAwesome
の下にコマンドといったように、Laravelの規律に従いつつ、機能別の単語に合ったnamespaceに置くようにしています。
Pros
まだ全ての機能に対しては実現できていませんが、実現できた機能は廃止するときに大変削除しやすかったです。
補足
全ての機能を
App\Domain
以下に置く方針も考えたのですが、Laravelの経験者が実装するときの学習コストが無駄に上がってしまうデメリットも鑑みて、Laravelに依存しない純粋なドメインロジックのみApp\Domain
以下に置くやり方で現状は進めています。このへんはFWとどう向き合うかによって異なります。補足2
機能名の英語ですが、ユニーク性にこだわるあまり長い名前にするとタイピングがしんどいので、多少厳密な英訳でなくてもタイピングしやすい名前にするのがおすすめです。そもそも機能名なんて割と仕様の都合で変わりますし。(経験済み)
フレームワークが提供するORMに機能に関する知識を渡さない
続いてはWebアプリケーションフレームワークが往々にして提供しているORMに関する指針です。
Laravelだと
Eloquent Model
が該当しますので、Eloquent Model
を例に話を進めます。Railsだと
Active Record
、TypeORMだとEntity
が該当すると思います。削除しやすい設計をする上で重要なのは、データベースとやり取りするためのORMに機能に関する知識を渡さないことです。
ORMのクラスは機能と独立したディレクトリに置く
往々にしてこれらのクラスは、データベースのテーブルと1:1の関係性で作られます。しかし、例えばusersテーブルは様々な機能で利用されるテーブルでしょうから、SomeAwesomeディレクトリを切ってその下に置くのは不適切と考えるほうが自然です。
そのため、
Eloquent Model
に関しては機能名のディレクトリの下に置くルールを適用しません。シンプルにApp\Models
の下に置きます。ORMに機能に関する知識をもたせるとどうなるか
例えば、Q&A機能を持ったサービスを開発しているとして、すでに質問したことのあるユーザーかどうかを判断するアクセサをモデルに実装したとします。Laravelでは
XXXAttribute
というメソッドを生やすことでアクセサを定義できます(個人的には分かりにくいのであまり好きではな略)。public function getHasQuestionsAttribute(): bool { return $this->questions->isNotEmpty(); }これだけだと非常にシンプルですが、実際にはここに「誹謗中傷の含まれた質問だったため運営から非表示対応された質問は除く」といったドメイン知識が混ざってくるとしましょう。
public function getHasQuestionsAttribute(): bool { // 雑に書いているので動くかわかりませんが、一応これが動く前提で話を進めます return $this->questions->where('is_hidden', false)->isNotEmpty(); }この書き方ですとquestionsを取得した後
Collection
のメソッドで絞っているのでパフォーマンス良くないですし、「運営が質問を非表示対応できる」機能の一部がここに現れてしまっており、今後この機能を廃止したときに削除漏れしてしまう可能性があります。このように、
Eloquent Model
を使い回す前提の機能開発は、どこに機能の実装があるか不透明になってしまい、削除しにくくなる、または削除したと思っても消しきれておらずバグを誘発する可能性があります。機能ごとにデータを取得・保持・整形するクラスを用意する
というわけで指針としては、前節における
App\Domain\SomeAwesome
といった機能別のnamespace以下に、各機能ごとのデータを取得・保持・整形するクラスを用意することになります。データの取得は機能ごとに
Repository
を作成します。同時に、データを保持するためのDTOを作成しておき、Repository
はORMを使ってデータベースから値を取得するものの、戻り値としてはDTOを返すように実装します。(※ドメイン駆動設計のアプローチではここでのDTOがEntityだったりValueObjectと呼ばれていますが、ここではDTOと便宜上呼ぶことにします)弊社で組んでいる簡単な
Repository
で例を示すと下記の感じです。<?php namespace App\Domain\SomeAwesome\Infrastructure; use App\Domain\SomeAwesome\Domain\Entity\Teacher; use App\Domain\SomeAwesome\Domain\Repository\TeacherRepositoryInterface; use App\Models\Teacher as TeacherModel; final class MySQLTeacherRepository implements TeacherRepositoryInterface { public function find(int $userId): Teacher { $teacherData = TeacherModel::findOrFail($userId); return Teacher::reconstructFromRepository( $teacherData->id, $teacherData->name ); } }
App\Models\Teacher
を使ってデータを取得しているものの、Repository
の返り値としてはTeacher
というEntity
になっていることがわかると思います。(※ここでのEntity
はDDDにおけるEntity
を指しますが、本記事では単にデータの入れ物と思っていただければ差し支えないです)機能の仕様は、
Entity
のreconstructFromRepository
メソッド内で表現したり、このRepository
で投げるSQLで表現します。いずれにせよ、Eloquent Model
自体には何もメソッドが足されていないことがポイントです。クエリを実行するための
Repository
という存在、およびデータを運ぶDTOを作成することによって、機能に依存した実装をORMから引き剥がすことができるという話でした。細かい機能でも気軽に1クラスに切り出す
最後に書くのは、細かい機能でも気軽にクラスとして切り出すというものです。
ここまでの話でも似たようなことを言ってきましたが、一応セクションとして用意しておきます。
フォーマッタの例
まずは、フォーマッタの実装について挙げます。
弊社で以前、「生徒の学年表記を短縮して表示したい」という要件が来ました。
「小学1年生、小学2年生、小学3年生」という配列を「小学1〜3年生」と表示してほしいという要件です。同じ条件が中学生、高校生にもあります。
ただ実装するだけですと、
API Resource
内でprivateなメソッドを切って変換用の関数としたり、Eloquent Model
のAttributeとしてgetFormattedGradeAttribute
などを切っても良かったでしょう。しかしここでは、専用の、
XXXXGradeFormatter
というクラスを作成することにしました。そして、そのクラスはたった1つのpublicメソッドであるformatGradesToText
を持つようにしました。final class XXXXGradeFormatter { /** * 対象学年をテキスト形式にフォーマットする * * @param MasterGrade[] $grades * @return string|null */ public static function formatGradesToText(array $grades) {Pros
こうするとテストも書きやすくなりますし、もちろん、削除したいときはクラス名でIDEを使って依存を検索すればすぐにわかります。
クラス名にXXXXとついているのは、機能の名前です。単にGrade Formatterという命名にすると、その機能以外で学年のフォーマットをしたいときに自動的に仕様が同じになります。そのときに仕様が同じとは限りませんし、そうなればリファクタしたらいい話です。最初から過度に使われることを想定するのは悪手が多いと思います。
一応、テストの例を下記に示します。学年のデータしか用意しなくていいのでテストがスリムです。
final class XXXXGradeFormatterTest extends SetUpUserDataTestCase { /** * @dataProvider dp__grade_patterns */ public function testFormatGradesToTextTest($gradeNames, $expectText) { $grades = []; //...中略 $this->assertEquals($expectText, XXXXGradeFormatter::formatGradesToText($grades)); } public function dp__grade_patterns() { return [ [ ['小学2年生'], '小学2年生', ], //...中略 [ ['小学1年生', '小学2年生', '小学4年生', '小学5年生', '小学6年生', '中学1年生', '中学2年生', '中学3年生', '高校1年生', '高校2年生', '高校浪人'], '小学1・2、4〜6年生、中学1〜3年生、高校1・2年生、高校浪人', ], [ ['小学1年生', '小学2年生', '小学3年生', '小学5年生', '小学6年生', '中学1年生', '中学3年生', '中学浪人', '高校1年生', '高校2年生', '高校3年生', 'その他'], '小学1〜3、5・6年生、中学1・3年生、中学浪人、高校1〜3年生、その他', ], ]; } }外部APIへのリクエストの例
外部のAPIへのリクエストも、リクエストごとにクラスに切り出すのがよいです。
弊社ではHeadless CMSの
microCMS
を使っており、基本はフロントエンドから呼び出すのですが、まれにバックエンドから呼ぶことがあります。外部APIへリクエストするとき、下記のようにAPIのパスを呼び出し側のクラスに直接書くのはアンチパターンです。
$response = (new Client())->get( "何かしらのAPIパス", [ 'headers' => [ 'X-API-KEY' => '何かしらのAPIキー' ], 'timeout' => 30 ] );というのも、これが複数のクラスで使われるAPIだった場合、もしAPIが廃止になったり変更になったときの影響範囲が文字列検索でしか見つけられなくなるからです。
下記のように専用のクラスに入れておくことで、IDEの依存を検索すれば見つけられるようになり、より手軽と言えます。
public function __construct( GeneralArticleMicroCmsClient $generalArticleMicroCmsClient ) { $this->generalArticleMicroCmsClient = $generalArticleMicroCmsClient; } // ...中略 $response = $this->gradeArticleMicroCmsClient->get();※本気で組むならRepositoryとしてインターフェースを作成して、裏で
microCMS
を使っていることを隠蔽したほうが良いと思います。マサカリ防止割と初心者のうちは次々とクラスを切るという発想が浮かばないものですが、切ってみるとテストも書きやすいし再利用も効く、そして削除するときも楽になるのでおすすめです。
まとめ
色々と削除しやすい設計について書いてみました。
エンジニアの学習過程という観点でも、クラスやインターフェースを気軽にサクサク作るレベルになる前にDDDやクリーンアーキテクチャを学んでも理解が追いつかないんじゃないかなと思っているフシが有り、まずは機能別にクラスを分けていこうという標語はレベル差を吸収できて便利じゃないかなあと思ってます。
クラスを次々に作っていくような方針になっているので、日に日にクラス名や変数名の命名の難しさと向き合うことになってきています。
個人的にはクラス名などの中に、主語動詞目的語をどのように入れるべきかの明確な指針が欲しいです。。。誰か知っている方がいれば教えて下さい!
よければTwitterもフォローお願いします。本記事はPHPについて書いてますが本当はフロントエンドが好きなのでフロントのツイートもしています。
- 投稿日:2020-07-14T17:48:39+09:00
hogeプロジェクトにbarでアクセスする設定
nginx.conf# location プレフィックス # = 完全一致 # ^~ 前方一致。一致したら正規表現を適用しない。 # ~ 正規表現(大文字・小文字を区別する) # ~* 正規表現(大文字・小文字を区別しない) # なし 前方一致 location /bar { # よくわからん時はエラーログを参考にする # error_log logs/laravel_error.log debug; # rootとalias違い # root → /nginx/html/hoge/public/bar # alias → /nginx/html/hoge/public alias /nginx/html/hoge/public; # try_files # $uri $uri/ ファイル検索 # /index.php?$query_string rewriteと同様 try_files $uri $uri/ /index.php?$query_string; # /hoge/foo にアクセスがあった場合はファイルがないので # rewriteして再評価している # REQUEST_URIは変更されずlaravelで読み込まれる if (!-e $request_filename) { rewrite ^ /bar/index.php last; } location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; include fastcgi_params; # laravelはindex.phpしかアクセスしない fastcgi_param SCRIPT_FILENAME $document_root/index.php; } }
- 投稿日:2020-07-14T16:37:40+09:00
Class env does not exist in... の原因を一瞬で突き止める方法
わりとハマったのでメモを残します。
環境
- Laravel 5.8
- PHP 7.1
方法
App\Exceptions\Handler::report()内にdd($exception)を追記し、ダンプしたとこで処理を止めてエラーの内容を確認する。
Class env does not exist in...
Laravelを使っていると、ごく稀に以下のような感じで "Class env does not exist in..." というエラーに遭遇します。
このエラーが出ると、artisanコマンドもcomposerコマンドも動かないし、何をやっても "Class env does not exist" というエラーメッセージが出力されるだけの状態になり、結構面倒です。// こんな感じのエラー。滅多にないが、すごくたまに出る Fatal error: Uncaught ReflectionException: Class env does not exist in /var/www/html/vendor/laravel/framework/src/Illuminate/Container/Container.php on line 788 ReflectionException: Class env does not exist in /var/www/html/vendor/laravel/framework/src/Illuminate/Container/Container.php on line 788 Call Stack: 以下スタックトレース...なおかつこのエラーはenvが無いことが原因でもなければ、よくあるcomposer周りのミスが原因でもない場合が結構あるので、エラーメッセージを額面通り受け取ると、本来不具合がある部分に気づけないまま時間を溶かす恐れがあります。
dd()で本来のエラー原因を探す
「envが無いとか言われたけどenvはある。たぶん何かやらかしたんだろうけど、こんなエラーを引き起こすようなことをした覚えはないし、心当たりもない」
みたいな時は、一旦"Class env does not exist..."から離れてもっとマシなエラーメッセージが得られるようにすれば、解決に近づくことがあります。app/Exceptions/Handler.phppublic function report(Exception $exception) { dd($exception) //<-この1行を追加 parent::report($exception); }これでダンプされた時点で処理が止まり、もっとマシなエラーメッセージとスタックトレースが確認できるようになります。
これで得られたエラーメッセージをもとに探ってゆけば、割と楽に解決にたどり着けるんじゃないかなと思います。
- 投稿日:2020-07-14T14:39:18+09:00
Laravelとかインストール
本番環境ではnode.jsを利用せず、Laravel7とVue.js使いたかった開発環境メモ。
Composerのインストール
- Composer-Setup.exeをダウンロードしてインストールする。
Developer modeはチェックしない。- 環境変数PATHにC:\Users\ユーザー名\AppData\Roaming\Composer\vendor\binを追加する。
Laravelインストーラーのダウンロード
composer global require laravel/installerひたすら待つ。
WindowsではC:\Users\ユーザー名\AppData\Roaming\Composer\vendor\laravel\installer
にダウンロードされる。Laravelプロジェクトの作成
否応なく最新Laravelでプロジェクトを作る時は、下記のどちらか。
ひたすら待つ。laravel new SampleProjectcomposer create-project --prefer-dist SampleProjectLaravelのバージョンを指定したプロジェクトを作る時。(下記は7系の最新)
予め求めているバージョンを決定している時は、バージョンを細かく指定すればいい。
バージョンは、Composer - laravel/laravelで管理されているバージョンが指定できる。v7の最新バージョンで作りたい場合
composer create-project --prefer-dist "laravel/laravel=7.*" SampleProjectv6.12.0で作りたい場合
composer create-project --prefer-dist "laravel/laravel=6.12.0" SampleProject--prefer-dist は、zipでダウンロードしてくるオプションなので、指定した方が早くダウンロードしてセットアップが終わるらしい。
Laravelバージョン確認
cd SampleProject php artisan -Vで表示されるバージョンは、laravel/laravelのものではなくて、laravel/frameworkのもの。
Laravel起動を試す
cd SampleProject php artisan serveんで出てきたURLにアクセスすると動く。
Ctrl+Cで終了させる。node.jsインストール
node.jsから推奨版をダウンロードしてインストールする。
次へ次へでいい。モジュールのインストール
cd SampleProject npm installSampleProject/node_modules が作られる。
なんか警告でまくるけどよくわからんので放置。vue.jsのインストール
cd SampleProject npm install vueコンポーネントの作成
SampleProject/resources/js/HeaderComponent.vue を作る。
HeaderComponent.vue<template> <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <h1>This is header!</h1> </div> </div> </div> </template>Viewの作成
SampleProject/resources/views/welcome.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"> <title>Laravel</title> <!-- laravelmix style --> <meta name="csrf-token" content="{{ csrf_token() }}"> <link rel="stylesheet" href="{{ mix('css/app.css') }}"> <link rel="stylesheet" href="{{ mix('css/welcome.css') }}"> <!-- Fonts --> <link href="https://fonts.googleapis.com/css?family=Nunito:200,600" rel="stylesheet" type="text/css"> </head> <body> <div id="app"> <header-component></header-component> </div> <!-- laravelmix-style --> <script src="{{ mix('js/app.js') }}"></script> </body> </html>app.jsの修正
SampleProject/resources/js/app.js を修正する。
HeaderComponent.vue を認識させる。app.jsrequire('./bootstrap'); window.Vue = require('vue'); Vue.component('header-component', require('../components/HeaderComponent.vue').default); const app = new Vue({ el: '#app' });cssの追加
SampleProject/resources/sass/welcome.scss を作成する。
welcome.scss.container { h1 { color: red; } }webpack.mix.jsの修正
SampleProject/webpack.mix.js を修正する。
作成したwelcome.scss をcssとして利用できるようにする。const mix = require('laravel-mix'); mix.js('resources/js/app.js', 'public/js') .sass('resources/sass/app.scss', 'public/css'); mix.sass('resources/sass/welcome.scss', 'public/css');npmでコンパイル
cd SampleProject npm run devSampleProject/public 配下に、jsやcssが出来上がる。
prodで実施するとjsやcssがminimizeされるのとvue.jsのライセンスファイルが作られる。
npm run prod実行
cd SampleProject php artisan servevueが処理しきる前に一瞬表示されるのを防ぐ
v-cloak を利用すると防げる。
<div id="app" v-cloak>[v-cloak] { display: none; }
- 投稿日:2020-07-14T14:26:24+09:00
【Larave】バリテーション
書籍のアウトプットとして
フィールドごとのエラー表示
テンプレをこのように修正する。
<p>{{$msg}}</p> @if(count($errors)>0) <p>入力に問題があり。再入力</p> @endif <form action="/hello" method="post"> <table> @csrf @if($errors->has('name')) <tr><th>ERROR</th><td>{{$errors->first('name')}}</td></tr> @endif <tr><th>name:</th><td><input type="text" name="name" value="{{old('name')}}"></td></tr> @if($errors->has('mail')) <tr><th>ERROR</th><td>{{$errors->first('mail')}}</td></tr> @endif <tr><th>mail:</th><td><input type="text" name="mail" value="{{old('mail')}}"></td></tr> @if($errors->has('age')) <tr><th>ERROR</th><td>{{$errors->first('age')}}</td></tr> @endif <tr><th>age:</th><td><input type="text" name="age" value="{{old('age')}}"></td></tr> <tr> <th></th><td><input type="submit" value="send"></td> </tr> </table> </form>これでフィールドごとにエラーを表示できる。
firstでメッセージを取り出す
@if($errors->has('name')) <tr><th>ERROR</th><td>{{$errors->first('name')}}</td></tr> @endif <tr><th>name:</th><td><input type="text" name="name" value="{{old('name')}}"></td></tr>$ifディレクティブで
$errors->has('name')
をチェックしている。
hasはエラーが発生しているかをチェックする。$errors->has(項目名)エラーが発生していれば
$errro->first('name')
という値を出力しているがfirstは指定した項目最初のエラーメッセージを取得する。$error->first(項目名)これでname="name"のinputで発生したエラーが表示できる。
firstとget
firstは最初のエラーメッセージしか得られない
すべてのエラーメッセージを取得したい場合はgetを使う。$変数=$error->get(項目名)これ配列でまとめて返される。
以下はエラーメッセージを取得する3つのメソッド
名前 説明 all すべてのエラーメッセージを配列で取得 first 指定した項目の最初のエラーメッセージを文字列で取得 get 指定した項目のエラーメッセージをすべて取得 ここまでをまとめると
@ifで\$error->hasのチェックをし、trueなら\$error->firstや#error->getでエラーメッセージを取り出して表示する。@errorディレクティブを使う
しかし上記の手順よりも簡単にバリテーションチェックとエラーメッセージを活用できるのが\$errorディレクティブ
エラーメッセージの表示
#error(名前) ...$messageでメッセージを表示 @end$error @errorディレクティブは()に指定した項目のエラーをチェックし、このディレクトティブ内に記述した内容を表示する。 このディレクティブでは発生したエラーメッセージが**\$message**という変数に渡される。 ##使ってみる。 ```php5 <p>{{$msg}}</p> @if(count($errors)>0) <p>入力に問題があり。再入力</p> @endif <form action="/hello" method="post"> <table> @csrf @error('name') <tr><th>ERROR</th><td>{{$message}}</td></tr> @enderror <tr><th>name:</th><td><input type="text" name="name" value="{{old('name')}}"></td></tr> @error('mail') <tr><th>ERROR</th><td>{{$message}}</td></tr> @enderror <tr><th>mail:</th><td><input type="text" name="mail" value="{{old('mail')}}"></td></tr> @error('age') <tr><th>ERROR</th><td>{{$message}}</td></tr> @enderror <tr><th>age:</th><td><input type="text" name="age" value="{{old('age')}}"></td></tr> <tr> <th></th><td><input type="submit" value="send"></td> </tr> </table> </form>$ifを#errornに書き換えただけ。これでさっきと同じようにエラーが表示される。
バリデーションをカスタマイズ
フォームリクエストとカスタムバリデーションでバリデーションをカスタマイズする。
フォームリクエストについて
validateメソッドはコントローラに処理を書く必要があり、入力値検証はホアに任せたい。そこでフォームリクエストが生まれた。
フォーム用拡張リクエスト
フォームリクエストはリクエストをフォーム利用のために拡張したもの。
LaravelehaクライアントからのリクエストはRequestクラスのインスタンスとして送られる。このRequestを継承して作成されたのがFromRequest。
これでフォームに関する機能をリクエストを組み込める。送信されたフォームの内容をチェックするのはコントローラにあるよりもリクエストの内部で処理してくれる方がスマート。
フォームリクエストの作成
ではフォームリクエストを作成していく。
php artisan make:request HelloRequestHttp/Requestに作成される。
作成されたフォームリクエストはFormRequestを継承し、Requestを継承しリクエストの機能をベースにバリデーションなどのフォーム処理機能が追加されている。
FormRequestには以下の2つのメソッドが追加されている。
authorize
このフォームリクエストを利用するアクションでフォームリクエストの利用が許可されているかどうかを示す。戻り値がfalseならHttpExtentionという例外が発生してフォーム処理ができなくなる。
rules
適応されるバリデーションの検証ルールを設定する。
HelloRequestを修正する
public function authorize() { if($this->path()=='hello'){ return true; }else { return false; } } public function rules() { return [ 'name'=>'required', 'mail'=>'email', 'age'=>'numeric|between:0,150', ]; }\$this->path()でアクセスしたパスをチェックしている。
ここでhello以外は利用っできないようにしている。
バリデーションに関する部分をすべて消し、単にviewでテンプレとmsg変数を返すだけに修正する。POST送信時のコントローラのアクションを修正する。
//use App\Http\Requests\HelloRequest;を追記する public function post(HelloRequest $request){ return view('hello.index',['msg'=>'正しく入力された']); }ポイントは引数がRequestからHelloRequestに変わっていること
これでHelloRequestをの内容を元にバリデーションされる。メッセージのカスタマイズ
もう少しカスタマイズしてみる。
これまでのエラーを日本語にしてみる。FromRequestのmessageメソッドをオーバーライドする。
HelloRequestに追加する。public function messages() { return [ 'name.required'=>'名前は必須項目', 'mail.email'=>'メアドは必要', 'age.numeric'=>'整数で記入', 'age.between'=>'0から150で入力', ]; }このmessageはFromRequestがバリデーション機能をエラーメッセージを必要とした時に呼びされるメソッド。配列でまとめられいて中身を見ると、
'name.required'=>'名前は必須項目',「'項目名.ルール名'=>'メッセージ'」というメッセージ情報を記述する。
これはそれぞれのフィールドごとに用意した1つ1つのルールごとに用意する。バリデータの作成
バリデーション機能ではFormRequestにあるvalidateメソッドを使って行われる。
validateはRequestインスタンスと検証ルールの引数を渡されることで自動的にフォームの値のチェックを行う、問題があればGETのページにリダイレクトしてフォームの再表示をする。場合によってはエラーがあったらフォームページに行くのではなく、別の処理をさせたい場合もある。こういうときはバリデータを独自に用意して処理できる。
バリデータはバリデーションを行う機能のことでLaravelではValidatorというクラスがある。コントローラーのvalidateメソッドを呼び出さず、このValidatorクラスのインスタンスを作成して処理することでバリデーションの処理をカスタマイズできる。
バリデータを使ってみる
バリデータは送信されたフォームを受け撮ったアクション内で作成する。
今回はpostメソッドに書いていく//use Validator;を追加しておく public function post(Request $request) { $validator=Validator::make($request->all(), [ 'name'=>'required', 'mail'=>'email', 'age'=>'numeric|between:0,150', ]); if ($validator->fails()) { return redirect('/hello')->withErrors($validator)->withInput(); } return view('hello.index', ['msg'=>'正しく入力された']); }/helloにアクセスステ確認してみると独自のバリデータでチェックが行われる。
バリデータ利用の基本
バリデータはValidatorインスタンスを作成するだけで使えるが、makeで作る必要がある。
$validator=Validator::make(値の配列,ルールの配列);
引数 説明 第1引数 チェックする値をまとめた配列。フォームをそのままチェックするなら$request->all()でOK 第2引数 検証ルール情報を配列で。 エラーのチェックはこのように。
if ($validator->fails()) {...エラー時の処理...}failsはバリデーションのチェックに失敗したかどうかを調べるもの。
戻り値がtureならエラーが発生している。入力フォームへのリダイレクト
ここではエラー発生時にGETのページにリダイレクトしているが、振るうにリダイレクトしたらエラーメッセージやフォーム値が渡せない。そこでこのようにしている。
return redirect('/hello') ->withErrors($validator) ->withInput();リダイレクトはredirectで行う。
指定のアドレスに移動させるだけならredirectだけでいい。今回はエラーメッセージとフォーム入力情報をリダイレクトに追加している。
それがwithErrorsとwithInputwithErrors
引数にValidatorインスタンスを渡して、Validatorで発生したエラーメッセージをリダイレクト先まで引き継げる。
withInput
送信されたフォームの値をそのまま引き継げる。
クエリ文字列にバリデータを適用する
バリデータはフォーム以外のチェックにも使える。
例としてクエリ文字列で渡された値をチェックする。
コントローラのindexメソッドを修正する。public function index(Request $request) { $validator=Validator::make($request->query(),[ 'id'=>'required', 'pass'=>'required', ]); if($validator->fails()){ $msg='クエリーに問題がある。'; }else { $msg='ID/PASSを受け付けた。フォームを入力'; } return view('hello.index', ['msg'=>$msg,]); }/hellにそのままアクセスするとモンぢがあると表示され
/hello?id=xx&pass=xx
の形式で送信するとうまく表示される。ここではValidator::makeメソッドの引数にkのような値を指定している。
$request->query()これは送信されたクエリー文字列を配列形式でまとめたものを返す。
/hello?id=taro&pass=yamada ↓ ['id'=>'taro','pass'=>'yamada']第2引数ではこのように指定されている。
[ 'id'=>'required', 'pass'=>'required', ]検証ルールがまとめられている。
あとは$validator->fails()をチェックして表示するメッセージを設定するだけ。
「チェックする項目と値を連想配列にしてValidator::makeに渡す。」という長エレがわかればどんな値でもバリデーションでチェックができる。エラーメッセージのカスタマズ
バリデータを利用する場合のエラーメッセージのカスタマイズはValidator::makeを呼び出す時に指定できる。
validator=Validator::make(値の配列,ルール配列,メッセージ配列);第3引数にエラーメッセージ配列を用意する。これは先のエラーメッセージカスタマイズ時に使用したものと同じもの。
理解のためにPOST送信された時にメッセージ表示をしてみる。public function post(Request $request) { $rules=[ 'name'=>'required', 'mail'=>'email', 'age'=>'numeric|between:0,150', ]; $messages=[ 'name.required'=>'名前は必須項目', 'mail.email'=>'メアドは必要', 'age.numeric'=>'整数で記入', 'age.between'=>'0から150で入力', ]; $validator=Validator::make($request->all(), $rules, $messages); if ($validator->fails()) { return redirect('/hello') ->withErrors($validator) ->withInput(); } return view('hello.index', ['msg'=>'正しく入力された']); }これで日本語でエラーが表示される。
条件に応じてルールを追加する
必要に応じてルールを追加したい場合はValidatorクラスのsometimesというメソッドを使う
sometimes
処理を実行した結果によって新たにルールを追加できる。
$validator->sometimes(項目,ルール名,クロージャ);クロージャはこのような形をしている。
function($input){ ...処理を実行... return 真偽値; }\$inputには入力された値をまとめたものが渡される。
ここから\$input=>nameのように取り出せる。
戻り地は真偽値。
真偽値 説明 true 何もしない false somerimesで指定したルールを指定の項目に追加 ルール追加をする。
コントローラのpostメソッドを優勢
public function post(Request $request) { $rules=[ 'name'=>'required', 'mail'=>'email', 'age'=>'numeric', ]; $messages=[ 'name.required'=>'名前は必須項目', 'mail.email'=>'メアドは必要', 'age.min'=>'0際以上で入力', 'age.max'=>'200歳以下で入力', ]; $validator=Validator::make($request->all(), $rules, $messages); $validator->sometimes('age','min:0',function($input){ return !is_int($input->age); }); $validator->sometimes('age','max:200',function($input){ return !is_int($input->age); }); if ($validator->fails()) { return redirect('/hello') ->withErrors($validator) ->withInput(); } return view('hello.index', ['msg'=>'正しく入力された']); }今回は'age'=>'numeric'だけになっている。
minルールを見てみる$validator->sometimes('age','min:0',function($input) { return !is_int($input->age); });クロージャで!is_int($input->age)の値を返している。
これにより\$input->ageの値が整数の場合はfalseが返され、'min:0'のルールが'age'に追加される。
- 投稿日:2020-07-14T13:34:56+09:00
Laravel学習用備忘録
この記事の主目的は、自分(筆者)のための備忘録である。
筆者は初心者であるため、本記事は正確ではないことをご了承いただきたい。なお、参考にしたのは、
1. https://awesome-linus.com/2019/06/05/laravel-tutorial-basic-from-zero
2. https://www.hypertextcandy.com/laravel-tutorial-introduction
3. https://www.techpit.jp/courses/enrolled/777553
の3つの記事である。
不具合が生じるときは、上記の記事を参考にすること。プロジェクト作成、sqliteの設定まで。
データベースはファイル一つで管理可能なsqliteを使用することにする。
前提:windowsにphp, composer, sqlite3がインストールされている。
プロジェクト作成
- VS Codeのパワーシェルを起動
- cdコマンドでプロジェクトを作りたいところまで移動(laravel学習用フォルダあり)
composer create-project laravel/laravel=[version数を指定(LTSの6が良いか)] [プロジェクト名]
cd [プロジェクト名]
php artisan serve
のあとに、http:127.0.0.1:8000にアクセスでロケットが出ればOK。sqliteの設定
ni database/database.sqlite
注)niは新しいファイルを作成するコマンド。touchはパワーシェルで使用できない。- .envファイルを次のように変更。
.envDBDB_CONNECTION=sqlite # DB_HOST=127.0.0.1 # DB_PORT=3306 # DB_DATABASE=homestead # DB_USERNAME=homestead # DB_PASSWORD=secretこれでとりあえず、始められるようになった。
その他の備忘録
コントローラーの作成
php artisan make:controller [コントローラ名]
ですよ!マイグレーションファイルの作成からテーブルの作成(マイグレート)まで
- 下記コマンドを実施。ただし、「pets」のところは任意のテーブル名にすること。
php artisan make:migration create_pets_table --create=pets
- /database/migrations/内に今作ったcreate_pets_tableあり。その中を編集して、テーブルを定義する。
php artisan migrate
でマイグレートを実行。テーブルができた。- ※なおマイグレーション実行後に、前の状態に戻したいときは、
php artisan migrate:rollback
を実施。改めてマイグレーションファイルを直して、マイグレートする。モデルの作成
- モデルを作成するというのは、ORM対応することみたいなイメージなのかな。
- 上で作ったpetsテーブルに対応する、Pet モデルクラスを作成することで、勝手にLaravelさんが認識してくれるらしい。php artisan make:model Pet
ポイントは、テーブル名は複数形、modelクラスは単数形ということ。
- モデルの作成に失敗したとき:よくわからんが、テーブルを消す、マイグレーションファイルを消す→composer dump-autoload
を実行(名前空間の再読込らしい)→再度マイグレーションファイルの作成→モデル定義→マイグレートを実施する。リレーションの追加
- articlesテーブルにusersテーブルのidを参照する、user_idというカラムを作る。
- articlesマイグレーションファイル内に、
$table->foreign('user_id')->references('id')->on('users');
を記述する- このとき、外部キーにusersテーブルの主キーidと関連付けて、user_idとすると、このあとの処理が楽になる。
- Articleモデルにリレーションを記述する(参考3を参照する)。
sqliteの操作
sqlite3 [sqlファイルのパス名]
で選択したsqlファイルを操作できるようになる- プロンプトに>sqliteと出たら、sqliteコマンドを打てる状態
.table
でテーブル一覧を取得.schema [テーブル名]
でテーブル定義を確認select * from [テーブル名];
(セミコロン忘れないで!)- この記事を参照すると良さそう → SqliteでDBの中身を確認
テーブルにテストデータを挿入する
- シーダーを使ってみる
php artisan make:seeder FoldersTableSeeder
「Folders」の部分は適宜自分のテーブル名(複数形)にする- 詳細はこの記事を参照。
- その他にコントローラーにデータを配列で記載するのもあり。その際は配列の前に(object)をつけること。Laravelで受け取るのは配列型ではなく、オブジェクト型だからだそうだ。
$sample_datas = [(object)[連想配列], (object)[連想配列],];
アクセサの使い方
- モデルクラスの作成時はプロパティにカラムデータを持っていない。
- モデルクラス内の$attributesというプロパティで配列で管理されている。
- アクセサとは、get〇〇Atributeというメソッドを定義することで、あたかもデータをモデルクラスのプロパティとして値を取得できるようになる。
- 例えば、Personテーブルにfavoriteカラムが定義されているとする。favoriteカラムのデータを取得したいときは以下のようにする。Personモデルクラス内で
public function getFavoriteTextAttribute(){処理};
を記述して、コントローラーやテンプレートで使うときに$person->favorite_text;
とする。注意として、定義時はキャメルケース、使用時はスネークケースとすること。\@extendの使い方
- ベースファイルを作る。埋め込む予定のところに
@yield('[section名]')
を記しておく。- 各viewファイルにベースを呼び出すために、
@extends('[ベースviewファイル名]')
を記す。
- 埋め込みたい部分を
@section('[section名]')~@endsection
で記す。改行込みの文章をbladeでそのまま表示する方法
ブレードファイル内で
$hoge
が改行を含む文章としたら、{!! nl2br(e( $hoge )) !!}
とすることで、改行を含むそのまま表示できる。
- 投稿日:2020-07-14T13:04:26+09:00
Webサーバーの立ち上げからアプリケーションの作成まで全て一人でやってみた件
初めに
今まで書籍でLaravelについて勉強したり、Laravelの仕組み(バインドについてなど)を調べて記事を書くことなどをしてきたのですが、最近それらが飽きてきてしまったのでLaravelを使用したWebアプリケーションをAWSのツールを用いて作成してみました。そして、このアプリを作成した経験を記事にすれば自分と同じような境遇の人の助けとなったり、このアプリケーションの宣伝になるかもと思ったので、このアプリケーションの作成やAWSのツールを使用した際に個人的に苦労したこと・気を付けたことをここへ書いてみました。
この記事は大きく分けて四章あるのですが、章ごとの内容は独立しているので気になるところを好きなように読んでください。Webアプリ宣伝編
今回私が作成したWebアプリケーションはデンデンデン・・・・バーン!!
「ロッテンポテト」です!(トマト?何のことですか?)
このアプリは映画レビューサイトです。もっと詳しく言うと、管理する人(僕です)が最近見た映画を紹介するので、それに対しレビュワーたちが気ままにレビューをする感じです。
このサイトのトップページはこんな感じです。
ほかのページはこんな感じです。
画像を見てみると新規登録とか、満足度とかありますが気になる方はトップページを見てみて下さい。また、このWebアプリに関する疑問やこんな映画をみんなとレビューしてみたいな、などの要望がありましたら、このアプリ用のtwitterアカウントへDMを送ってください!
twitterアカウントはこちら
【映画レビューサイト解説しました!!】
— ロッテンポテト (@RottemPotato) July 7, 2020
本日より映画レビューサイト「ロッテンポテト」を開設しました!
本サイトは管理する人が最近見た映画を紹介し、その映画に対してレビューを書くものとなっています。
暇つぶしがてらぜひ遊びに来てください~
(質問等はDMでお願いします。)AWS編
※インフラ、ネットワーク、Webサーバーに関する知識が全くない中で自分なりに調べてこの章を作成しました。なので間違いがあるかもしれません。もし間違い等がありましたら、指摘していただけるとありがたいです。
今回自分が使用したAWSのツールの関係を図にするとこんな感じです。
この図に書いてある丸付きの番号は一般ユーザーがWebサーバー(今回はEC2)へリクエストを送るまでの道順を表しています。この番号通りにそれぞれのツールのやっていることを説明すると、
- 一般ユーザーがブラウザなどからドメインを通してWebサーバーへアクセスする際まずはユーザーからAWSのDNSサーバーであるRoute53へ、ドメインに紐づいたWebサーバーのグローバルIPアドレスを教えるようにという命令が出される。そしてRoute53はElastic IPで管理しているWebサーバーのグローバルIPアドレスをユーザーへ教える。
- グローバルIPアドレスを取得したユーザーはVPC(ネットワーク空間のこと。ここにWebアプリを構成するツールを置く。)の入り口であるインターネットゲートウェイを通る。
- インターネットゲートウェイからロードバランサーまではhttps通信(http通信より安全に通信が行える通信)でリクエストの内容が伝えられ、そのリクエストはルートテーブル(VPCの外からリクエストが来た際の宛先を設定する)で設定されたサブネット内のEC2へ送られる。
- ロードバランサーから送られたリクエストはセキュリティグループにてEC2へ通すか否かの判断が下され、許可が出ればそのリクエストはEC2へ行きその結果がWebアプリへ反映される。
補足事項
グローバルIPアドレスとは?
AWSでサーバーを作成するにあたり二つのIPアドレスが使用されます。一つがグローバルIPアドレス
でもう一つがプライベートIPアドレス
です。グローバルIPアドレスはElastic Ipなどで後から割り当てる、VPC外からみたサーバーのアドレスを示すもので、プライベートIPアドレスはサーバーの作成時に自動的に割り当てられる、VPC内からみたサーバーのアドレスです。もしVPC内のみ(ローカルの環境)で通信を行う際はプライベートIPアドレスのみで大丈夫です。しかしVPCの外から通信を送る場合(例:第三者が利用者となるWebアプリケーションを運用する場合)はグローバルIPアドレスがなくてはサーバーへアクセスできません。Certificate Manager とは
ユーザーに対し、このWebアプリはhttps通信(SSL通信)を行っていることを証明する働きをしています。この証明をしているWebアプリはブラウザで見る際にURLの左側に鍵マークがあります。このマークがないWebアプリはユーザーのデータが漏洩する可能性が高いので、注意してください。参考にした記事
AWS初心者 入門編 脱・知ってるつもり!AWSのネットワーク関連用語を基礎からおさらい
ウィキペディア HTTPS
AWS ルートテーブル自分が使用したAWSのツールの概要は以上です。今度は導入の際に自分が行ったことや参考にした記事をツールごとに紹介していきます。
AWSのツールを使う前に
AWSアカウントを作成する際の注意点やAWSの料金管理の仕方は以下の記事から教わりました。AWSを初めて使うけどどうすれば良いかよくわからないという方はこの記事を参考にしてみてください。
Qita AWSアカウントを取得したら速攻でやっておくべき初期設定まとめVPC&EC2&RDS
AWSでのVPCの立ち上げからEC2、RDSの作成は以下の記事を参考にしました。各章ごとの内容がわかりやすく書いてありますので、自分の知りたいことをこの記事から探すような活用の仕方が良いと思います。
(下準備編)世界一丁寧なAWS解説。EC2を利用して、RailsアプリをAWSにあげるまでroute53
ドメインの作成からドメインとグローバルIPアドレスの結びつけをroute53で行いました。
参考にした記事は以下の二つです。
Route 53 でドメインを取得・購入する(2019版
AWS Amazon Route 53 の開始方法https通信(SSL通信)
EC2を作成し、Elastic IPでグローバルIPアドレスを設定しただけだと、http通信でユーザーとサーバーの通信が行われるため安全な通信とは言えません。(通信に関する知識が全くないため、どこが危険なのかはわかりません。)そこでhttp通信をhttps通信に切り替える必要があります。
自分がhttp通信をより安全なhttps通信に切り替える時やhttps通信(SSL通信)を行っていることに対する証明書を発行する時に参考にした記事はこちらです。
ナレコムAWSレシピ AWSでWebサイトをHTTPS化 その1:ELB(+ACM発行証明書)→EC2編Webサーバーソフトウェア編
自分のOS、Apacheのバージョンはこちらです。
ツール バージョン Ubuntu 18.04.4 LTS Apache 2.4.29 LAMPの導入などはいろんな方が記事を書いてくれているので、それぞれの環境に合うものを参考にしていただければと思います。この章では自分が今までWebアプリケーションをデプロイした経験がないゆえにはまったことがあったので、それを紹介します。
ちなみに自分がLAMPを導入する際に参考にした記事はこちらです。
Install PHP 7.4 on Ubuntu 18.04Apacheではまったこと
自分がはまったことは、ブラウザでドメインを入力したときに自動的に
自分のプロジェクト/public/index.php
を表示するようにApacheを設定するにはどうすれば良いのかということです。自分が見つけたこの問題に対する解決策は、Apacheの設定ファイル(/etc/apache2/apache.conf
など)内のhttp通信(port80を使用)のときに表示するファイルのパスを設定する部分(<VirtualHost *:80>
から</VirtualHost>
にかけて)にDocmentRoot /var/www/html/自分のプロジェクト/public/
を加えるというものでした。しかし自分の環境の場合それだけではうまくいきませんでした。なぜなら、自分のようにUbuntuをOSとしている場合のApacheの設定ファイルは一つにまとまっているわけではなく、いくつかの設定ファイルに分裂していたからです。
ではDocmentRoot ~
をどこへ加えたのかというと、/etc/apache2/sites-available/000-default.conf
に加えることでうまくいきました。
(自分のWebアプリケーションの通信はインターネットゲートウェイからロードバランサーまではhttps通信ですが、EC2からロードバランサーまではhttp通信のためhttp通信の設定を行う/etc/apache2/sites-available/000-default.conf
を書き換えました。もしEC2からロードバランサーまでをhttps通信で行う場合はおそらくhttps通信の設定を行う/etc/apache2/sites-available/default-ssl.conf
を書き換える必要があるかと思います。)この問題に対する解決策は下の記事を参考にさせていただきました。
index意外のページにアクセスすると404エラーが発生するアプリケーション編
このWebアプリケーションに使用した各ツールのバージョンは以下の通りです。
ツール バージョン php 7.4 Laravel 7.12.0 Vue 2.6.11 Laravel(バックエンドorサーバーサイド)
Laravelのインストール時にはまったこと
メモリの小さいEC2を使用したがゆえにLaravelの一部しかインストールされないという問題にはまり、そのとき自分がteratailで質問したときに得た回答と参考にした記事は以下の通りです。間違えてメモリを1GBにしてしまった時などにはぜひ参考にしてみて下さい。
AWSのEC2上でのLaravelとAWSでのRDS(MySQL)の環境で"php artisan migrate"を実行した時のエラー
Life with IT swap領域拡張手順(ファイル割当)コードを書く上で気を付けたこと
Laravelを使用しコードを書く上で気を付けたことは3つです。
- 設計パターンに
MVC(Model View Controller)
を用いる- MVCのうちのControllerの内容はなるべく分かり易いものにする
- phpとRDBとの手続きやOAut認証の手続きはModel内に収める
1の理由はADR(Action Domain Response)など設計パターンには様々なものがありますが、初めてFWに触れる自分にとって単純なものを選ぶ方がやりやすいと思ったからです。
2にある「分かり易い」の基準は二つあります。一つ目はController内のメソッドの引数と返り値には必ず型の指定を行う、二つ目はController内のすべてのコードの質を一定の状態に保つというものです。二つ目のコードの質を判断するために自分はPHPStan
という静的解析ツールを使用しました。このツールはレベル別にコードの間違いを指摘してくれます。レベルの一番低いものだと、コード内に宣言されていない変数やメソッドが使用されていないかを確認してくれ、レベルが上がるにつれてメソッドの引数の型が指定されているかや返り値の型の指定がされているかなどを確認してくれます。コードの質のチェックだけではなく、コードのデバックにも使用できて便利です。
2,3を気を付けるようになった理由は自分が他の方が以前書いたコードの修正を行った時の経験から来ています。私が修正依頼を受けたコードはGoogleAppScript(JavaScriptにGoogleカレンダーなどのGoogleのツールと簡単に連携が取れるライブラリが付け加えられた言語)で書かれたものでした。GASにはTypeScriptのような型の指定をする機能はありませんし、修正を行うコード自体にLaravelにおけるpublic/index.php
のような処理の全体が分かり易く書かれたコードはありませんでした。そのため個々のメソッドの役割や処理全体の把握にとても時間がかかりました。今回のWebアプリケーションは自分単独で開発・運用するもので自分以外の人が関わることはありませんが、未来の自分は他人と同じような理解力しかないので、そんな自分でもすんなり理解できるにはどうすれば良いのか考えた結果、今回のWebアプリケーションのコードを書く際も2,3を気を付けるようにしました。機能を実装する上で気を付けたこと
機能を作成する際に気を付けたことは
小さく作り、小さく試し、小さく実装する
ということです。例えばこのような要件の機能を付けることになったとします。
ユーザの入力事項を指定のデータベースへ登録する。
この機能を作成するには以下の手順で作成するとします。
- 登録するデータベースの作成
- 入力値をデータベースへ登録する機能の作成(Model)
- 入力値をModelへ伝え、Modelの結果によって動作を変える機能の作成(Controller)
- 入力値のバリデーションの作成
- 入力ホームの作成
- フォームとデータベースの登録の機能を結びつけるためのルーティング
この機能が正常に作成・実装できたかどうかを判断する方法の一つとして、これらの機能を一気に作成・実装し、Laravelのもとからあるデバック機能を使用してエラーの有無をブラウザ上で確認する方法があります。この方法はスピーディーに実装できる反面、本番用のデータベースへ値を入れなくてはならなかったり、複数のパターンで複数回テストするのに手間がかかったりします。そこで自分が行った方法は、手順を一つ進めるごとにその手順で行ったことが要件を満たすものかテストを行うという方法です。例えば、1が完了したらLaravelのテスト機能を使用して要件に合致するレコードが作成できたのか確認し、2が完了したら作成した機能にテスト機能を使用して入力値を入れることで指定のレコードへ登録できたかを確認するという感じです。この方法は、一つ一つ手順を進めるごとにテストを作成しなくてはいけないので実装のスピードは遅くなる反面、Laravelのテスト機能のおかげで本番のデータベースへ値を入れる必要がなかったり、
factory()
などのヘルパ関数を使用して様々なパターンで100件単位のテストを一瞬で行えたり、個別にテストを行うことによりエラーの原因を絞って考える(例えば、もし手順3の段階でエラーが出た場合、2までは正常だったため原因は手順3で行ったことにあると違いないと推測できる)ことができます。
Laravelのテスト機能をはじめから使用するのは大変かもしれませんが、Laravelの公式ドキュメントでもたくさんのテスト機能の紹介がされているので、Laravelを使用するのならばぜひ使ってみて下さい。Vue(フロントエンド)
LaravelとVueの両方で開発経験がなくてLaravelでアプリケーションを開発する方へ
これは自分の質問に答えてくれたエンジニアの方が言ってくれたことであり、自分もLaravelとVueを組み合わせて開発していて思ったことなのですが、もしLaravelとVueのどちらも今まで触ったことがない場合はLaravelのみでWebアプリケーション開発を行う方が良いということです。なぜならWebアプリケーション開発で二つのFWを組み合わせて使用しようと思うと、考えることがFWが一つのときの倍ぐらいに増えてデプロイまでが長くなってしまうからです。デプロイまでが長くなると気力が一気に落ちると思うので、最初のうちはなるべく単純なものの方が良いかなと思います。
LaravelにおけるVueの実装
LaravelでのVueの実装は以下の記事を参考にしました。
Laravel7からVue.jsを使う最短レシピ自分がドはまりしたところ
自分がLaravelとVueの組み合わせたときに一番悩んだのはVueのコンポーネントにフォームを作成した際、そのフォームの値をどうやってCSRF(ユーザーになりすますことで他のサイトから不正にリクエストを送ること)対策をしたうえでバックエンドへ伝えるかということです。LaravelのViewの役割をもつ
blade.php
でのLaravel7に対するCSRF対策はこんな感じです。//省略 <form action="バックエンドまでのパス" method="post"> @csrf <input name="name" type="text"> </form>これをVueのコンポーネントのフォームでも同じようなことをしようとすると
@csrf
の部分が無効となり419エラーで怒られてしまいます。この問題は、コンポーネントにCSRF対策を書くのではなく、コンポーネントを表示しているblade.php
にjQueryでこのように付け加えることで解決できます。$('body').on('submit', 'form', function () { $(this).append('@csrf') });このコードで書いてあることは、body要素の中のform要素にsubmitというイベント(
<input type="submit">
を実行する)が発生した時にform要素に@csrf
を加えるということです。解決方法は他にもいろいろあると思います。この解決方法は自分がteratailで質問した際に教えて頂きました。その時の質問と回答はこちらです。
Laravelを使用したVueのcomponent内のformのpostリクエストで419エラーLaravelとVueにおけるこの他のCSRF対策を紹介した記事はたくさんありました。また、Laravelの公式ドキュメントではCSRF対策をしているミドルウェアを無効にする方法が紹介されていました。しかし、CSRF対策が書かれた記事に書いてある内容が自分にとって複雑に感じるものが多かったり、CSRF対策を無効にしてWebアプリケーションの安全性を下げるのは良くないと思いjQueryを使用した方法を選びました。
終わりに
Webアプリケーションを作成しただけではなく、それらを運用する環境まで整えたのでたくさんの時間(3か月くらい)がかかりました。Webアプリケーションを作成しようと思わなかったら感じる必要のないストレスを感じました。しかしこの経験を通し、自分の頭の中でふわふわ浮いていたネットや書籍で知った言葉たちが、ようやく実感のある言葉になったなーという感覚を持てるようになりました。
このWebアプリケーションのデプロイまでに自分がAWSに対して払った料金はドメイン取得時の千円ぐらいです。またEC2で初めに入れたOSが気に食わなければ何回も無料で作り直せます。(現に自分はAmazon Linux ⇒ Ubuntuへ変更しました。)AWSのEC2などのサービスは初めの12年は無料で使用できるので、初めてのWebアプリケーションのデプロイは年間で料金を払うようなレンタルサーバーよりもAWSを使用した方が良いと個人的に思っています。(BillingなどでAWSのコスト管理はしっかり行ってください。)
なにも知らなかった自分がWebアプリケーションを作成できるようになったのは、無償でteratail・コミュニティなどで自分の質問を答えていただいた方々や分かり易いLaravelの公式ドキュメントを書いてくれた方々、書籍(PHPフレームワーク Laravel Webアプリケーション開発)に携わった方々、ネットで分かり易い記事を書いてくれた方々、お金がない日本人学生でもAWSを使いやすいようにしてくれた方々のおかげです。今後自分と同じような部分で悩んでいる人へ解決策を伝えたり、OSSへの貢献(できるかなー?)するなどで恩返しできれば良いなと思います。
- 投稿日:2020-07-14T09:12:18+09:00
Laravel ルートパラメータ持ちのURLにリダイレクトする
目的
- ルートパラメータが設定されているURLにパラメータ値を指定してリダイレクトする方法をまとめる
実施環境
- ハードウェア環境
項目 情報 OS macOS Catalina(10.15.5) ハードウェア MacBook Pro (13-inch, 2020, Four Thunderbolt 3 ports) プロセッサ 2 GHz クアッドコアIntel Core i5 メモリ 32 GB 3733 MHz LPDDR4 グラフィックス Intel Iris Plus Graphics 1536 MB
- ソフトウェア環境
項目 情報 備考 PHP バージョン 7.4.3 Homwbrewを用いて導入 Laravel バージョン 7.0.8 commposerを用いてこちらの方法で導入→Mac Laravelの環境構築を行う MySQLバージョン 8.0.19 for osx10.13 on x86_64 Homwbrewを用いてこちらの方法で導入→Mac HomebrewでMySQLをインストールする 前提条件
- 実施環境と同じ環境が構築されていること。
前提情報
- 公式ドキュメントを読んでおくと理解が早いかもしれない。
/user/{user_id}
にリダイレクトする方法を記載する。ルーティングには下記の様な記載があるとする。ルーティング設定部分のみ記載する。
web.phpRoute::get('/user/{user_id}', 'UserHomeController@index');読後感
- ルートパラメータが設定されているURLへのリダイレクト方法が分かる。
- 名前付きルーティングにパラメータを渡す方法が分かる。
実施(一つのルートパラメータ)
ルーティングファイルを開きリダイレクト先のルーティングの記載を編集し、ルーティング情報に名前をつける。
web.phpRoute::get('/user/{user_id}', 'UserHomeController@index')->name('user.index');リダイレクト処理を行いたい場所に下記の記載を行う。(下記が実行されると
/user/1
にリダイレクトする。)return redirect(route('user.index', [ 'user_id' => 1, ]));実施(複数のルートパラメータ)
ルーティングファイルを開きリダイレクト先のルーティングの記載を編集し、ルーティング情報に名前をつける。
web.phpRoute::get('/user/{user_id}/{age_id}', 'UserHomeController@index')->name('user.index');リダイレクト処理を行いたい場所に下記の記載を行う。(下記が実行されると
/user/1/2
にリダイレクトする。)return redirect(route('user.index', [ 'user_id' => 1, 'age_id' => 2, ]));簡単なまとめ
ルーティング情報に名前をつける方法
//ルーティング情報->name('ルーティング情報の名前') Route::get('/user/{user_id}/{age_id}', 'UserHomeController@index')->name('user.index'); //上記の記載でルーティング情報に「user.index」という名前が付けられた。名前付きのルーティング情報にリダイレクトする方法
return redirect('ルーティング情報に付けた名前');リダイレクト先のURLにルートパラメータがあるときにリダイレクトする方法
return redirect('ルーティング情報に付けた名前', [ 'ルートパラメータ' => リダイレクト時にルートパラメータに入る値, );参考文献
- 投稿日:2020-07-14T08:49:22+09:00
【Laravel】実行されたDBクエリの確認ができるやつを書いた
はじめに
自分はここ最近Laravelを使ったお仕事をしているのですが、こういった悩みがありました。
- QueryBuilder便利だけど、実行されるクエリが全くわからん
- レビューする側だと共感していただけるのでは??
- 狙ったクエリが書かれているのか自信が無い
- このメソッド叩くと無駄なクエリが流れるんだが
- etc
今までは、PHPUnitでseederなどでデータを用意してクエリを実行するメソッド書いてアサーションして、お、期待するレコード取れたよね、やったやった!!で済ましていたのですが、いちいち条件網羅するデータを用意するのが難儀だったりします。
(実行されるクエリだけ分かればいいケースも中にはあると思うのです)くぅ、実行されるクエリだけ見たい、見せたい、それさえ出来れば、、そんな悩みを抱えていたのであります。1
実行されるクエリの確認をなるべく楽にサクッとできるものが欲しい!! というわけで、Laravelで実行されたDBクエリの確認できるやつ書いた
— しまぶ (@shimabox) July 8, 2020
shimabox/laqu: Laqu is Laravel Db Query Helper. https://t.co/vYLHeXUxukLaravelで実行されたDBクエリの確認ができるやつを書いてみました。2
(よく分からない前フリですいません。。)何を解決するか
Laqu(ラク?レイク?) は、
- 期待するクエリが流れているかPHPUnitでアサーション可能です
- traitを用意しています
- クエリが流れるメソッドを渡すと、どのようなクエリが流れるか確認することができます
- 実行されたクエリの中から一番早かったもの、一番遅かったものが抽出できます
- ソートも可能です
- ビルド後(パラメータがバインド済み)のクエリが確認できます
を行うことができます。実行時間のところはBasic Database Usage - Laravel - The PHP Framework For Web Artisansで出された値(time)を利用しているので、そこまで当てにならないかもしれませんが実行されたクエリの確認は行なえます。
なお、開発中に利用されることを想定していますのであしからずです。
要件
PHP7.2
以上- Laravel
5.8.x
,6.x
,7.x
とりあえずなるべく最新のLaravelが動く環境であればOKだと思います。
インストール
composerを使います。
$ composer require --dev shimabox/laqu使い方
QueryAssertion
期待するクエリが流れているかPHPUnitでアサーションするためのものです。
traitです。assertQuery()
を使います。<?php use Laqu\QueryAssertion; use Tests\TestCase; class QueryAssertionTest extends TestCase { use QueryAssertion; private $exampleRepository; protected function setUp(): void { parent::setUp(); $this->exampleRepository = new ExampleRepository(); // 仮 } public function queryTest() { // 基本的な使い方 $this->assertQuery( // クエリが実行される処理をクロージャに渡します function () { $this->exampleRepository->findById('a123'); }, // 期待するクエリを書きます 'select from user where id = ? and is_active = ?', // バインドされる値を配列で定義 // (bindするものがない場合は空配列を渡すか、引数は渡さないでOK) [ 'a123', 1, ] ); // 複数のクエリを確認 // 基本的には 1メソッド1クエリの確認 を推奨しますが、中には1メソッドで複数クエリが流れる場合も // あると思います。 // その場合は下記のようにクエリとバインド値を配列で対になるように定義してください。 $this->assertQuery( function () { // 例えばこの処理で複数のクエリが流れるとします $this->exampleRepository->findAll(); }, // 期待するクエリをそれぞれ配列で定義 [ 'select from user where is_active = ?', // ※1 'select from admin_user where id = ? and is_active = ?', // ※2 'select from something', // ※3 ], // バインドされる値を二次元配列で定義(bindするものがない場合は空配列を渡してください) [ [ // ※1. 1, ], [ // ※2. 'b123', 1, ], // ※3 はバインド無しの想定なので空配列を渡します [], ] ); } }こんな感じで、クエリのアサーションが行えます。
QueryAnalyzer
クエリが流れるメソッドを渡して、どのようなクエリが流れたのか確認することができます。
QueryAnalyzer::analyze()
で実行されたクエリの結果(Laqu\Analyzer\QueryList
)が取得できます。
※ QueryListの中身はLaqu\Analyzer\Query
です<?php use Laqu\Facades\QueryAnalyzer; /** @var Laqu\Analyzer\QueryList */ $analyzed = QueryAnalyzer::analyze(function () { // クエリが実行される処理をクロージャに渡します $author = Author::find(1); $author->delete(); }); /* Laqu\Analyzer\QueryList {#345 -queries: array:2 [ 0 => Laqu\Analyzer\Query {#344 -query: "select * from "authors" where "authors"."id" = ? limit 1" -bindings: array:1 [ 0 => 1 ] -time: 0.08 -buildedQuery: "select * from "authors" where "authors"."id" = 1 limit 1" } 1 => Laqu\Analyzer\Query {#337 -query: "delete from "authors" where "id" = ?" -bindings: array:1 [ 0 => "1" ] -time: 0.03 -buildedQuery: "delete from "authors" where "id" = '1'" } ] } */ dump($analyzed);この取得結果をもとに
一番実行時間が早かったクエリの抽出
extractFastestQuery()
/* Laqu\Analyzer\Query {#337 -query: "delete from "authors" where "id" = ?" -bindings: array:1 [ 0 => "1" ] -time: 0.03 -buildedQuery: "delete from "authors" where "id" = '1'" } */ dump($analyzed->extractFastestQuery());一番実行時間が遅かったクエリの抽出
extractSlowestQuery()
/* Laqu\Analyzer\Query {#344 -query: "select * from "authors" where "authors"."id" = ? limit 1" -bindings: array:1 [ 0 => 1 ] -time: 0.08 -buildedQuery: "select * from "authors" where "authors"."id" = 1 limit 1" } */ dump($analyzed->extractSlowestQuery());クエリの実行時間でソート
sortByFast()
,sortBySlow()
/* array:2 [ 0 => Laqu\Analyzer\Query {#337 -query: "delete from "authors" where "id" = ?" -bindings: array:1 [ 0 => "1" ] -time: 0.03 -buildedQuery: "delete from "authors" where "id" = '1'" } 1 => Laqu\Analyzer\Query {#344 -query: "select * from "authors" where "authors"."id" = ? limit 1" -bindings: array:1 [ 0 => 1 ] -time: 0.08 -buildedQuery: "select * from "authors" where "authors"."id" = 1 limit 1" } ] */ dump($analyzed->sortByFast()); /* array:2 [ 0 => Laqu\Analyzer\Query {#344 -query: "select * from "authors" where "authors"."id" = ? limit 1" -bindings: array:1 [ 0 => 1 ] -time: 0.08 -buildedQuery: "select * from "authors" where "authors"."id" = 1 limit 1" } 1 => Laqu\Analyzer\Query {#337 -query: "delete from "authors" where "id" = ?" -bindings: array:1 [ 0 => "1" ] -time: 0.02 -buildedQuery: "delete from "authors" where "id" = '1'" } ] */ dump($analyzed->sortBySlow());ビルド後のクエリ取得
getBuildedQuery()
Laqu\Analyzer\QueryList
から要素を特定して利用します。
※ QueryListは配列として扱えます// select * from "authors" where "authors"."id" = 1 limit 1 echo $analyzed[0]->getBuildedQuery(); // delete from "authors" where "id" = '1' echo $analyzed[1]->getBuildedQuery();が利用できます。
Helper
ヘルパーとして以下機能も提供しています。
QueryLog
QueryLogはBasic Database Usage - Laravel - The PHP Framework For Web Artisansの処理をラップしたものです。
QueryLog::getQueryLog()
<?php use Laqu\Facades\QueryLog; $queryLog = QueryLog::getQueryLog(function () { Author::find(1); }); /* array:1 [ 0 => array:3 [ "query" => "select * from "authors" where "authors"."id" = ? limit 1" "bindings" => array:1 [ 0 => 1 ] "time" => 0.12 ] ] */ dump($queryLog);察している人は察していると思いますが、他のメソッドはこの機能をフル活用しています。
そのため、実行時間関連はそこまで精密ではありません。QueryHelper
クエリとバインドパラメータを渡すと、実行されるクエリの確認ができます。
※ pdo-debug/pdo-debug.php at master · panique/pdo-debug を利用しています
QueryHelper::buildedQuery()
<?php use Laqu\Facades\QueryHelper; $now = Carbon::now(); $from = $now->copy()->subDay(); $to = $now->copy()->addDay(); $query = 'select * from authors where id in (?, ?) and name like :name and updated_at between ? and ?'; $bindings = [ 1, 2, '%Shakespeare', $from, $to, ]; $buildedQuery = QueryHelper::buildedQuery($query, $bindings); // select * from authors where id in (1, 2) and name like '%Shakespeare' and updated_at between '2020-07-07 00:37:55' and '2020-07-09 00:37:55' echo $buildedQuery;Formatter
その他機能として、クエリのフォーマット機能も提供しています。
こちらは、doctrine/sql-formatter: A lightweight php class for formatting sql statements. Handles automatic indentation and syntax highlighting. を利用しています。
デフォルトはNullHighlighter
を利用していますが、Cli、HTMLでのフォーマットも可能です。詳しくは https://github.com/shimabox/laqu#queryformatter を参照してください。
その他
Facade
Laravelっぽく?ファサードを使ってみました。
実際に作ってみると、どういう仕組で動いているのかなんとなく理解が深まった気がします。GitHub Actions
みようみまねでGitHub Actionsを使ってみました。
laqu/run-tests.yml at master · shimabox/laqu
これで、PHPのバージョンやLaravelのバージョンごとにテストの実行やlintができているのでそこそこ自信をもって提供できています。おわりに
というわけで長くなってしまいましたが、Laravelで実行されたDBクエリの確認ができるやつを書いた話は以上となります。
shimabox/laqu: Laqu is Laravel Db Query Helper.
もしよかったら使ってみてください。
(使ってみて、ここはこうしたらいいとか、こうすべきだとかあればプルリクください)
QueryBuilderなどを使ってクエリを作るところと実行するところを分けている設計ならば、toSql()を使って確認することも可能でしょうがそんなフェーズはとうに過ぎていた。。 ↩
Basic Database Usage - Laravel - The PHP Framework For Web Artisans のように、
enableQueryLog()
などを書くのも方法としてはありますが、それをいちいち書くのも面倒だったのでライブラリで吸収しちゃおうという作戦です ↩
- 投稿日:2020-07-14T08:44:42+09:00
Laravel で作ったシステムに Excel をアップロードしたら拡張子が zip になった
環境
環境は
ローカルのHomestead
と本番のVPS
の2つで、どちらでも発生。
- Laravel Framework 5.8.24
- PHP 7.3.4 (PHP 7.2.16)
- Ubuntu 18.04.2 LTS (CentOS Linux release 7.6.1810)
- Homestead (ConoHa VPS Laravelイメージ)
何がどうなった?
エクセルをアップロードし、
$file->guessExtension()
で拡張子を取得したところzip
となってしまった。
さらに$file->getMimeType()
をみたところapplication/zip
となっていた。クライアントの情報を取得する関数を使うと、それぞれ
$file->guessClientExtension()
が
xlsx
となり、$file->getClientMimeType()
はapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheet
となった。
表にまとめると以下。
extension mime_type larval zip application/zip client xlsx application/vnd.openxmlformats-officedocument.spreadsheetml.sheet エクセルの mime_type なっが。
どうして?
finfo
がうまく判定できないらしい。
ライブラリをガンガン追って行ったところ、Laravel の問題というよりは、内部で使用している Symfony のクラスの内部で使用しているfinfo
に行きついた。
実際にfinfo
で mime_type を判定させてみると、application/zip
となってしまった。
application/zip
から拡張子を推測するとzip
になるという流れである。
StackOverflow の関連記事
上記記事を読むと、ほかの Microsoft の Office 系ファイルもダメらしい。どうする?
解決策はいくつかあると思う。
- Symfony の該当クラスは複数の guesser を扱えるので、Laravel で頑張って拡張する
- finfo を修正する
- 素直にクライアントの拡張子を利用する
などなど。
自分は 上記のどれも採用せず、拡張子を推測するための関数をつくった(理由は後述)。
作ったと言っても、Laravel/Symfonyが用意したものと、先人の知恵を組み合わせただけだが。基本方針
- 拡張子に関してはクライアント(ファイルアップロード者)を信じるという選択は取りたくない
- Laravel を拡張するのはしんどそう
- finfo を修正するのは移植性がない
- 不具合報告があったのはとりあえず excel だけ
参考ソース
public function guessExtension(UploadedFile $file): string { $extension = $file->guessExtension(); if ($extension !== 'zip') { return $extension; } $guessedMimeType = $file->getMimeType(); $clientMimeType = $file->getClientMimeType(); if ($guessedMimeType === $clientMimeType) { return $extension; } $map = [ 'application/vnd.ms-word.document.macroEnabled.12' => 'docm', 'application/vnd.openxmlformats-officedocument.wordprocessingml.template' => 'dotx', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx', 'application/vnd.ms-powerpoint.template.macroEnabled.12' => 'potm', 'application/vnd.ms-powerpoint.addin.macroEnabled.12' => 'ppam', 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12' => 'ppsm', 'application/vnd.ms-powerpoint.presentation.macroEnabled.12' => 'pptm', 'application/vnd.openxmlformats-officedocument.presentationml.slideshow' => 'ppsx', 'application/vnd.openxmlformats-officedocument.presentationml.template' => 'potx', 'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx', 'application/vnd.ms-excel.template.macroEnabled.12' => 'xltm', 'application/vnd.ms-excel.addin.macroEnabled.12' => 'xlam', 'application/vnd.ms-excel.sheet.binary.macroEnabled.12' => 'xlsb', 'application/vnd.ms-excel.sheet.macroEnabled.12' => 'xlsm', 'application/vnd.openxmlformats-officedocument.spreadsheetml.template' => 'xltx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx', ]; return $map[$clientMimeType] ?? $extension; }作った後に「 Symfony の同様の力技を行使する guesser を使う手もあったな」などと思ったけど、いろんな mime_type を検討するのはしんどいので、とりあえずこれで。
- 投稿日:2020-07-14T00:27:06+09:00
Dockerで構築したLaravel環境に、VSCodeでステップ実行デバッグを仕掛ける
元記事 Dockerで構築したLaravel環境に、PHPStormでステップ実行デバッグを仕掛ける
← 元元記事 最強のLaravel開発環境をDockerを使って構築する【新編集版】目的
Dockerで構築したLaravel環境で実行されているPHPコードに対して、
Microsoft製の「VSCode」からステップ実行デバッグができるようにしたい。概要
PHP定番デバッグツール xdebug を使用して実現していきます。
元記事でサーバサイド側の設定まで設定済みであることが前提。
本記事ではクライアント側であるVSCodeの設定のみ記載します。
(元記事、元元記事共に大変参考になりました。感謝。。)環境
- 冒頭に書いた元記事および元元記事の手順でLaravel環境を構築済であること。
- VSCode最新版を使って開発している方。(Mac版で動作確認済。)
方法
クライアント側作業(お手元のMac)
VSCodeを起動する。
PHP Debugをインストールしていない場合は PHP Debug - Visual Studio Marketplace をインストールしておく
VSCodeでdocker-laravel プロジェクトを開く。
メインメニューからデバッガ設定画面を開く。
launch.jsonを作成後、以下の設定を貼り付ける
- portはサーバ側で設定した値と同じになるようにする
- pathMappings
- サーバ側のアプリケーション配置場所:"/work/backend"
- クライアント側のアプリケーション配置場所:"${workspaceRoot}/backend"
{ "version": "0.2.0", "configurations": [ { "name": "Listen for XDebug", "type": "php", "request": "launch", "port": 9000, "pathMappings": { "/work/backend": "${workspaceRoot}/backend" } } ] }.vscode/launch.jsonが下記画像のように作成されていればOK
* ドキュメントルートがDocker-backendの下にブレークポイントの設定
- 一時停止した時の挙動が確認ができるように、変数に文字をセットするようコードを修正。
-- Web.php <?php use Illuminate\Support\Facades\Route; /* |-------------------------------------------------------------------------- | Web Routes |-------------------------------------------------------------------------- | | Here is where you can register web routes for your application. These | routes are loaded by the RouteServiceProvider within a group which | contains the "web" middleware group. Now create something great! | */ Route::get('/', function () { $hoge = 'welcome'; return view($hoge); });デバッグ開始
ブラウザでPHPサーバーにアクセスしてみる。
18行目で処理が一時停止、左側のタブで変数に格納された文字列を確認できる
変数にマウスオーバーしても値を確認できる
参考リンク