- 投稿日:2020-12-16T22:33:06+09:00
LaravelにDDDを導入して1年経った所感(達成したこと / 課題点 / モデリングの難しさなど)
背景
昨年公開したこちらの記事「Laravel でドメイン駆動設計(DDD)を実践し、Eloquent Model 依存の設計から脱却する」の続編です。
弊社で開発している「オンライン家庭教師マナリンク」の実装に DDD のアプローチを用いています。導入して 1 年が経過したので、いろいろと所感を述べていきます。
目次
- どうして DDD を導入したか
- DDD で達成できたこと
- まだ難しいと思うこと
- 結局モデリングが一番難しい
どうして DDD を導入したのか?
導入した当初の動機は Laravel で Eloquent Model 中心に実装するアーキテクチャに嫌気が差したからです。
Eloquent Model 中心に実装するとあちこちにドメイン知識が散らばって、実装した自分ですらよくわからない、という状態によく陥りました。弊社はベンチャー企業で、トラフィックの多さを設計で考慮することはあまり無いのですが、一方で機能追加が盛んなため、より問題が顕著でした。
DDD を実践した期間
導入して 1 年、と言いましたが、今年実装したものを思い返すと、
- 決済機能(決済処理、決済の対象となる指導コースの CRUD、各種社内管理画面等)に 4 ヶ月
- 先生 ↔ 生徒のコミュニケーションツール開発に 3 ヶ月(Nuxt↔Firebase↔React Native)
- 先生検索機能に 2 ヶ月
- ドメイン変更 2 ヶ月(事業のピボット)
- その他 1 ヶ月
という感じで、DDD をやったのは決済周りなので、4 ヶ月程度しかやってないですね。
※フロントエンドは簡易的な Repository パターンとか Hooks を導入する程度で、DDD はやっていないです。
実装した PHP のクラス数は雑に見て 200〜300 くらい、テストコードは 100 くらいでした(普段結合テストしか書いてないことがバレるw)。
DDD で達成できたこと
DDD を導入することで達成できたことを、【アーキテクチャ】と【ドメイン】の観点でまとめます。
【アーキテクチャ】Eloquent Model からの脱却
まず、先の記事のタイトルにもあった”Eloquent Model からの脱却”ですが、一言でいうと達成できました。
Eloquent Model の各クラスには以下のような変化がもたらされました。
- 個々の機能でのみ使われるような実装がなくなった
- 結果として、リレーションの定義や、ごくわずかな Scope が残りました
そして、そのために以下の方針で実装を行ってきました。
- Repository でのみ Eloquent Model を活用する
- Repository では取得したデータは原則として Entity に詰め替えて返す
ものすごくシンプルな Repository Interface の例を貼っておきます。Repository の戻り値が Entity になっているのがポイントです。
<?php namespace Packages\XXXXX\Domain\Repository; use Packages\XXXXX\Domain\Entity\GeneralUser; interface GeneralUserRepositoryInterface { public function find(int $id): ?GeneralUser; }【ドメイン】Entity にモデルの振る舞いを表現させる
機能名を表す namespace (ここでは”XXXXX”) の配下に 一般ユーザーを Entity とした"GeneralUser "というクラスを用意し、メソッドとして一般ユーザーがサービス上で行う振る舞いを実装していきます。
以下のようなイメージです。
namespace Packages\XXXXX\Domain\Entity; interface GeneralUserEntityInterface { /** * ユーザーは問い合わせと、支払いと、問い合わせのキャンセルができることがInterfaceだけ見ると読み取れる */ public function inquire(Teacher $teacher): Inquiry; public function makePayment(Inquiry $inquiry); public function cancelInquiry(Inquiry $inquiry); }※Entity の Interface までわざわざ作成することはさほど多くないですが、たまに作ります
以上のように、機能ごとに異なるふるまいを持っている Entity を切っていくことで Eloquent Model の責務を削るというアーキテクチャ上のメリットはもちろん、サービス上で誰(アクター)が何をできるのかが機能ごとに明示されるようになり、ドメイン知識が明確になるというメリットを得ました。
まだ難しいと思うこと
続いて、DDD を実践する上で難しいと思うことを書きます。
一覧系で Entity を使うと問題が生じる
Entity を中心に設計していると、一覧系のページが増えるに従って、どんどん Entity が肥大化していってしまう問題が浮上しました。
どうして一覧系で Entity を使うと肥大化するのか?
一覧画面で表示したいデータと、Entity に持たせるべきデータが必ずしも一致しないからです。
例えば弊社ですと、オンライン家庭教師の一覧画面で、各先生にその先生が用意しているオンライン指導のプラン(指導コースと呼んでいます)を 最大 3 件付帯して表示する、という要件がありました。
ここで、先生に指導コースを付帯して表示するためには、Teacher Entity に $courses といった命名のプロパティを持たせることで、表示系でも使えます。
しかし、この方法には「画面ごとに指導コースの取得ロジックや件数が違っていると、表示最適化のために Entity がどんどん Fat になっていく」という懸念があります。この画面では3件、あの画面では5件、この画面ではこの科目の指導コースに絞って最大3件、といった感じで条件が変わってくると Entity が肥大化します。
Teacher Entity に
getCoursesForHogeHogeListPage()
といったメソッドが次々増えていくのは本来ドメインモデルがやるべき責務なのか?という悩みがあります。言ってしまえば SEO 対策のために作るページも多いので、そこに Entity を使っているとどんどん表示ロジックが増えていきます。対処方針
思いついた実装方針を 3 つ挙げてみました。私は(大半のケースにおいて)一番最初の方針を採択しています。
- 取得系の処理は、Entity にデータを詰め込まずに、専用の DTO クラス(API Resource のような責務)を使う
- 検索系の処理は Repository から返す値をそもそも DTO のクラス型にする
- API のパスから DTO まで一気通貫で分かりやすく namespace 等を管理できたり、OpenAPI の Model と上手に連携し、ViewModel のような概念で扱えるともっと良いのかもしれない
- 諦めて取得系の処理は Eloquent Model をそのまま取り回す
- 表示のための Attribute が Eloquent Model に生えまくって本末転倒感がある
- DDD をやるほどでもない機能や事業領域にはこちらを採用しています。正直言うと上記で例示した先生の一覧ページでは単に Eloquent Model の Collection を API Resource に入れてまるっと返しています
- Read はデータソースごと切り替えられるようにして CQRS にガッツリ取り組む
- 弊社はそんな事業規模じゃない(≒DB のパフォーマンスがボトルネックになっていない)
- とはいえ、事業規模が小さいなら DDD やらなくていいとか、適当に実装していいかというと別の問題。ちょうどいいバランスを模索していきたい
結局モデリングが一番難しい
一周回ってここ最近は DDD で一番難しいのは結局モデリングだなと思っています。
昨年初めて Laravel×DDD に取り組み始めた頃は、PHP でどうやって Entity を表現するか?ValueObject を表現するか?Enum は?といった実装レイヤの悩み事が多かったです。まあ今も多いんですが。
しかし、そのあたりに慣れてくると、そもそもモデリングの時点でどれくらい精度高くドメインモデルが導き出せるか、また、実装しながらどれだけモデリングを磨き込んでいけるかが重要なのではと思えてきました。
モデリングとはなにか?
(※個人的な定義です)
要件定義の文章から、モデルとモデル同士の関連性を導き出すことです。要件はいつも文章で表現されますが、文章というのは大変曖昧だし複雑です。
「生徒がオンライン家庭教師に料金を支払えるようにして」という要件の中に、いったいどれだけのモデルが潜んでいるでしょうか。見ただけでは「生徒」「オンライン家庭教師」「料金」「支払う(という行為)」が浮かびますが、ここにさらに「支払えるか支払えないかの前提条件」とか「支払うことのできる料金の範囲」といったルール的な概念もあれば、オンライン家庭教師から見ると生徒に支払ってもらった金額は「売上」に該当するので、「売上」というそもそも要件に現れていないモデルも見えてきます。
このように、要件に出てくる登場人物、アクション、条件、アクションの結果生成されるデータといったモデルを見抜き、それらの関連性を見出すのがモデリングです。
モデリングが大事だと思う理由は?
要件の抜け漏れを発見できたり、要件の広がりに答えられる可能性を作れるからです。モデル同士の関連性を図に表していると、「あれ?ここって 1:1 のつもりで書いているけど 1:多にならん?」といった初歩的な見落としに気がついたり、稀にですが「この登場人物とこっちの登場人物は汎化の関係になってないか?」といった、最終的に Interface や抽象クラスに落とし込めるヒントを得たりすることがあります。
実践 DDD といった書籍も読みましたが、書籍は引き出しを与えてくれるだけで、実際にどの引き出しをいつ開くかは自分が決めるべき、といった感じです。
つまるところ、DDD のパターンを脳死で導入しないためにモデリングをしている気がします。
私はドメインサービスとかイベントはあまり使わないようにしています。知った当初は面白い概念だなと思いましたが、モデリングも実装もままならないうちにこれらに手を出すとカオスになりました。実装が落ち着いたり、サービスの検証を回していって要件がしっかり固まってきた頃に見返すと、ここはドメインサービスが必要そうだ、といったことがようやく見えてきた気がします。
どうやってモデリングしているか?
ユースケース図を書くか、クラス図を書いています。クラス図を plantUML で書くのがお気に入りなのですが、本当は紙とかホワイトボードをもっと使いこなすほうがいいのかもしれないです。なんだか plantUML 書くのに必死で発想に集中できない気がするんですよね。クラス図がモデリングとイコールかというと違う気もしますし・・・
plantUMLの例@startuml テスト用 note top of samplePackage ノートを書くことができる end note package samplePackage { class InquireUser InquireUser : id: UserId InquireUser : +makePayment(Teacher, amount): PaymentHistory class Teacher Teacher : id: TeacherId InquireUser "0..1" -- "*" Teacher class PaymentHistory PaymentHistory : teacherId PaymentHistory : +getUserPaid() PaymentHistory : +getUserId() PaymentHistory : +getCreatedAt() InquireUser "0..1" -- "*" PaymentHistory } @enduml実際モデリングする時間はあるのか?
実際、モデリングだけで丸 1 日掛けてます!とかは無いです。長くても数時間で終わらせたら少しずつソースコードを書きます。まずは Entity や ValueObject から書いて、Repository の Interface だけ書いて、組み合わせて UseCase を作っていくわけですが、節目節目で不自然なところがないか振り返り、こうやったほうがより的確な表現なのではと思ったら更新していきます。
Repository は Interface だけ書くことで、データベース設計と脳味噌を働かせるタイミングを分けて、API も作らず UseCase までで留めて一旦書き上げることで、ついついフロントエンドを実装したい衝動を抑えたりしています。延々とモデリングばかりして動くものを作らないと事業としては意味がないので、開発速度はできるだけ変えずに、開発の進め方を改善している感じです。
その他所感とか言いたいこと
Enum 便利
Enum は以下の抽象クラスを使っています。便利です。
<?php namespace Packages\Base\Domain\ValueObject; use InvalidArgumentException; use ReflectionObject; /** * @see https://speakerdeck.com/twada/php-conference-2016?slide=40 */ abstract class Enum { private $scalar; public function __construct($value) { $ref = new ReflectionObject($this); $consts = $ref->getConstants(); if (! in_array($value, $consts, true)) { throw new InvalidArgumentException("value [$value] is not defined."); } $this->scalar = $value; } final public static function __callStatic($label, $args) { $class = get_called_class(); $const = constant("$class::$label"); return new $class($const); } final public function value() { return $this->scalar; } final public function __toString() { return (string)$this->scalar; } }ValueObject をもっと上手くなりたい
ValueObject のモデリングについては、先日以下のツイートを見かけまして、ValueObject にはまだまだ開拓の余地があるなと思いました。
ビジネスルールを表現する5点セット。
— 増田 亨. (@masuda220) December 5, 2020
値オブジェクト(金額、数量、日付、地点、...)
区分オブジェクト(商品区分、サービス区分、状態区分、...)
範囲オブジェクト(価格帯、期間、地域、...)
コレクションオブジェクト(順序・集合・写像)
表オブジェクト(価格表、判定表、...)まだまだビジネスルールを UseCase に書いてしまっている余地がありそうです。範囲オブジェクト、区分オブジェクトあたりは結構 if 文で済ませてしまっているような気がします。
ルールを個別のオブジェクトとして明示することで、より仕様をソースコードで表現しやすくなるように見えます。モデリングの段階で、こういった条件や範囲を意識的にモデリングするとソースコードにも反映できやすそうです。
Interface は神
最近抽象クラスを使わずに Interface で疎結合に組むのが楽しいです。Repository だけではなく、外部アクセスも Interface にします。また、これはOOPの話になりますが、特定の Entity に特定の振る舞いを持たせるためにその振る舞いのみを持った Interface を Implements させる(つまり、複数の Interface を Implements した Entity を作ったり、Interface を Implements した抽象 Entity クラスを作り、その拡張として実 Entity を作成する)など、Interfaceを有効活用してEntityを組むのも割と好きです。
Interface の扱いは今後も考察していきたいです。▼ 参考 この質疑応答がとてもいいです
https://softwareengineering.stackexchange.com/questions/398455/depend-on-ddd-entities-or-interfacesUsing Object-Oriented Programming techniques helps you define your domain model and describe the state of all entities, as well as the relations between different entities.
テストコードは神
PHPUnit でテストコードを書くようにしています。ほとんど API 単位の結合テストだけしか書いていないのにも関わらず、大変品質に貢献してくれています。GitHub Actions で自動テストを回すので、思わぬデグレをほとんど事前に防ぐことができます。
来年は「テストデータの整備」「単体テストにもチャレンジ」「フロントエンドでも jest×composition-api でテスタブルにする」あたりを標語に頑張っていこうと思います。
まとめ
ドメイン駆動設計を導入して 1 年間運用することで達成できたことと、今後の課題点を述べてきました。
アーキテクチャ面では Entity を使いこなすことで Eloquent Model への依存を実際に卒業できました。
ドメインをモデリングする部分はまだまだ上達の余地があると考えており、アーキテクチャや設計パターンへの理解を深めることと両軸で上達していくことが重要だと思っています。
告知
私が CTO を務めている「オンライン家庭教師マナリンク」では、エンジニアを募集しております。
2020 年 12 月 現在、Web エンジニア及び、React Native エンジニアを探しております。
本日のLTスライドです!
— 名人さん|オンライン家庭教師マナリンクCTO (@Meijin_garden) November 25, 2020
PHPUnitで単発/月額決済ありサービスを自動テストしている話https://t.co/8K3jXUITRr
#laravelltマナリンクでは、オンライン家庭教師の先生方のために、サイト上で自身のプロフィールを魅力的に発信できるようにしたり、オンライン指導専用アプリをリリースするなど、次々にプロダクトを開発しています。日々新しい技術を勉強して、試す機会を探している方にはうってつけな環境です。
ベンチャー企業ですが、CTO/CEO ともにテストコードを使って品質保持することに理解はありますし、オンライン家庭教師という新しい働き方のドメインを作っていくという意味で、問題解決領域としても大変興味深いのではと思っています。
興味あれば 上記の Twitter に DM でご連絡をください!
- 投稿日:2020-12-16T22:29:07+09:00
LaravelでHerokuデプロイ時、HTTPS通信にならないときに確認すること
問題
- サイト内ページがHTTP通信で表示されてしまう。*Chromeの場合(「保護されていない通信」の警告が出る。)
- CSSが反映されていない。
環境
- Windows 10
- Google Chrome
- Heroku
- PHP 8.0.0
- laravel 8.16.1
やったこと(3つ)
Herokuを本番環境に設定する
コマンドラインで本番環境に設定。
.envファイルはプッシュされないため、忘れがち?heroku config:set APP_ENV=production本番環境でHTTPSを強制する
見出しの通り、HTTPSを強制します。
AppServiceProvider.phppublic function boot() { if (\App::environment(['production'])) { \URL::forceScheme('https'); } }HTTPをHTTPSにリダイレクトさせる
ミドルウェアで、HTTPをHTTPSにリダイレクトさせます。
まずは、コマンドラインでミドルウェアを作成します。php artisan make:middleware ForceHttpToHttps次に、Middlewareの中身を書いて、
ForceHttpToHttps.phppublic function handle(Request $request, Closure $next) { if (\App::environment(['production']) && $_SERVER["HTTP_X_FORWARDED_PROTO"] != 'https') { return redirect()->secure($request->getRequestUri()); } return $next($request); }最後に、Kernelに突っ込みます。
Kernel.phpprotected $middleware = [ \App\Http\Middleware\ForceHttpToHttps::class, // 追加 ];まとめ
上記の方法で、問題が2つとも解決しました。
(CSSが反映された理由は不明です・・・)
1. サイト内ページがHTTP通信で表示されてしまう。*Chromeの場合(「保護されていない通信」の警告が出る。)
2. CSSが反映されていない。
- 投稿日:2020-12-16T20:21:38+09:00
Laravel Jetstreamとはなにか、どう実装されていて、どうすればカスタマイズできるか
Laravel Jetstreamの概要
Laravel JetstreamはLaravel 8から標準になった認証機能のパッケージで、7までのlaravel/uiや、5.8までの組み込みの
artisan make:auth
の代わりとなるパッケージ。Laravel本体とは独立して存在しているが、Laravelのインストール時にいっしょに設定することも可能。
: composerとartisanで composer require laravel/jetstream php artisan jetstream:install livewire: あるいは、Laravelのインストール時に laravel new <DIR> --jetJetstreamは単体のパッケージではなく、いくつかのパッケージを寄せ集めて、その上で統合している。各パッケージが担当する機能については以下のようになっている。
- Laravel Fortifyがセッションベースの認証を。
- Laravel SanctumがAPI認証を。
- LivewireかInertia.jsがユーザプロフィール・チーム管理周りのUIのビューを。
- Tailwind CSSがUIのデザインを。
さらにJetstream自体がルートやビュー、コントローラのスカフォールド等を担当する。
この記事ではビューにはLivewireを使う前提とし、Inertia.jsは取り扱わない。またTailwind CSSも取り扱わない。
その前提で、それぞれの機能をどう変更するべきかの簡易なリファレンスを提供する。
参考:
Jetstreamのインストール/インストール 1.0 Laravel Jetstreamビューを変更するには
機能自体は提供されたものをそのまま使うとしても、デザインをそのまま使うことは少ない。また機能を変更する場合にはデザインも同時に変更することがほとんどだろう。たとえばログインにメールアドレスとは別にログインIDを用意して使うようにするだけでも、ログインフォームの項目名の修正が必要だ。
ということでまずビューの変更方法から始める。
Laravel Jetstreamで提供されるビューは大まかに以下の3つに分けられる。
auth
以下にある、Fortifyによる基本的な認証機能のためのもの。vendor/laravel/jetstream/resources/views/components
にある一般的なUIのためのBladeコンポーネント。api
,profile
,teams
以下にあるLivewireコンポーネントのビュー。認証機能のための
auth
以下のビュー
resources/views/auth
以下に存在しているビューは、単純に直接編集すればよい。ログインフォームであれば、resources/views/auth/login.blade.php
だ。中身を見てみると、見慣れないものがある。
<x-guest-layout>
のような、x-
接頭辞のあるタグだ。これはLaravel 7(8ではなく)から導入されたコンポーネントの新しい書き方で、laravel/uiでは使われていなかった。
<x-guest-layout>
はapp/View/Components/GuestLayout.php
コンポーネント経由でresources/views/layouts/guest.blade.php
を読み込んでいるので、このファイルを変更すればよい。これらのビューはFortifyのためのものではあるが、Jetstreamによって提供されている。
参考:
コンポーネントの表示/Bladeテンプレート 8.x Laravel一般的なUIのための
x-jet-
接頭辞のBladeコンポーネント
auth
以下のビューを見てみると、接頭辞がx-jet-
となるコンポーネントが見つかる。これらはJetstreamが登録するBladeコンポーネントで、実体はvendor/laravel/jetstream/resources/views/components/
以下にある。ボタンやモーダルなど、ビューの再利用性を上げるためによく使うコンポーネントが登録されているが、これらも変更なしで使うことはほとんどないだろう。
変更する場合は
artisan vendor:publish --tag=jetstream-views
でresources/views
以下に出力した上で編集する。なおこれらのコンポーネントは
Laravel\Jetstream\JetstreamServiceProvider
(vendor/laravel/jetstream/src/JetstreamServiceProvider.php
,App\Providers\JetstreamServiceProvider
とは別物。Jetstreamのほとんどの機能はこのサービスプロバイダによってLaravelアプリケーション内で使用可能になっている。)で一つずつエイリアスを登録されている。そのため同じディレクトリにビューファイルを保存しても、x-jet-
接頭辞で読み込むことはできない。コンポーネントのエイリアスは、
Blade::getClassComponentAliases()
で一覧できる。エイリアスが登録されていればそれが最優先で、次にクラスベースコンポーネント、最後にresources/views/components
以下のコンポーネント(匿名コンポーネント)という優先順で読み込まれる。Livewireコンポーネント
auth
以下のビューでは使われていないが、ログイン後のレイアウトであるlayouts.app
やユーザプロフィールページであるprofile.show
などでは、@livewire
というBladeディレクティブが使われている。
@livewire
は@include
のような挙動をする。ディレクティブのある位置にほかのビューを読み込むような挙動だ。ただしBladeのビューではなく、Livewireコンポーネントを読み込む。Jetstreamが提供するビューを変更する中で、このディレクティブに対する理解も必要になってくる。とはいえLivewire自体を詳細に解説するのがこの記事の目的ではないため、説明は最小限に留める。ほか同様、どこを変更すればいいのかのみ説明する。
artisan jetstream:install livewire
した時点で存在するビュー内で、@livewire()
に渡されている文字列には、'api'
,'profile'
,'teams'
が接頭辞に来るものと、'navigation-dropdown'
がある。これらの名前と対応するビューは
resources/views
以下に、Bladeビューと同じ対応方法でビューが存在する。'api.api-token-manager'
であれば、resources/views/api/api-token-manager.blade.php
にある。これだけ見るとLivewireコンポーネントがBladeビューのような名前・ファイルの対応になっているように見えるが、じつは違う。
むしろBladeコンポーネントに近く、Livewireコンポーネントの名前は、ビューではなくクラスに対応している。Bladeコンポーネントはクラスがない場合はビューにフォールバックしているが、Livewireコンポーネントの場合はそれもない。
foo.bar
というコンポーネントは、App\Livewire\Foo\Bar
と関連付いている。このコンポーネントが最終的にresources/views/foo/bar.blade.php
というビューを読み込んでいたとしても、それはBarクラス内でそのビューを読み込んでいるからという以上のことはない。誤解しやすいところなので注意しよう。
artisan livewire:make Foo\\Bar
でLivewireコンポーネントのスカフォールドを作成してみるとわかりやすい。
resources/views/livewire/foo/bar.blade.php
というビューが作られるが、これはBar
クラスのrender()
メソッドで明示的に読み込むようにしているから使われるだけで、これを変えてしまえば、このビューは無視される。Jetstreamが提供するLivewireコンポーネントは
Laravel\Jetstream\JetstreamServiceProvider
でエイリアスが登録されている。Bladeコンポーネントと違い、エイリアスを一覧するメソッドは用意されていないが、
artisan tinker
からdump(app('livewire'))
などしてみると、$componentAliases
というプロパティの中で定義が確認できる。エイリアスが登録されていない場合は、名前からクラス名に変更され、読み込まれる。上記の
foo.bar
であれば、App\Livewire\Foo\Bar
となる。参考:
最近Livewireの公式ドキュメントが日本語訳された。Laravel公式ドキュメントの日本語訳者によるものだ。
2.x Livewire使用するビュー自体を変更する
ここまでは主にビューの内容を変更する方法について書いた。だが使用するビュー自体を変更したい場合もあるだろう。そのようなカスタマイズも用意されている。簡単に紹介する。
Fortifyのビュー
Fortifyのビューは、
Laravel\Jetstream\JetstreamServiceProvider
内で、Fortify::viewPrefix('auth.')
というメソッドで、auth
以下から読み込むように指示されている。変更する場合は
App\Providers\JetstreamServiceProvider::boot()
内などで、viewPrefix()
を実行すればよい。<?php namespace App\Providers; use Laravel\Fortify\Fortify; class JetstreamServiceProvider extends ServiceProvider { public function boot() { Fortify::viewPrefix('myauth.'); } }
Laravel\Fortify\Fortify::viewPrefix()
の実装を見てみるとわかるが、使用する個々のビューを変更することも可能だ。同クラス内にビューごとに専用のメソッドが用意されている。<?php namespace App\Providers; use Laravel\Fortify\Fortify; class JetstreamServiceProvider extends ServiceProvider { public function boot() { Fortify::loginView('login'); } }とすれば、
resources/views/login
が使用される。参考:
認証 1.0 Laravel Jetstream の"ビューレンダリングのカスタマイズ"以下。Bladeコンポーネント
Laravel\Jetstream\JetstreamServiceProvider
で、Blade::component()
によってエイリアスが登録されている。エイリアスは上書きできるので、これもApp\Providers\JetstreamServiceProvider::boot()
内などで登録すればよい。<?php namespace App\Providers; use Illuminate\Support\Facades\Blade; class JetstreamServiceProvider extends ServiceProvider { public function boot() { Blade::component('my-section-border', 'jet-section-border'); } }ビューは開発環境でもキャッシュされ、ビュー自体の変更がなければ再コンパイルされないため、Bladeコンポーネントを変更した際はビューのキャッシュをクリアしないと有効にならない場合がある。そのような場合は
artisan view:clear
する。Livewireコンポーネント
Bladeコンポーネント同様だが、エイリアスを登録するメソッドは
Livewire::component()
だ。<?php namespace App\Providers; use App\Http\Livewire\MyNavigationDropdown; use Livewire\Livewire; class JetstreamServiceProvider extends ServiceProvider { public function boot() { Livewire::component('navigation-dropdown', MyNavigationDropdown::class); } }ルートを変更するには
Jetstreamによって定義されるルートには2系統ある。
- Jetstreamによる、ユーザプロフィールやチーム関連、またLivewireやInertia.js用のAPI関連のルート。
- Fortifyによるログインページなどの認証周りのルート。
artisan route:list
でどのようなルートが登録されているか確認できる。login
,logout
,register
などは2のFortifyによるもので、profile.show
や、名前のないapi/user
パスのルートは、1のJetstreamによるものだ。それぞれ変更方法は微妙に違う。
Jetstreamによるルート
JetstreamによるルートはいつものJetstreamパッケージ内のJetstreamServiceProviderから定義されている。実体はLivewireを使用している場合は
vendor/laravel/jetstream/routes/livewire.php
となる。ここでは機能の有効無効によるオン・オフ以上のカスタマイズはできない。たとえばチーム機能の有効・無効によってチーム機能関連のルートは有効・無効になるが、それだけだ。
<?php # vendor/laravel/jetstream/routes/livewire.php use Illuminate\Support\Facades\Route; use Laravel\Jetstream\Http\Controllers\CurrentTeamController; use Laravel\Jetstream\Http\Controllers\Livewire\TeamController; use Laravel\Jetstream\Jetstream; Route::group(['middleware' => config('jetstream.middleware', ['web'])], function () { Route::group(['middleware' => ['auth', 'verified']], function () { // Teams... # チーム機能が有効な場合のみ、以下のルートが有効になっている if (Jetstream::hasTeamFeatures()) { Route::get('/teams/create', [TeamController::class, 'create'])->name('teams.create'); Route::get('/teams/{team}', [TeamController::class, 'show'])->name('teams.show'); Route::put('/current-team', [CurrentTeamController::class, 'update'])->name('current-team.update'); } }); });なのでたとえばプロフィール編集ページのパスを変更したいと思った場合は、手動でルートを設定しなければならない。
一から設定してもいいが、Jetstreamのルートファイルをpublishして、カスタマイズすることもできる。
php artisan vendor:publish --tag=jetstream-routesで
routes/jetstream.php
が配置されるので、それを自由にカスタマイズし、追加で、
- RouteServiceProvider等で上記ファイルを読み込む。
- 元のルートをJetstreamServiceProvider::register()あたりで
Jetstream::ignoreRoutes()
とすることによって無効化する。このJetstreamServiceProviderはApp\Providers
の方なので注意。<?php namespace App\Providers; use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider; use Illuminate\Support\Facades\Route; class RouteServiceProvider extends ServiceProvider { public function boot() { $this->routes(function () { Route::namespace($this->namespace) ->group(base_path('routes/jetstream.php')); }); } }<?php namespace App\Providers; use Illuminate\Support\ServiceProvider; use Laravel\Jetstream\Jetstream; class JetstreamServiceProvider extends ServiceProvider { public function register() { Jetstream::ignoreRoutes(); } }Fortifyによるルート
こちらはもう少しカスタマイズが考えられていて、たとえば接頭辞をつけるだけであれば
config/fortify.php
に設定を追加するだけで可能だ。path
というキーにたとえば'bar'
という値を設定すれば、ログインページのパスは'/bar/login'となる。<?php # config/fortify.php return [ 'path' => 'bar', ];だがそれ以上のカスタマイズ方法は用意されていない。
vendor:publish
で出力することもできず、完全に手動でルートを設定するしかない。元のルートを無効にする場合はJetstreamと同じように、
Fortify::ignoreRoutes()
を使う。<?php namespace App\Providers; use Illuminate\Support\ServiceProvider; use Laravel\Fortify\Fortify; class JetstreamServiceProvider extends ServiceProvider { public function register() { Fortify::ignoreRoutes(); } }その上で、
vendor/laravel/fortify/routes/routes.php
を参考に必要なルートを設定すればよい。機能を変更するには
Jetstreamが提供する機能も、Fortifyが提供する機能も、コントローラアクション単位での変更はできない。ユーザ登録に
Laravel\Fortify\Http\Controllers\RegisteredUserController::store
を使わず自前のメソッドを使うような変更をしたいならば、ルートの変更の項で見たようにルート自体を差し替えるしかない。とはいえコントローラアクションは抽象的に実装されており、変更しないでもかなりの部分までカスタマイズができるようになっている。
上記の例でいえば、じっさいのユーザ作成の実装も、作成後のレスポンスも簡単にカスタマイズ可能だ。これについてはDIによる機能の変更の項で説明する。
features設定による機能のオン・オフ
JetstreamでもFortifyでも、カスタマイズされがちな機能については簡単にオン・オフの切り替えができるようになっている。
Jetstreamではプロフィール写真のアップロード、チーム機能などが、Fortifyではユーザ登録やパスワードリセットのような、以前は
Auth::routes()
への引数で切り替えていた機能や、プロフィールの更新、2要素認証などが、簡単に切り替えられる。それぞれ設定変数
jetstream.features
,fortify.features
を使用する。<?php # config/jetstream.php use Laravel\Jetstream\Features; return [ 'features' => [ # プロフィールの写真のアップロードを有効にする Features::profilePhotos(), ], ];ただし一部の機能は、
features
の設定だけでは不完全だ。メールアドレスの確認には、7まで同様プロバイダクラスにMustVerifyEmailインターフェイスを実装する必要があるし、チーム機能に関しては、laravel new
やartisan jetstream:install
の時点で有効にしておかないと、features
で設定しただけでは動作しないどころかエラーになったりする。参考:
認証 1.0 Laravel Jetstreamの"メール確認"以下。
プロファイル管理 1.0 Laravel Jetstreamの"プロフィール写真の有効化"以下。
API 1.0 Laravel Jetstreamの"APIサポートの有効化"以下。DIによる機能の変更
Fortifyが提供するコントローラでは、アクションを直接差し替えることこそできないが、重要な部分はDIによって差し替えられるようになっている。
ログインフォームの表示やログイン・ログアウト処理のための
Laravel\Fortify\Http\Controllers\AuthenticatedSessionController
を見てみると、ログインフォームを表示するlogin()
メソッドはapp()
で解決したサービスを返している。これらは単純に別のクラスをバインドすることで差し替えることもできるが、専用のメソッドも用意されている。
Fortify::loginView()
は名前通りログインフォームのビューを差し替えることができるし、Fortify::createUsersUsing()
はユーザ作成オブジェクトを差し替えられる。前者は差し替えるクラス等を直接指定するのではなく、ビュー名を渡すこともできるのでバインドするよりも簡易だ。これらをApp\Providers\FortifyServiceProvider
などで実行する。<?php namespace App\Providers; use Illuminate\Support\ServiceProvider; use Laravel\Fortify\Fortify; class FortifyServiceProvider extends ServiceProvider { public function boot() { Fortify::loginView('myauth.login'); Fortify::createUsersUsing(MyCreateNewUser::class); } }Jetstreamが提供するコントローラでも、主にチーム関係の機能を名前がuseで始まるメソッドや、Usingで終わるメソッドで差し替えられる。
参考:
認証 1.0 Laravel Jetstreamの"ビューレンダリングのカスタマイズ"以下、また"User認証のカスタマイズ"以下。
セキュリティ 1.0 Laravel Jetstreamの"パスワード確認方法のカスタマイズ"以下。ログイン処理のカスタマイズ
現代のログイン処理は複雑だ。IDとパスワードによる認証だけではなく、ログイン試行回数をチェックしたり、追加の要素で認証したいかもしれない。そのようなニーズにも簡単に応えられるよう、Fortifyのログイン処理は非常にカスタマイズしやすく作られている。
具体的にはミドルウェアのような動作をする「ログインパイプライン」によって、指定した機能を連鎖的に実行する形になっている。
設定
fortify.pipelines.login
の値として、handle()
メソッドを持ったクラスを指定すれば、それらのクラスが順番に実行される。<?php # config/fortify.php use Laravel\Fortify\Actions\AttemptToAuthenticate; use Laravel\Fortify\Actions\EnsureLoginIsNotThrottled; use Laravel\Fortify\Actions\PrepareAuthenticatedSession; use Laravel\Fortify\Actions\RedirectIfTwoFactorAuthenticatable; return [ 'pipelines' => [ 'logins' => [ EnsureLoginIsNotThrottled::class, RedirectIfTwoFactorAuthenticatable::class, AttemptToAuthenticate::class, PrepareAuthenticatedSession::class, ], ], ];デフォルトでは2要素認証、通常のパスワード認証、認証時のセッション再生成が実行されるようになっている。このデフォルト値自体は
fortify.pipelines.login
には設定されていないので注意が必要だ。ドキュメントにあるように、
Fortify::authenticateUsing()
を使って設定することも可能だ。参考:
認証 1.0 Laravel Jetstreamの"認証プロセスのカスタマイズ"以下。おまけ:
auth:sanctum
について最後に、Jetstreamを読み解いていく中できっと気になる
auth:sanctum
について簡単に説明する。
artisan jetstream:install
すると、routes/web.php
にauth:sanctum
ミドルウェア(正確にはsanctum
ガードを使うよう指示されたauth
ミドルウェア)を使うルートが登録される。このミドルウェアはなんだろうか。Sanctumは最初に書いたようにAPI認証用の機能だが、ここで登録されているルートはダッシュボードのもので、普通にセッションベースで認証できている。
結論を言うと、
sanctum
ガードは、sanctum.guard
設定で指定したガードか未指定であればweb
ガードが有効な場合はそちらを見るようになっている。そのためログインしない状態で、Bearerトークンで認証するような場合以外は'auth'
とパラメータなしで指定する場合と変わらない。つまりダッシュボードでsanctum
ガードを使う必要はおそらくない。
sanctum
ガードは\Laravel\Sanctum\Guard
で実装されている。最後に
Jetstreamによって導入される機能の変更方法を見てきた。ほとんどの機能は最終的には2つのサービスプロバイダ、
JetstreamServiceProvider
とFortifyServiceProvider
(それぞれvendor
内の方)によって提供されている。そこだけ覚えておけば、自力で結論に辿り着くのも容易だ。この記事のライセンス
この文書はCC BY(クリエイティブ・コモンズ表示4.0国際ライセンス)で公開する。この文書内のサンプルコードはMITライセンスで公開する。
- 投稿日:2020-12-16T17:51:56+09:00
Laravelのパスワードリセットをユーザー名など、メールアドレス以外で行うようにする
Laravelの標準のAuthではパスワードリセットはメールアドレスで行いますが、これをユーザー名などに変更したいことがあると思います。
背景
私が現在開発しているシステムでは元々はDB上ではメールアドレスがユニークで、ユーザーの識別(ログインやパスワードリセット)はLaravelの標準の機能でカバーできていました。
しかし、開発が一通り終わる頃、「同じユーザーが一つのメールアドレスで複数のアカウントを扱えるようにしたい」という要求が出てきたため
、仕様を変更する必要に迫られました。
幸い、システム内ではログインした後はユーザー登録時に自動で付加されるDB内の連番のIDを使用してユーザーの識別をしていたため、問題となるのはログインとパスワードリセットだけでした。
今回は少々手こずったパスワードリセットの機能について書き留めておきたいと思います。
「パスワードリセットをメール以外でする」という題名にしましたが、実際には私のシステムではユーザー名が無い(上記の連番のIDはありますがユーザーは認識しない)ので、メールアドレスを入力してもらい、複数のアドレスがあった場合はどちらをリセットするか選択してもらう画面を挟み、最終的にはIDをPOSTすることでリセットの機能を実現しています。
なので、メールアドレスの代わりにusernameなどを入力してもらうだけで良い場合は、ソース内のidを適宜usernameなどに置き換えればOKです。なお、私が使用しているのはLaravel8ですが、Laravel5系でもいけると思います。
パスワード再設定画面のビューの変更
メールアドレスを入力してもらった後に複数のメールアドレスがあるかどうかをチェックし、複数あった場合はアカウントを選択する画面を表示したいので、actionを"{{ route('password.email') }}"から以下のように変更します。
メールアドレスの代わりにusernameを使いたいだけの場合はformはそのままにしてemailのフィールドをusernameに変更すれば良いでしょう。/resources/views/auth/passwords/email.blade.php<form method="POST" action="/password/multiple_email_check">ルーティング追加
ルーティングにmultiple_email_checkを追加します。最初のgetの行はパスワードリセットのsubmitをした後にmultiple_email_checkに戻ってくるので、最初のパスワードリセットのページに戻るようにしています。
/routes/web.phpRoute::get('/password/multiple_email_check', 'Auth\ForgotPasswordController@showLinkRequestForm'); // Redirected after sending email Route::post('/password/multiple_email_check', 'Auth\ForgotPasswordController@multiple_email_check');アカウント選択機能を追加
Usersテーブル内に同じメールアドレスがあるかチェックし、一つだけ見つかれば「このアドレスでリセットしますがよろしいですか?」というメッセージを表示し、複数の場合はアカウントを選択する画面を表示します。
/app/Http/Controllers/Auth/ForgotPasswordController.phppublic function multiple_email_check(Request $request) { $users = User::where('email', $request->email) ->where('status', 1) ->get(); if($users->count() <= 1) { // アカウントが一つだけの場合 $user = $users->first(); return view('auth/passwords/email_confirm', compact('user')); } else { // アカウントが複数の場合 foreach($users as $user) { $account = User:: ->where('users.id', $user->id) ->first()->toArray(); $accounts[] = $account; } return view('auth/passwords/select_account', compact('accounts')); } }確認画面(アカウントが一つの場合)
IDをPOSTするようにすれば良いのであとは適当に。
/resources/views/auth/passwords/email_confirm.blade.php<form method="POST" action="{{ route('password.email') }}"> @csrf {!! Form::hidden('id', $user->id) !!} <div class="col-md-8 offset-md-2 mb-3"> {{ $user->email }}にパスワード再設定用のリンクを送信します。よろしいですか? </div> <div class="form-group row mb-0"> <div class="col-md-12 text-center"> <button type="submit" class="btn btn-primary"> 送信 </button> </div> </div> </form>確認画面(アカウントが複数の場合)
複数のアカウントがあった場合、アカウントを選択してもらいます。かなり適当ですが、以下のような感じでこちらも選択されたIDがPOSTされるようにすればOKです。
/resources/views/auth/passwords/select_account.blade.php@foreach ($accounts as $account) <form method="POST" action="{{ route('password.email') }}"> @csrf <button class="btn btn-primary form_submit">このアカウントをリセット</button>{{ $account['name'] }} <input type="hidden" name="id" value="{{ $account['id'] }}"> </form> @endforeachメソッドのオーバーライド
メールアドレスではなく、IDで動作するようにLaravel標準のメソッドをController内でオーバーライドします。usernameなどで行う場合はidの部分を適宜置き換えてください。
/app/Http/Controllers/Auth/ForgotPasswordController.phppublic function sendResetLinkEmail(Request $request) { $this->validate($request, ['id' => 'required'], ['id.required' => 'Please enter your id.']); $response = $this->broker()->sendResetLink( $request->only('id') ); if ($response === Password::RESET_LINK_SENT) { return back()->with('status', trans($response)); } return back()->withErrors( ['email' => trans($response)] ); }/app/Http/Controllers/Auth/ResetPasswordController.phppublic function showResetForm(Request $request, $token = null) { return view('auth.passwords.reset')->with( ['token' => $token, 'id' => $request->id] ); } protected function credentials(Request $request) { return $request->only( 'id', 'password', 'password_confirmation', 'token' ); } protected function sendResetFailedResponse(Request $request, $response) { return redirect()->back() ->withInput($request->only('id')) ->withErrors(['id' => trans($response)]); } protected function rules() { return [ 'token' => 'required', 'id' => 'required', 'password' => 'required|confirmed', ]; }メール内容を変更
通常、パスワードリセットを要求した後に送信されるメール内のボタンにはメールアドレスがパラメーターとして付加されるようになっていますが、そのままではメールアドレスが複数あった場合にアカウントの識別ができないので、これをIDにしてパスワードリセット画面を表示した時にアカウントを識別できるようにします。
以下の例ではついでにメール文を日本語化しています。/app/Providers/AppServiceProvider.phppublic function boot(UrlGenerator $url) { ResetPassword::toMailUsing(function ($notifiable, $token) { return (new MailMessage) ->subject('パスワード再設定') ->greeting('パスワード再設定の要求を受け付けました。') ->line('下のボタンをクリックしてパスワードを再設定してください。') ->action(Lang::get('Reset Password'), url(config('app.url').route('password.reset', ['token' => $token, 'id' => $notifiable->id], false))) ->line('もし心当たりがない場合は、本メッセージは破棄してください。'); }); }ここまでで、パスワードリセットの画面からメールアドレスを入力し、メールが送信されるまでが動作するようになったと思います。
実際にやってみてメールが届くか確認してみてください。あとはメール内のボタンが押されたらパスワードリセット画面を表示し、リセットするだけです。
パスワードリセット画面
通常は以下のようにメールアドレスのフィールドがありますが、これを表示しても仕方がないのでその項目を消去すると同時に、ID(またはusername)をhiddenに設定してPOSTされるようにします。
/resource/views/auth/passwords/reset.blade.php//追加 <input id="id" type="hidden" name="id" value="{{ $id }}"> // 以下の行は削除 <div class="form-group row"> <label for="email" class="col-md-4 col-form-label text-md-right">{{ __('E-Mail Address') }}</label> <div class="col-md-6"> <input id="email" type="email" class="form-control @error('email') is-invalid @enderror" name="email" value="{{ $email ?? old('email') }}" required autocomplete="email" autofocus> @error('email') <span class="invalid-feedback" role="alert"> <strong>{{ $message }}</strong> </span> @enderror </div> </div>これで完成です。
参考にしたサイト
https://stackoverflow.com/questions/49590205/password-resetting-in-laravel-when-email-address-is-not-unique
https://krishan.blog/articles/2019-12-12/modify-password-reset-notification
- 投稿日:2020-12-16T16:55:29+09:00
WindowsPCにComposer インストールしたメモ
Windowsに「MAMP4.2」と「Composer2」をインストールする
みなさんこんにちは
ジーズアカデミー 主席講師 山崎ですm(_ _)m
今回はLaravelをMAMPで動作させる際に受講生が困っていた内容の一つを記事にしました。
※今後同じ事をやる人のためにメモを残します。(重要)Windwos/MAMP4.2で検証した内容です。
対象(インストールした環境)
・ Windows10
・ MAMP4.2(php7.4.1)
・ Composer(ver.2)Windowsに「Composer」をインストール
1.「 Composer 」ダウンロード
https://getcomposer.org/Composer-Setup.exe
2.「 Composer 」インストール
3.「 Browse...」をクリックしてMAMPのPHPバージョンフォルダ内にある「php.exe」を選択
・チェックボックスルも入れておく。
4.「Create a php.ini file」にチェックを入れる!
5.何も入力しない
5.「Install」ボタンをクリック
6.「Next>」ボタンで次へ
7.「Finish」ボタンをクリック
<< 「composer install」「composer update」で赤い文字のエラーが出た場合 >>
1.php.iniの修正
上記「3」で選択したphp.exeと同フォルダ内に「php.ini」ファイルができてるのでphp.ini;extension=fileinfo ↓ ↓ ↓ extension=fileinfo上記、";" コメント外し保存
2.MAMP再起動
php.iniの修正を反映させるため、MAMPを再起動します。
以下、追加メモ
*LaravelでリポジトリをGitcloneしてきた場合(以下コマンド)
composer install
cp .env.example .env
(laravelフォルダ内で実行)php artisan key:generate
(laravelフォルダ内で実行)上記3つのコマンドを打つ理由は
- .envが入っていない
- KEYが発行されていない
- vendorフォルダが入っていない
*Laravelの動作を確認する場合
1.「 MAMP 」→ 「 Preferences...」を選択
2.「 Web Server 」タブを選択 → 「 Select... 」ボタンを選択
3.フォルダ選択で「 Laravel 」フォルダ内の「 public 」を選択
「 http://localhost/ 」と「 public 」はイコールとなります。
4.「 http://localhost/ 」でブラウザから確認可能になります。
※MAMPが再起動してなければ、再起動してから確認しましょう!!
以上
- 投稿日:2020-12-16T16:08:50+09:00
Laravel 「The page has expired due to inactivity.」の非常に稀な解決法
何が起きているか
最初に言っておきます。
CSRF はフォームにちゃんと入っていて設定も出来ています。
よくある入れ忘れ、設定漏れではありません。
結構、悩みました。ログインをすると、必ず1回は以下が表示されます。
「The page has expired due to inactivity.
Please refresh and try again.」もう一度ログインを実行すると、目当てのページへ遷移します。
結論
このケースは非常に稀な気がしますす…。
検証環境は Laravel 5.6 です。・これが間違い ↓
<form method="POST"・こうしたらちゃんとログイン出来ました…。
<form method="post"自分も、ここだけ大文字にしてしまった理由が分からないのですが…
(他の画面では要素はすべて小文字で書いているのに…)
そこですか、と…。こんなミスをする人も、まずいないのだろうなと思いますが…。
同じように CSRF の設定かと思って延々、悩まれている方が、
広い世界にはいるかも知れないと思い、ここに記録を残しておきます…。
おねがい
詳しいことが分かる方がいらっしゃいましたら、解説をお願い致します…。
- 投稿日:2020-12-16T14:46:07+09:00
Laravel + Vue.js の知識をアウトプットしていく
次の記事でLaravelをアウトプットしていますが、今回はLaravel + Vueでまとめていきます。
Laraveの知識をアウトプットして、資産化してます準備
$ composer create-project laravel/laravel sampleproject --prefer-dist "7.*"プロジェクトの作成を実施
もろもろの初期設定はこちらの記事を参考にしてください
Laraveの知識をアウトプットして、資産化してます認証機能をインストール$ composer require laravel/ui:^2.4 --devvueのインストール$ php artisan ui vue --auth $ npm install bootstrap-vue bootstrap $ npm install && npm run devvue-routerのインストール
vue-routerのインストールnpm install --save vue-router下記を追加する。
resources/app.jsimport VueRouter from 'vue-router'; 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({ // (1) mountする要素。<div id="app">なので、#app el: "#app", router });app.blade.phpに
<router-view>
を追加しますapp.blade.phpにrouter-viewを追加<body> <div id="app"> <router-view></router-view> </div> <!-- Scripts --> <script src="{{ mix('js/app.js') }}"></script> </body>urlをつける場合は公式ドキュメントのこちら
Vuexのインストール
stateで情報をやりとりするために利用する。
ドキュメントはこちら$ npm install --save-dev vuexnpm run devvue-routeをimportする
Laravel Mixで読み込み
mixに変更<!-- Styles --> <link href="{{ mix('css/app.css') }}" rel="stylesheet"> <!-- Script --> <script src="{{ mix('/js/app.js') }}" defer></script>デバッグのインストール
composer require itsgoingd/clockworkddのように変数をデバッグ可能
clock(User::all());流れ
- コンポーネントの作成
- app.jsにimport
- HTMLにコンポーメントを追加して表示。
- vue-routerでURLと画面を切り替え
コンポーネント(Component)の作成
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>app.jsにimport
app.js// import import HeaderComponent from "./components/HeaderComponent"; // componentのタグ名を決める Vue.component("header-component", HeaderComponent);HTMLにコンポーメントを追加して表示。
example-componentの読み込み<div id="app"> <header-component></header-component> <router-view></router-view> </div>vue-routerでURLと画面を切り替え
直接componentを挿入する場合は、
Vue.component
で挿入するが、ページ遷移ごとに挿入させるcomponentを切り替えたい場合は、Vue-routerで切り替えていきます。Vue.use(VueRouter); const router = new VueRouter({ mode: "history", routes: [ { path: "/tasks", name: "task.list", component: TaskListComponent }, { path: "/tasks/:taskId", name: "task.show", component: TaskShowComponent, props: true }, { path: "/tasks/create", name: "task.show", component: TaskShowComponent, props: true } ] }); const app = new Vue({ el: "#app", router });上記のVue-routerの記述で、pathごとのcomponentを定義している。
このcomponentは<router-view>
に挿入される。それでpathごとに異なるcomponentが挿入されてページ遷移される。<div id="app"> <router-view></router-view> </div>CRUD機能の実装
ログイン機能の実装
Laravel 7.x Laravel Sanctum
参考になるQiita記事sanctumインストール
コマンドcomposer require laravel/sanctum構成ファイルの公開を実施します
コマンドphp artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"マイグレーションの実行
コマンドphp artisan migrate
CreatePersonalAccessTokensTable
が実行され、personal_access_tokens
テーブルが追加されます。
また、config/sanctum.php
も追加されますKernel.phpにsanctumのミドルウェアを追加
SPAの認証として利用できるように、Kernel.phpファイルのapiミドルウェアにSanctumのミドルウェアを追記します。これでAPIに対するリクエストでセッション・クッキーによる自動認証が可能となります。
Http/Kernel.php<?php namespace App\Http; use Illuminate\Foundation\Http\Kernel as HttpKernel; // ↓追加 use Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful; class Kernel extends HttpKernel { /** * The application's global HTTP middleware stack. * * These middleware are run during every request to your application. * * @var array */ protected $middleware = [ \App\Http\Middleware\TrustProxies::class, \App\Http\Middleware\CheckForMaintenanceMode::class, \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, \App\Http\Middleware\TrimStrings::class, \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, ]; /** * The application's route middleware groups. * * @var array */ protected $middlewareGroups = [ 'web' => [ \App\Http\Middleware\EncryptCookies::class, \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, \Illuminate\Session\Middleware\StartSession::class, // \Illuminate\Session\Middleware\AuthenticateSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class, \App\Http\Middleware\VerifyCsrfToken::class, \Illuminate\Routing\Middleware\SubstituteBindings::class, ], 'api' => [ EnsureFrontendRequestsAreStateful::class, //追加 'throttle:60,1', 'bindings', ], ]; ~略~ }コントローラーの作成
ログイン用のコントローラーを作成php artisan make:controller LoginController自前のログイン処理を作成するために、Auth::attemptを記述していく。
LoginController.php<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Validation\ValidationException; class LoginController extends Controller { public function login(Request $request) { //validation $credentials = $request->validate([ 'email' => 'required|email', 'password' => 'required' ]); //認証処理 if (Auth::attempt($credentials)) { //認証に成功した場合 return response()->json(['message' => 'Login successful'], 200); } //エラーメッセージの作成 throw ValidationException::withMessages([ 'email' => ['The provided credentials are incorrect'], ]); } //logout処理を追加 public function logout() { //ログアウトの実行 Auth::logout(); //ログアウト成功したレスポンスをreturnする。 return response()->json(['message' => 'Logged out'], 200); } }自前のエラー文を作成するために、
use Illuminate\Validation\ValidationException;
$validator->errors()->add($key, $message)
することで、自由にメッセージを追加することができます。
こちらの記事も参考になります
GitHubはこちらthrowについて
ValidationExceptionの記述$validator = Validator::make([], []); $validator->errors()->add('title', 'タイトルのエラーです。'); throw new ValidationException($validator); // もしくは throw ValidationException::withMessages(['title' => 'タイトルのエラーです。']);例外処理の基本<?php try { // 例外が発生する可能性のあるコード } catch (Exception $e) { // 例外が発生した場合に行う処理 } ?>ルーティングの記述
APIでの認証を実施するため、api.phpでルーティングを記述する。
api.php<?php use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; Route::middleware('auth:sanctum')->get('/user', function (Request $request) { return $request->user(); }); Route::post('/login', 'LoginController@login'); Route::post('/logout', 'LoginController@logout');その他
リダイレクト
this.$router.push({name: 'task.list'});
でnameを指定してリダイレクトすることが可能
- 投稿日:2020-12-16T13:03:05+09:00
LaravelのFacadeの仕組みを完全に理解した
Symfonyのほうから来ましたこんにちは。
長年「宗教上の理由でLaravelには入門しません」と言っていた私ですが、 副業 でLaravelで作られたアプリケーションに関わることになり、とうとうLaravelを触り始めました。悪名高きFacade
私がLaravelに入門したことがなくても名前を知っていたのが
Facade
です。
カンファレンスや勉強会で知り合った範囲のLaravelユーザー達が口を揃えて「Facadeはよくない」と言っていたので、「なんかしらんけど良くないもの」として名前だけ認識していました。Laravel製アプリケーションに関わることになって、ソースコードをgit cloneして最初に調べたのが
app/
配下のuse文です。
use Illuminate\Support\Facades
を検索すると、あー…あるある…。Facadeの何が良くないか?
これはLaravelの上級者達によって既にあちこちで語られているので、「Facade よくない」とかで検索してみてください。
私はドメインのコード内に記載することでLaravelへの暗黙的な依存が発生するのが一番良くないと思います。
Laravel側のアップデートや仕様変更によってドメインのコードが左右されると、アプリケーションのメンテナンスしやすさが下がります。はるか昔のバージョンからずっとフレームワークをバージョンアップできない事になりかねません。また、Facadeが使われているクラスのユニットテストをしたいときに、LaravelのDIコンテナの初期化が必須になるのもマイナスです。
テストの実行にかかる所要時間が伸びると、テストが億劫になり、テストを避ける(ちょっとした仕様変更の際にテストを省略しようとする、手元でテストしない、「急いでるのでCI落ちてますがマージお願いします!」)悪い習慣がついてしまいます。
継続的にテストしていくために、テストは「速く」実行できることがとても重要です。IDEで補完されない(これはプラグインとかで解決できるのかも?)というのも地味に面倒な点です。
Facadeは何をしているのか?(仕組み)
「良くないもの」と再認識したFacadeを取り除いていくためには、まずFacadeが何をしているのか理解した上で、Facadeを使わない書き方を考えていく必要があります。
オープンソースを使うメリットはソースコードが見れることですから、Facadeのソースコードを見てみましょう。
https://github.com/laravel/framework/blob/8.x/src/Illuminate/Support/Facades/Facade.phpソースコードを見ると、Facadeを使って
DB::hoge()
のようなことをした場合、まず呼び出されるのはFacade::__callStatic()
です。abstract class Facade { public static function __callStatic($method, $args) { $instance = static::getFacadeRoot(); if (! $instance) { throw new RuntimeException('A facade root has not been set.'); } return $instance->$method(...$args); } }
getFacadeRoot()
によりFacadeの対象のインスタンスを取り出し、そのインスタンスに対してstaticコールされたメソッドを実行していることがわかります。ではFacadeの対象インスタンスをどのように取り出しているのかと言うと…
abstract class Facade { public static function getFacadeRoot() { return static::resolveFacadeInstance(static::getFacadeAccessor()); } protected static function resolveFacadeInstance($name) { if (is_object($name)) { return $name; } if (isset(static::$resolvedInstance[$name])) { return static::$resolvedInstance[$name]; } if (static::$app) { return static::$resolvedInstance[$name] = static::$app[$name]; } } }
getFacadeAccessor()
により対象のサービス名を特定した上で、static::$app
(DIコンテナ)からその名前のサービスを取得しています。
つまり、class Hoge extends Facade { protected static function getFacadeAccessor() { return 'hoge'; } }のようなFacadeがあって
Hoge::foo($bar);のように呼び出されているのであれば、DIコンテナ上で
hoge
という名前で定義されているサービスのクラス名を調べて、autowireしてやれば同等のことができるというわけです。- Hoge::foo($bar) + $this->hoge->foo($bar);IDEの補完もきくし、テスト時にピュアなPHPUnitのTestCaseクラスでテストが書けます。クリーン
![]()
まとめ
アプリケーション内のソースコード内を概観して、Facadeが使われているのは AOP がマッチしそうなケースだなーと思いました。
DBとかRedisとか使っちゃってるのはだめだぞ☆って感じですけども。ごく一般的なLaravel製アプリケーションを、メンテ性高く長期運用できるシステムへ改善する過程を一緒に体験してみたい(一緒に苦労してみたいとも言う…)方がいたら https://www.wantedly.com/projects/475929 からご連絡くださいね
![]()
もしこれを読んで、最初からFacadeがなくてすべてがDIされている世界(Symfony)に興味を持った方は Symfony Advent Calendar 2020 も覗いてみてください
- 投稿日:2020-12-16T11:46:53+09:00
はじめまして
自己紹介
はじめまして。情報系大学3年のTaYです。普段はPHPやJSを触っています。
最近ではゼミでPythonも触っています。(ほんとに触っているだけ)自分のやってきたことの備忘録として始めてみました。
今はLaravelを独学中で簡単な掲示板を作っています。
完成したら載せようかな〜と考えています!
特に書くこともないので今回はこの辺で
- 投稿日:2020-12-16T08:59:29+09:00
【Laravel】validateメソッドを用いたバリデーション方法
validateメソッドとは
Laravelではフォームバリデーション機能が提供されており、validateメソッドはその内の一つです。
実体はIlluminate\Http\Requestクラスのメソッドです。ちなみに、Illuminateは
vender/laravel/framework/src/Illuminate
にあります。validateメソッドによるバリデーションの実装方法
viewの作成
viewを作っていきます。
resources/views/test.blade.php<form action="submit" method="post"> <div> <label for="title">タイトル:</label> <input type="text" id="title" name="title"> </div> <div> <label for="body">本文:</label> <input type="text" id="body" name="body"> </div> <div> <input type="submit" value="送信"> </div> @if ($errors->any()) <div> <ul style="list-style: none;"> @foreach ($errors->all() as $error) <li style="color: red; font-size: 14px;">{{ $error }}</li> @endforeach </ul> </div> @endif {{ csrf_field() }} </form>
$errors
はLaravelの定義済み変数です。
バリデーションエラーが発生すると、自動的に$errors
にエラーメッセージが格納されます。
@foreach
で取り出して使いましょう。ちなみに、csrf_field()を書き忘れると、419エラーになってしまうのでちゃんと書きましょう。
参考:【Laravel5】たまに出てくる「the page has expired due to inactivity. please refresh and try again」を表示させない
https://qiita.com/sola-msr/items/8a0ea0abe510245760ac遷移先ページは以下の感じです。
resouces/views/submit.blade.php<p>submitted!!</p>Controllerの作成
TestControllerを作成します。
php artisan make:controller TestControllerメソッドを書いていきます。
app/Http/Controller/TestController.php<?php namespace App\Http\Controllers; use Illuminate\Http\Request; class TestController extends Controller { public function index() { return view('test'); } public function submit() { // バリデーションルールの定義 $rules = [ // titleとbodyに入力必須 'title' => 'required|min:3|max:5', 'body' => 'required|min:1|max:10', ]; // バリデーション実行 $request->validate($rules); return view('submit'); } }
$rule
に連想配列でルールを定義し、$request->validate($rules)
でバリデーションを実行しています。バリデーションルールは、Laravelで定義済みのルールを適用できます。
定義済みルールの一覧は、以下の記事が参考になるのでどうぞ。参考:Laravelのバリデーションで指定できる内容をざっくりまとめ直しました。
https://qiita.com/fagai/items/9904409d3703ef6f79a2バリデーションの実行は、
$request->validate($rules);
の一行で完結です。
OKの場合は処理を続行、NGの場合はリダイレクトの処理を自動でやってくれます。裏の処理をほとんど意識せずに実装できてしまいますね。
ルーティングの作成
TestControllerのルートを通します。
Route::get('/', 'TestController@index'); Route::post('submit', 'TestController@submit');表示の確認
- 投稿日:2020-12-16T06:40:33+09:00
Laravel 6.x 非同期通信(Ajax) 【axios】 【Vue.js】 【Laravel-mix】 で簡易的なECサイトのカート(買い物かご)を作成
制作環境
Windows 10
Laravel : 6.18.35
Laravel/ui : 1.0
Laravel-mix : 5.0.1
Bootstrap : 4.0.0
Vue.js : 2.5.17
XAMPP
PHP : 7.4.3
Visual Studio Codeはじめに
この記事はプログラミングをはじめたばかりの素人が、できたことをメモするのに利用しています。
内容には誤りがあるかもしれません。買い物かごを作成する際にうまくいかなかったので、改善策を見つけるのに作成した小規模プログラムです。
とりあえず完成した物を先に紹介し、後ほどうまくいかず試行錯誤した点を記載したいと思います。機能実装が目的のため、デザイン(見た目)にはあまりこだわっていません。
また、記述も必要最低限にしています。
一部デザインの整形にBootstrapを使用しております。
Bootstrap、Vue.jsも含め、Laravel-mixを使用して記述してます。作成要件
- ユーザーの切り替えができる(ログイン機能を付けないので、手動でユーザーを切り替える仕様にします)。
- 商品の情報を取得し、一覧表示する。
- 商品の購入数を指定し、非同期通信でカートに追加できる。
- カートに入っている商品点数を、非同期通信で表示する。
- カートに追加する際はバリデーションを行い、エラーがあればメッセージを表示させる。
- カートへの登録が完了したら、購入数は空に戻す。
- ユーザーを切り替えるとカートに入っている商品点数も、非同期通信で変更される。
イメージ
それでは、作成していきましょう。
マイグレーションファイルの作成
データベースへテーブルを作成したいと思います。
今回作成するのは、商品を管理するproducts_table
とカート用のcarts_table
です。プロジェクトのディレクトリでターミナルを起動し、以下を実行してください。
php artisan make:migration create_products_table続けて
php artisan make:migration create_carts_table作成が完了したら、
database>migrations
内にある作成されたファイルを開き、以下のように記述します。create_products_tablepublic function up() { Schema::create('products', function (Blueprint $table) { $table->bigIncrements('id'); $table->string('name')->comment('商品名'); $table->string('price')->comment('価格'); $table->timestamps(); }); }create_carts_tablepublic function up() { Schema::create('carts', function (Blueprint $table) { $table->bigIncrements('id'); $table->unsignedTinyInteger('user_id')->comment('ユーザーID'); $table->unsignedBigInteger('product_id')->comment('商品ID'); $table->foreign('product_id')->references('id')->on('products')->onDelete('cascade'); $table->string('quantity')->comment('購入数'); $table->timestamps(); }); }モデルの作成
次にモデルを作成していきます。
モデルはModels
フォルダの中に作成していきます。ターミナルを起動し、以下を実行してください。
php artisan make:model Models/Product続けて
php artisan make:model Models/Cart作成されたファイルは
app>Models
の中にあります。
各ファイルを以下のように記述してください。Product.php// リレーションのため追記 use Illuminate\Database\Eloquent\Relations\BelongsTo; class Product extends Model { // 変更を許可するカラムを指定します protected $fillable = [ 'name', 'price' ]; // リレーションのためのメソッドです // これでカートの情報がProduct側から取得できます public function cart() { // 紐付けるモデルを指定し返します return $this->belongsTo('App\Models\Cart'); } }Cart.php// リレーションのため追記 use Illuminate\Database\Eloquent\Relations\HasMany; class Cart extends Model { protected $fillable = [ 'user_id', 'product_id', 'quantity' ]; public function product() { return $this->hasMany('App\Models\Product'); } }ビューの作成
resources>views
内に新しくproduct.blade.php
を作成し、以下のように記述します。
ちなみに、Vueテンプレートは一切使用していません。product.blade.php<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <meta name="csrf-token" content="{{ csrf_token() }}"> <link rel="stylesheet" href="{{ mix('css/app.css') }}"> <title>商品一覧</title> <style> [v-cloak] { display: none; } </style> </head> <body> <div id="app" v-cloak> <div class="container"> <div class="row mt-2"> <label>ユーザー選択 <select name="user" id="user" v-model="user"> <option value="1">1</option> <option value="2">2</option> <option value="3">3</option> </select></label> <p class="cart_text ml-auto">◆カートの中身(@{{ items }})</p> </div> <h1>商品一覧</h1> <p class="err_msg text-danger">@{{ errors.quantity }}</p> <div class="row justify-content-center"> @foreach ($productTable as $product) <div class="product p-3 border border-success col-3"> <h3>商品名</h3> <p>{{ $product->name }}</p> <h3>価格</h3> <p>{{ $product->price }}円</p> <form id="form{{ $product->id }}"> @csrf <label>購入数: <input type="text" name="quantity" size="2">個</label> <br> <input type="hidden" name="product_id" value="{{ $product->id }}"> <input type="hidden" name="user_id" v-model="user"> <button type="button" @click="addCart({{ $product->id }})">カートに追加</button> </form> </div> @endforeach </div> </div> </div> <script src="{{ mix('js/app.js') }}"></script> </body> </html>ポイント
{{ $product->name }}のように表示されているのは
blade
側の構文で@{{ items }}のように
@
が先頭についているのはVue側の構文です。<style> [v-cloak] { display: none; } </style>ここで設定しているスタイルは、Vue側の構文を表示する際に一瞬{{ }}が表示されるのを防ぐためのものです。
フォームリクエストの作成
バリデーションはフォームリクエストで行うようにします。
ターミナルで以下を実行して下さい。php artisan make:request CartRequest
app>Http>Requests
内にファイルが作成されるので、開いて以下のように記述します。CartRequest.phpclass CartRequest extends FormRequest { public function authorize() { // 今回認証は行わないのでtrueにします return true; } // 正規表現で正の整数だけパスするようにしてます public function rules() { return [ 'quantity' => 'regex:/^\d+$/' ]; } public function messages() { return [ 'quantity.regex' => '購入数は正の整数を入力してください' ]; } }ポイント
今回は非同期通信の中でフォームリクエストを使用しバリデーションを行いますが、通常のバリデーション処理と違い注意点があります。
通常であれば自動的に元のページへのリダイレクトレスポンスが作成され、エラーメッセージもフラッシュメッセージとしてセッションに保存されますが、非同期通信の場合はJSONが返されるだけで、レスポンスの作成は行われず、リダイレクトもしません。コントローラの作成
ターミナルで以下を実行します。
php artisan make:controller ProductController
app>Http>Controllers
内にファイルが作成されるので、開いて以下のように記述します。ProductContoroller// モデル利用のため追記 use App\Models\Product; use App\Models\Cart; // フォームリクエスト使用のため追記 use App\Http\Requests\CartRequest; class ProductController extends Controller { public function index() { // 商品情報を全て取得します $productTable = Product::all(); // 取得した内容をビューに渡します return view('product', compact('productTable')); } // カートに商品を追加するメソッドです public function add_cart(CartRequest $request) { // フォームリクエストを通過したリクエストの値を全て$formに代入します $form = $request->all(); // 不要な項目を削除します unset($form['_token']); // Cartモデルをインスタンス化(実体化)します $cartTable = new Cart; // 登録する値を各項目に一気に代入します $cartTable->fill($form); // Cartテーブルにデータを保存します $cartTable->save(); // カートからユーザーIDが同じ物だけ抽出して数をカウントします $cart = $cartTable->where('user_id', $request->user_id)->count(); // カウントした数を返します return $cart; } // カートの商品点数をカウントするメソッドです public function get_total(Request $request) { // カートからユーザーIDが同じ物だけ抽出して数をカウントして返します $cart = Cart::where('user_id', $request->user_id)->count(); return $cart; } }ルーティングの作成
routes
内のweb.php
を開いて以下のように記述します。web.phpRoute::get('/product', 'ProductController@index')->name('product'); Route::post('/ajax/product', 'ProductController@add_cart')->name('add_cart'); Route::get('/ajax/product', 'ProductController@get_total')->name('cart_total');Vue.jsの作成
resources>js
内のapp.js
を開き、下の方を以下のように記述します。app.js// Vueをインスタンス化(実体化)しappに代入します const app = new Vue({ // Vueを使用する範囲(仮想DOM)を指定します el: '#app', // 初期値で渡す値を設定します data() { return { // 現在選択されているユーザーです user: '', // カートの商品点数です items: '', // バリデーションのエラーメッセージです errors: {}, } }, methods: { // カートに商品を非同期通信で追加するメソッドです addCart(id) { // アクセス先のURLを作成しurlに代入します let url = '/ajax/product' // アクセス先に送信するデータをparamsに代入します let params = $('#form' + id).serialize() // thisが使えなくなるのでthatに代入し使えるようにします let that = this // エラーメッセージを初期化します that.errors = {} // axiosで非同期通信を開始します axios.post(url, params) // thenで通信成功時の処理を記載します // コントローラからの返り値がresに代入されます .then(res => { // コントローラからの返り値(商品点数)をitemsに代入します that.items = res.data // 購入数の値を空に戻します $('#form' + that.user)[0].reset() // catchで通信失敗又はバリデーションエラー時の処理を記載します // フォームリクエストからの返り値がerrorに代入されます }).catch(error => { // ここで使用する変数errorsを定義します var errors = {} // for...in分でキーの数だけ処理を繰り返します for (var key in error.response.data.errors) { // errorsにキーと値を代入します errors[key] = error.response.data.errors[key].join() } // errorsに抽出したエラーメッセージを代入します that.errors = errors }) }, }, // watchで値の変更の監視を行います watch: { // userの値が変更された(ユーザーを切り替えた)時の処理です user: function() { // アクセス先のURLを作成しurlに代入します let url = '/ajax/product?user_id=' + this.user // thisが使えなくなるのでthatに代入し使えるようにします let that = this // エラーメッセージを初期化します that.errors = {} // axiosで非同期通信を開始します axios.get(url) .then(res => { // resで受け取ったコントローラの返り値(商品点数)をitemsに代入します that.items = res.data }) } } })ポイント
errors[key] = error.response.data.errors[key].join()最後の.join()が抜けると、エラーメッセージの表示が
購入数は正の整数を入力してください
ではなく、
["購入数は正の整数を入力してください"]
と、余計なものが表示されます。
コンパイル
ここまで記述したら、最後にコンパイルを行います。
ターミナルで以下を実行してください。npm run dev 又は npm run watch-poll動作確認
/product
にアクセスし、動作を確認してみてください。
要件が全て満たされていたら成功です。作成時にハマった点
ユーザー切り替え時のカートの点数の取得
最初Vueには以下のように記述を書いていました。
beforeUpdate() { let url = '/ajax/product?user_id=' + this.user let that = this axios.get(url) .then(res => { that.items = res.data }) },この記述だと、ユーザー変更時にカート内の商品点数を取得してはくれるのですが、同じ処理が2回行われてしまいます。
先ず、ユーザーが切り替わったことで変更とみなされ、処理が走ります(1回目)。
次に、items
に値が入ることで変更とみなされ、処理が走ります(2回目)。
2回目の後に再度items
に値が代入されますが、値が全く同じなので、変更とみなされず処理は走りません。最終的にwatchを使用し、監視する項目を指定することでうまくいきました。
バリデーションのエラーメッセージの取得
これが一番ハマリました。
最初は@errorディレクティブを使用し、エラーメッセージを表示するようビューに記載していたのですが、非同期通信の場合リダイレクト処理は行われないので、通常フラッシュメッセージとしてセッションに保存されるエラーメッセージが受け取れませんでした。改善策としてセッションに手動でエラーメッセージを保存しようと試みましたが、まず非同期通信の為ページが更新されないので、セッションに保存したところで反映されませんでした。
一部のDOMだけを更新させることも考えましたが、思う通りにできる記述方法を見つけることができませんでした。
また、色々名称等を試しましたが、セッションにどういうキーで、どうい形で、どんな値が保存されているのかわからず、@errorディレクティブを動かすことができませんでした。最終的に@errorディレクティブの使用は諦めました。
Vueでのバリデーションのエラーメッセージの取得
最初エラーメッセージを表示させて際、ポイントで記載していますが、余計なものが表示されてしまいました。
["購入数は正の整数を入力してください"]
[""]が不要です。console.logで受け取ったデータを確認したところ、["購入数は正の整数を入力してください"]この表示の他に、[0]で購入数は正の整数を入力してくださいという値があるのがわかりました。
そこで、以下のように記述を変更しうまくいきました。
errors[key] = error.response.data.errors[key][0]うまくいきはしたのですが、どうしても[0]がの記述が気になりました。
今回バリデーションルールが1つしかないからいいものの、複数の場合に大変そうで、更に色々探して最終的に掲載しているjoin()
を使うかたちにしました。カートの件数のカウント
これも結構ハマリました。
以下うまくいかなかったコントローラの記述です。
$cart = $cartTable->find($request->user_id)->count();カートの点数が常に1になります。
find()
では1件の値しか取れていないようです。$cart = $cartTable->find($request->user_id)->get()->count();テーブルのレコード全件がカウントされてしまいます。
find()
で抽出しても、get()
がくると全部抽出されてしまいます。最終的にwhereで条件を指定しうまくいきました。
個人的にはfind()
でいけると思ったのですが・・・
- 投稿日:2020-12-16T04:39:35+09:00
Laravel用のGithubActionsがいきなり落ちるようになった。[ Process completed with exit code 2. ]
結論
PHPのデフォルトのバージョンが勝手に変わっていた。
PHP7.4使っていたが8.0になっていた。何が起きたか
つい先日まで正常に動いていた
github actions
が落ちるようになった。
Process completed with exit code 2.
がでて落ちてる。下記が実行してるaction。
落ちてるのは
Install Dependencies
の箇所
composerから依存関係インストールしてるだけ。github-actionsname: Laravel on: push: branches: [master, develop] pull_request: branches: [master, develop] jobs: laravel-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Copy .env run: php -r "file_exists('.env') || copy('.env.example', '.env');" - name: Install Dependencies run: composer install -q --no-ansi --no-interaction --no-scripts --no-suggest --no-progress --prefer-dist - name: Generate key run: php artisan key:generate - name: Directory Permissions run: chmod -R 777 storage bootstrap/cache - name: Create Database run: | mkdir -p database touch database/database.sqlite - name: Execute tests (Unit and Feature tests) via PHPUnit env: DB_CONNECTION: sqlite DB_DATABASE: database/database.sqlite run: php artisan test直し方
まずは原因を探る。
composerを実行しているコマンドから-q
または--quiet
を削除します。
これはメッセージを出力しないようにするものなのですが、エラーの現在は状況がわからなくなります。composer install -q --no-ansi --no-interaction --no-scripts --no-suggest --no-progress --prefer-dist # -qを抜く composer install --no-ansi --no-interaction --no-scripts --no-suggest --no-progress --prefer-distそれを再度pushし直すと当然エラーが出ますので、ようやく原因がわかります。
一部ですが下記のようなエラーが出ていました。
PHPのバージョンが違うので入れれないてきなメッセージです。Problem 1 - Root composer.json requires php ^7.2.5 but your php version (8.0.0) does not satisfy that requirement. Problem 2 - asm89/stack-cors is locked to version v2.0.1 and an update of this package was not requested. - asm89/stack-cors v2.0.1 requires php ^7.0 -> your php version (8.0.0) does not satisfy that requirement. Problem 3 - bensampo/laravel-enum is locked to version v1.38.0 and an update of this package was not requested. - bensampo/laravel-enum v1.38.0 requires php ~7.1 -> your php version (8.0.0) does not satisfy that requirement. Problem 4 - composer/composer is locked to version 1.10.13 and an update of this package was not requested. - composer/composer 1.10.13 requires php ^5.3.2 || ^7.0 -> your php version (8.0.0) does not satisfy thatなので、PHPのバージョンを切り替えてあげます
shivammathur/setup-php@v2
を使い簡単にPHPのバージョンを切り替えられます。(参考)下記をワークフローに追加
- name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: "7.4"最終的に下記のようになりました。
どうせ折り畳めるので-q
はそのままなくすことにしました。github-actionsname: Laravel on: push: branches: [master, develop] pull_request: branches: [master, develop] jobs: laravel-tests: runs-on: ubuntu-latest steps: - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: "7.4" - uses: actions/checkout@v2 - name: Copy .env run: php -r "file_exists('.env') || copy('.env.example', '.env');" - name: Install Dependencies run: composer install --no-ansi --no-interaction --no-scripts --no-suggest --no-progress --prefer-dist - name: Generate key run: php artisan key:generate - name: Directory Permissions run: chmod -R 777 storage bootstrap/cache - name: Create Database run: | mkdir -p database touch database/database.sqlite - name: Execute tests (Unit and Feature tests) via PHPUnit env: DB_CONNECTION: sqlite DB_DATABASE: database/database.sqlite run: php artisan test
- 投稿日:2020-12-16T01:22:49+09:00
実は簡単!?HerokuにアップロードしたCleraDB(MySQL)をCLIで接続する方法
heroku上にアップロードしたCleraDB(MySQL)を接続する方法に凄く手間がかかったので、同じ境遇にいる人の助けになればなと思い、自分の備忘録も兼ねて書いていきます。
開発環境
- windows10
mysql --version mysql Ver 8.0.22 for Win64 on x86_64 (MySQL Community Server - GPL)以下のコマンドを打ち、データベースのユーザー名、パスワード、ホスト名、データベース名を取得します。
heroku config:get CLEARDB_DATABASE_URLすると、このようにデータベースの情報が返ってくると思います。
「CLEARDB_DATABASE_URL: mysql://[ユーザー名]:[パスワード]@[ホスト名]/[データベース名]?reconnect=true」
この情報を元に、CLIで接続を行っていきます。
注意:MySQLのコマンドプロンプトではなく、windowsに搭載されているコマンドプロンプトを使ってください(Macならターミナル)
mysql -u[ユーザー名] -p[パスワード] -h[ホスト名] //末尾にダブルセミコロン(;)は付けないでください。付けるとエラーが出ます。すると、以下のような実行結果が返ってきます
~ Your MySQL connection id is id名 Server version: 5.5.62-log MySQL Community Server (GPL) ~最後にデータベースを選択してください。(これめっちゃ忘れる)
use [データベース名]; //ここ、ダブルセミコロン(;)付ける Database changed後は好きにMySQLの構文を打ってください
select * from users;んね?実は簡単♪
- 投稿日:2020-12-16T00:30:24+09:00
vuetify-loader1.6にアップデートしてビルドできなくなった場合の対処法
LaravelMixを使ってビルドしている場合にvuetify-loaderをバージョンアップすると下記のようなエラーが出てビルドできなくなった。
Error: [VueLoaderPlugin Error] vue-loader 15 currently does not support vue rules with oneOf. at VueLoaderPlugin.applyどうやらvue-loaderとvuetify-loaderの読み込み順があべこべになっているため発生しているようで、vuetify-loaderの読み込み方を変更する必要があるみたい。
Before
wepack.mix.jsmix.webpackConfig({ //~中略 plugins: [ new VuetifyLoaderPlugin(), ], //~中略 })After
https://github.com/vuetifyjs/vuetify-loader/issues/144#issuecomment-659308887
wepack.mix.jsmix.webpackConfig({ plugins: [ //ここで読み込まない ], }) //ここで追って読み込む mix.extend('vuetify', new class { webpackConfig (config) { config.plugins.push(new VuetifyLoaderPlugin()) } }) mix.vuetify()
- 投稿日:2020-12-16T00:01:44+09:00
Laravel livewire を 使用して画像プレビュー、リアルタイムバリデーションを実装する
こんにちは
こんにちは。T.tsubasaと申します。
東南アジア発のスタートアップスタジオ「GAOGAO」に所属させていただいています。
普段はPHP(フレームワークはLaravel)を用いて開発案件に携わっております。
また、プログラミング研修事業「GAOGAOゲート」のメンターもしておりますので、もしご興味あれば是非ご連絡ください。では本題に移ります。
Livewire
Laravel8からメジャーになった、Livewireを活用し画像プレビュー・リアルタイムバリデーションを実装していきます。
突然ですが、このキャラクターをご存知でしょうか・
https://laravel-livewire.com/
ドキュメントを見ていただくと、左上に可愛い、そして若干上下に動く物体をご確認いただけます。
可愛すぎる....このマスコットキャラは「タコ」でしょうか。「クラゲ」でしょうか。
ご存知の方がいらっしゃいましたら、ご教授願います。そんな冗談はさておき、Laravel8でメジャーになったLivewireを触ってみたので、便利だった機能をメモがてらアウトプット致します。
Livewireとはなんぞ
Livewire is a full-stack framework for Laravel that makes building dynamic interfaces simple, without leaving the comfort of Laravel.
https://laravel-livewire.com/公式ドキュメントには、Livewireとはなんぞやが英語で記載されています。
Livewireを熟知する人物、まだLivewireを知らない人物らから繰り出される、短い掛け合いをご覧いただけます。その掛け合いかなり意訳ですが、PHPを用いるだけで、VueやReactがなしえることを代替できるよ!と書いてあると私は認識しました!
また、実際にJSを書くことなくリアルタイムな実装がLivewireにて可能となったと感じました。Livewireを用いることで、PHPのみでインタラクティブなフロントエンドの画面を作成することができます。
今回はLivewireを用いてアプリを開発していく中で、便利だった機能についてご紹介します。
- Real-time Validation
- Temporary Preview
今回は上記に2つについて、一例のコードを示しつつLivewireでの実装の容易さを感じていただきたいと思います。
Real-time Validation
Livewireではバリデーションをリアルタイムで行うことができます。
updated
フックを使用します。
下記は一例です。app/http/livewire/students.php //省略 class Students extends Component { public $firstname; public $lastname; public $email; public $phone; public $image; public $photoStatus = true; // validationのルールを定義 protected $rules = [ 'firstname' => 'required|min:3', 'lastname' => 'required|min:3', 'email' => 'required|email', 'phone' => 'required', 'image' => 'image|max:1024|mimes:jpeg,png,jpg,gif', ]; // inputタグが更新された際に、validateを行う。validateOnlyメソッドはinputタグ一つ一つに対してvalidateを行うことができます。 public function updated($propertyName) { $this->validateOnly($propertyName); } // 以下には、保存の処理等を記入しているが今回は省略。 }Bladeは、普段のformと少し変わっています。
// create.blade.php <form> <input type="text" wire:model="firstname"> @error('firstname') <span class="error">{{ $message }}</span> @enderror <input type="text" wire:model="lastname"> @error('lastname') <span class="error">{{ $message }}</span> @enderror <input type="text" wire:model="email"> @error('email') <span class="error">{{ $message }}</span> @enderror <input type="text" wire:model="phone"> @error('phone') <span class="error">{{ $message }}</span> @enderror <button type="submit">Save Contact</button> </form>Temporary Preview
画像をアップロードした際に、アップロードした画像をPreviewで表示したい時に非常に便利です。
Livewireを使用しない場合には、JSでコードを書く必要がありますがLivewireを使用することで簡単に画像のプレビューを実装することができます。// create.blade.phpのform内に追記。 <input type="file" wire:model="image"> @error('image') <span class="error">{{ $message }}</span> @enderror @if ($image) @php try { $url = $image->temporaryUrl(); $photoStatus = true; }catch (RuntimeException $e){ $this->photoStatus = false; } @endphp @if($photoStatus) <img src="{{ $url }}" class="w-auto h-64"> @endif @endifPreviewは
temporalyUrl()
メソッドを用いることで簡単に表示することが可能となります。@phpを使用し、かなり複雑(読みにくい)コードが繰り出されていますが、こちらは故意です。
@if($image)以下は、<img src="{{ $url }}" class="w-auto h-64">
だけでも綺麗に動くように見えます....(見えるだけ)しかし、その実装にした場合にはプレビュー画像を表示できない為、
.heic
などの画像をアップロードすると、エラー画面が表示されてしまいました。validationも反応しつつ、画像のプレビューも表示しようとしてしまうのです...
実際に試していただくと、バグが起きていることをご理解いただけると思います。
このあたりは非常に使いにくいので、アップデートを期待ですね。今回のこの条件分岐は
https://github.com/livewire/livewire/issues/1106
こちらを参考に実装しました。先人感謝。Livewireを使用してみて
Laravelでの開発を主としている方にとっては非常に便利だと感じました。
しかし、いざLaravelではないフレームワークを使用するとなった際にはLivewireを使用することはできません。私が少しアプリを作成する中で感じた感想と致しましては、Laravelを使っていてPHPだけで簡易的なリアルタイムなバリデーションを行いたい方にはとてもオススメできるものであるなと感じます。
一方で、Vue.jsなどを学習している方に関しての、Livewireを活用するメリットとしては
簡単な実装であれば、Livewireを活用することですぐに実装できる点であると思います。いずれは、PHPだけでフロントエンドまでリッチに実装できるのかな.....
私もLaravel8に関してはまだまだ勉強せねばならぬと感じつつ、本記事を終了にさせていただきます。
コメント等ございましたら。お気軽にご連絡ください!