Top page  1/39
03
2019

Laravel小規模アプリ開発時の設計方針とかメモ

CATEGORYPHP
最近開発してたLaravelアプリ(APIサーバー)が、だいたい良い感じに(?)出来上がってきたので、参考用に開発時に決めた設計方針とかこういうクラス構成になったとか、そういうのをまとめてみる。
なお、実際にこの方針をいくらか試した、お勉強用リポジトリがこちら。バージョンは5.8。

前提

まず最初に前提条件をまとめる。
  1. ごく小規模なアプリである(サーバーサイド開発は俺一人)
  2. 性能要件も低い(想定リクエスト数は数rpsレベル)
  3. 少なくとも2, 3年は運用する
  4. インフラは決まっている(FaaSとかは不可)

大方針

次いで全体的な方針とそれによって決めたことなど。
  1. 性能要件が低いから、出来るだけごく普通の定番の構成で済ませ、他の人に渡しやすいようにする。
    → PHP + Laravel + MySQL + Redis
  2. かつ小規模なので、アプリの造りもLaravelの標準的なやり方で行う。Laravel経験者ならすぐ触れるソースを目指す。
    → 普通のMVCでやる。オレオレフレームワーク的な仕組みは極力避ける(ただしサービス層だけは設けた)。
  3. OSSライブラリなどはあれば出来るだけ用いる。自前で実装するより有名なライブラリを使う。
  4. 管理画面的なものも必要だが、Laravel側ではAPIのみ提供する形にして、造りを統一する。
    (実際の管理画面のフロントはAngularで実装。既にAngularの資産があったことと、JSで作った方がユーザー的に使い易いので。画面についてはLaravelとは別扱いとして割り切り。)
  5. 機能ごとにアプリを分割したりはしない。1アプリに全て(通常のAPI、管理画面用のAPI、バッチ等々)含める。

Tag: PHP Laravel

21
2019

LaravelでAPIパラメータをCamel Caseにするとめんどくさい

CATEGORYPHP
相変わらずLaravelネタ。Laravelのモデルのプロパティ名はイコールDBの列名なので、普通だと user_id みたいなDBで主流のスネークケースになる。で、JSON変換時もそれがそのまま使われるのだが、昨今のJSONのAPIだとキー名は userId みたいなキャメルケースが主流だと思うので、リクエスト/レスポンスではキャメルケースに変換したい。しかし、設定一発で解決とはいかず、ちょっと面倒な対応が必要だったので、その辺メモ。なお、確認したバージョンはやや前後するがLaravel 5.7。

いろいろ調べた結果、Laravelのモデルをキャメルケースで扱う手法としては、以下の3パターンがありそうだった。
  1. ミドルウェアでリクエスト/レスポンスのキー名を変換する。
  2. モデルのgetAttribute()/setAttribute()でキー名を変換する。
  3. モデルのfill()/toArray()でキー名を変換する。
その他にも、「DBの列名をキャメルケースにする」とか「個別にgetter/setter等を定義する」とかもあるけど現実的じゃないので除外で。
自分が試した限り、3が一番影響範囲とかも少なくよさげだった。以下、各パターンについて解説。

1. ミドルウェアでリクエスト/レスポンスのキー名を変換する。

一番根こそぎな奴。全リクエスト/レスポンスが通るミドルウェアで、キーを変換してsetData()やらで上書きしてやればよいはず。
OSSの変換ミドルウェアも見かけたので、うまくいけばそれ組み込むだけで済むかも。
(自分が試したときはなんか動かなかった?ので自作した。)

メリットとしては、入力も出力も全部残さず変換できること。後述する他の方法だと、場合によっては変換漏れがありえるが、ミドルウェアでやれば基本的に全部処理できる。

逆にデメリットとしては、基本的に全部変換するしか出来ないこと。例えば、このパラメータだけスネークケースで返したい、とかいうものがあると、困ってしまう。
あと、任意のJSONデータを保存する機能、とかがあったりすると、詰んでしまう。
なので、APIの仕様やフォーマットを自分で決められる状況以外ではやや使い難い。
(フォーマットが不定のAPIだけミドルウェアオフにして個別に対応とか、代替手段はあるが。)

また、JSONレスポンスのつもりだけで実装してしまって、テキストやバイナリ、ストリームを返すAPIが考慮漏れでエラーになるとか、そういう地味な事故も。
ミドルウェアは影響範囲が大きいので、一見単純だけどちょっと使い勝手悪そうだった。

2. モデルのgetAttribute()/setAttribute()でキー名を変換する。

