- 投稿日:2019-12-09T23:36:08+09:00
【Laravel】 認証でユーザを取得するとき,特定のスコープに限定する
問題
class User extends Model implements Authenticatable { use SoftDeletes; }Auth::user() // 論理削除ユーザは除外される
SoftDeletes
を使用しているAuthenticatable
の場合,グローバルスコープで論理削除を除外するものが付与されるので,認証時にも除外される。ところが論理削除とは違う「有効アカウント」「無効アカウント」といった表現をstatus={0,1}
のように行っている場合,素の状態では対応できない。ここでは,できるだけ Laravel 標準の Auth に乗っかる形での実装を目指して対応してみる。(備考)【Laravel】 認証や認可に関する補足資料 - Qiita
実装
AuthScopable
インタフェースの作成モデルごとに実装は異なるので,共通して使えるようにインタフェースを作成する。命名や引数・返り値の方は Laravel の標準的なスコープの規約に従っている。
<?php namespace App\Auth; use Illuminate\Database\Eloquent\Builder; interface AuthScopable { /** * Add a scope for authentication. * * @param \Illuminate\Database\Eloquent\Builder $query * @return \Illuminate\Database\Eloquent\Builder */ public function scopeForAuthentication(Builder $query): Builder; }
ScopedEloquentUserProvider
の作成
EloquentUserProvider
は Laravel の標準実装にあり,既定ではこれが使用されている。今回はここの一部を継承して,認証でクエリが発行されるときにAuthScopable
で示されるスコープを適用できるようにしてみる。<?php namespace App\Auth; use Illuminate\Auth\EloquentUserProvider; class ScopedEloquentUserProvider extends EloquentUserProvider { /** * @param null|\Illuminate\Database\Eloquent\Model $model * @return \Illuminate\Database\Eloquent\Builder */ public function newModelQuery($model = null) { $query = parent::newModelQuery($model); $instance = $query->getModel(); if ($instance instanceof AuthScopable) { $query = $instance->scopeForAuthentication($query); } return $query; } }
AuthServiceProvider
での登録<?php namespace App\Providers; use App; use App\Auth\ScopedEloquentUserProvider; use App\Policies; use App\User; use Illuminate\Contracts\Container\Container; use Illuminate\Support\Facades\Auth; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; class AuthServiceProvider extends ServiceProvider { /** * The policy mappings for the application. * * @var array */ protected $policies = [ App\User::class => Policies\UserPolicy::class, ]; /** * Register any authentication / authorization services. */ public function boot(): void { $this->registerPolicies(); // EloquentUserProvider をオーバーライド Auth::provider('eloquent', function (Container $app, array $config) { return $app->make(ScopedEloquentUserProvider::class, [ 'model' => $config['model'], ]); }); } }モデルへの適用
あとは書くだけ!
class User extends Model implements Authenticatable, AuthScopable { public function scopeForAuthentication(Builder $query): Builder { return $query->where('status', 1); } }Auth::user() // ステータスが 0 のユーザは除外されるおまけ
標準的なスコープ命名規則に従っているので,一応こんな使い方もできます…
(多分そんなに使わないと思うけど)User::where('email', 'xxx@example.com')->forAuthentication()->firstOrFail()User::where('email', 'xxx@example.com')->scopes(['forAuthentication'])->firstOrFail()追記
OSS化しました
mpyw/scoped-auth: Apply specific scope to for user authentication.
- 投稿日:2019-12-09T20:31:25+09:00
Composerと、Homebrewのまとめ
~はじめに~
僕は、つい最近エンジニアとしてqnoteに就職した21歳のオトコです。
まだまだ技術として自慢出来るものや、人間として誇れるものも何もありませんがこれから少しずつでも前に進んで行こうと思っています。
元々、自動車販売の営業や自販機の補充をする仕事などをやっていて、そこでは自分の好きな事をやれている実感がなかったので転職しました。(ほんとはもっといろんな理由がありますが、、、笑)
最初はRubyとrailsを学習していて、PHPやLaravelは触った事もありませんでした。
(ゆうてもRubyも使いこなしていた訳ではありませんが。笑)
そこで、Laravelの開発環境を整えている最中、『何やねんこれ』って思った"Composer"と
"Homebrew"について初心者なりに調べて自分なりにまとめてみました。出来るだけ分かりやすい説明を心掛けます
(間違っていたら指摘してくださいっっっm(_ _)m)そもそもComposerとはなんぞや??
Laravel開発環境とかで調べてると、「とりあえずComposerインストールしてな( ^∀^)」くらいの感じで言われる事が多く、Composerが何なのかという説明がない事が多かったです。
「じゃあ調べよう!!!!!!!!!!!!!!!!」
と思って調べました?
一言で言うとComposerは、「PHPのパッケージ依存性管理システムである。」
「ん☺️???何それ☺️」って初心者は思う訳ですよ。じゃあ噛み砕いていきましょう。パッケージの依存管理とは何か??
まず最初にパッケージの概念について。
例えですが、一つ一つの部品(モジュール)を集めて箱に入れるとその箱の名前は(パッケージ)になります。
要するに、様々な部品が入った塊がいろんな機能を持った的な感じかと僕は思ってます。
※パッケージをいくつか集めてインストール出来る様にすると『ライブラリ』になります。じゃあ依存とは??
こんな男性も少なくないと思いますが、彼女に「お前がいないと俺もう無理だ。生きていけないよ」っていうのが人間の依存だと思うのですが、それと一緒で、パッケージをインストールしても、他のパッケージ(彼女)がないと動けないパッケージがあります。~だからなんやねん~
って思ったそこのあなた。ここでComposerの出番です。
例えば、欲しいパッケージがあるとします。
「このパッケージほしいなああ〜〜。でも、このパッケージをインストールしたらあのパッケージもインストールしないとだし。あれもこれも。。。。(泣)」
あ。おワタ。。。依存地獄や。。。
でも、Composerを使うとコマンド一発で必要なパッケージを全てインストールできちゃうんです。
※packagistという所からパッケージを取得します。packagistはインターネット上に公開されているメインリポジトリです。Composerのいい所
- リポジトリを汚さなくて済む。(Composerでインストールしたパッケージは「Vender」というディレクトリに一括で保存される。)
- ライブラリの依存性はcomposer.jsonに書き出される為、分かりやすい。自分で作成するか、対話形式で作成することも出来る。
- 一回のコマンドで依存するパッケージを全て持ってこれる。
- composer.lockのおかげでチームに共有しやすい。
共有するには
同じチームのメンバーが、Composerで管理されたライブラリを落とすにはcomposer.jsonを共有すればいい。
composer.jsonがあるディレクトリ上で$ composer installを実行するだけで依存しているライブラリをインストールする事ができる。ここでのcomposerの挙動
1 各ライブラリを見に行く
2 見に行ったライブラリに依存性があるか確認。
3 1,2を繰り返し確認を行っていく。そこで思うのは、毎回これを共有した皆んながやると思うと時間かかってしょうがなくね??
でも大丈夫!「composer.lock」
composer.lockというファイルが生成されている。
これは、composer.jsonの中にあるライブラリを取得するのに実際にどのファイルを落としたのかをひとまとめにしたもの。
つまり、composer.lockがあるからいちいち依存性を確認しに行かなくてもいい。初めてライブラリをインストールした人と同じファイル、バージョンのものをインストールできる。composerでよく使用するコマンド
ここではよく使うComposerのコマンドを紹介していきます。
$composer init
composerを使ったプロジェクトを自作するときに使う。ちゃちゃっとライブラリを書きたいときなど。対話式でいくつか設定を加えることもできるが、全部スキップしても問題はない。
$composer createーproject
git cloneして、composer installするのと全く同じ結果になる。
$composer require <package> [:tag]
composerを使ってパッケージや、ライブラリをインストールする。
ライブラリの情報は「composer.json」に記載され、実際に何をインストールしたかの情報が「composer.lock」に記載される。オプションとして、「--dev」をつけると開発環境専用のパッケージとしてインストールが出来、composer.jsonの中の「require-dev」の項目に追加される。
$composer install
composer.jsonに記載されれている内容、もしくはcomposer.lockに従ってパッケージをインストールする。パッケージが更新されたら再度このコマンドを実行する。
このコマンドを打つことにより、チームメンバーはパッケージの依存性を解決することができる。
$composer update
composer.lockを無視してパッケージをcomposer.jsonを元にインストールする。
現在の依存性を更新して全部最新化したいときなどに使う。
$composer remove <package>
requierの逆バージョン。
必要のないパッケージを取り除く。Homebrewとは?
Homebrewとは、macOS用パッケージマネージャーのことです。
パッケージマネージャーって何??
ターミナルからコマンドを実行することにより、簡単にパッケージをイントールしたりアンインストールできるツール。
Homebrewの使い方
Homebrewでは、「brew」コマンドを使ってパッケージの管理を行う。
代表的なbrewコマンド↓
$brew install パッケージ名
パッケージのインストールを行う。「brew」コマンドで一番よく使うコマンドになる。
$brew uninstall パッケージ名
パッケージのアンインストールを行う。
$brew update
Homebrew本体のアップデートを行う。
パッケージの最新バージョンが使えるようになる、使えるパッケージが増えたりする。
$brew upgrade
パッケージの更新。インストールしてあるパッケージは全て更新される。
$brew upgrade パッケージ名
インストールしている特定のパッケージを更新する。
$brew list
インストール済みのパッケージ一覧表示。
$brew search パッケージ名
パッケージの検索。Homebrew豆知識
Homebrewの意味を皆さんご存知でしょうか??
直訳すると、「自家醸造」になります。
ロゴから見てもわかりますが、ビールの自家醸造というなんとも酒好きにはたまらないネーミングセンスです。
僕は、ビールよりウィスキーの方が好きですが酒好きには変わりありません( ´∀`)
このHomebrewでは「自家醸造」にまつわる事が使う上でのヒントとなるようです。
参考記事
https://qiita.com/omega999/items/6f65217b81ad3fffe7e6「brew」
これは「醸造する」という意味になりますが、「makeする、作る」という意味になる。「homebrew」
これは「自家醸造する」という意味ですがこれは「userが自分で作り、使う」という意味になる。「celler」
ワインセラーとかの'セラー'と一緒で「保管しておく、保存先」という意味になる。「keg」
樽や醸成用、意味としては「作るための材料」になる。「formula」
直訳すると、「式」になるがそれまでの過程、方法、手順という意味になる。例えがとてもわかりやすかったので引用させていただきます。。。
homebrewとは「ユーザが自らパッケージをビルドして使用する」ことのメタファーで「ビールを自家醸造して保存する・飲む」ことを意味しているのです。
手順(調理法formula)通りにパッケージをビルド(醸造)して保存(/usr/local/cellerに格納)して、使う(/usr/loca/binにリンク)ってことのようです。まとめ
ComposerやHomebrewの動きが全然わからなかった僕ですが、一つ一つの役割や特徴が分かると頭に入って来やすかったです。
まだまだ勉強不足で知らないことばっかりですが、分からない事が分かる様になる楽しさがたまらなく好きなので、
これからも勉強して、少しでも会社に貢献できればなと思っております。
これを機に、勉強したことを少しずつ記事にして投稿していければなと思いますのでよろしくお願いします。
(正しいことを覚えたいので、間違っている事があればガンガン指摘していただいて構いませんので、、、、)最後に
qnoteの皆さん。まだまだこんなへなちょこりんで迷惑ばっかりかけていますが、皆さんに少しでも追いつけるように頑張りますのでこれからもよろしくお願いします!!!
- 投稿日:2019-12-09T18:14:55+09:00
laravel homesteadをインストールしようとしたらできなかった話
Laravel homesteadをインストールしようとした
ちょっと所用でlaravelのテスト環境がほしくなったため、vagrantで環境を作ろうと思った。
何度かvagrantで環境をつくってはいるものの、トリアタマなため、調べながらコマンドをぽちぽち…。すると、エラーが出てインストールできなかった。
$ vagrant up Bringing machine 'homestead' up with 'virtualbox' provider... ==> homestead: Box 'laravel/homestead' could not be found. Attempting to find and install... homestead: Box Provider: virtualbox homestead: Box Version: >= 9.0.0 ==> homestead: Loading metadata for box 'laravel/homestead' homestead: URL: https://vagrantcloud.com/laravel/homestead ==> homestead: Adding box 'laravel/homestead' (v9.1.0) for provider: virtualbox homestead: Downloading: https://vagrantcloud.com/laravel/boxes/homestead/versions/9.1.0/providers/virtualbox.box homestead: Download redirected to host: vagrantcloud-files-production.s3.amazonaws.com homestead: Calculating and comparing box checksum... The specified checksum type is not supported by Vagrant: sha512. Vagrant supports the following checksum types: md5, sha1, sha256?????
checksumがちがうらしい
The specified checksum type is not supported by Vagrant: sha512. Vagrant supports the following checksum types: md5, sha1, sha256
ここの部分。指定されたチェックサムは sha512 だけど、vagrantがサポートしてるのは以下のとおりだよ、と言われていました。
…じゃあどうすれば????全く同じことを思ってる人がいました。
https://stackoverflow.com/questions/59133168/laravel-homestead-vagrant-box-error-the-specified-checksum-type-is-not-supportちなみにここでも動かない!って言ってた
https://github.com/laravel/homestead/issues/1325vagrantをアップデートすればいいらしい
質問への回答をみると、どうやら、新しいバージョンはチェックサムのサポートが変わったらしい。
なので、vagrant自体をアップデートすればいいよ!とのこと。
現在の最新バージョンは2.5.6とのこと。
どれどれ…$ vagrant --version Vagrant 2.2.5はい。
パッケージをインストールしてきて、アップデート。$ vagrant --version Vagrant 2.2.6これでいけるでしょう!!!
$ vagrant up Vagrant failed to initialize at a very early stage: The plugins failed to initialize correctly. This may be due to manual modifications made within the Vagrant home directory. Vagrant can attempt to automatically correct this issue by running: vagrant plugin repair If Vagrant was recently updated, this error may be due to incompatible versions of dependencies. To fix this problem please remove and re-install all plugins. Vagrant can attempt to do this automatically by running: vagrant plugin expunge --reinstall Or you may want to try updating the installed plugins to their latest versions: vagrant plugin update Error message given during initialization: Unable to resolve dependency: user requested 'vagrant-hostsupdater (= 1.1.1.160)'次はなんだ。
初期化に失敗したらしい
どうも初期化に失敗したらしい。
vagrant plugin expunge --reinstall
を実行すれば、自動で解決するよ、とのことなので実行。$ vagrant plugin expunge --reinstall This command permanently deletes all currently installed user plugins. It should only be used when a repair command is unable to properly fix the system. Continue? [N]: All user installed plugins have been removed from this Vagrant environment! Vagrant will now attempt to reinstall user plugins that were removed. Installing the 'vagrant-hostsupdater' plugin. This can take a few minutes... Fetching: vagrant-hostsupdater-1.1.1.160.gem (100%) Fetching: micromachine-3.0.0.gem (100%) Fetching: vagrant-vbguest-0.22.1.gem (100%) Vagrant failed to properly resolve required dependencies. These errors can commonly be caused by misconfigured plugin installations or transient network issues. The reported error is: Unable to resolve dependency: user requested 'vagrant-vbguest (= 0.20.0)'解決しなかった
vagrantのプラグインもアップデートしよう
vagrant plugin update
してみて、とも書いてあったので、チャレンジ。$ vagrant plugin update Updating installed plugins... Updated 'vagrant-vbguest' to version '0.22.1'!ようやく、ここで
vagrant up
できるようになりました!!まとめ
checksumのエラーは、日本語のページがなかったので、もしかしたら私のような人がいるかもしれない…と思い、まとめました。
もしかしたらいないかもしれない。まぁ、ひさびさに使うものは、まずアップデートからしたほうがいいですね!
- 投稿日:2019-12-09T16:53:33+09:00
Laravelでドメイン駆動設計を実践し、Eloquent Model依存の設計から脱却する
この記事はドメイン駆動設計#1 Advent Calendar 2019の10日目の記事です。
やったこと
自社サイトのバックエンドをLaravelで実装して半年間が経ち、初期に考えた設計にいろいろと綻びが出てきたと感じていました。
そんな中、ちょうど実践ドメイン駆動設計やWeb+DB Pressで特集された体験DDDを読むことができたので、さっそくいくつかの機能をDDDで実装してみました。
本記事では「もともとLaravelで実践していたEloquent Model依存の設計」の問題点を提起し、「DDDを取り入れて実装した結果」のソースコードや考え方、そのメリットを記載しています。結論
- LaravelのModelをあらゆるレイヤーで使うと改修が難しくなる
- 開発する機能のユースケースを主語と述語で文章に表現し、そのままUseCase層の実装として表現する
- EntityやValueObjectに制約条件をまとめ、適切に例外を吐く
- LaravelのModelはORMとしてのみ利用する
- PHPの言語自体の限界はあるので、命名の工夫などで適宜我慢する
- 実運用の際はどの機能から、どこまで完璧主義でDDDをやるか考える
初期の設計
まずは「もともとLaravelで実践していたEloquent Model依存の設計」についてお話します。
簡単に自己紹介をさせていただきますと、今年の3月まではLIFULLという会社でWebエンジニアをしていたのですが、転職して、以降はNoSchoolという教育ベンチャーでCTOをやっています。
学生が質問し家庭教師が回答する勉強Q&Aサイトを運営しており、開発する上で意識すべきデータは「質問者・回答者」「質問」「回答」などです。Laravelの設計は、おおまかに下記の方針で行いました。
- テーブルごとにLaravelのModelを作成する(User、Question、Answer・・・)
- それぞれのModelを使ってデータをCRUDするRepositoryを作成する(UserRepository、QuestionRepository、AnswerRepository・・・)
- Repositoryからデータを取ってきたあと、サービスの仕様に合わせて整形する目的で、Serviceを作成する(UserService、QuestionService、AnswerService・・・)
- 同様にControllerを作成する(UserController、QuestionController、AnswerController・・・)
このように、あくまでテーブル構成を思い浮かべ、テーブル構成に対応したModelを作成し、以降Repository、Service、Controllerと、レイヤー化しているように見えるけど、実際はただ単にデータをリレーしているだけのアーキテクチャを組んでいました。
Modelの扱い
例えば「質問を1件取得する」場合、下記の手順で実装します。
① QuestionRepositoryに返り値として
Question
Modelを返す質問取得メソッドを実装public function findQuestion(int $id): Question { return Question::findOrFail($id); }② 次にQuestionServiceに
findQuestion(int $id)
をQuestionRepositoryのfindQuestion
を読んで返すだけの内容で実装します。public function findQuestion(int $id): Question { return $this->repository->findQuestion($id); }③ 最後にQuestionControllerを実装します。
public function findQuestion(int $id) { return view('question.find', [ 'question' => $this->service->findQuestion($id) ]); }④ bladeファイルの中では$questionを起点にデータをリレーションで取得して表示します。
<h1 class="title-question"> {{ $question->title }} </h1> <span class="author-name">{{ $question->user->name }}</span> <span>さんの質問</span> @foreach ($answers as $question->answer) <section class="answer-item"> ... 以降、質問の各回答が並ぶ ...このように、【Eloquent ModelをViewまで返す】方法で実装しました。
ModelをViewまで取り回すメリット
何にせよ開発が非常に速いです。
{{ $question->user->name }}
というのは、Question.phpにpublic function user(): BelongsTo { return $this->belongsTo(User::class)->withDefault([ // ... ]); }のようにリレーションが設定されていれば、View層でUserがQuestionのプロパティかのように繋げてアクセスできるということを示しています。
Userにリレーションが設定されていれば、さらにそこから繋げてデータにアクセスできます。
これはつまり、【View層で新しいデータが欲しいときは、Modelだけ変更すればRepository、Service、Controllerの変更が不要である】ことを示します。
もちろんRepositoryやServiceの変更を伴う改修も多いですが、新しいデータをViewに出すという要件に対しては、何も考えなければ、Questionモデルから順番にリレーションをたどってほとんどのデータにアクセスできるため改修スピードを速めることが可能です。例えばあるとき、「質問一覧画面には質問本文の最初の20文字だけ表示して欲しい」という要件があったとしましょう。下記のようにbladeファイルに直接PHPのプログラムを書くのはちょっと憚られますよね(※str_limit_jaは全角文字ベースで文字列をトリミングする関数とします)。
{{ str_limit_ja($question->body, 20) }}このとき、RepositoryやServiceを変更しなくても、Question.phpにAccessorを増やせば実装が終わります。
Question.phppublic function getShortBodyAttribute($value): string { return str_limit_ja($this->body, 20); }このようにAccessorを書けば、Viewでは
{{ $question->short_body }}で20文字にトリミングされた質問文が表示できます。
この魅力にハマった我々は、次から次へと改修案件をこの方法で捌いていきました。
我々も「Viewにロジックを載せるのは悪手」ということくらいは知っていたので、Viewにロジックを避けつつQuestionに関する処理をまとめて書けるAccessorは優秀だ!と考えました。
弊社はスタートアップということも有り、次から次へと副業のエンジニアさんも入ってきました。入ってきて既存実装を見て、「なるほどModelにAccessorを生やすのか」と真似していきました。こうしてEloquent Modelはあらゆるページに、様々なリレーションとAccessorを伴って広まっていったのです。後々やってくる苦難の道など窺い知る余地もなく・・・
発生しうる問題
このようにテーブルを起点にクラス設計を固めてしまったある日、こんな要件が発生したとします。
【質問一覧では20文字まで表示だけど、ユーザーのプロフィールで見れるユーザーの質問一覧には質問本文を40文字まで表示してね】
いつもどおりAccessorを増やそうとすると、すでにこんな実装が・・・
Question.phppublic function getShortBodyAttribute($value): string { return str_limit_ja($this->body, 20); }こうなると、諦めてbladeに
str_limit_ja
を実装するか、medium_body
のようなヤバいネーミングのAccessorを増やすか・・・みたいな選択肢になってきますね。逆もまた然りです。
【こないだの質問一覧の20文字さ、ちょっと短すぎるから一覧→詳細の遷移率上げるために30文字に増やしてよ】
よしきた、と変更したところ
Question.phppublic function getShortBodyAttribute($value): string { + return str_limit_ja($this->body, 30); - return str_limit_ja($this->body, 20); }なんと、全く関係なかったはずのユーザーのプロフィールで見れる質問一覧の文字数も30文字に増えてしまいました。
いつの間にか誰かがprofile.blade.phpでも
$question->short_body
を使っていたようです。※以上の内容はフィクションですが、実際に似たような事案が何度も発生しました。
なぜこのような問題が起こったのか?
一言で言えば、
【質問データがユーザーからどう見えるかは、ユースケースによって異なるはずなのに、全てのケースにおいて同じClassのオブジェクトを返して実現していたから】
だと考えています。
- 質問個別ページで見る質問
- 質問一覧ページで見る質問
- プロフィールページで見る質問
- 質問投稿者自身が質問個別ページで見る質問
- ログアウトユーザーが見る質問、ログインユーザーが見る質問
- 質問を投稿するときに入力する質問
といった、サービス内でも様々なユースケースに応じて姿を変える「質問」を、そもそも全て単一の「Question.php」のインスタンスで実現しようとしたことに無理があったはずです。
これはUserでも近いことが発生してしまっていました。NoSchoolの勉強Q&Aサイトでは、現役の家庭教師や塾講師が学生ユーザーの質問に回答します。それぞれのユーザーはメールアドレスやパスワードなどの認証情報を同じ
users
テーブルに所有していたことからUser.php
でModelを作成し、取り回していました。これにより、たとえば生徒しか使えない機能を実現する場合でも、理論上全てのタイプのユーザーが入りうる
$user
がbladeにやってくるから余分にif文を書かないといけない。といった問題が起こっていました。補足
LaravelをAPIサーバーメインとして活用する場合、APIリソース(https://readouble.com/laravel/6.x/ja/eloquent-resources.html) という機能を活用すれば、APIから返る値を仕様に応じたパラメータに整形する役目を担うレイヤーを作成できます。
NoSchoolでもNuxtでSPAとして開発を始めたときや、iOSアプリを開発したときにAPIを組みましたが、そのときはAPIリソースを使うことで、リレーションが盛り込まれたModelを最小限にしてクライアントに返すように実装できました。対処療法ではありますが、APIリソースによってModelの取り回しによる問題のいくつかは解消できますので、個人的にはLaravelの機能で一番好きな機能です。
補足2
一方、ルートモデルバインディング(https://readouble.com/laravel/6.x/ja/routing.html) はLaravelの中でも指折りのヤバい機能だと思っています。これはControllerで直接Action Methodの引数にModelインスタンスを受け取れるというものです。
ControllerがModelをいきなり扱えるためレイヤードアーキテクチャの根本から覆してしまいますし、直接Modelを受け取るために、いわゆるN+1問題を解消するためのeager loadを行うための
with
メソッドを噛ますことができません。しかし、これも開発時間短縮を実現できるため一時期弊社内で流行し、かなり多用された結果、
Question.php
モデル自身にwith
プロパティが設定され、質問に関連する全クエリに強制的にwith
が走るという状況が発生してしまいました。ヤバいです。設計の問題点を受けての考察
ここまで考えて、当時の僕が考えたのは下記のようなことでした。
- Repository→Serviceの時点で、LaravelのEloquent Modelを返さず、用途に応じた独自のオブジェクトを返すべきなのではないか?
- そのオブジェクトはシンプルなPHPのClass(いわゆるPOPO)として実装し、プロパティへの型補完が効くように実装すれば透明性も担保できる
- Repositoryの役目はEloquent Model→POPOへの変換に特化させる
- Service〜Viewが扱うオブジェクトは独自オブジェクトだけになるから、影響範囲を絞り込める!
このとき僕が考えた【Repositoryが返してくる独自のオブジェクト】が、DDDの文脈で言うところの【Entity】に落ち着いてくると思うのですが、当時の僕はそこまで考えられていませんでした。
ただ、具体的な実装方法が固まりきらなかったためこの考察は考察しただけで終わり、またModelを流用しまくる日々に戻りました。
転機
そんな日々に転機が訪れ、DDD実践へ繋がっていった転機が2つ有りました。
転機① スマートフォンアプリ開発
1つ目はネイティブアプリの開発です。
Webと同等以上の機能を有するiOSアプリをリリースするにあたって、1人でアプリで利用するAPIを全て組みました。初めてスマホアプリ開発に関わって気がついたことは色々ありすぎるので別途記事にまとめられたらと思うのですが、DDD関連で気がついたことといえば、やはり影響範囲についてでした。
ネイティブアプリはWebと違って、リリースするとユーザーがアップデートしない限りこちらから関与することは原則できません(強制アップデートを除く)。
今までWebだけで考えていた影響範囲がアプリにも広がり、さらにアプリもバージョンごとに考えないといけないことを考えると、それら全てのデバイスから質問データにアクセスするときはQuestion.phpを通っているという事実が、恐ろしく思えてきました。
さきほどの
short_body
のような独自Accessorをアプリ向けのAPIでも利用しようものなら、もう改修が怖くなって改善スピードが低下する未来が想像できました。結果、Postmanを使ってAPIテストを組むことでお茶を濁したのですが、テストは出口対策なので、設計レベルで改善できることはないかな、と考える時間が増えました。
転機② Web+DB Pressの特集【体験DDD】
まあもう今回の記事はこの特集に関して本当にありがとうございました勉強になりましたって言いたいだけの記事と言っても過言ではないのですが笑
このころいわゆるEric本を買って読んだものの何一つわからず手元でEntityっぽものを組んで、いや違う、こんなの実戦投入できないと頭を悩ませていた中、この特集の話を聞いて速攻買いました。
実際にUseCase層、永続化層、Domain層の解説とともに(Javaではありますが)生のソースコードが添付されており、非常にイメージしやすい内容になっていました。やはりエンジニアはソースコードで会話するのが一番です。
LaravelでDDDを実践する
お待たせしました。実際に実務上の施策でDDDを取り入れた設計・実装をやってみた内容をまとめていきます。
DDDを実践したときの手順
- 実現する機能に登場する人物、扱われるデータをUMLに落とし込み、関係性やそれぞれの成立条件を可視化する
- 機能で実現するUXを「◯◯が■■する」といった主語と述語で表現される文章にまとめる
- UseCaseを実装。EntityとRepositoryはモックで、あくまで2. で作成した文章を実現するように実装する
- Entityの中にデータの初期化や変更を実装。
- UseCaseとのインターフェース用にValueObjectを実装
- 最後にRepositoryを作成するためにテーブル構成を考え、LaravelのModelを作成しORMとして永続化・検索処理を実装
1. UML作成
まずは実現する機能に登場する人物、扱われるデータをUMLに落とし込みます。
書くときはPlantUMLという、YAMLファイルでUMLを記載できるツールを使って書きました。
具体的には、VSCodeにPlantUMLのプラグインを入れてUMLを記述していきます。公式ドキュメントを読めば書き方はすぐにわかります。
UMLといっても色々あると思いますが、僕はクラス図を使って書いています(もちろん、この時点で、このクラス設計で実装しよう、というものを決定できるわけはないので、ここでクラスとして表現したものに設計が引っ張られないように注意します)。
雑にスクリーンショットを貼りましたが、このようにYAMLファイルを左に、UMLのプレビューが右に出た状態で編集でき、最後にPNGなどでエクスポートできます。
業界によってはUMLを書いてから実装なんて当たり前かもしれませんが、スタートアップで働いていて正直そういった仕様を明記しないことに甘えていたので、久々にUMLを書きました。
詳しい書き方は「体験DDD」に書いてありますが、個人的には、各クラスから吹き出しを生やして、制約条件を明記していくところがポイントです。そこで記述した制約条件をできる限り後述するEntity、またはValueObjectに徹底的に閉じ込めていくことが重要です。
2. 「◯◯が■■する」といった文章にまとめる
次に、実現したい機能についてユースケース図を書くか、または「◯◯が■■する」という文章を箇条書きでまとめます。
例えば質問投稿機能を作成するとしましょう。
質問投稿機能を作るとき、エンジニアであれば
【本文やタグといった質問データを受け取り、現在のログインユーザーID777とともにデータベースにInsertする】
と考えるでしょう。しかし、「◯◯が■■する」の形式で考えれば
【学生ユーザーが質問を投稿する】
と記載できます(※学生が質問する勉強質問サイトの場合)。
後述するUseCaseを実装するときに、個人的な考えですが、「◯◯が■■する」の文型で書くように実装することが重要だと思っています。
経験上、前述の【データベースにInsertする】という思考で実装すると、Service層(以下、UseCase層)のところまでデータベースの構成を意識した実装が漏れ出てきます。データベースのことをUseCase層より上位のレイヤーが忘れて実装できるように、あくまで現実世界に即した、極論を言えばエンジニア以外の人にも通じるような表現に、実装する機能を落とし込んで記述できることが大事です。
補足
ここで主語と述語で文章表現することを徹底しなかった僕の失敗談があります。
定額課金機能を実装したときに、決済ベンダーでPay.jpを利用させていただいているのですが、【Pay.jp上に課金履歴を保存する】と考えながら実装したため、アプリ経由の課金(In App Payment)やキャリア決済の実装等を考慮した段階で、本来抽象化されているはずのUseCase層が再利用しにくくなっていることに気が付きました。本当は【ユーザーが課金する】と考えながら実装することで、ユーザーEntityや課金履歴Entity、または独自のHelperなどにPay.jp独自のロジックを閉じ込める工夫ができたはずです。3. UseCaseを実装
ここまで考えたあと、実装を始めます。最初の実装をEntityなどからはじめる方もいるかもしれませんが、僕はいまのところUseCaseから作り始めるのが好きです。
UseCase層は、ユーザーが自社のサービスを利用する場面を表現する層です。
さきほど2. で落とし込んだ「◯◯が■■する」という粒度の情報を持っている層になります。
では、さきほどから例示している【学生ユーザーが質問を投稿する】UseCaseを実装してみた例を示します。
QuestionPostUseCase.php<?php namespace App\QuestionPost\UseCase; use App\QuestionPost\Domain\ValueObject\UserAccountId; use App\QuestionPost\Domain\ValueObject\QuestionBody; use App\QuestionPost\Domain\ValueObject\QuestionTags; use App\QuestionPost\Domain\Repository\QuestionerRepositoryInterface; use App\QuestionPost\Domain\Repository\QuestionRepositoryInterface; use App\QuestionPost\Domain\Entity\QuestionEntity; use App\QuestionPost\Domain\Entity\QuestioningUserEntity; use App\QuestionPost\Domain\Exception\QuestionPostFailedException; final class QuestionPostUseCase { private $questionerRepository; private $questionRepository; // ※ここはLaravelのAppServiceProviderでRepositoryの実体をDIします public function __construct( QuestionerRepositoryInterface $questionerRepository, QuestionRepositoryInterface $questionRepository ) { $this->questionerRepository = $questionerRepository; $this->questionRepository = $questionRepository; } public function execute( // ポイント1 UseCaseの引数はValueObjectがGOOD UserAccountId $userId, QuestionBody $body, QuestionTags $tags ): QuestionEntity { // ポイント2 Repositoryから質問者Entityを取得 // @var QuestioningUserEntity $questioner = $this->questionerRepository->getQuestioner($userId); // ポイント3 質問者Entityが質問を投稿 // $question は QuestionEntityのインスタンス $question = $questioner->postQuestion($body, $tags); // ポイント4 永続化 return $this->questionRepository->saveQuestion($question); } }ポイント1 UseCaseの引数はValueObjectがGOOD
UseCaseはおおむねLaravelでいうとControllerから呼ばれることが多いですが、その際の引数は
ValueObject(後述します)
がおすすめです。一番いけないと思うのはArrayを渡すパターンです。
$request->all()
でリクエストの内容を取得し連想配列でレイヤーを超えてデータを渡していくことが僕は多かったのですが、内容が不透明になって、結局あとからRepositoryなどでisset feat. 三項演算子地獄を生むことになります。
例えばintと型を定義すると、どんな数値であっても入ってくることができますが、UserAccountId
といった独自の型を定義すれば、より安全に、ヒューマンエラーを防いで扱うことができます。ポイント2 Repositoryから質問者Entityを取得
実現したい機能は【学生ユーザーが質問を投稿する】なので、まずは主語となる「学生ユーザー」を用意します。
「学生ユーザー」はすでに登録済みのユーザーなので、データベースに永続化されています。そのためRepository経由で
QuestioningUserEntity(1人の質問するユーザーを示すEntity)
を取得します。「学生ユーザー」なので
StudentEntity
といった名称でも良いかもしれませんが、僕の見解としては、今後仕様変更で学生以外の種別のユーザーが質問可能になる可能性、などの幅を残すために「質問者Entity」くらい抽象化した命名でもいいのではと考えています。ポイント3 質問者Entityが質問を投稿
※Entityのソースコードは後述します
次に
QuestioningUserEntity
に実装されている(この時点ではモックですが)postQuestion
メソッドを実行することで質問を投稿します。引数には質問作成に必要な
ValueObject(後述します)
を受け取り、投稿に成功するとQuestionEntity(質問内容を示すEntity)
を返します。ポイントは、
QuestionEntity
型のインスタンスは、このQuestioningUserEntity
に実装されたpostQuestion
経由ではないと生成できないように実装することです。するとソースコード上で「質問は必ずQuestioningUserEntityに該当するユーザーが投稿する」ということを暗黙のうちに示すことができます。このようにEntityの生成ルートを縛ることで、今回の例だと、「user_idがNULLの質問データをテーブルにInsertしてしまう」といった事故を防ぐことができますし、
Q&Aサイトと一口にいっても掲示板のように匿名ユーザーでも書き込めるサイトもある中で、このサイトは必ずユーザーアカウントが存在する場合のみ質問できるのだ、ということをソースを読む人に伝えられます。ポイント4 永続化
作成した質問EntityはRepositoryによって永続化(=データベースへの保存)します。
Entityの作成と、永続化はわけるほうがお互いの責務が分離されて望ましいと思います。
以上でUseCaseの解説を終わります。EntityやValueObjectの説明をしないで話を進めるのが辛くなってきたので先に進みますね。
4. Entityの実装
質問投稿機能の例では現在「質問者Entity(
QuestioningUserEntity
)」と「質問内容Entity(QuestionEntity
)」が登場しています。
QuestioningUserEntity
だけ、実装をざっくり例で示します。QuestioningUserEntity.phpfinal class QuestioningUserEntity { private $userAccountId; private $userType; // ポイント1 最重要!コンストラクタをプライベートにする private function __construct() {} // ポイント2 Repositoryが現在のデータを入れる静的メソッドを作る public static function reconstructFromDatabase( UserAccountId $userAccountId, UserType $userType ): QuestioningUserEntity { // プライベートコンストラクタはクラス内からは呼べます。new self()等でも可 $questioningUser = new QuestioningUserEntity(); $questioningUser->userAccountId = $userAccountId; $questioningUser->userType = $userType; return $questioningUser; // 返すのはインスタンス } // ポイント3 質問投稿メソッド public function postQuestion( QuestionBody $body, QuestionTags $tags ): QuestionEntity { // ここで質問を作成できないユーザーの場合は例外をThrow if ($this->userType !== UserType::STUDENT) { throw QuestionPostFailedException::withMessages( [ 'message' => '質問を作成する権限がありません' ] ); } // QuestionEntityがインスタンス化されるルートがここだけになる→学生以外のユーザーは決して質問を作成できない $question = QuestionEntity::createByQuestioningUser( QuestionBody $body, QuestionTags $tags ); return $question; } }ポイント1 最重要!コンストラクタをプライベートにする
まずはコンストラクタを明示的にプライベートにすることが大切です。インスタンスが作られる方法を特定のメソッドのみに絞ることで、絶対に不整合なデータや思わぬデータをデータベースから取得したり、保存できなくなります。
ポイント2 静的メソッドでインスタンスを作成させる
プライベートコンストラクタになったら、どうやって外のクラスがインスタンスを作成するかと言うと、PublicかつStaticなメソッドでインスタンスを作って返すようなものを作ります。
今回の例だとシンプルですが、プロパティの多いデータの場合はNULLableなデータはここでクラスメンバ変数にNULLを代入するなどします。
そうすることで、ソースコード上で、「このメンバ変数はNULLableですよ」「このメンバ変数は必ず(ValueObject)型の値が入りますよ」ということが表現できます。
僕は今まで何度も、「この連想配列のこのキーには何が入っているんだ?」と関数の引数を見て困惑することがありましたが、こうやって生成元を縛ったクラスにしておけば、読み解くのが容易になります。
QuestioningUserEntity
の場合は生成元がRepository、すなわちデータベースからのデータ読み取り時のみなので、reconstructFromDatabase
という命名にしています。僕の認識が正しければPHPではこのメソッドはRepositoryからしか呼べないといった制限を自然にはかけれないはずなので、仕方なく命名で担保しています。ポイント3 他のEntityを生成するメソッドで制約条件を明記
(例外設計についてはまだ自分の中で正解が固まっていないので、このほうが良いと思いますといったご意見をお待ちしています!)
QuestioningUserEntity
は、postQuestion
メソッドで質問作成に失敗した場合、独自で設計した例外「QuestionPostFailedException
」をThrowします。ここでの失敗というのは、例えば
QuestioningUserEntity
に格納されているユーザーが「学生ユーザー」ではなかった場合や、他にも「1時間に1問しか質問できない」といったサービス独自の制約があったときにその制約に弾かれた場合などです。
こういった制約条件はpostQuestion
メソッド内に集約されていて、UseCaseからは条件の詳細を知ることは無いようにします。
QuestionPostFailedException
はLaravelで用意されているValidationException
を継承して実装するのがいまのところおすすめです。というのも、ValidationException
のサブクラスがThrowされるとLaravelの例外ハンドラ(Handler.php
)がステータスコード422(APIの場合)でクライアントに返してくれるからです。例外がAPIのエラーメッセージやステータスコードを管理しているのが少し責務の位置づけが妙な気もしているのですが、Laravelを使ってきた自分としては自然なのでこの方法でやっています。
これらの制約条件をパスしたときのみ
QuestionEntity
が同じような静的公開メソッドによってインスタンス化されて返り値となります。質問作成時にのみ判断できる制約条件がある場合は、この
createByQuestioningUser
内部で実装するイメージです。
QuestionEntity
の例示は省きます。5. ValueObject(VO)の実装
ValueObjectは、その名の通り値をオブジェクトとしてよりリッチに表現できる余地を残す仕組みです。
さっそく実装内容を示しますが、VOはいたってシンプルではあります。
UserAccountId.phpfinal class UserAccountId { private $id; private function __construct() { } public static function create(int $primitiveId): UserAccountId { if ($primitiveId <= 0) { throw InvalidUserAccountException::withMessages([ 'message' => 'ユーザーIDが不正です' ]); } $instance = new UserAccountId(); $instance->id = $primitiveId; return $instance; } public function toInt(): int { return $this->id; } }僕はEntityと同じ要領で、createメソッドのみからインスタンスを生成できるようにしていて、そこでintはintでも0以下のintは許さないよ、といった数値のValidationを噛ませているイメージです。
また、実際はデータベースの永続化等でintに直さないといけない場面もあるので、
toInt
メソッドを実装しています。たったこの程度で実装が終わることが多いので、当初は実装する意味はないかなーと思っていたのですが、結局意義はあるなと思ったので、原則ほとんどの値に対してVOを作っています。
ValueObjectの意義
ValueObject(VO)の意義は主に以下の3つあると思っています。
- メールアドレスの形式検証のように、一般的な検証ロジックを集約する
- 個々の値に対して成立する制約条件を表現する
- 引数をVOにしてレイヤー間のデータをやり取りすることで、引数の順番をミスするなどのケアレスミスを防ぐ
検証ロジックに関しては、Laravelなら
FormRequest
を使ったほうが便利じゃないか、という意見があると思います。しかし、FormRequestの場合、あのクラスの役目は「このリクエストにはどんなパラメータがあるのか、またはそれは必須か」という存在確認と、「それぞれのパラメータの値は【一般的に妥当か】」(メールアドレス検証など)と、「それぞれのパラメータはサービスを成立させる上で問題ないか」(メールアドレスが他のユーザーと被っていないか)というサービス仕様上の検証の3つほどの観点が混ざってしまっています。
具体的に何が問題かと言うと、弊社のようにWebとiOSアプリにサービスを展開する場合、ほぼ似ているけど微妙に入力パラメータの違うAPIが複数生まれて、それぞれFormRequestを作成していると、上記で言うところの【サービス仕様上の検証】が複数のファイルにまたがって記述されることになります。
すなわち、ある日「ユーザーのメールアドレス重複を許す」という決定が例えば下ったとして(ヤバいけど)、そのときの変更範囲が各プラットフォームごとに発生するということです。それって大袈裟だなというか、Presentation層に業務ロジックによるValidationが漏れ出ているから変更範囲が広がっているのだなと感じます。
なので、僕としてはFormRequestは便利なのですが、あれだけで検証ロジック全て終わりではなくて、サービスの仕様に依存するものはValueObjectとかEntityで検証ロジックを表現しよう、と思うのです。メールアドレスの書式検証のような、ユースケースにあまり依存しないものはVOで、メールアドレスが既存のユーザーに被っているかどうか、というユースケースに寄ったものはEntityやドメインサービス(※ここでは書かないですが詳しくは体験DDDや実践DDDを参照)に書き込みます。
また、関数の引数をVOにすることで、連想配列の中身がわからないとか、引数が全部intだからうっかり順番を間違えてしまう、というようなミスを防ぐことができます。
6. Repositoryの実装
ようやくここまで来て永続化の話ができるようになりました。最初のLaravelのEloquent Model依存の設計手法ではテーブル設計から考えていたので、ここまで進めてようやくRepositoryを考えるというのは、僕にとってはかなり斬新です。もちろんテーブル設計が複雑な場合、結局EntityやUseCaseが引っ張られる可能性はありますが、原則UseCaseやEntityから考えるのが良いと思います。
Repositoryがやることは至ってシンプルです。ここでは
QuestionerRepository
の具体実装を示します。忘れた方はUseCaseの説明に戻って、getQuestioner
メソッドを使っている箇所を探してみてください。QuestionerRepository.phpfinal class QuestionerRepository implements QuestionerRepositoryInterface { public function getQuestioner(UserAccountId $userId): ?QuestioningUserEntity { $userOrm = new \App\Model\User(); $userData = $userOrm->find($userId->toInt()); if ($userData === null) { return null; // or throw an Exception } return QuestioningUserEntity::reconstructFromDatabase( $userId, UserType::create($userData->user_type) ); } }Repositoryの実装のポイントは、なんといってもLaravelのEloquent ModelをORMでのみ利用するというところです。
$userOrm = new \App\Model\User(); $userData = $userOrm->find($userId->toInt());ここで懐かしのUserモデルが利用され、findメソッドによって指定したIDのユーザーインスタンスを取得します。
しかし最終的には先述の
reconstructFromDatabase
によってQuestioningUserEntity
にWrapされ、IDとユーザータイプだけを持ったインスタンスとしてユースケース層に渡っていくこととなります。この方法によって、僕が頭を悩ませていた、Modelがどこからでも使われていて影響範囲が読めない問題を防ぎます。ユースケースやドメイン知識ごとに適切なEntityと、そのEntityに必要なデータだけ取得、保存するRepositoryを作成することで、もちろんファイル数やクラス数は爆増しますが影響範囲を絞ることに成功します。テスタビリティも向上することでしょう。
補足
ここまでで一通りの説明は終了します。最後に補足をいくつか。
UseCaseのクラス設計
僕は1ユースケース1クラスで作るのが気に入っています。唯一のメソッド
execute
のみを有するイメージです。なぜかというと、
UserUseCase
のような抽象的な名前にしてしまうと、なんでもかんでもそのファイルに実装が詰め込まれ、可読性の低下、複雑にするだけの再利用といった結果を生むだろうと思ったからです。Interfaceについて
レイヤ間の抽象化にInterfaceの実装は欠かせません。僕は現状、RepositoryにのみInterfaceを作成し、実装するというルールにしています。UseCaseもInterfaceを作ったほうが良いのかなとは思いますが、単純に手間なのでやっていません。。。
AppServiceProvider.phpでInterfaceと実装をBindさせるように設定しています。
どこまでDDDするのか?
もちろんサービス全体をDDDで作り直すのが理想でしょうが、正直今の自分にその余裕はないです。弊社がスタートアップというのもありますが、技術都合でDDDに変更しなければならない!というのを押し通すのは難しいなと思っています。
とはいえ、現時点で弊社でDDDに挑戦しているのは「サイトに登録している家庭教師への指導依頼=コンバージョン」と、「学生ユーザーが限定機能を開放する定額課金プラン=実際に金銭が動く」というサイト内でもかなり高難度かつミスが許されない部分です。こういった特定の機能であれば、ある意味他機能から独立するのが望ましい上に、経営層へ実装に時間をかけ保守性およびテスタビリティを高める説明が自然にできるため、実践したという流れになります。
テストは書いているのか?
最初テストを書かなかったのですが、DDDで開発しているとEntityやVO、UseCaseの作り直しが開発の過程でしばしば発生するので、テストを書いていないと変更が億劫になりいずれ手抜き実装が爆誕することが想定されました。
現在はPHPUnitを使って、UseCase単位のテストは書くようにしています。また、結合テストとしてHTTPテストも記述しています。
詳しくは別の記事などで書ければと思いますが、Laravelでは
TestCase
クラスが独自拡張されていて、setUp
メソッドをオーバーライドして利用することでEloquent Modelやfactoryメソッドをテストケースで利用、再利用することができます。setUp
メソッドをオーバーライドせずに使うとDIなどのLaravelの初期ロード(正式名称なんですかね)が動かないのでテストが書きにくいです。テストも書くとなると、尚更事業優先度が非常に高いところからチャレンジするのが向いているなと感じているところです。
ディレクトリ構成は?
下記のようなディレクトリ構成でやってみています。既存設計がもうそこそこの規模になっているので、ルートディレクトリごと分けてしまっています。
app/ ├── Console ├── Constant ├── Domain // ここのディレクトリ配下はDDDのアプローチで設計・実装している │ ├── { DomainName } // 扱う事業領域名 │ │ ├── Domain // ドメイン層 │ │ ├── Infrastructure // 永続化層 │ │ └── UseCase // ユースケース層(旧設計がServiceという単語を使っており、意識して分けるためにUseCaseとした) │ ├── { DomainName } │ ├── { DomainName } │ ├── ... │ ├── Base // DDD全体でベースとなるClass。将来的にはEntityやVOの基底クラスも作りたい │ │ └── Exception // ValidationExceptionを拡張したclassを配置 │ ... ├── Events ├── Exceptions ├── Helpers ...まとめ
旧質問投稿UseCase
DDDをやる前だったら、質問投稿時のUseCase(Service層)はこんなシンプルな実装になるでしょう。
QuestionService.phppublic function postQuestion(array $params, int $userId) { $question = $this->questionRepository->storeQuestion($params, $userId); return $question; }この実装に比べれば、これまで説明した実装は、ソースコードが仕様を説明し、適切な制約をかけているという観点で非常に情報量が多い実装になっていることがわかると思います。
具体的には投稿内容がarrayにまとめられているよりVOになっているほうがわかりやすい、どんなユーザーが質問投稿できるか理解しやすいなどです。結論
- LaravelのModelをあらゆるレイヤーで使うと改修が難しくなる
- 開発する機能のユースケースを主語と述語で文章に表現し、そのままUseCase層の実装として表現する
- EntityやValueObjectに制約条件をまとめ、適切に例外を吐く
- LaravelのModelはORMとしてのみ利用する!!
- PHPの言語自体の限界はあるので、命名の工夫などで適宜我慢する
- 実運用の際はどの機能から、どこまで完璧主義でDDDをやるか考える
以上です
思った以上に長文になりましたが、今の自分のDDDの実力はこんなところです。もっと上手に設計できるように経験を積んで、運用を経て痛い目に遭っていこうと思います!
しかしやっぱり型のある言語がいいですね。最近はフロントもバックエンドもTypeScriptで組むのが良いんじゃないかと思えてきています(過激派)。
ぜひTwitterでも繋がっていただけると嬉しいです。
https://twitter.com/Meijin_gardenまた、よかったらLaravelの初期設計をやった頃のQiitaの記事もぜひ。
【実録】WordPressサイトをAWS+Laravel+Nuxtにフルリプレイスした話(技術選定編)
- 投稿日:2019-12-09T09:13:35+09:00
Docker 環境に Sftp コンテナを構築して Laravel と連携する
Laravel2 Advent Calendar 2019 - Qiita の 9日目 の記事です。
サンプルリポジトリ
https://github.com/ucan-lab/laravel6-sftp
前準備
まずはLaravelの環境を用意します。
$ git clone git@github.com:ucan-lab/docker-laravel.git laravel6-sftp $ cd laravel6-sftp $ make create-projectできました。
詳しい構築方法は過去記事を参考ください。
- 【忙しい人向け】カップ麺より早く作るDockerでLaravel開発環境構築
- Laravelの開発環境をDockerを使って構築する
- 【初心者向け】20分でLaravel開発環境を爆速構築するDockerハンズオン
Sftpコンテナを作成する
ベースコンテナは atmoz/sftp 使用します。
対向先のディレクトリを作成する
$ mkdir sftp-store $ echo "hello" > sftp-store/world.txt認証用の公開鍵、秘密鍵を作成する
$ mkdir .ssh $ ssh-keygen -t rsa -b 4096 -N "" -f .ssh/ssh_host_rsa_keyGit管理対象外設定をする
.gitignore
/.ssh /sftp-store鍵ファイルやSftp内のデータはGit管理したくないため。
docker-compose.yml
services: app: volumes: - ./.ssh/ssh_host_rsa_key:/root/.ssh/ssh_host_rsa_key sftp-server: image: atmoz/sftp volumes: - ./sftp-store:/home/foo/share - ./.ssh/ssh_host_rsa_key.pub:/home/foo/.ssh/keys/ssh_host_rsa_key.pub command: foo::1001
docker-compose.yml
の差分を抜き出してます。
sftp-server
に公開鍵を配置して、共有ディレクトリ(/home/foo/share
)にローカルディレクリ(./sftp-store
) をマウントします。Sftp コンテナを構築
$ docker-compose down $ docker-compose up -d --buildLaravel で Sftp 設定を行う
$ docker-compose exec app ash
app
コンテナ内で実行します。Sftpライブラリ導入
$ composer require league/flysystem-sftp
SftpServiceProvider を作成する
$ php artisan make:provider SftpServiceProvider
app/Providers/SftpServiceProvider.php
<?php namespace App\Providers; use Illuminate\Support\Facades\Storage; use Illuminate\Support\ServiceProvider; use League\Flysystem\Filesystem; use League\Flysystem\Sftp\SftpAdapter; class SftpServiceProvider extends ServiceProvider { /** * Bootstrap services. * * @return void */ public function boot() { Storage::extend('sftp', function ($app, $config) { return new Filesystem(new SftpAdapter($config)); }); } }Filesystem に sftp 用のアダプタを追加します。
作成した SftpServiceProvider を登録する
config/app.php
'providers' => [ // ... App\Providers\SftpServiceProvider::class, ],Sftp ディスク設定を追加する
config/filesystems.php
'disks' => [ 'sftp-disk' => [ 'driver' => 'sftp', 'host' => 'sftp-server', 'port' => 22, 'username' => 'foo', 'privateKey' => '/root/.ssh/ssh_host_rsa_key', 'root' => 'share', 'timeout' => 10, 'directoryPerm' => 0755, ], ],お試し
$ php artisan tinker >>> Storage::disk('sftp-disk')->get('world.txt'); => "hello\n"差分のコード
参考
- 投稿日:2019-12-09T07:21:11+09:00
Laravelの通知ライブラリでslackボタンアクション機能を実装
Laravel Advent Calendar 2019 - Qiita の 9 日目 の記事です。
Laravelの通知ライブラリを使ってボタン付きのslack通知を実装した記事を
下書きのままにしていたので書き上げることにしました..!Laravelの通知ライブラリ
laravel5.8以降のslack通知機能に変化
通知クラスが外部ライブラリに変わりました!
5.8以降のslack通知(公式)composerを使って入れないと使えなくなりました。
composer require laravel/slack-notification-channelちなみに
v5.8
以前はguzzle
を入れれば使える形でした..。
5.8以前のslack通知(公式)(実は
v5.7.16
以降から通知クラスは外部ライブラリ化されてますが、ドキュメントは5.8以降しか変更されてないので注意です。(今5.7
をupdateするならメジャーアップデートしましょう。))Nexmoの通知クラスと一緒にframwork内から削除されています。
slack通知のボタンアクション機能とは?
公式のgifだと以下のような表示。
ボタンが付いていて押された後、元のメッセージを変えて1人しか押せなかったり、誰が押したか表示したり色んな機能が考えられそうです..!
今までのslack通知クラスでボタン機能使えなかったの?
そうなんです!
当然、slack側のAPIには用意されていたのでguzzle
で直接叩いたり、Custom Channels
を使って実装することは出来ましたが、Laravelが用意してくれている通知クラスでは使えませんでした。環境
Laravelの開発環境をdockerで作るなら、@ucan-labさんのLaravelの開発環境をDockerを使って構築するがおすすめです。常に更新されてる!
slack側の設定は以下を参考に
・Slack APIを使用してメッセージを送信する
slackのボタンアクション通知の仕組みはこちらを参考にしました!
・slackで単純なボタン付きメッセージを送る通信ライブラリを使って実装
さて、通信ライブラリを使って実装してみましょう。
slack-notification-channel通知したいチャンネルにIncoming Webhooksの設定をしてwebhookを取得します
Incoming Webhooksについてはこの記事を参考にしました。
SlackのIncoming Webhooksを使い倒すslack通知機能をまず作ります。この時、
.env
に設定したwebhookをconfig
に書いておき、routeNotificationForSlack
メソッドで指定します。SlackNotificationService.php<?php namespace App\Services; use App\Notifications\SlackButtonMessage; use App\Notifications\SlackSend; use App\Notifications\SlackSendQuestion; use Illuminate\Notifications\Notifiable; class SlackNotificationService { use Notifiable; /** * SlackチャンネルのWebhookURLを返す * * @return string */ public function routeNotificationForSlack() { return config('services.slack.button'); } /** * 送信メソッド * @param $message */ public function send() { // 通知 $this->notify(new SlackButtonMessage()); } }上記の送信メソッドで呼ばれるslackボタン通知用のNotificationクラスを作成
SlackButtonMessage.php<?php namespace App\Notifications; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\SlackMessage; use Illuminate\Notifications\Notification; /** * Slack通知クラス */ class SlackButtonMessage extends Notification { use Queueable; private $sendMessage; private $title; /** * Create a new notification instance. * * @return void */ public function __construct() { } /** * Get the notification's delivery channels. * * @param mixed $notifiable * @return array */ public function via($notifiable) { return ['slack']; } /** * Slack通知処理 * * @param mixed $notifiable * @return \Illuminate\Notifications\Messages\SlackMessage */ public function toSlack($notifiable) { return (new SlackMessage) ->from('Test通知', ':face_vomiting:') ->content('これはテストです :face_vomiting:') ->attachment(function ($attachment) { $attachment->action('googleリンク','https://www.google.com/','primary'); }); } }そして積みました...。
slack-notification-channel
はボタン通知に対応していますが、ボタン押した際のアクションは単純なURLリンクしか設定できないようです!
SlackAttachment.php
の所定の位置を見ると確かにそうなってます。SlackAttachment.php/** * Add an action (button) under the attachment. * * @param string $title * @param string $url * @param string $style * @return $this */ public function action($title, $url, $style = '') { $this->actions[] = [ 'type' => 'button', 'text' => $title, 'url' => $url, 'style' => $style, ]; return $this; }gifのようにボタン押下後に、元のメッセージに色々アクションを実装したい場合は、無理ですね。
あくまでも通知クラスのライブラリということですね。GuzzleでAPI実装
書いていきます!
SlackButtonService.php<?php namespace App\Services; use GuzzleHttp\Client; class SlackButtonService { public function postMessage( $message ) { $client = new Client([ 'headers' => [ 'Content-Type' => 'application/json' ] ]); $response = $client->post( config( 'services.slack.button' ), ['body' => $message] ); logger($response->getBody()); $data = json_decode( $response->getBody()->getContents()); return $data; } }送信メッセージ出す所
send.phpprivate $slack; /** * Create a new controller instance. * * @return void */ public function __construct( SlackButtonService $service) { $this->slack = $service; } public function send(){ $message = "こんにちは!"; $data = array( "text" => $message ); $actions = [ "id" => "1", "name" => "test", 'type' => "button", 'text' => "こんにちは!", "value" => "button_1", ]; $data += [ "attachments" => [ [ "callback_id" => "test", "fallback" => "More details...", 'actions' => [$actions], ] ] ]; $payload = json_encode($data); $res = $this->slack->postMessage($payload); }slackからのレスポンス処理を設定(Interactive MessagesでここのURLを設定しておくことでPOSTされます)
web.phpRoute::group(['prefix' => 'slack'], function() { Route::post('/api/response', 'ApiController@getSlackResponse'); });slackからのPOSTに対してcsrfトークンチェックを外す
VerifyCsrfToken.php<?php namespace App\Http\Middleware; use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware; class VerifyCsrfToken extends Middleware { /** * Indicates whether the XSRF-TOKEN cookie should be set on the response. * * @var bool */ protected $addHttpCookie = true; /** * The URIs that should be excluded from CSRF verification. * * @var array */ protected $except = [ // '/slack/api/*', ]; }レスポンス処理
ApiController.phppublic function getSlackResponse(Request $request){ $temp = json_decode($request['payload'],true); $temp['original_message']['text']=$temp['user']['name'] . 'が押しました!'; return response($temp['original_message']); }こんな感じで通知が来た後に...
みたいに元のメッセージを変更したり出来ます!
slackを使っている会社では、slack連携による業務改善やエンゲージメントを高めるような施策が今後も増えてくると思います!
この記事が少しでも実装の助けになればと思います。
- 投稿日:2019-12-09T00:34:36+09:00
昔を思い出しながらLaravel、FuelPHP、Codeigniterを比較してみた。
こんにちは!葛山です。
今回会社でアドベントカレンダーをやろうということで僕も慣れないブログを書いてみることにしました。
業務でコードに触れることは少なくなったのですが、最近Laravelを勉強したので昔使っていたFuelPHPやCodeigniterとの比較記事を書こうと思います。はじめに
パーソンリンクは2011年に僕が立ち上げた会社なのですが、少人数だったので僕がPMを担当しながらプログラマーも兼務しているという状況でした。
最初はCodeigniter、2012年からはFuelPHPをメインに使っていました。なぜPHPなのか
僕がPMの時にPHPを採用していた理由は「人が集めやすい」からです。
開発案件は急に始まるので、その際一番苦戦するのが人員の確保です。
当時はうちのメンバーの人数も少なく、外部の方と一緒に即興でチームを組むこともありましたので(開発では割とそれがスタンダードなのかも)その際一番募集をかけやすかったのがPHPでした。PHPはWEBアプリ開発において人気のプログラミング言語なのですが、フレームワークの種類が多すぎて何を使うべきかわかりませんよね。
そこで今回は、数あるフレームワークの中で僕が推しているLaravel、FuelPHP、Codeigniterについて比較と解説をしていきたいと思います。学習コスト
僕の体感だと
Laravel > FuelPHP > CodeIgniter
です。CodeIgniterを初めて使った時、それまでZend FrameworkやSymfonyなどを使っていましたが、学習コストの少なさに驚きました。
CodeIgniterで街コンサイトを作ったのですが、PHPさえ知っていればほぼ学習せずに作り上げるところまではいけました。ピュアなPHP+αってイメージです。
FuelPHPとCodeIgniterの差は機能の多さで、フレームワークとしての本質はあまり変わらない印象です。まだ勉強を始めたばかりなのですが、LaravelはRuby on Railsみたいないイメージでコーディング規約もちゃんとあるので大規模開発にも適応できそうです。
開発環境の構築も簡単でドキュメントも読みやすかったです。
AWSやSNSとの連携等、機能がたくさんあるので使いこなそうと思うと学習コストはかかりますが、これさえあれば大体のwebアプリ開発は完結できそうです。処理速度
検証していないので後日上げますw
他の記事を読んだところこの中ではCodeIgniterが一番高速らしいですね!柔軟性と拡張性
CodeIgniter > FuelPHP > Laravel
です。CodeIgniterが一番コーディング規約などの制限が緩く、自分で書きやすいように書いていくことができます(僕は簡単なシステムを作るときはModelはほぼ書かずに既存ORMのみで書きました)
フレームワークの拡張もしやすくcoreをオーバーライドすることで簡単に拡張できます。
個人的には2人以上が関わるプロジェクトにはオススメしませんが、今はもしかすると良い管理方法があるかもしれません。。FuelPHPもCodeIgniterの開発者の一部の人たちが作っていることもあり、似ている部分は多いです。
規約が緩いのは変わらないですが、Oilコマンドでクラスの自動生成ができるので、そこである程度型が決まってきます。
ディレクトリ構成もわかりやすく、coreの拡張がし易いです。
下記のようなディレクトリ構成になっていますが、appとcoreの配下がほぼ同じ構成になっているので拡張したいcoreがあればそれをappでオーバーライドして記述すれば良いのです。
ルートディレクトリ/
├fuel/
│ ├app/
│ ├core/
│ └packages/
│
└public/
├assets/
└index.php
また、CodeIgniterではデフォルトでドキュメントルートにシステムファイルが置かれてしまっていましたが、publicディレクトリに切り離されたのでセキュリティ的にも良くなりました。Laravelはプラグインが充実していることとフレームワーク自体の完成度が高いため、coreなどを柔軟に拡張することは前提としていないのですが、ベースとなるController等どのプロジェクトでも拡張するようなクラスはデフォルトでapp配下に置いてあるのでそれを拡張していく感じです。
それ以外を拡張する場合は少し面倒で、ただcoreを継承するだけではなくService Providerを書かなければいけなかったり工程が多いので柔軟性は他の二つに比べると一番低いとしました。
ただし、前述したようにcomposerで簡単に高機能なプラグインが導入できるので拡張性は高いとも言えます。まとめ
ここまで僕の所感を元にPHPのフレームワーク比較をしてきましたが、現在のイチ推しはLaravelです。
理由はフレームワークの性能以上に、メンテナンスの頻度やプラグインの豊富さなどがビジネス要件を多く満たせるかの鍵になってくるので、今トレンドのものを選択するのが一番良いからです。
これからPHPを学ぶ人はLaravelから始めてみましょう!
- 投稿日:2019-12-09T00:00:40+09:00
PHPerのためのLaravel forgeとDigital Oceanで簡単サーバー管理
この記事はうるる Advent Calendar 2019 9日目の記事です。
はじめに
この記事は下記の方を対象にしています。
- インフラやサーバー周りが苦手のPHPのサーバーサイドエンジニア
- 自分でサービス開発をしたいエンジニア
- エンジニアなりたての方インフラとかサーバーとかよくわからないし、怖いし、面倒ですよね。私もそうでしたので、よくわかります。
私自身はプログラマ歴4年のサーバーサイドエンジニア。自分でサービスをやっているため、必要に迫られてサーバー管理を嫌々やっています。(本当にサーバー管理キライですw)
この記事ではLaravel forgeとDigital Oceanを使って楽に環境構築を行います。
Laravel forge is 何??
- Laravel 開発者のためのサーバー管理ツール
- Laravel開発者のTaylor Otwellさんを中心に開発。Laravelとの相性が抜群に良い
- Digital OceanやAWS、その他のVPSと簡単に連携でき、すぐにLaravelのアプリケーションが動かせる
- Gitからの自動デプロイ、Redis,Queueなどアプリケーションに必要なことが簡単に設定できる
Digital Ocean is 何??
- アメリカのクラウドサービスの会社。 VPS、DB、クラウドストレージサービスを安く、シンプルな使い勝手で提供。
- アメリカのスタートアップがよく使っている
- AWSよりシンプルで安く、簡単なサービスと認識してもらえればOK!
forgeを使って、Digital Oceanにサーバーを作る
- forge, DOともにアカウントを取得する
- forgeは5日間無料トライアルあり。月額12ドルから使えます。
- DOは利用した分だけ課金されます。サーバーは最小構成なら月500円程で立てれます。
- DOの管理画面にてAPIを発行
- forgeの画面にて先ほどのDOのAPIを入力
- forgeの画面でcreateサーバーにて、Digital Oceanをクリック
- 連携が成功していたらcredentialにDigital Oceanの自分の名前が表示されます
- Nameを決めて、Region,Server sizeなどを選択して、create Serverボタンをクリック。今回はadvent-calendarというサーバーを作ります
- すぐにDigial Oceanと連携して構築し始めます。
- DO側の画面
サイトの公開
デプロイしたい時は右上のDeploy Nowをクリック
自動デプロイは Enable Quick Deploy をクリック。これにしておくとGitが更新されるたびに自動デプロイが走ります
設定したドメインは、ドメインを管理しているサイトでDNSでAレコードにIPアドレスを追加すれば表示されます。(今回の例ではIPアドレスは159.89.200.65。 Digital Oceanのサイトでも確認できるが、ご丁寧にforgeの中でもちゃんと書いてある)
- advent-calendar-test.comはアクセスしてみても、私がドメイン持ってないので表示されません。念のため。
その他設定
SSLも設定可能。Let's encryptあるので、わかんない人はそれクリックしておけばOK。勝手にSSLが反映されます。
その他補足
- 設定はServer details, Site detailsの二つがあり、設定するものによって場所が違う。サイトがシンプルすぎて、何がどこにあるか最初わからないので注意する。
- ForgeはSudo passwordがたまに求めらるが、サイト上からは見れない。provisioning時のメールにある。最初わからなすぎてパニクった。
- Digital Oceanはサーバーのバックアップを1週間ごとに取ってくれる。外部サービス使えば、毎日バックアップしてくれます。
- DOは、managed Database (RDSのパクリ)、 Spaceというオブジェクトストレージ(S3のパクリ)を持っています。この二つは安いのに超便利。 これ以外にもロードバランサーやちょっとしたサーバー監視ツールもある。小規模サービスであればAWS出なくても、DOで十分に事足ります。
- Digital Oceanに関してはこの記事がよく説明されています。https://nekonenene.hatenablog.com/entry/hello-digitalocean
まとめ
forgeとDigital oceanを使えば、簡単にサーバー構築が出来ます。二つとも海外のサービスなので、基本英語。なので、最初はとっつきにくいです。ただ、その分ネットに情報はたくさんあるし、わかってくれば使いこなせます。
forge のキャッチコピーはServer management doesn't have to be a nightmare.
直訳すると「サーバー管理は悪夢である必要がありません」という意味です。 そのコピーの通り、誰でも簡単に操作できるし、楽に管理できます!価格も安いし、ぜひ、使ってみてくださいねー!
Advent Calendar 9日目でした。
明日10日目は Hiroaki Moriyamaさんによる記事を乞うご期待!
https://adventar.org/calendars/4548