20200928のlaravelに関する記事は10件です。

[Laravel] Composer require で依存関係によるエラーを回避する方法

概要

Composer でインストールを行う際、以下のようなエラーが出た時に対処した方法をまとめる。

Installation failed, reverting ./composer.json to its original content.

環境

Laravelの versionは 6.18.40

$ php artisan --version
Laravel Framework 6.18.40

解決方法

本エラーはインストールするライブラリが他パッケージで別versionで使用している依存関係によるエラーであるため、composer.jsonでインストールを行うversionを指定することで回避可能。
今回は、laravel/ui1.0として追記している。

composer.json
    "require": {
        "php": "^7.2",
        "fideloper/proxy": "^4.0",
        "laravel/framework": "^6.18.35",
        "laravel/tinker": "^2.0",
        "laravel/ui": "^1.0",
        "laravelcollective/html": "^6.0"
    },

その後、versionを指定してインストールを行う。

$ composer require laravel/ui:^1.0

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

【webpack】sassをバンドルする際に出たエラーの対処

環境

PHP 7.3.8
Laravel 6.18.40
webpack 4.27.1

TypeError: text.forEach is not a function ... というエラー

Docker開発環境下で、npm run watch-poll でビルドを試みるも以下エラーで失敗する。

ERROR in ./resources/scss/app.scss
Module build failed (from ./node_modules/css-loader/dist/cjs.js):
TypeError: text.forEach is not a function
    at /var/www/node_modules/extract-text-webpack-plugin/dist/loader.js:145:16
(略)

結論から言うと、sassのバンドルにはcss-loadernode-sasssass-loaderstyle-loaderを用いているが、それぞれのバージョンによる互換性の問題であった。

今回はcss-loaderのバージョンを4.3から下げる事で改善した。

npm uninstall --save-dev css-loader
npm install --save-dev css-loader@3.5.3

最終的に以下の構成でビルドを確認した。

package.json
"css-loader": "^3.5.3",
"laravel-mix": "^4.0.7",
"lodash": "^4.17.13",
"node-sass": "^4.14.1",
"resolve-url-loader": "^2.3.1",
"sass": "^1.15.2",
"sass-loader": "^7.1.0",
"style-loader": "^1.2.1",

参考にした記事

npm がどうしてもエラーになる
Laravel6でVueを使おうとnpm run watchをするとエラーが出る

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

Laravelディレクトリ構成+DBマイグレーション

今回はLaravelディレクトリ構成+DBマイグレーションを説明していきます。

ディレクトリ構成

image.png

ディレクトリ名 役割
app アプリのメインとなるところ
bootstrap 初期処理やキャッシュなど
config アプリの設定
database データベース(マイグレーション)
public 画像、JS、CSSなど
resources bladeなど(HTML)
routes アプリのURL設定
storage セッションやログ
tests テスト用
vendor Comoposerの依存内容

マイグレーション

マイグレーションとは

SQLを直接使わなくても、データベースを管理できるLaravelの仕組みです。

マイグレーションファイル

マイグレーションファイルは「どんなテーブルを作るか」というテーブルの設計書です。設計書を作ったらそれを実行しなければテーブルはできないので、この実行にあたるのがマイグレーションです。

テーブル作成の流れ

1. マイグレーションファイルを生成
2. ファイルにテーブル定義を書く
3. マイグレーションファイルを実行して、DBに反映

ファイルの場所とファイル名ルール

image.png

ファイル生成コマンド

 $ php artsan make:migration create_users_table 

(例)カラム追加ver

$ add_カラム名_to_テーブル名_table

実行コマンド

$ php artisan migrate

マイグレーションの中身

・upとdownメソッド
 upが実行(作成)で、downが元に戻す(削除)
・Schemaファサードを使っている
 例) Schema::create テーブルを作成