この辺で解説されてるやつ。紹介しつつも、実際に試してはいない(汗

モデルの全プロパティアクセスが通るgetAttribute()/setAttribute()でキー名を変換することで、キャメルケースでのプロパティアクセスを可能とする。
メリットとしては、JSON変換に限らずプロパティアクセスでもキャメルケースが使用できること。
スネークケースはPHPのプロパティの命名規則としては異端なので、ソースコード的には全部キャメルケースに統一できて大変よさそう。

デメリットとしては、JSON変換についてはこれだけでは結局足りないこと。
結局、これプラス次のtoArray()とかもやらないといけないので、そうなるとあえて採用するメリットは低いかなと思った。

3. モデルのfill()/toArray()でキー名を変換する。

という事で考えた案。モデル周りでキー名の自動変換が必要になるのは結局、JSONのリクエストパラメータをモデルにそのまま設定する/モデルをそのままJSONレスポンスとして返す、の2箇所だけな気がしたので、その場合に使われるfill()/toArray()だけを上書きして対応する(実装例)。

メリットは、影響範囲がミドルウェアと違ってモデルに閉じているので、使い易いこと。
デメリットは、逆に影響範囲が狭いので、モデル以外のものは別途対応しなければいけないこと(たぶんPaginatorぐらいで、それはカスタムクラス差し替えで対応できた)。

また、fill()/toArray()以外の部分はあくまでそのままなので、個別にプロパティを参照する場合などは、ちゃんとスネークケースとキャメルケースを使い分けないといけない(newとかcreate()とかは間接的にfill()を呼んでいるのでセーフ)。

あと、モデルに入れるタイミングで変換しているという都合上、バリデーターもキャメルケースと使い分けないといけない。
かつ、悪意を持って同じパラメータをキャメルケースとスネークケースで送るとか、そんな可能性もあるかも?
なので、全体的にプログラマーが注意しないと事故りやすい手法かもしれない。


今回は、まさに任意のJSONデータを保存する機能がアプリの中核だったので、最初1で実装したものの最終的に3に移行した。
Laravelが標準で対応してくれれば一番良いのだけれど、現状自分でやるしかなさそうなので、ご参考ください。

Tag: PHP Laravel

17
2019

LaravelでSQLログを出力する

CATEGORYPHP
最近続くLaravelネタ。今回はデバッグ用のSQLログの出し方について。試したバージョンは5.8。

まず結論から。辿り着いた個人的最適解はこれ。これをこんな感じにEventServiceProvider辺りに入れてください。
\DB::listen(function ($ev) {
\Log::debug("DB({$ev->connectionName}): {$ev->sql}; bindings=" . \json_encode($ev->connection->prepareBindings($ev->bindings)) . ' time=' . sprintf("%.2fms", $ev->time));
});
Event::listen('Illuminate\Database\Events\TransactionBeginning', function ($ev) {
\Log::debug("DB({$ev->connectionName}): start transaction");
});
Event::listen('Illuminate\Database\Events\TransactionCommitted', function ($ev) {
\Log::debug("DB({$ev->connectionName}): commit");
});
Event::listen('Illuminate\Database\Events\TransactionRolledBack', function ($ev) {
\Log::debug("DB({$ev->connectionName}): rollback");
});
以下解説。LaravelではデバッグログをONにしたら自動でSQLログが出てくる…という仕組みは無いみたいなので、SQL実行時のイベントを受け取って手動でログを出力する。
Connection::enableQueryLog() というメソッドはあるが、これはDB::listen()のタイミングでメモリ上に同じ内容を保存するだけっぽいので、特に使うメリットなさそうだった。)

次いで、トランザクションの開始終了は↑のイベントでは取れないので、別途こちらもイベントを受け取って手動でログ出力する。

さらに3つ目。SQLのバインド変数が入ってくる$ev->bindingsだが、ここには実際のSQLに渡されるキャストされた値、ではなくキャスト前の値が入ってくるので、prepareBindings()を通して、出来るだけ実際の値にする。
DateTimeInterfaceとかboolとか以外のインスタンスはそのままだけど…。)


以上、LaravelのSQLログの出し方をググるといろいろページが出てくるけど、その通りにやったら3つ目が抜けてて見事にハマったのでまとめてみた。参考までに。

Tag: PHP Laravel

06
2019

LaravelでCache-Controlヘッダーを設定する

CATEGORYPHP
Laravelでは、レスポンスのCache-Controlヘッダーがデフォルトでは no-cache, private で返るみたいなんだけど、キャッシュしてもいいAPIでは設定変えたかったのでその方法。

ググると自前でミドルウェア作って~みたいな情報も出てくるけど、結論からいうとLaravel 5.6からSetCacheHeadersというミドルウェアが標準で用意されているので、今はこれを↓みたいに有効にするだけでよい。
Route::middleware('cache.headers:public')->group(function () {
Route::get('news', 'NewsController@index');
});
最終的に呼ばれるのはResponse::setCache()。オプションの解説は見当たらなかったが、publicとかetagとかを指定するとそれが有効になるようだ。

Tag: PHP Laravel

04
2019

LaravelでPaginatorをカスタムクラスに差し替える

CATEGORYPHP
またLaravelネタ。Laravel 5.7でJSON APIを作るにあたって、標準のLengthAwarePaginatorをそのまま使うと余計なプロパティが返ってしまうのでオーバーライドしたかった。最初はModel::paginate()をオーバーライドしようかと思ったけど、そんなことせずとも以下のようにDIで簡単に出来た。
namespace App\Models;

use Illuminate\Pagination\LengthAwarePaginator;

class JsonLengthAwarePaginator extends LengthAwarePaginator
{
public function toArray() : array
{
return [
'currentPage' => $this->currentPage(),
'data' => $this->items->toArray(),
'from' => $this->firstItem(),
'lastPage' => $this->lastPage(),
'perPage' => $this->perPage(),
'to' => $this->lastItem(),
'total' => $this->total(),
];
}
}
namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
public function boot() : void
{
}

public function register() : void
{
$this->app->bind(
'Illuminate\Pagination\LengthAwarePaginator',
'App\Models\JsonLengthAwarePaginator'
);
}
}
paginate()でLengthAwarePaginatorインスタンスを生成してるのはBuildsQueries::paginator()だったけど、こいつが親切にもDIを使ってインスタンスを作っているので、設定を書き換えるだけでいけた。
なお、simplePaginate()の場合はPaginatorクラスが返るので、必要ならそちらも設定忘れずに。

Tag: PHP Laravel