20201216のlaravelに関する記事は15件です。

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 件付帯して表示する、という要件がありました。

Image from Gyazo

ここで、先生に指導コースを付帯して表示するためには、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

Image from Gyazo

実際モデリングする時間はあるのか?

実際、モデリングだけで丸 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 にはまだまだ開拓の余地があるなと思いました。

まだまだビジネスルールを 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-interfaces

Using 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 エンジニアを探しております。

マナリンクでは、オンライン家庭教師の先生方のために、サイト上で自身のプロフィールを魅力的に発信できるようにしたり、オンライン指導専用アプリをリリースするなど、次々にプロダクトを開発しています。日々新しい技術を勉強して、試す機会を探している方にはうってつけな環境です。

ベンチャー企業ですが、CTO/CEO ともにテストコードを使って品質保持することに理解はありますし、オンライン家庭教師という新しい働き方のドメインを作っていくという意味で、問題解決領域としても大変興味深いのではと思っています。

興味あれば 上記の Twitter に DM でご連絡をください!

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

LaravelでHerokuデプロイ時、HTTPS通信にならないときに確認すること

問題

  1. サイト内ページがHTTP通信で表示されてしまう。*Chromeの場合(「保護されていない通信」の警告が出る。)
  2. 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.php
public function boot()
{
    if (\App::environment(['production'])) {
        \URL::forceScheme('https');
    }
}

HTTPをHTTPSにリダイレクトさせる

ミドルウェアで、HTTPをHTTPSにリダイレクトさせます。
まずは、コマンドラインでミドルウェアを作成します。

php artisan make:middleware ForceHttpToHttps

次に、Middlewareの中身を書いて、

ForceHttpToHttps.php
public 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.php
protected $middleware = [
  \App\Http\Middleware\ForceHttpToHttps::class, // 追加
];

まとめ

上記の方法で、問題が2つとも解決しました。
(CSSが反映された理由は不明です・・・)
1. サイト内ページがHTTP通信で表示されてしまう。*Chromeの場合(「保護されていない通信」の警告が出る。)
2. CSSが反映されていない。

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

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> --jet

Jetstreamは単体のパッケージではなく、いくつかのパッケージを寄せ集めて、その上で統合している。各パッケージが担当する機能については以下のようになっている。

  • 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つに分けられる。

  1. auth以下にある、Fortifyによる基本的な認証機能のためのもの。
  2. vendor/laravel/jetstream/resources/views/componentsにある一般的なUIのためのBladeコンポーネント。
  3. 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-viewsresources/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系統ある。

  1. Jetstreamによる、ユーザプロフィールやチーム関連、またLivewireやInertia.js用のAPI関連のルート。
  2. 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が配置されるので、それを自由にカスタマイズし、追加で、

  1. RouteServiceProvider等で上記ファイルを読み込む。
  2. 元のルートを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 newartisan 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.phpauth:sanctumミドルウェア(正確にはsanctumガードを使うよう指示されたauthミドルウェア)を使うルートが登録される。

このミドルウェアはなんだろうか。Sanctumは最初に書いたようにAPI認証用の機能だが、ここで登録されているルートはダッシュボードのもので、普通にセッションベースで認証できている。

結論を言うと、sanctumガードは、sanctum.guard設定で指定したガードか未指定であればwebガードが有効な場合はそちらを見るようになっている。そのためログインしない状態で、Bearerトークンで認証するような場合以外は'auth'とパラメータなしで指定する場合と変わらない。つまりダッシュボードでsanctumガードを使う必要はおそらくない。

sanctumガードは\Laravel\Sanctum\Guardで実装されている。

最後に

Jetstreamによって導入される機能の変更方法を見てきた。ほとんどの機能は最終的には2つのサービスプロバイダ、JetstreamServiceProviderFortifyServiceProvider(それぞれvendor内の方)によって提供されている。そこだけ覚えておけば、自力で結論に辿り着くのも容易だ。

この記事のライセンス

クリエイティブ・コモンズ・ライセンス
この文書はCC BY(クリエイティブ・コモンズ表示4.0国際ライセンス)で公開する。

この文書内のサンプルコードはMITライセンスで公開する。

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

Laravelのパスワードリセットをユーザー名など、メールアドレス以外で行うようにする