・カラムの作成はBlueprintオブジェクトのtableメソッドを使う
 例) $table->string('name')名前カラムを作成

   public function up()//作成
    {
        if(!Schema::hasTable('blogs')) {//もしblogsというtableがなかったら
            Schema::create('blogs', function (Blueprint $table) {
                $table->id();//idのカラム
                $table->string('title',100);//titleというカラムをstring型で100文字の制限をつけた状態で作る
                $table->text('content');//contentというカラムをtext型で作る
                $table->timestamps();
            });
        }

    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()削除
    {
        Schema::dropIfExists('blogs');
    }

追加したいカラムなどを書き終えたらmigrateしていきます。

$ php artisan migrate

終わりに

参考サイトを貼らせて頂きます。

公式ドキュメント
https://readouble.com/laravel/7.x/ja/migrations.html

参考サイト
https://www.youtube.com/watch?v=wkW1spp_LO8

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

Laravel8.0入門してみた感想

ララベル.かわいいなまえだね.
でも「Web職人のためのフレームワーク」みたいなキャッチコピーでかっこいいね.

ちゅき(._.)

というわけで入門した感想を述べます.

感想1. 環境の構築 → めちゃ簡単

僕はこれまでRuby on RailsとReactに触れたことがあります.
使ってるOSはWindows10.
するとどうなるか.

環境構築で死す

コマンドプロンプトにerrorの嵐

僕はもう疲れました.
こんなことならフレームワークなんか使わない方が早いや.
どうせザコだし僕.

そう思ってたある日,なんとなくPHPを使い始めて,それとなくLaravelにたどり着きました.
「どうせまた環境構築で死すんだろうな...」
半ばあきらめながらネットに落ちている記事を見ながらXAMPPやらLaravelやらを入れました.

...は?

一度もエラーせず構築できた.

こんなことがあるんですね.
もうこの時点で僕のハートはLaravel様の者でした.

「Windows想いなLaravel,好きです.」
僕はそう呟いて,そっと眠りについた.(え?)

感想2. ネット記事より書籍を読め

「プログラミングの勉強に金はかけない」
そんな当たり前の常識の元,Laravel入門初日の僕はネット記事で学習をしていました.

なんかよくわからん

そもそもルートとかわからん.
データベースとか触ったことない.

うん,わからん.

公式ドキュメントもよくわからん.

この時点で僕は,少しだけLaravelに嫌気がさしていました.
だっていい記事全然ないんだもん.

すると,
メール「Kindle Unlimitedの定期購読,更新したで!」
僕「あ,Laravelの書籍あるんでね??」

ありました.
世間で「青本」と呼ばれているよさげなLaravelの本は,Kindle Unlimitedでは読めなかったけど,別のがありました.
でも全然わかる.なんか道筋めちゃ見える.テンション爆上げ.

Laravelめちゃええやん

環境構築で死してばかりの僕がフレームワークを扱っている.
その喜びを握られてまた,僕はLaravelに恋をしたのである.

感想3.何をすればいいかが分かりやすい

そろそろLaravelのシステムについて言及したい.

フレームワークでよく思うことは,
「どのファイルに何書けばいいかわからん.」
ということです.
まあこればっかりはいかにちゃんと学ぶかみたいなところはあるかもしれませんが.

ただ,Laravelはめちゃわかりやすかった.
ざっくり言えば,

  1. コントローラー(○○Controller.php)で諸々の機能を作る.
  2. Webページのテンプレート(○○.blade.php)を作って機能を埋め込む.
  3. ルート(web.php)で,さっき作ったテンプレートを表示できるようにする.

というステップをひたすら踏んでいくだけです.
多分.

まあまだ入門二日目なので,まだ難関は色々あるんだろうけど.

感想4. Bladeが直感的で便利

前項でWebページのテンプレートを作ると言いましたが,これはBlade(ブレード)と呼ばれるのだそうです.
Bladeを使うと何がいいかというと,普通のHTMLの構文を使いつつ,制御構文(ifやfor文)も使えるということ.

例えば,$userという変数の値が'boyfriend'だったときに<p>私だけを見て</p>を表示するテンプレートlovemessage.blade.phpを作るとすれば,

lovemessage.blade.php
//...
<body>
    @if($user == 'boyfriend')
        <p>私だけを見て</p>
    @endif
</body>
//...

と書けば,彼氏だけに愛のメッセージを伝えることができます.

直感的!!便利!!

ちなみに,@~で続く構文のことをディレクティブと呼び,@whileやら@switchやら@untillやら,一通りそろってます.

感想5. コントローラー ≒ Unityのスクリプト

前々項で
「コントローラー(○○Controller.php)で諸々の機能を作る」
と言いました.

でも入門者からしたら
「コントローラーって何?機能?は?」
って感じですよね.

個人的な意見ですが,
コントローラー≒Unityのスクリプト
です.(Unityしたことない方ごめんなさい)

Unityでプレイヤーのカメラを制御するスクリプトPlayerCamera.csとか作りますよね?
そんな感じです.

Unity
1つの役割(カメラの制御,プレイヤーの制御,敵の生成など)に対して1つのスクリプト
Laravel
1つの役割(トップページ,ログインページ,ブログページなど)に対して1つのコントローラー(多分)

というイメージです.

Unity
スクリプトにグローバル変数(`public int enemySpeed;`とか)をつくる → 敵キャラのスピードいじる
Laravel
コントローラーのpublicな関数がテンプレートに変数を渡す → ページに表示される数字いじる

という...イメージです...。(あれ,なんか微妙?)
とりあえず例を見てください.(お願いします)

次の例は,game/enemy.blade.php(gameディレクトリにあるenemyという名前のテンプレート)に,10から100までの乱数を持った変数$speedを渡す関数randomSpeed()を持つコントローラーEnemyspeedControllerです.

EnemyspeedController.php
<?php
//略
class EnemyspeedController extends Controller{

    public function randomSpeed(){
        return view('game.enemy',[
                'speed' => random_int(10,100)
        ]);
    }

    //略
}

...。

なんかUnityっぽくないですか?(ゴリ押し)

感想6. テンプレート → Bladeじゃなくてもいい

なんだ.Bladeじゃなくてもいいのね.

テンプレートをテンプレートたらしめる定義は,そのphpファイルが`resources/views'ディレクトリにあることなんでしょうね.多分.

感想7. 文字列の結合,そうやってするんだ.

これはPHPに関する感想かもしれませんが...。

世に存在する多くのプログラミング言語で文字列を結合するときは,「+」で加算すると思うのですが,PHPでは「.」で結合するようです.

下が例です.

javascript
str = 'Hello,';
console.log(str+' World!');
//'Hello, World!'が出力される

str += ' World!';
console.log(str);
//'Hello, World!'が出力される
php
$str = 'Hello,';
echo($str.' World!');
//'Hello, World!'が出力される

$str .= ' World!';
echo($str);
//'Hello, World!'が出力される

終わり

とりあえずここまでです.
また感想があれば追加します.
さらば!!

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

Laravel初期設定

Laravelでプロジェクトを作成していく際の初期設定をやっていきます。
ここではLaravel ,Composerを導入し終えた状態で進めていきます。また、使うPCはMacです。

1.初期設定(ターミナル)

まず、ターミナルでMAMPのhtdocs内にプロジェクトを作成していきます。

$ cd /Applications/MAMP/htdocs

移動できたら、下記のコマンドを入力してblogというアプリを作成します。

$ composer create-project laravel/laravel --prefer-dist blog

最後に Application set key successfully.と表示されれば、
MAMPのhtdocs内にblogというプロジェクトを作成できました。
次に作成したmyblogに移動して、初期設定を行います。

$ cd blog

移動できたら初期設定を行います。
権限の設定です。Laravelではログファイルはデフォルトでapp/storage/logsディレクトリの下に作成されます。このディレクトリ以下にnginxはapacheユーザでの書き込み権限が必要です
権限に関してはこちら

$ chmod -R 777 storage
$ chmod -R 777 bootstrap/cache

storageとcacheがこれで権限が一番緩い状態になります。以上でターミナルによる初期設定は終了です。

2.初期設定(MAMP)

次にMAMPの設定です。localhostに接続した際のトップページを設定していきます。ここは設定してもしなくても大丈夫です。
1. MAMPを開く。
2. Desktopの左上のバーのところのMAMPを押してPreferenceを押す。
3. 真ん中のselectを押して、/Application/MAMP/htdocs/blog/publicを選択。
4. OKをクリック
5. start serverをクリック
6. localhostにアクセス
image.png
これはLaravelのトップページがpublic配下に存在するためです。localhostに接続した際に以下の画面が表示されれば正しく接続できています。

image.png

3.DB作成

webブラウザ上のMAMPの中でTOOLSのPHPmyadminを開きます。
image.png
1. サイドバーのnewを押して新しくDBを押す。
2. Create Databaseと出てくるので名前のところにblogと追加。
3. 右のセレクトボックスをutf8mb4_unicode_ciとする。
4. Createボタンを押す。
左側のサイドバーにblogと追加されれば完了です。

次にご自身のテキストエディターでblogを開いてください。.env.exampleというファイルが存在するのでひらいてください。.envファイルという似たようなファイルも存在しますが、.env.exampleの方に記載する事でGitで管理できるので複数人で開発をする際にはこちらに記載する方が良いらしいです。間違ってたらすいません。

image.png
1. 一行目のAPP_NAMEをlocalhostからblogに変更。
2. DB_PORTをご自身のPORT番号に変更。
3. DB_DATABASEをblogに変更。
4. DB_PASSWORDをrootに変更。
5. 15行目にDB_SOCKET=/Applications/MAMP/tmp/mysql/mysql.sockを追加。
(ご自身のPORT番号やパスワードはweb上のMAMPを開いていただいて、少し下にスクロールしていただくと書かれています。)

次に、mysqlのバージョンによっては文字化けする可能性があるのでそれを防ぐためのコードを追加していきます。テキストエディターで、 app/Providers/AppServerProviders.phpを開きます。以下の様に変更を加えてください。
1. 6行目にuse Illuminate\Support\Facades\Schema;を追加。
2. 27行目にSchema::defaultStringlength(191);を追加。
image.png

Timezone

Timezoneの変更をしていきます。
1. config/app.phpに移動
2. 70行目のUTCをAsia/Tokyoに変更
3. 83行目をlocaleを「ja」に変更

image.png

これで初期設定は終了になります。

終わりに

以上が初期設定でした。次回はLaravelでMVCの作成をしていきます。参考サイトを貼っておきますので是非確認してみてください。

https://www.youtube.com/watch?v=yaitzPzBzuI&t=600s
https://www.ritolab.com/entry/49
https://readouble.com/laravel/7.x/ja/database.html
https://qiita.com/shisama/items/5f4c4fa768642aad9e06

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

post_max_sizeとupload_max_filesizeの違い

php.iniにおけるupload_max_filesize と post_max_sizeの違いについて。
ちなみにphpinfo()で確認できる。

upload_max_filesize

一つのファイルの最大ファイルサイズ。
この値を超えてしまうと、$FILES[‘hoge’][‘error’]にUPLOADERR_INI_SIZEがセットされる。

post_max_size

POST送信全体のデータサイズ。
このpost_max_sizeを超えるサイズのPOST送信があった場合、$_FILES[‘hoge’][‘error’]はnullとなり、エラーコードはセットされない。

気をつけること

PHPでは、設定したpost_max_sizen値より、ポストしたファイルサイズが大きかった場合、リクエスト内容そのものが破棄されるため、
リクエストに対してファイルサイズの大きさを制限するバリデーション(size)を使用してもバリデーション できない。
例えば、

php.ini
upload_max_filesize = 20MB
post_max_size = 10MB

だったとして、POSTメソッドで、送信するファイルサイズが15MBだった場合、リクエスト内容そのものが破棄される。

→URLルーティングされたコントローラクラスより先にリクエストを受け取ることができる、Middlewareを利用して、バリデーションを行う。

参考

https://pointsandlines.jp/server-side/php/laravel-post_max_size
https://ageo-soft.info/programming_languages/php/81/

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

[Laravel]OAuthによるログイン時に前画面からパラメータを引き継ぐ方法

laravel-v6.18.40 socialite-v5.0.1

はじめに

OAuthによるログインの実装は、一度外部サービスへリダイレクトを行って、外部サービスから自サービスのコールバックエンドポイントにリダイレクトを受けるという流れになります。
ログイン時には、「どの画面からログインを試みたのか?」 「どういう登録導線からログインされたのか?」 といったログイン処理前の情報を受け取りたいニーズがある場合があります。

このとき、ログイン画面からコールバックエンドポイントに情報を伝達したい場合どうしたらいいかという話です。
ログインに限らず、登録のケースでも同じようにできます。

セッションを利用する

https://readouble.com/laravel/7.x/ja/session.html

HTTPはステートレスなので、リクエストをまたがって情報を保持するには以下のいずれか対応をする必要があります。

  1. POSTパラメータまたはGETパラメータを次ページに引回す
  2. セッションにデータを紐づけて一時的に保持する
  3. クッキーにユーザを特定するキーを仕込んで、DBに保存した値と紐づける

1の方法はFacebook認証では実現できませんでした。1
3の方法でも実現できますが、2の方がお手軽なので選択しませんでした。

OAuthによるログインはSocialiteというOAuthパッケージを利用する前提です。

FacebookAuthController.php
    // OAuth先サービスにリダイレクト
    public function redirectToProvider(Request $request)
    {
        // どのページからログインを試みたのかセッションに保持する
        $request->session()->push('loggedin-from','register-page-01');


        return Socialite::driver('facebook')->redirect();
    }

    // OAuth先サービスからのリダイレクトを受けるエンドポイント
    public function handleProviderCallback(Request $request) {
        // セッションから情報を取得する
        $loggedInFrom = $request->session()->get('loggedin-from', no-data);

        // 略) OAuth先から情報を取得してログインする処理
    }

  1. GETクエリは設定でき、OAuthの連携先であるFacebookにGETパラメータが送られるが、コールバック時にGETパラメータを返してくれなかったので、自サービスが受けるコールバックまでデータを伝搬できなかった。 

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

Laravelでクリーンアーキテクチャに近づけてみる

目次

  • はじめに
  • バージョン
  • もととなるシンプルなCRUD
  • クリーンにしてみる
    • 準備
    • packagesディレクトリの全体像
    • Entities
    • Gateways
    • Use Cases(とControllers)
  • テストを書いてみる

はじめに

Laravelを使ってクリーンアーキテクチャっぽいことをやってみました。
"クリーンアーキテクチャっぽくしつつLaravelの機能(主にEloquent)を使う"
ということを試してみました。
自分の理解がまだ浅いこともあって、"クリーン" かどうかと聞かれると正直微妙なところですが、MVCフレームワークの恩恵を受けつつクリーンアーキテクチャを採用してみる一つの妥協点としてはアリなんじゃないかと思います。

実際のコードはこちら
https://github.com/koyablue/laravel_clean_architecture

バージョン

PHP 7.4.4
Laravel 6.18.40

もととなるシンプルなCRUD

  • ユーザーがいて、そのユーザーがメモの作成/編集/削除ができる
  • 作成したメモの一覧と詳細が表示できる

というシンプルなCRUDで試してみます。
例として以下のようなものを想像してください。

controller
app/Http/Controllers/MemoController.php
<?php

namespace App\Http\Controllers;

use App\Models\Memo;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class MemoController extends Controller
{
    /**
     * 一覧
     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
     */
    public function index(){
        $user = Auth::user();
        $memos = $user->memos;
        return view('index', compact('memos'));
    }

    /**
     * 詳細表示
     * @param $memoId
     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
     */
    public function show($memoId){
        $memo = Memo::find($memoId);
        return view('detail', compact('memo'));
    }

    /**
     * 新規作成
     * @param Request $request
     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
     */
    public function create(Request $request){
        $user = Auth::user();
        $input = $request->get('content');
        $memo = new Memo();
        $user->Memos()->save(
            $memo->fill(['content' => $input]));
        return redirect(route('index'));
    }

    /**
     * 編集画面
     * @param $memoId
     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
     */
    public function edit($memoId){
        $memo = Memo::find($memoId);
        return view('edit', compact('memo'));
    }

    /**
     * 更新
     * @param Request $request
     * @param $memoId
     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
     */
    public function update(Request $request, $memoId){
        $memo = Memo::find($memoId);
        $memo->fill(['content' => $request->get('content')])->save();
        return redirect(route('index'));
    }

    /**
     * 削除
     * @param $memoId
     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
     */
    public function delete($memoId){
        $memo = Memo::find($memoId);
        $memo->delete();
        return redirect(route('index'));
    }
}


クリーンにしてみる

  • Laravelの機能(Eloquent, FormRequestなど)をある程度使った上で、なるべくクリーンアーキテクチャに近づけてみる

ということを心がけながら、上記のCRUDをクリーンアーキテクチャっぽくしてゆきましょう。
ということで、あの図を貼っておきます。
CleanArchitecture.jpg

準備

スクリーンショット 2020-09-27 19.03.45.png
appとおなじ階層にpackagesというディレクトリを作成します。今回はこの中でいろいろ細かく区切って実装してゆくことにしました。
ディレクトリを追加したら、ちゃんとLaravelに読み込んでもらえるようcomposer.jsonも編集しておきます。

composer.json
"autoload": {
        "psr-4": {
            "App\\": "app/"
        },
        "classmap": [
            "database/seeds",
            "database/factories",
            "packages" <-これを追加
        ]
    }

事前準備はこれで終わりです。

packagesディレクトリの全体像

ちなみに完成後のpackagesディレクトリは、以下のようになります
スクリーンショット 2020-09-27 20.38.44.png
このディレクトリの置き方がいいのか悪いのかちょっと自信がありませんが、とりあえず今回は、こんな感じの配置なんだなーと思っていただければ。

Entities

Entityが何かを調べたり考えたりしていたら正直いつまでたっても先に進まない気がしたので、とりあえず
「アプリケーションの中で一番重要そうなモデル(達)」
というふうに捉えて実装しようと思います。(ちなみにこちらの記事がとてもわかりやすかったです。https://nrslib.com/clean-ddd-entity/)

packages/Domain/Domain
というディレクトリをEntities用に用意します。
今回のサンプルは

  • ユーザーがいて、そのユーザーがメモの作成/編集/削除ができる
  • 作成したメモの一覧と詳細が表示できる

というものでした。
"ユーザー" と "メモ"が重要っぽく見えますので、Entityとして必要なのはユーザーとメモと考えるのが妥当かなと思います。(本当はたぶんもっとしっかり整理/洗い出しを行わないといけないんですが...)

今回メモのCRUDに焦点を当てているので、ユーザーは一旦置いておいて、メモのドメインモデルのみ作成します。

packages/Domain/Domain/Memo/Memo.php
<?php

namespace packages\Domain\Domain\Memo;

class Memo
{
    private int $id;
    private int $userId;
    private string $content;
    private \DateTime $createdAt;

    /**
     * Memo constructor.
     * @param int $id
     * @param int $userId
     * @param string $content
     * @param \DateTime $createdAt
     */
    public function __construct(int $id, int $userId, string $content, \DateTime $createdAt)
    {
        $this->id = $id;
        $this->userId = $userId;
        $this->content = $content;
        $this->createdAt = $createdAt;
    }

    /**
     * @return int
     */
    public function getId(): int
    {
        return $this->id;
    }

    /**
     * @return int
     */
    public function getUserId(): int
    {
        return $this->userId;
    }

    /**
     * @return string
     */
    public function getContent(): string
    {
        return $this->content;
    }

    /**
     * @return \DateTime
     */
    public function getCreatedAt(): \DateTime
    {
        return $this->createdAt;
    }
}

Gateways

DBとやりとりする部分です。

  • packages/Domain/Domain/Memo/MemoRepositoryInterface
  • packages/Infrastructure/Memo/MemoRepository
  • packages/UseCase/Memo/QueryService/MemoQueryServiceInterface
  • packages/Infrastructure/Memo/MemoQueryService

の4つをGatewaysとして作成しました。
永続化系(Repository)と参照系(QueryService)の処理を分けて、それぞれにinterfaceと実装クラスがあるというような感じです。

Eloquentをどこで使うかを考えた結果、この層で使うことにしました。
ただし、あくまでも処理の途中で使用するだけで、返す値はドメインモデルか、xxDtoと名付けた専用モデルにするよう心がけました。

MemoRepositoryInterface
packages/Domain/Domain/Memo/MemoRepositoryInterface.php
<?php
namespace packages\Domain\Domain\Memo;

interface MemoRepositoryInterface
{
    public function save(Memo $memo): Memo;

    public function update(int $memoId, string $content): Memo;

    public function delete(int $memoId);
}

MemoRepository
packages/Infrastructure/Memo/MemoRepository.php
<?php

namespace packages\Infrastructure\Memo;

use packages\Domain\Domain\Memo\Memo;
use packages\Domain\Domain\Memo\MemoRepositoryInterface;
use App\Models\Memo as EloqMemo;

class MemoRepository implements MemoRepositoryInterface
{
    /**
     * @param Memo $memo
     * @return Memo
     */
    public function save(Memo $memo): Memo
    {
        $eloqMemo = new EloqMemo();

        $eloqMemo->fill(
            [
                'user_id' => $memo->getUserId(),
                'content' => $memo->getContent()
            ])->save();

        return new Memo($eloqMemo->id, $eloqMemo->user_id, $eloqMemo->content, $eloqMemo->created_at);
    }

    /**
     * @param int $memoId
     * @param string $content
     * @return Memo
     */
    public function update(int $memoId, string $content): Memo
    {
        $eloqMemo = EloqMemo::find($memoId);
        $eloqMemo->fill(['content' => $content])->save();
        return new Memo($eloqMemo->id, $eloqMemo->user_id, $eloqMemo->content, $eloqMemo->created_at);
    }

    /**
     * @param int $memoId
     */
    public function delete(int $memoId)
    {
        $eloqMemo = EloqMemo::find($memoId);
        $eloqMemo->delete();
    }
}

MemoQueryServiceInterface
packages/UseCase/Memo/QueryService/MemoQueryServiceInterface.php
<?php

namespace packages\UseCase\Memo\QueryService;

use packages\Domain\Domain\Memo\Memo;
use packages\UseCase\Memo\Dto\MemoDetailDto;
use packages\UseCase\Memo\Dto\MemoEditDto;

interface MemoQueryServiceInterface
{
    public function fetchUsersMemo(int $userId): array;

    public function getMemoDetail(int $memoId): MemoDetailDto;

    public function getEditTarget(int $memoId): MemoEditDto;

    public function findById(int $memoId): Memo;
}
MemoQueryService
packages/Infrastructure/Memo/MemoQueryService.php
<?php

namespace packages\Infrastructure\Memo;

use packages\Domain\Domain\Memo\Memo;
use packages\UseCase\Memo\Dto\MemoDetailDto;
use packages\UseCase\Memo\Dto\MemoEditDto;
use packages\UseCase\Memo\Dto\UsersMemoDto;
use packages\UseCase\Memo\QueryService\MemoQueryServiceInterface;
use App\Models\Memo as EloqMemo;

class MemoQueryService implements MemoQueryServiceInterface
{
    /**
     * @param int $userId
     * @return array
     */
    public function fetchUsersMemo($userId): array
    {
        $eloqMemoList = EloqMemo::where('user_id', $userId)->get()->all();
        $usersMemoDtoList = array_map(function ($eloqMemo){
            return new UsersMemoDto($eloqMemo->id, $eloqMemo->content, $eloqMemo->created_at);
        }, $eloqMemoList);

        return $usersMemoDtoList;
    }

    /**
     * @param int $memoId
     * @return MemoDetailDto
     */
    public function getMemoDetail(int $memoId): MemoDetailDto
    {
        $eloqMemoModel = EloqMemo::find($memoId);
        return new MemoDetailDto($eloqMemoModel->id, $eloqMemoModel->content, $eloqMemoModel->created_at);
    }

    /**
     * @param int $memoId
     * @return MemoEditDto
     */
    public function getEditTarget(int $memoId): MemoEditDto
    {
        $eloqMemoModel = EloqMemo::find($memoId);
        return new MemoEditDto($eloqMemoModel->id, $eloqMemoModel->content);
    }

    /**
     * @param int $memoId
     * @return Memo
     */
    public function findById(int $memoId): Memo
    {
        $eloqMemoModel = EloqMemo::find($memoId);
        $memo = new Memo($eloqMemoModel->id, $eloqMemoModel->user_id, $eloqMemoModel->content,
            $eloqMemoModel->created_at);

        return $memo;
    }
}


Use Cases(とControllers)

この層はControllerからの呼び出しと合わせて説明します。

Use Casesは、アプリケーションができることを表す部分です。
アプリケーションができることというのは、今回でいうと

  • メモの作成
  • メモの更新
  • メモの削除
  • メモの一覧表示
    etc...
    みたいなことを意味します。
    対象となる部分が多いので、永続化処理と参照処理をそれぞれ一つずつ紹介します。他の部分を確認されたい場合は、上に貼ったGitHubのリンクからコードをご覧になってください。

create

メモの新規作成処理です。
引数として渡ってきたパラメーターの値からMemoモデルを作成し、MemoRepositoryのsaveメソッドに渡します。
以下がinterfaceと実装クラスです。

packages/UseCase/Memo/Create/MemoCreateUseCaseInterface.php
<?php

namespace packages\UseCase\Memo\Create;

use packages\Domain\Domain\Memo\Memo;

interface MemoCreateUseCaseInterface
{
    public function create(MemoCreateRequest $request): Memo;
}

packages/Domain/Application/Memo/MemoCreateInteractor.php
<?php

namespace packages\Domain\Application\Memo;

use Carbon\Carbon;
use packages\Domain\Domain\Memo\Memo;
use packages\Domain\Domain\Memo\MemoRepositoryInterface;
use packages\UseCase\Memo\Create\MemoCreateRequest;
use packages\UseCase\Memo\Create\MemoCreateUseCaseInterface;

class MemoCreateInteractor implements MemoCreateUseCaseInterface
{
    private MemoRepositoryInterface $memoRepository;

    /**
     * MemoCreateInteractor constructor.
     * @param MemoRepositoryInterface $memoRepository
     */
    public function __construct(MemoRepositoryInterface $memoRepository)
    {
        $this->memoRepository = $memoRepository;
    }

    /**
     * @param MemoCreateRequest $request
     * @return Memo
     */
    public function create(MemoCreateRequest $request): Memo
    {
        $memo = new Memo(mt_rand(), $request->getUserId(), $request->getContent(), Carbon::now());
        return $this->memoRepository->save($memo);
    }
}

MemoRepositoryのsaveメソッドでは、以下のようにEloquentのモデルを新規作成しDBに保存->Memoモデルを返すという処理を行っています。

MemoRepository
    /**
     * @param Memo $memo
     * @return Memo
     */
    public function save(Memo $memo): Memo
    {
        $eloqMemo = new EloqMemo();

        $eloqMemo->fill(
            [
                'user_id' => $memo->getUserId(),
                'content' => $memo->getContent()
            ])->save();

        return new Memo($eloqMemo->id, $eloqMemo->user_id, $eloqMemo->content, $eloqMemo->created_at);
    }

MemoControllerからは以下のように呼び出しています
バリデーションはFormRequestを使いました

MemoController
    /**
     * 新規作成
     * @param MemoCreateFormRequest $request
     * @param MemoCreateUseCaseInterface $interactor
     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
     */
    public function create(MemoCreateFormRequest $request, MemoCreateUseCaseInterface $interactor)
    {
        $userId = Auth::user()->id;
        $content = $request->get('content');
        $memoCreateRequest = new MemoCreateRequest($userId, $content);
        $interactor->create($memoCreateRequest);
        return redirect(route('index'));
    }

updateとdeleteも基本的には同じ処理です。渡されたパラメーターからMemoモデルを作成し、Repositoryで永続化処理を行います。
ただ、deleteについては戻り値がありません。何を返すのがベストか正直わかりませんでした。(どうやら戻り値がないのがクリーンとされているらしいです。図らずもクリーンになってしまったのかもしれません)


index

作成したメモの一覧表示です。
パラメーターの値をMemoQueryServiceのfetchUsersMemoに渡して、DBからデータを取得する処理を依頼します。

packages/UseCase/Memo/MemoIndexUseCaseInterface.php
<?php

namespace packages\UseCase\Memo\Index;

interface MemoIndexUseCaseInterface
{
    public function getMemoList(MemoIndexRequest $request): array;
}

MemoQueryServiceのfetchUsersMemoメソッドでは、検索結果の値からUsersMemoDtoというモデルを作成し、その配列を返します。

MemoQueryService
    /**
     * @param int $userId
     * @return array
     */
    public function fetchUsersMemo($userId): array
    {
        $eloqMemoList = EloqMemo::where('user_id', $userId)->get()->all();
        $usersMemoDtoList = array_map(function ($eloqMemo){
            return new UsersMemoDto($eloqMemo->id, $eloqMemo->content, $eloqMemo->created_at);
        }, $eloqMemoList);

        return $usersMemoDtoList;
    }
packages/Domain/Application/Memo/MemoIndexInteractor.php
<?php

namespace packages\Domain\Application\Memo;

use packages\UseCase\Memo\Index\MemoIndexRequest;
use packages\UseCase\Memo\Index\MemoIndexUseCaseInterface;
use packages\UseCase\Memo\QueryService\MemoQueryServiceInterface;

class MemoIndexInteractor implements MemoIndexUseCaseInterface
{
    private MemoQueryServiceInterface $memoQueryService;

    /**
     * MemoIndexInteractor constructor.
     * @param MemoQueryServiceInterface $memoQueryService
     */
    public function __construct(MemoQueryServiceInterface $memoQueryService)
    {
        $this->memoQueryService = $memoQueryService;
    }

    /**
     * @param MemoIndexRequest $request
     * @return array
     */
    public function getMemoList(MemoIndexRequest $request): array
    {
        return $this->memoQueryService->fetchUsersMemo($request->getUserId());
    }
}

UsersMemoDtoは以下のようになっています

UsersMemoDto
<?php

namespace packages\UseCase\Memo\Dto;

class UsersMemoDto
{
    private int $memoId;
    private string $content;
    private \DateTime $createdAt;

    /**
     * UsersMemoDto constructor.
     * @param int $memoId
     * @param string $content
     * @param \DateTime $createdAt
     */
    public function __construct(int $memoId, string $content, \DateTime $createdAt)
    {
        $this->memoId = $memoId;
        $this->content = $content;
        $this->createdAt = $createdAt;
    }

    /**
     * @return int
     */
    public function getMemoId()
    {
        return $this->memoId;
    }

    /**
     * @return string
     */
    public function getContent()
    {
        return $this->content;
    }

    /**
     * @return \DateTime
     */
    public function getCreatedAt()
    {
        return $this->createdAt;
    }
}

MemoControllerのindexは以下の通りです。

MemoController
    /**
     * 一覧
     * @param MemoIndexUseCaseInterface $interactor
     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
     */
    public function index(MemoIndexUseCaseInterface $interactor)
    {
        $userId = Auth::user()->id;

        $memoIndexRequest = new MemoIndexRequest($userId);
        $usersMemoDtoList = $interactor->getMemoList($memoIndexRequest);

        $memoViewModels = array_map(function ($usersMemo){
            return new MemoViewModel($usersMemo->getMemoId(), $usersMemo->getContent());
        }, $usersMemoDtoList);
        $MemoIndexViewModel = new MemoIndexViewModel($memoViewModels);

        return view('index', compact('MemoIndexViewModel'));
    }

返ってきたUsersMemoDtoの配列からMemoViewModelというモデルの配列を作成し、その配列を引数に持たせてMemoIndexViewModelというモデルを生成します。
イメージとしては

//このMemoはEloquentのMemo
Memo::where('user_id', $userId)->get();

で返ってくるCollection => MemoIndexViewModel
そのCollectionの中身のMemoモデル => MemoViewModel
みたいな感じです。

MemoViewModelとMemoIndexViewModelの内容はこのようになっています。

app/Models/MemoViewModel.php
<?php

namespace App\Models;

class MemoViewModel
{
    public int $id;
    public string $content;
    public $createdAt;

    /**
     * MemoViewModel constructor.
     * @param int $id
     * @param string $content
     * @param \DateTime|null $createdAt
     */
    public function __construct(int $id, string $content, \DateTime $createdAt = null)
    {
        $this->id = $id;
        $this->content = $content;
        $this->createdAt = $createdAt;
    }
}

app/Models/Memo/MemoIndexViewModel.php
<?php

namespace App\Models;

class MemoIndexViewModel
{
    public array $memos;

    /**
     * MemoIndexViewModel constructor.
     * @param MemoViewModel[] $memos
     */
    public function __construct(array $memos)
    {
        $this->memos = $memos;
    }

}

viewではEloquentのモデルと同じように展開できます

resources/views/index.blade.php
<table class="table table-hover">
    <tbody>
        @if(!empty($MemoIndexViewModel->memos))
            @foreach($MemoIndexViewModel->memos as $memo)
                <tr>
                    <td>{{$memo->content}}</td>
                    <td>
                        <button type="button" class="btn btn-outline-primary">
                            <a href="{{route('show', ['memoId' => $memo->id])}}">show</a>
                        </button>
                    </td>
                    <td>
                        <button type="button" class="btn btn-outline-primary">
                            <a href="{{route('edit', ['memoId' => $memo->id])}}">edit</a>
                        </button>
                    </td>
                    <td>
                        <form method="POST" action="{{route('delete', ['memoId' => $memo->id])}}">
                            @csrf
                            <button type="submit" class="btn btn-outline-danger">delete</button>
                        </form>
                    </td>
                </tr>
            @endforeach
        @endif
    </tbody>
</table>

テストを書いてみる

メモ作成機能のテスト

//TestBaseでユーザーを作成しています

<?php

namespace Tests\Unit;

use packages\Domain\Application\Memo\MemoCreateInteractor;
use packages\Infrastructure\Memo\MemoRepository;
use packages\UseCase\Memo\Create\MemoCreateRequest;
use Tests\Base\TestBase;

class MemoCreateInteractorTest extends TestBase
{
    public function testMemoCreate()
    {
        $userId = $this->user->id;
        $str = null;
        for ($i = 0; $i < 10; $i++){
            $str .= chr(mt_rand(97, 122));
        }
        $content = $str;
        $repository = new MemoRepository();
        $memoCreateRequest = new MemoCreateRequest($userId, $content);
        $interactor = new MemoCreateInteractor($repository);
        $createdMemo = $interactor->create($memoCreateRequest);

        var_dump($createdMemo);

        $this->assertNotNull($createdMemo);
        $this->assertNotNull($createdMemo->getId());
        $this->assertEquals($userId, $createdMemo->getUserId());
        $this->assertEquals($str, $createdMemo->getContent());
        $this->assertNotNull($createdMemo->getCreatedAt());

    }
}

やってみた所感

今回サンプルを作成してみて思ったこと

  • QueryServiceはそのまま呼び出してもいいのでは
  • 参照系の処理の結果を、専用のDTOに詰めるのか、ドメインモデルに詰めるのか、場合によってどうするのが適切なのかをちゃんと考えないとかなり不便になりそう。
  • ↑も含めて、機械的に同じような実装をしていると詰みそう。しっかり意識しながら実装する必要があるので、そういうクリーンさは体感できたかも(と言いつつ依存の方向とかは正直あまり意識できていない...)
  • viewで使うためのview modelが無限に増殖しそう
  • 共通化できそうな処理をある程度共通化してしまっていいのか悩む。なんとなく、あんまり共通化しすぎない方が変更に強そう

参考にした記事一覧

多いので別記事にまとめました。
https://qiita.com/koyablue/items/e0e8d66803bef789b6bc

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

Laravelでクリーンアーキテクチャを実装するときに参考にしたいリンク集

  • DDDとかクリーンアーキテクチャについて調べるにあたって参考にした記事一覧です
  • Laravelで実装することを目的として調べたので、ほぼPHP/Laravel系の記事です
  • いいのがあれば随時追加予定

"Laravelでクリーンアーキテクチャ" の実装サンプルが見たい場合

実際のコードを見たい人向け

Entityとは何かわからなくなった場合

正直わからないです

Eloquentをクリーンアーキテクチャで使いたくなった場合

「フレームワークに依存しない」の正義に疑問を抱いた人向け

参照系の実装でつらみを感じた場合

一覧表示画面などの実装中に不穏な空気を感じ取った人向け

うっかり "is not instantiable" エラーに出くわした場合

bindし忘れた人向け

(おまけ) シリコンバレーではどうなのか知りたい場合

井の中の蛙は嫌な人向け

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

Laravelでneo4jを使ってみる。プッシュ通知メモ

push通知のライブラリ入れる細かい設定は他サイト参照

composer require laravel-notification-channels/webpush

※以下neo4jで注意する点

デフォルトのモデルはNeoEloquentを使っていないので変更

config/webpush.php
'model' => \App\Models\PushSubscription::class,

モデル

app/Models/PushSubscription.php
<?php

namespace App\Models;


use Vinelab\NeoEloquent\Eloquent\Model as NeoEloquent;

/**
 * @property string $endpoint
 * @property string|null $public_key
 * @property string|null $auth_token
 * @property string|null $content_encoding
 * @property \Illuminate\Database\Eloquent\Model $subscribable
 */
class PushSubscription extends NeoEloquent
{
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
     protected $label = 'PushSubscriptions'; 
    protected $fillable = [
        'endpoint',
        'public_key',
        'auth_token',
        'content_encoding',
    ];

    /**
     * Create a new model instance.
     *
     * @param  array $attributes
     * @return void
     */
    public function __construct(array $attributes = [])
    {
        if (! isset($this->connection)) {
            $this->setConnection(config('webpush.database_connection'));
        }

        if (! isset($this->table)) {
            $this->setTable(config('webpush.table_name'));
        }

        parent::__construct($attributes);
    }

    /**
     * Get the model related to the subscription.
     *
     * @return \Illuminate\Database\Eloquent\Relations\MorphTo
     */
    public function subscribable()
    {
        return $this->morphTo();
    }

    /**
     * Find a subscription by the given endpint.
     *
     * @param  string $endpoint
     * @return static|null
     */
    public static function findByEndpoint($endpoint)
    {
        return static::where('endpoint', $endpoint)->first();
    }
}

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