この記事ではPHPでクラスを一から作る時によく使う小技を紹介します。例はLaravel前提のコードですが、パターン自体には関係ありません。Fuelでも素でも使えます。
配列で初期化をします。new した時に引数順でバグを起こすことがなくなります。
class FooBar { public $foo; public $bar; public function __construct(array $attributes) { if (! isset($attributes['foo'])) { throw new \RuntimeException('fooが必要です'); } $this->foo = $attributes['foo']; $this->bar = $attributes['bar'] ?? 'デフォルト値'; } } new FooBar([ 'foo' => 'hoge', 'bar' => 'fuga', ]);
$thisを返すことによってメソッドをチェーンできます。Builderパターン向けです。
マンガでわかる Builder – Qiita
class HogeBuilder { protected $state = 0; public function next(){ $this->state++; return $this; } public function pre(){ $this->state--; return $this; } public function getState(){ return $this->state; } } echo (new HogeBuilder)->next()->next()->pre()->pre()->next()->getState(); // 1
abstaract classなりinterface + traitで一部メソッドの実装を強制します。強制する部分はなるべく単純でユースケースに左右されやすい部分のみにするのが扱いやすいです。Factory MethodパターンやTemplate Methodパターンにおすすめです。
マンガでわかる Factory Method – Qiita
マンガでわかる Template Method – Qiita
/** * バリデーション機能を持つクラスであるための契約を定義 */ interface ValidatableContract { /** * バリデーションルール定義 * @return array */ public static function rules(): array; /** * バリデーション実行 * @param array $attributes * @return bool */ public static function validate(array $attributes): bool ; } /** * 複雑な実装を担当 * @mixin ValidatableContract */ trait Validatable { public static function validate(array $attributes): bool { // なんか複雑なバリデーション処理 } } /** * クラスによって自由に変わって記述が簡易に済む rulesメソッドの実装だけ担当 * Class User */ class User implements ValidatableContract { use Validatable; public static function rules(): array { return [ 'email' => ['string', 'max:100', 'email:rfc'], 'name' => ['string', 'max:32'], ]; } } // Userコントローラで function update(){ if(!User::validate(Request::post())){ throw new ValidationError(); } // 正常処理 } // Postコントローラで function create() { if(!Post::validate(Request::post())){ throw new ValidationError(); } // 正常処理 }
より巨大な対象をテンプレート的に作るには、あるインターフェースを満たした複雑な表現をしている値クラス、インターフェースを満たした値クラスを使って処理をする親玉抽象クラス、インターフェースを満たした値クラスを使うことを宣言する子クラス、を作るといいです。値クラスの考えにはValueObjectが役に立ちます。
/** * 検索処理実体を持つ親玉クラス */ abstract class AbstractSearch { /** * @var Builder */ public $query; /** * BaseSearchService constructor. */ public function __construct() { $this->query = $this->from(); } abstract protected function select(): array; abstract protected function from(): Builder; /** * @return array<SearchWhereContract> */ abstract protected function where(): array; abstract protected function defaultQuery() { return $this->query; } /** * 検索処理 */ public function search($search, $orderBy){ $query = $this->from() ->defaultQuery() ->select($this->select()); if (count($search)) { $where = $this->where(); foreach ($search as $key => $value) { if (array_key_exists($key, $where)) { $this->query = $where[$key]->buildQuery($this->query, $value); } } } // orderBy関連のいろいろ return $query; } } /** * ユースケースに応じた値クラスをどう使うか決める子クラス */ class UserSearch extends AbstractSearch { protected function select(): array { return [ 'id', 'name', 'email', ]; } protected function from(): Builder { return User::query(); } /** * SearchWhereを満たしたインスタンスの配列を返す。 * @return array<SearchWhere> */ protected function where(): array { retrun [ 'id' => new PerfectMatch('id'), 'name' => new PartialMatch('name'), 'email' => new PartialMatch('email'), ]11 } } /** * 値クラス用のインターフェース */ interface SearchWhereContract{ /** * クエリを構築して構築後のクエリを返す * @param Builder|Builder $query * @param mixed $value * @return Builder|Builder */ public function buildQuery($query, $value); } /** * 値クラスの実体A. 完全一致検索 */ class PerfectMatch implements SearchWhereContract { /** * @var Closure */ private $isSkip; /** * @var string */ private $column; public function __construct(string $column, ?Closure $isSkip = null) { $this->column = $column; $this->isSkip = $isSkip ?? static function ($value) { return $value === null || $value === '' || $value === []; }; } public function buildQuery($query, $value) { if (! $this->isSkip->__invoke($value)) { return $query->where($this->column, '=', $value); } return $query; } } /** * 値クラスの実体B.部分一致検索 */ class PartialMatch implements SearchWhereContract { /** * @var Closure */ private $isSkip; /** * @var string */ private $column; public function __construct(string $column, ?Closure $isSkip = null) { $this->column = $column; $this->isSkip = $isSkip ?? static function ($value) { return $value === null || $value === '' || $value === []; }; } public function buildQuery($query, $value) { if (! $this->isSkip->__invoke($value)) { return $query->where($this->column, 'like', "%${value}%"); } return $query; } }