- 投稿日:2020-09-28T23:22:35+09:00
[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/ui
を1.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
- 投稿日:2020-09-28T23:00:14+09:00
【webpack】sassをバンドルする際に出たエラーの対処
環境
PHP 7.3.8
Laravel 6.18.40
webpack 4.27.1TypeError: 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-loader
、node-sass
、sass-loader
、style-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",参考にした記事
- 投稿日:2020-09-28T18:12:52+09:00
Laravelディレクトリ構成+DBマイグレーション
今回はLaravelディレクトリ構成+DBマイグレーションを説明していきます。
ディレクトリ構成
ディレクトリ名 役割 app アプリのメインとなるところ bootstrap 初期処理やキャッシュなど config アプリの設定 database データベース(マイグレーション) public 画像、JS、CSSなど resources bladeなど(HTML) routes アプリのURL設定 storage セッションやログ tests テスト用 vendor Comoposerの依存内容 マイグレーション
マイグレーションとは
SQLを直接使わなくても、データベースを管理できるLaravelの仕組みです。
マイグレーションファイル
マイグレーションファイルは「どんなテーブルを作るか」というテーブルの設計書です。設計書を作ったらそれを実行しなければテーブルはできないので、この実行にあたるのがマイグレーションです。
テーブル作成の流れ
1. マイグレーションファイルを生成
2. ファイルにテーブル定義を書く
3. マイグレーションファイルを実行して、DBに反映ファイルの場所とファイル名ルール
ファイル生成コマンド
$ 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
- 投稿日:2020-09-28T17:10:58+09:00
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はめちゃわかりやすかった.
ざっくり言えば,
- コントローラー(○○Controller.php)で諸々の機能を作る.
- Webページのテンプレート(○○.blade.php)を作って機能を埋め込む.
- ルート(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では「.」で結合するようです.
下が例です.
javascriptstr = '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!'が出力される終わり
とりあえずここまでです.
また感想があれば追加します.
さらば!!
- 投稿日:2020-09-28T16:52:02+09:00
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/cachestorageと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にアクセス
これはLaravelのトップページがpublic配下に存在するためです。localhostに接続した際に以下の画面が表示されれば正しく接続できています。3.DB作成
webブラウザ上のMAMPの中でTOOLSのPHPmyadminを開きます。
1. サイドバーのnewを押して新しくDBを押す。
2. Create Databaseと出てくるので名前のところにblogと追加。
3. 右のセレクトボックスをutf8mb4_unicode_ciとする。
4. Createボタンを押す。
左側のサイドバーにblogと追加されれば完了です。次にご自身のテキストエディターでblogを開いてください。.env.exampleというファイルが存在するのでひらいてください。.envファイルという似たようなファイルも存在しますが、.env.exampleの方に記載する事でGitで管理できるので複数人で開発をする際にはこちらに記載する方が良いらしいです。間違ってたらすいません。
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);を追加。
Timezone
Timezoneの変更をしていきます。
1. config/app.phpに移動
2. 70行目のUTCをAsia/Tokyoに変更
3. 83行目をlocaleを「ja」に変更これで初期設定は終了になります。
終わりに
以上が初期設定でした。次回は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
- 投稿日:2020-09-28T14:49:06+09:00
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.iniupload_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/
- 投稿日:2020-09-28T11:33:58+09:00
[Laravel]OAuthによるログイン時に前画面からパラメータを引き継ぐ方法
はじめに
OAuthによるログインの実装は、一度外部サービスへリダイレクトを行って、外部サービスから自サービスのコールバックエンドポイントにリダイレクトを受けるという流れになります。
ログイン時には、「どの画面からログインを試みたのか?」 「どういう登録導線からログインされたのか?」 といったログイン処理前の情報を受け取りたいニーズがある場合があります。このとき、ログイン画面からコールバックエンドポイントに情報を伝達したい場合どうしたらいいかという話です。
ログインに限らず、登録のケースでも同じようにできます。セッションを利用する
https://readouble.com/laravel/7.x/ja/session.html
HTTPはステートレスなので、リクエストをまたがって情報を保持するには以下のいずれか対応をする必要があります。
- POSTパラメータまたはGETパラメータを次ページに引回す
- セッションにデータを紐づけて一時的に保持する
- クッキーにユーザを特定するキーを仕込んで、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先から情報を取得してログインする処理 }
GETクエリは設定でき、OAuthの連携先であるFacebookにGETパラメータが送られるが、コールバック時にGETパラメータを返してくれなかったので、自サービスが受けるコールバックまでデータを伝搬できなかった。 ↩
- 投稿日:2020-09-28T09:37:05+09:00
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をクリーンアーキテクチャっぽくしてゆきましょう。
ということで、あの図を貼っておきます。
準備
appとおなじ階層にpackagesというディレクトリを作成します。今回はこの中でいろいろ細かく区切って実装してゆくことにしました。
ディレクトリを追加したら、ちゃんとLaravelに読み込んでもらえるようcomposer.jsonも編集しておきます。composer.json"autoload": { "psr-4": { "App\\": "app/" }, "classmap": [ "database/seeds", "database/factories", "packages" <-これを追加 ] }事前準備はこれで終わりです。
packagesディレクトリの全体像
ちなみに完成後のpackagesディレクトリは、以下のようになります
このディレクトリの置き方がいいのか悪いのかちょっと自信がありませんが、とりあえず今回は、こんな感じの配置なんだなーと思っていただければ。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
- 投稿日:2020-09-28T09:36:07+09:00
Laravelでクリーンアーキテクチャを実装するときに参考にしたいリンク集
- DDDとかクリーンアーキテクチャについて調べるにあたって参考にした記事一覧です
- Laravelで実装することを目的として調べたので、ほぼPHP/Laravel系の記事です
- いいのがあれば随時追加予定
"Laravelでクリーンアーキテクチャ" の実装サンプルが見たい場合
実際のコードを見たい人向け
Entityとは何かわからなくなった場合
正直わからないです
- ドメイン駆動設計のエンティティとクリーンアーキテクチャのエンティティ
- お前らがModelと呼ぶアレをなんと呼ぶべきか。近辺の用語(EntityとかVOとかDTOとか)について整理しつつ考える
- Entity in DDD ≠ Entity in Clean Architecture
Eloquentをクリーンアーキテクチャで使いたくなった場合
「フレームワークに依存しない」の正義に疑問を抱いた人向け
参照系の実装でつらみを感じた場合
一覧表示画面などの実装中に不穏な空気を感じ取った人向け
うっかり "is not instantiable" エラーに出くわした場合
bindし忘れた人向け
(おまけ) シリコンバレーではどうなのか知りたい場合
井の中の蛙は嫌な人向け
- 投稿日:2020-09-28T09:32:53+09:00
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(); } }