なにがしたい?
たとえば下記のような「会社登録」のための関数クラスを作ったとします。
$request はフォームからの入力やAPIでポストされたデータを想定しています。
class Create
{
// コンストラクタで関連したオブジェクトを取得しているけど省略
public function __invoke(array $request)
{
// 会社の作成
$company = ($this->company_create)($request['company']);
// 店舗の作成
$request['store']['company_id'] = $company['id'];
$store = ($this->store_create)($request['store']);
// 管理スタッフを作成
$request['staff']['store_id'] = $store['id'];
$staff = ($this->staff_create)($request['staff'],'company_admin');
return $company;
}
}
これを呼び出すコードはこんな感じです。
public function store(Request $request, Create $companyCreate)
{
$company = $companyCreate($request->all());
return view('company.show',compact('company'));
}
ところが、フォームがレガシーコードで、CompanyCreate()が想定している$requestのフォーマットと違うことが判明。コントローラで配列のフォーマットを変換しないといけない。
でもチョット待って!
その「CompanyCreate()が想定している $requestのフォーマット」ってどんなの? 仕様書に書いてある? うわ、仕様書めっちゃ古いしコレ大丈夫なん? あかんやん、DBに store_name が無いって怒られる。他にも足りないのめっちゃあるし、なんやねんこれーーー!
とならないように、**必要なデータがなにか?**が、 Company\Create.php を見ただけでわかるようにならないか?という課題です。
NOW IN PROGRESS...
既知の問題があって継続的改善中。
- 要素数が可変の配列に対応できない
'stores[]'=>
か'stores.*'=>
と書きたい - Laravelのバリデータ使えば良いんじゃないか
- でもそれだと「余分な要素のカット」ができない
- デフォルト値のセットもできない
- 必要なデータが何かはわかるようになるけど……
結論
こんな実装をしました。
class Create
{
use \Usecases\Concerns\ArrayFilterDefault;
// コンストラクタで関連したオブジェクトを取得しているけど省略
/**
* 引数に渡すデータ配列のフォーマット
* ここに指定されたカラム以外のデータは無視される
* null 以外を指定すると、省略時のデフォルト値になる
*/
protected $request_format = [
'company' => [
'name' => null,
'comment' => null,
'sample' => false, // デフォルト値
],
'store' => [
'name' => '本店事務所',
'pref' => null,
'address1' => null,
'address2' => null,
'tel' => null,
'comment' => null,
],
'staff' => [
'name' => null,
'email' => null,
'password' => null,
],
];
public function __invoke(array $request)
{
$request = $this->ArrayFilterDefault($this->request_format, $request);
// 略
}
}
トレイトはこんな感じ。
namespace Usecases\Concerns;
trait ArrayFilterDefault {
// http://arstropica.com/uncategorized/google-analytics-http-api/
private function array_intersect_key_recursive(array $array1, array $array2) {
$array1 = array_intersect_key($array1, $array2);
foreach ($array1 as $key => &$value) {
if (is_array($value) && is_array($array2[$key])) {
$value = $this->array_intersect_key_recursive($value, $array2[$key]);
}
}
return $array1;
}
// https://wpscholar.com/blog/filter-multidimensional-array-php/
private function array_filter_recursive( array $array, callable $callback = null ) {
foreach ( $array as &$value ) {
if ( is_array( $value ) ) {
$value = $this->array_filter_recursive( $value, $callback );
}
}
$array = is_callable( $callback ) ? array_filter( $array, $callback ) : array_filter( $array );
return $array;
}
/**
* 与えられた配列を、指定されたフォーマットの配列に合わせる
*/
protected function ArrayFilterDefault(array $defaults, array $options)
{
return $this->array_filter_recursive(
array_replace_recursive(
$defaults,
$this->array_intersect_key_recursive(
$options,
$defaults
)
)
, function($val) { return !is_null($val) && ($val!==[]); }
);
}
}
呼び出し元はこのようになりました。
public function store(Request $request, Create $companyCreate)
{
$data = [
'company' => [
'name' => $request->name,
'comment' => $request->comment,
],
'store' => [
'pref' => $request->prefecture,
'address1' => $request->address,
'address2' => $request->address_rest,
'tel' => $request->tel,
'comment' => $request->comment,
],
'staff' => [
'name' => $request->manager_name,
'email' => $request->manager_email,
'password' => $request->password,
],
];
$company = $companyCreate($data);
return view('company.show',compact('company'));
}
解説
配列を指定の書式に矯正する
アイデアとしては、Createのクラス定義に欲しい配列の定義を書いておいて、その配列に指定されているキーしか受け付けない、というものです。まずはそんなフィルタが作れないかなーと。Laravelの$fillableのようなものですが、多次元配列に対応したい。
array_replace_recursive( $defaults, array_intersect_key($options, $defaults));
array_intersect_key
http://php.net/manual/en/function.array-intersect-key.php#115006
2つの配列の共通のキーのみ残す。
array_intersect_key_recursive
http://arstropica.com/uncategorized/google-analytics-http-api/
それを再帰的に適用する。
array_filter
http://php.net/manual/ja/function.array-filter.php
特定の値を取り除く
array_filter_recursive
https://wpscholar.com/blog/filter-multidimensional-array-php/
それを再帰的に適用する。
デフォルト配列には必要なカラムを書いておき、基本的にすべての値を null にしておきます。
array_intersect_key_recursive すると両方にあるキーだけ残るので、最後に値がNULLのものを取り除けば完了。その際、空配列を除去するかどうかは判断の分かれるところです。今回は除去しましたが、デフォルト値としてから配列を与えたいケースがある場合は実装を変更する必要があるかも。
$defaults = [
'company' => [
'name' => null,
'comment' => null,
'sample' => false, // デフォルト値
],
'store' => [
'name' => '本店事務所', // デフォルト値
'pref' => null,
'comment' => null,
],
'staff' => [
'name' => null,
'email' => null,
'password' => null,
],
];
$options = [
'company' => [
// 'name' => 1, // 少ない
// 'comment' => 2, // 少ない
// 'sample' => 3, // 少ない
'DUMMY' => 'DUMMY', // 多い
],
'store' => [
'DUMMY' => 'DUMMY', // 多い
'name' => 4,
'pref' => 5,
'comment' => 9,
],
// 'staff' => [ // 少ない
// 'name' => 10,
// 'email' => 11,
// 'password' => 12,
// 'DUMMY' => 'DUMMY',
// ],
'DUMMY' => 'DUMMY', // 多い
],
[
'company' => [
'sample' => false, // デフォルト値だけ残る
],
'store' => [
'name' => 4,
'pref' => 5,
'comment' => 9,
],
// 'staff' => [], // 空配列は除去される
];
大量の引数問題
……というか最近考えていること
「引数がたくさんある」というのは、良くないコードだと言われています。それだけ多くのデータに依存しているから、テストもパターンが多くなるし、コードもとても読みにくくなります。
class CompanyController extends Controller
{
public function store(
$name,
$name_kana,
$zipcode,
$pref,
$address1,
$address2,
$tel,
$president,
$email,
$comment
)
{
// do something ...
}
}
じゃあ、こうしてみましょうか。
class CompanyController extends Controller
{
public function store( $data )
{
// do something ...
}
}
うん、スッキリ。
じゃあ、処理を書いていきましょう。
class CompanyController extends Controller
{
public function store( $data )
{
// do something ...
if( $data['pref'] == '東京都' ){
// do something ....
}
// do something ...
foreach( $data['users'] as $user ){
// do something ....
}
// do something ...
another_function( $data );
// do something ...
}
}
$data['..........?']
あれ?
$data って中身どうなってるんだっけ?
// ERROR: undefined index 'users'
foreach( $data['users'] as $user ){
おや?
$data に入っているはずのusersが入ってきてないぞ!
another_function( $data );
うわ!
$data まるごと渡しちゃってる。
この先でなにやってるか全然わかんないぞ。
ほら、言わんこっちゃない。
でも。
これって、$data の型が保証されていないからというのが問題の本質ではないかと思ったんですよね。もっというと、大量の引数で問題が顕著に現れるのは、全然関係ないオブジェクトや条件値が複数絡み合ってくるパターン。逆に、この例のように、1つ2つのオブジェクトに関した「データ」が大量にあるときは、それが全部見えたほうがむしろ都合がいいはず。
そこで
class CompanyController extends Controller
{
public function store(
$name,
$name_kana,
$zipcode,
$pref,
$address1,
$address2,
$tel,
$president,
$email,
$comment
)
{
// do something ...
}
}
これに戻ってくるんですが、さすがにガチで「引数」だとメンテナンス性が悪いし可読性が悪い。順番も覚えないといけないし、何より順番を間違えたらどえらいことに。そこで配列で「名前付き引数」にしました。さらに多次元配列を使うことでグルーピングも可能に。
こうして、Createが欲しい配列が常にその場に書いてあるようになれば、このように、大量に代入を並べて型変換していても、むしろスッキリ読みやすく、メンテナンス性も高くなったと思います。
public function store(Request $request, Create $companyCreate)
{
$data = [
'company' => [
'name' => $request->name,
'comment' => $request->comment,
],
'store' => [
'pref' => $request->prefecture, // Requestの書式が違うが
'address1' => $request->address, // 1つ1つ手渡し
'address2' => $request->address_rest, // こうすると
'tel' => $request->tel, // コアである
'comment' => $request->comment, // Createを修正せずに
], // うまく結合できる
'staff' => [
'name' => $request->manager_name, // Coreに合わせる
'email' => $request->manager_email, // コントローラは調整役
'password' => $request->password,
],
];
$company = $companyCreate($data);
return view('company.show',compact('company'));
}
昔はこんなコードをみたら「なんて冗長なことしてるんだ!」と、コード圧縮にやっ気になっていましたが、こういう、特に「モジュール同士のコミュニケーションポイント」は、一括でぽんとブラックボックス渡しをせず、冗長でも1つ1つ渡していくほうが、結合がゆるく、メンテナンスしやすいコードになる、こともあるのかな、と。
補足
もっというと、引数はそんなにガッツリ固まってなくても良くて、リソースごとに分けても良いし、
public function __invoke(array $company,array $store, array $staff)
{
$request = $this->ArrayFilterDefault($this->company_format, $company);
$request = $this->ArrayFilterDefault($this->store_format, $store);
$request = $this->ArrayFilterDefault($this->staff_format, $staff);
// 略
}
もっというと、ここはふつう、クラスだろ!
public function __invoke(Company $company,Store $store, Staff $staff)
{
// 略
}
というのはごもっともなのですが、諸事情あり、今回はよりプリミティブに配列で実装してみました。
諸事情と言うか、個人的な修行不足です。また次回挑戦します!