この記事では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;
}
}