【PHP】クラス設計の小技

 この記事では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;
    }
}
>株式会社シーポイントラボ

株式会社シーポイントラボ

TEL:053-543-9889
営業時間:9:00~18:00(月〜金)
住所:〒432-8003
   静岡県浜松市中央区和地山3-1-7
   浜松イノベーションキューブ 315
※ご来社の際はインターホンで「316」をお呼びください

CTR IMG