🔍

[関数系] findByを作ってみた

2021/06/15に公開

前提

本記事はQiitaからの移行(移行元)です。

概要

MVCでSELECTしたい箇所の呼び出しを書いていたら例のごとく、以下のようなたくさんのメソッドが出来上がった。

  • getById
  • getNameByEmail
  • getEmailById

etc...

このような状況を打破するために便利なメソッドを作ることができる。
いわゆるメタプログラミングと言われるもの。

findBy

このメソッド、「findById」みたいな形で呼ぶとidをWHERE句に使ってSELECTをやってくれる。
ではどうやったら実装できるのか。

findBy実装概要

MVCであることを前提とする。
また、今回はfindByの内容を把握したいだけなので、モデル呼び出しの際に指定するカラムは一つとする。

  1. findByを使いたいモデルの親モデルを作成する(大体はBaseModelとか)
  2. __call()メソッドを利用してメソッド名をゴニョゴニョするように実装する
  3. findByメソッドを実装する

大体こんな感じ。

実装して見ると

findBy

class BaseModel
{ 
    public function __call($method_name, $method_argument)
    {
        $column = '';
        
        if (strpos($method_name, 'findBy') === 0) {
            // findBy以降のcolumnを切取して最初を小文字に。
            $column = lcfirst(substr($method_name, 6)); 
        }
        
        // ここでreturnしないと返る値がNULLとなる。
        return $this->findBy($column, $method_argument[0]);
    }
    
    /**
     * マジックメソッド
     * 基本的にはfindByIdのような形で呼ばれることを前提とする。
     *
     * @param  string $where_column WHEREの条件に用いるカラム。今回は一つのカラムのみとする。
     * @param  string $where_value WHEREの条件に用いる値。今回は一つのカラムのみとする。
     * @return array|bool SELECTに成功したら結果の連想配列, 失敗したらfalse
     */
    public static function findBy($where_column, $where_value)
    {

        // もっとスマートな呼び出しもとクラス名取得もあったはずなので真似しない方が良いかも。
        $trace = debug_backtrace();

        // 呼び出し元のクラスを指定してそのオブジェクトのclass名を取得
        // 呼び出し元のモデルクラスの命名については、テーブル名の単数系を想定している
        // 例えば、ユーザーモデルであれば、Userクラス。
        // 呼び出しもとをモデルクラス以外にも対応する場合は、また実装を変える必要がある。
        $table = lcfirst(get_class($trace[1]['object'])).'s'; 
        
        $sql = "SELECT * FROM `{$table}` WHERE `{$where_column}`='{$where_value}'";
        
        $pdo = DbConnector::getPdo();
        
        $stmt = $pdo->prepare($sql);
        
        if ($stmt === false) {
            // Exceptionなど適切に失敗したときの処理を呼び出す。
            // ここでは、例外処理まで言及しないため、ひとまずfalseとしている。
            return false;
        }
        
        $stmt->execute();
        $result = $stmt->fetch(PDO::FETCH_ASSOC);
        
        return $result;
    }
}

上記のメソッドをUserControllerなどで例えば以下のように呼ぶ。

呼び出し側(Laravelを例として)
class UserController
{
     public $users;

     public __construct(User $users)
     {
         $this->users = $users;
     }

     public function sample()
     {
         return $this->users->findByEmail('sample@gmail.com');
     }
}

肝は__call()

これは存在しないメソッド名が呼ばれた時に呼ばれるメソッドのようで、staticのバージョン(__callStatic())もある。__callにfindByのようなものを入れておけば大体よしなにできるようになる。

https://qiita.com/ntm718/items/ac26b99a8a6bae0692ea

こちらにもあるが、他にもマジックメソッドを使うことで似たようなことをできる。
マジックメソッドで他にも候補があるが、今回は簡単に例として示すために__call()を選択している。

デメリット

簡単に呼び出しのメソッドを追加できるがデメリットも存在する。
わかった上で利用することが大切。

  1. interfaceで縛る効果がなくなる
  2. 1.により、補完が充実しなくなる
  3. 調査の際に、呼び出されたメソッドでgrepしても見つからないため、ログが追いづらい

使うと便利だが、使い所を選んだほうが良い。

GitHubで編集を提案

Discussion