Laravelの標準のAuthではパスワードリセットはメールアドレスで行いますが、これをユーザー名などに変更したいことがあると思います。

背景

私が現在開発しているシステムでは元々はDB上ではメールアドレスがユニークで、ユーザーの識別(ログインやパスワードリセット)はLaravelの標準の機能でカバーできていました。

しかし、開発が一通り終わる頃、「同じユーザーが一つのメールアドレスで複数のアカウントを扱えるようにしたい」という要求が出てきたため:sweat:、仕様を変更する必要に迫られました。

幸い、システム内ではログインした後はユーザー登録時に自動で付加される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.php
Route::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.php
public 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.php
public 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.php
public 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.php
public 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されるようにします。
reset.png

/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

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

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 」インストール

何もチェックせずに「Next>」

3.「 Browse...」をクリックしてMAMPのPHPバージョンフォルダ内にある「php.exe」を選択

・チェックボックスルも入れておく。


「Next>」ボタンで次へ

4.「Create a php.ini file」にチェックを入れる!


「Next>」ボタンで次へ

5.何も入力しない


「Next>」ボタンで次へ

5.「Install」ボタンをクリック

6.「Next>」ボタンで次へ

7.「Finish」ボタンをクリック

<< 「composer install」「composer update」で赤い文字のエラーが出た場合 >>

1.php.iniの修正

image.png
上記「3」で選択したphp.exeと同フォルダ内に「php.ini」ファイルができてるので

php.ini
;extension=fileinfo
 ↓ ↓ ↓ 
extension=fileinfo

上記、";" コメント外し保存

2.MAMP再起動

php.iniの修正を反映させるため、MAMPを再起動します。

以下、追加メモ

*LaravelでリポジトリをGitcloneしてきた場合(以下コマンド)

  1. composer install
  2. cp .env.example .env (laravelフォルダ内で実行)
  3. 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が再起動してなければ、再起動してから確認しましょう!!

以上

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

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 の設定かと思って延々、悩まれている方が、
広い世界にはいるかも知れないと思い、ここに記録を残しておきます…。


おねがい

詳しいことが分かる方がいらっしゃいましたら、解説をお願い致します…。


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

Laravel + Vue.js の知識をアウトプットしていく

次の記事でLaravelをアウトプットしていますが、今回はLaravel + Vueでまとめていきます。
Laraveの知識をアウトプットして、資産化してます

準備

$ composer create-project laravel/laravel sampleproject --prefer-dist "7.*"

プロジェクトの作成を実施

もろもろの初期設定はこちらの記事を参考にしてください
Laraveの知識をアウトプットして、資産化してます

認証機能をインストール
$ composer require laravel/ui:^2.4 --dev
vueのインストール
$ php artisan ui vue --auth
$ npm install bootstrap-vue bootstrap
$ npm install && npm run dev

vue-routerのインストール

vue-routerのインストール
npm install --save vue-router

下記を追加する。

resources/app.js
import 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 vuex
npm run dev

vue-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/clockwork

ddのように変数をデバッグ可能

clock(User::all());

流れ

  1. コンポーネントの作成
  2. app.jsにimport
  3. HTMLにコンポーメントを追加して表示。
  4. 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を指定してリダイレクトすることが可能

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

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クラスでテストが書けます。クリーン :relaxed:

まとめ

アプリケーション内のソースコード内を概観して、Facadeが使われているのは AOP がマッチしそうなケースだなーと思いました。
DBとかRedisとか使っちゃってるのはだめだぞ☆って感じですけども。

ごく一般的なLaravel製アプリケーションを、メンテ性高く長期運用できるシステムへ改善する過程を一緒に体験してみたい(一緒に苦労してみたいとも言う…)方がいたら https://www.wantedly.com/projects/475929 からご連絡くださいね :relaxed:

もしこれを読んで、最初からFacadeがなくてすべてがDIされている世界(Symfony)に興味を持った方は Symfony Advent Calendar 2020 も覗いてみてください :thumbsup:

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

はじめまして

自己紹介

はじめまして。情報系大学3年のTaYです。普段はPHPやJSを触っています。
最近ではゼミでPythonも触っています。(ほんとに触っているだけ)

