序
株式会社ROXXのサイタマサイドエンジニア、ぎゃるです(ノルマ完了)
なんか祭りがあるらしくて、書きました。
趣旨説明
本稿はLaravelをLaravelらしく使っていくための知恵、を少しずつ書き込んで行って貯める記事です。
「それ普通にただのPHPの使い方では」みたいな部分も多々含まれそうな気配がしていますので、タイトルにPHP / Laravelと表示しました。
総じて、技術的事実を端的に説明するというよりも、opinion basedなコンテンツになるはずです。ということなので断っておくと、所属会社・チームの組織的見解というよりはあくまで筆者の考え方です(まあ技術ブログに載せてはいますが)。
さておき、「明らかにそれ間違ってるだろ」という点については、お気軽にご指摘ください。
あ、メインコンテンツはヘッダー画像なので、ぜひみてくださいね!
Eloquent Modelのアクセサ機能について
日本語Docだとここにある「アクセサ」機能についてです。
(8.x以前のAPIでの説明に慣れているのでその前提で進めますが、考え方自体は9.x以後も使えるかなと思います。そのうちバージョンアップします……)
負の側面
濫用禁止シリーズのうちのひとつだと思っています。ということでまずは負を語ります。
①PHPDocやIDEのプラグインなどのサポートを得ていない環境だと補完が上手く利かない
まあこれについては環境をいい感じにしろという話もありますが、なるべく素朴な言語機能で理解できるコードの方が優れているのは確かだと思います。
アクセサ機能に限らずマクロや擬似マクロ的な挙動(PHPの場合、内部的にマジックメソッドやリフレクションが使われていたりするやつ)を、少なくともユーザーコード(ここではLaravelを使ってわたしたちWeb Backendの開発者が書いているコードくらいの含意)では意識しないでいいようなコードベースにしておきたい、みたいな思想もあります。
それでもなお取り入れたい場合には、それ相応の合意形成をしておき、周知が済んでいる状況を作りましょう。
②raw valueにアクセスしたい場合が出てきたときに困ることがある
使い方次第では問題ない側面ですね。元値も取り出せるような形でアクセスする(ex: CarbonImmutableへの詰め替え、asRawString()みたいなメソッドの生えたオブジェクトへの詰め替え)分には何も困りません。
一方、不可逆的な加工をすると困る場合もあります。極端な例を示すと下記のような話ですね。
public function getStarNameAttribute($starName)
{
return "あの日見た星の名前:" . $starName;
}
---- // 以下、ユーザーコード
$this->star_name // この場合、評価値は"あの日見た星の名前:アルタイル"のようになり、"アルタイル"だけで呼びたい場合に面倒
こういう、単一のカラム値の変換のみで完結しない、不可逆性の高い合成に使う場合は、素直にそれ用のgetterでも書いておくのが穏当な気がします。(単なるサンプルコードなので手を抜きますが、”あの日見た星の名前”部分は然るべき箇所で定数化して良いかなという気もしますね)
public function getStarNameWithHeadline()
{
return "あの日見た星の名前:" . $this->starName;
}
また、view, presenterのためにする変換であれば、そもそもモデルでやるべきかを見直して良い場合もありそうです。
③パッと見で単なるプロパティアクセスに見えて前処理の存在に気づかない。特にLaravelに慣れていない人だと、想定外挙動の原因がこれだった場合に結構困る
これが一番大きい気がしています。通常のプロパティ呼び出しと見た目が変わらない以上、Docに書いてある仕様なら知ってるでしょ、とは言いづらい気もしています。
どう使えばいいか
「ミューテタとセットで実装したいか」を基準に、アクセサとしての実装をするかどうかを検討すればいいと思っています。
「入出力のフォーマットの多様さないし煩雑さをモデルが吸収してくれるというメリットが勝る場面かどうか」とも言い換えられそうです。
例えば、日付系の値は複数の標準的な記法が並立しているのもあってどうあっても単一のformatで全てを解決するのが難しいものですが、「日付系カラムは必ずCarbonImmutableを介して扱われるという前提」をシステムレベルで持って置くと、そのあたりの管理がだいぶ楽になるとかそういうのです。
FW側のデフォルト仕様とか見てみましょう。ご存知の通り、created_at, updated_atなどの日時値をCarbon, CarbonImmutableを介して取り扱うことにこの機能を用いています。
あとは電話番号とかを3rdのユーティリティ系ライブラリ(ex: libphonenumberとか)やそのラッパークラス、値オブジェクトを介して扱うのも良い路線かもしれません。
例えば、「どのような値が入力されても、最終的にE164仕様に則った国際電話番号文字列としてDBにはstoreされる、ということをモデル機能で保証する」というのはLaravel way的には良いのかなと。
Docの見出しがあくまで「ミューテタ」であることも考慮していいでしょう。「Eloquent ModelをDBとのインターフェースとして捉えた際に、あるカラムの値をどのようなフィルターのもとに扱いたいか」を定めておくための機能として認識すると良さそう、という。
なお、単にMySQLのTINYINT型を二値的に扱いたい(要はbooleanに還元したい)程度のニーズであれば、$castsプロパティに初期値を代入しておくだけで良いかと思います。
Presenter的なレイヤーについて
LaravelでWeb Backendとしてapplication/jsonなAPIを構築する場合、Presenter的なレイヤーを用意した方がいいです。あまり異論のないところかなと思っていますが、一応。
脱線しますが、bladeとかのテンプレートエンジンでtext/htmlする場合でも、テンプレートファイルに値を渡す前にPresenter層的な責務を担うクラスを用意してviewのためにする加工を済ませておくと、テンプレート上に展開する変数のデータ構造が整理されていい感じになると思います。
閑話休題。
さて、特にOpenAPI Specificationなどの構造化されたAPI仕様書を導入している場合、仕様書側のモデル単位とアプリケーション側のそれとを一体的に修正・保守できるような仕組みを作っておくと、高凝集に書きやすくなって体験がいいと思います。
その際、どういうクラスを使って表現していくのがベターかを考えてみるのがこのセクションです。ただ、結論を先取りすると「わりかしなんでもいい」となります。
FW機能:JsonResourceを使う
API Response Schemaの都合で複数のモデルやそれ以外の値を用いたい場合は、下記のようにコンストラクタをいじってやるといいんじゃないでしょうか(親コンストラクタに$resourceに代入する値だけリレーしておき、他は普通に初期化)。
class SomeResource extends JsonResource
{
public function __construct(
SomeModel $resource,
private OtherModel $othermodel
) {
parent::__construct($resource);
}
public function toArray(): array
{
return [
"some_model" => $this->resource,
"other_model" => $this->otherModel
];
}
}
薄い親クラス:再帰的なtoArray()のみをabstract classやtraitに実装
JsonResourceと比べ、個別のユースケースに応じて、インターフェースの都合をいじりやすい点で優れています。
また、「Responseに関わる細かい諸々はJsonResourceクラスなどの内部よりもむしろController側でやりたいなぁ」という気持ちが強い場合にも、こっちの方が扱いやすいかもしれません。
return response()->json(
$presenter->toArray($someModel)
Response::HTTP_OK
);
3rd Partyのライブラリを使う:spatie/fractal等
アリだとは思いますが、依存管理先をやたらに増やしたくない点ではネガティヴがあります。
どういう文脈でも同じことが言えますが、バージョン変更時のデグレード懸念のカバーや、場合によってはabandoned化するリスクを考慮してでも導入したいかを問う必要はあると思います。
また、独自のAPIを構成しているようなライブラリの場合、当該ライブラリを使用したことのないメンバーの認知コストが微増するという負もあると思います。
個人的には、JsonResourceや自作の薄いクラスで困ったことがほぼないのでライブラリ入れなくてもいいレイヤーかなとは思っています。
ただし、シリアライズ時のパフォーマンスのために有用なライブラリを積極導入するという観点もあり得ます。が、シリアライズが重くなる原因に目を向けた方がいいような気もしています。
PHPの型ハンドリングに関して
型システムとかちゃんとわかっているとは口が裂けても言えないんですが、基本的な部分についてはここで言及しておこうと思います。
LaraStanやPsamlを動かせば解析可能なものの、コーディング時のエディタ上での解析は場合によってはあまり厳密でないため、気づくのが遅れ得ると考えています。そうした場合の予防策として認識しておくと良いことは書いておきます。
iterableな型の類、リスト的なるもの(Illuminate\Support\Collection, array等)
多くのリストっぽい型の値は空であり得ます。
-
isEmpty()
-
empty()
等での分岐制御を入れましょう。built-in関数のemptyは評価方針がわかりにくいと言われますが、個人的にはarrayに使う場合はまあいいかなと。
nullになり得ることがわかっている変数等
-
is_nullでの分岐
-
??(null coalescing operator、PHP7系以上)による代替値の提供
等を用いてハンドリングしておきましょう。
未定義可能性が分かっている変数等
-
isset()
-
??(null coalescing operator、PHP7系以上)による代替値の提供
-
?->(null safe operator、PHP8系)
等を用いて回避しましょう。
よくあるのは、連想配列のkeyが存在しないケースを考慮していなかったためにundefined indexのerrorを踏むやつです。
エラーハンドリングに関して
実装時レベルで想定可能だが、例外的だと判断されるようなケース
当該ケースをif文等でカバーして、独自例外クラスなどをthrowしましょう。
ライブラリのDocとかに@throwsがあった場合、①自分で拾って詰め替える②そのまま伝播させるがあり得ますが、文脈次第感があるのでここで言及を止めます。
例外種別に応じてレスポンスを出し分けたい・ログを吐き分けたいニーズが出てきたら、App\Exceptions\Handlerにその制御を入れると幸せになれるかもしれません。
そもそも例外機構(Exceptionおよびtry - catch構文)をなるべく使いたくないですが、Webの場合はネットワーク越しにミドルウェアを扱う場合など、どうしても実行時エラーを例外機構で取り扱う簡便さにニーズが出てきてしまう場面があり、仕方ないですね。
想定していないもの
言語・FW・ライブラリがthrowしたものをhandlerにキャッチさせ、ログ出しやレスポンス形成を行う形でのハンドリングとなります。
大抵の場合、実装修正が必要になるため、アプリケーションのエラー監視用の環境から自分たちに適切にリポートが飛んでくるようにインテグレーションを組んでおきましょう。Sentryとか便利ですね……
PHPDoc
ユーザーコードの場合、@paramや@returnは基本的に不要
Q. なんで?
A. 既に書いてあるから
Single Source of Truthという言葉があります。二重管理は良くないということですね。
この場合、言語機能側の型宣言・型注釈に記されている場合は、あえてPHPDoc側に書かないでもいいんじゃないかなという話です。
これを逆から言うと、言語機能上での宣言・注釈でもPHPDocでもいいから、ユーザーによる理解の助けのためにも、解析のためにも、何かしら正しい情報を書き込んでおきましょうということになりますね。
ただ、あえて@paramや@returnを付ける場合もあります。それは、現状のPHPの言語仕様のみでは表現できない入出力を表現したり、I/Oの表示と併せて想定し得る副作用についての情報を補ったりしたいときです。
仕様の説明について
単一の関数それ自体の仕様については、積極的に書いておきたいですね。
また、自然言語でシステムの振る舞い・ユースケースを説明するような文章をコメンティングしておくのも基本的には良いことだと思います。が、メンテの必要が生じる≒レビュー負荷も生じることは念頭に置きところです。ほどよく書いていきましょう。
上記のようなユースケースの説明の場合、PHPUnit Test側のテストメソッド名による仕様説明・表明に置き換えた方が、変更の必然性が高く、メンテナンスされやすくなるとも思います。
array-shapes記法など、連想配列(ハッシュマップ)に擬似的に型を注釈する方法について
tadsanさんの記事array shapes記法(Object-like arrays)と旧PSR-5記法で型をつける が分かりやすいです。
関数の返り値をクラスではなく連想配列にしたい場合はこちらの使用を検討してもいいかもしれません。
ただし、ユーザーコードで使われる値でない場合(例えばJsonResourceの子クラスのtoArray()や、FormRequestのrules()がこれに相当します。returnをFW側が使う部分ですね)は、むしろ書かない方がベターです。
なお、改行を挟むと支援ツール系に読まれないことが多い記法となります(たしか)。また、改行せずに一行を長くして対応すると、今度はLinter, Formatterの課している制約と共存できないかもしれません。
ともあれ、array shapes記法などを使いたい場面では、本来クラスとして表現することを検討するのが第一に想定される対処だとは思います。
が、代替として単なる箱としてのクラスを実装してしまうと、これはこれで結構アプリケーションを汚すこともある面で考えものです。
そのあたり是々非々で、チーム内で一緒に考えていくのが良さそうですね。
跋
筆者は普段からこのくらいの説明粒度で、かつなるべくやわらかくコードレビューとかしています。
そういう雰囲気でコーディングしたい人にとっては、弊社、楽しい場である可能性があります(リクルーティング前フリ)
というわけで弊社の求人はこちら!
ヘッダー画像を秒で作成してくださったデザイナーのezumさん、ありがとうございます!
ご本人の記事↓
ねこ(おなまえ:モドリッチ)の画像を提供してくださったぎぼんぬさん、ありがとうございます!
ご本人の記事↓