自分のやってきたことの備忘録として始めてみました。

今はLaravelを独学中で簡単な掲示板を作っています。

完成したら載せようかな〜と考えています!

特に書くこともないので今回はこの辺で:wave:

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

【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');

表示の確認

以下のようなフォームができました。
image.png

何も入力せずに送信ボタンを押してみます。
image.png
このようにエラーメッセージが表示されます。

バリデーションルールを満たすように、フォームに値を入れて送信ボタンを押すと、問題なく次ページに遷移します。
image.png

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

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を使用して記述してます。

作成要件

  • ユーザーの切り替えができる(ログイン機能を付けないので、手動でユーザーを切り替える仕様にします)。
  • 商品の情報を取得し、一覧表示する。
  • 商品の購入数を指定し、非同期通信でカートに追加できる。
  • カートに入っている商品点数を、非同期通信で表示する。
  • カートに追加する際はバリデーションを行い、エラーがあればメッセージを表示させる。
  • カートへの登録が完了したら、購入数は空に戻す。
  • ユーザーを切り替えるとカートに入っている商品点数も、非同期通信で変更される。

イメージ

メインページ
デフォルト.jpg

ユーザー選択でユーザーの切り替え
user.jpg

カート内の点数は()の中に表示させます
incart.jpg

購入数の指定ができます
post.jpg

カートに追加後は購入数を空に戻します
afterpost1.jpg

カートには点数が追加されます
afterpost2.jpg

ユーザーに合わせて表示されるカートの件数も変わります
userincart.jpg

バリデーションも実装します
validation.jpg

バリデーションでエラーがあればメッセージを表示します
errormsg.jpg

それでは、作成していきましょう。

マイグレーションファイルの作成

データベースへテーブルを作成したいと思います。
今回作成するのは、商品を管理するproducts_tableとカート用のcarts_tableです。

プロジェクトのディレクトリでターミナルを起動し、以下を実行してください。

php artisan make:migration create_products_table

続けて

php artisan make:migration create_carts_table

作成が完了したら、database>migrations内にある作成されたファイルを開き、以下のように記述します。

create_products_table
    public function up()
    {
        Schema::create('products', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('name')->comment('商品名');
            $table->string('price')->comment('価格');
            $table->timestamps();
        });
    }
create_carts_table
    public 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.php
class 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.php
Route::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()でいけると思ったのですが・・・

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

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

実は簡単!?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;

んね?実は簡単♪

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

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.js
mix.webpackConfig({
//~中略
  plugins: [
    new VuetifyLoaderPlugin(),
  ],
//~中略
})

After

https://github.com/vuetifyjs/vuetify-loader/issues/144#issuecomment-659308887

wepack.mix.js
mix.webpackConfig({
  plugins: [
    //ここで読み込まない
  ],
})
//ここで追って読み込む
mix.extend('vuetify', new class {
  webpackConfig (config) {
    config.plugins.push(new VuetifyLoaderPlugin())
  }
})
mix.vuetify()
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel livewire を 使用して画像プレビュー、リアルタイムバリデーションを実装する

こんにちは

こんにちは。T.tsubasaと申します。

東南アジア発のスタートアップスタジオ「GAOGAO」に所属させていただいています。

普段はPHP(フレームワークはLaravel)を用いて開発案件に携わっております。
また、プログラミング研修事業「GAOGAOゲート」のメンターもしておりますので、もしご興味あれば是非ご連絡ください。

では本題に移ります。

Livewire

Laravel8からメジャーになった、Livewireを活用し画像プレビュー・リアルタイムバリデーションを実装していきます。

突然ですが、このキャラクターをご存知でしょうか・
https://laravel-livewire.com/
ドキュメントを見ていただくと、左上に可愛い、そして若干上下に動く物体をご確認いただけます。
可愛すぎる....

twitter (3).png

このマスコットキャラは「タコ」でしょうか。「クラゲ」でしょうか。
ご存知の方がいらっしゃいましたら、ご教授願います。

そんな冗談はさておき、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
            @endif

Previewは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に関してはまだまだ勉強せねばならぬと感じつつ、本記事を終了にさせていただきます。

コメント等ございましたら。お気軽にご連絡ください!

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