2013年1月16日

CodeIgniter + 超シンプルObject Relational Mapping(ORM)





■ はじめに

このページで紹介するORMはCodeIgniterのメソッドから極力外れないように作成した非常に簡素なものです。後述しますが、既にCodeIgniterで使えるORM用のライブラリが存在します。多機能なものを使いたい方はそちらをお勧めします。

■ Object Relational Mapping(ORM)とは

オブジェクト関係マッピング(英: Object-relational mapping、O/RM、ORM)とは、データベースとオブジェクト指向プログラミング言語の間の非互換なデータを変換するプログラミング技法である。オブジェクト関連マッピングとも呼ぶ。実際には、オブジェクト指向言語から使える「仮想」オブジェクトデータベースを構築する手法である。
(引用元:オブジェクト関係マッピング)



■ CodeIgniterで使えるORMライブラリ





■ コンセプト


  • シンプル、簡単、拡張可
  • CodeIgnierのメソッドを極力変更しない
  • モデルファイルを自動で作成。



■ 完成図


実際に作成するファイルは MY_Loader.php, MY_Model.php, orm_helper.php の3つです。
モデルファイルは自動で作成されます。
MY_Controllerはこちらのページを参照してください。


■ 開発環境

Windows + WAMP + CodeIgniter 2.1.3 を使用しました。
$config['base_url'] は http://localhost/CodeIgniter_2.1.3/ になります。


■ データテーブル作成

コードを始める前にサンプル用のテーブルを作成します。

CREATE TABLE sample (
  id int(11) NOT NULL AUTO_INCREMENT,
  field1 varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  field2 varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  PRIMARY KEY (id)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

テーブル名がそのままモデルのファイル名、およびクラス名になるので、コントローラに利用する名称は避けるようにしてください。


■ モデル自動生成ヘルパの作成

たいていのORMだとデータベースに追加・変更・削除が発生した場合に自分でORM用のファイルを変更しなければならないですが、面倒なのでデータベースからモデルファイルを自動で作成する簡単なヘルパを作ります。

/application/helpers/orm_helper.php
<?php
/**
* Object-relational mapping (ORM) file generator
* 
* Generate folloing files 
* /modles/--database table name--.php
* /models/basemodels/base--database table name--.php
*/
function generate_models()
{
    $ci =& get_instance();
    $tables = $ci->db->list_tables();
    
    foreach($tables as $table)
    {
        $res = $ci->db->query('DESCRIBE `'.$table.'`');
        
        $primary = FALSE;
        $data = '<?php // '.$table.' table mapping'.PHP_EOL; 
        $data .= 'class Base'.ucfirst($table).' extends MY_Model'.PHP_EOL;
        $data .= '{'.PHP_EOL;
        foreach($res->result() as $row)
        {
            $data .= "\tvar $".$row->Field.";".PHP_EOL;
            if($row->Key == 'PRI')
                $primary = $row->Field;
        }
        
        if($primary)
            $data .= "\t"."var \$primary = '".$primary."';".PHP_EOL;
        else
            $data .= "\t"."var \$primary = FALSE;".PHP_EOL;

        $data .= "".PHP_EOL;
        $data .= "\tfunction __construct(\$table=null)".PHP_EOL;
        $data .= "\t{".PHP_EOL;
        $data .= "\t\tparent::__construct(\$table);".PHP_EOL;
        $data .= "\t}".PHP_EOL;
        
        $data .= '}';
        
        $base_model_path = CSTPATH.'models/BaseModels/';
        if( ! file_exists($base_model_path)) mkdir($base_model_path);
        // generate base model files
        file_put_contents($base_model_path.'Base'.ucfirst($table).EXT, $data);
        
        // check general model files
        if( ! file_exists(CSTPATH.'models/'.$table.EXT))
        {
            $data = '<?php'.PHP_EOL; 
            $data .= 'class '.ucfirst($table).' extends Base'.ucfirst($table).PHP_EOL;
            $data .= '{'.PHP_EOL;
            $data .= '}'.PHP_EOL;
            file_put_contents(CSTPATH.'models/'.$table.EXT, $data);
        }
    }
}

このヘルパは実行すると1つのデータテーブルから2つのファイルを作成します。


■ Modelの拡張


/application/core/MY_Model.php
<?php
class MY_Model extends CI_Model
{
    var $table;
    
    function __construct($table=null)
    {
        $this->table = ($table) ? $table : get_called_class();
        log_message('debug', ucfirst($this->table) . " Class Initialized");
    }
    
    
    /**
    * find by primary key
    * 
    * @param mixed $id
    * @return class object or FALSE
    */
    public function find($pk)
    {
        $ci =& get_instance();
        $ci->db->where($this->primary, $pk);
        $q = $ci->db->get($this->table);
        if($q->num_rows())
        {
            $array = $q->row_array();
            foreach($array as $key => $value)
            {
                $this->$key = $value;
            }
            // return object
            return $this;
        }
        else return FALSE;
    }
    
    
    public function save()
    {
        $ci =& get_instance();
        $primary = $this->primary;
        $table = $this->table;
        // temporary unset values to run query
        unset($this->primary);
        unset($this->table);
        if($where = $this->$primary)
        {   // update row
            $ci->db->where($primary, $where);
            $ci->db->update($table, $this);
        }
        else
        {   // insert new row
            $ci->db->set($this);
            $ci->db->insert($table);
        }
        
        // re-set value and return obj
        $this->primary = $primary;
        $this->table = $table;
        return $this;
    }    
}

$this->load->model('sample') でモデルをロードすると $this->sample が利用できるようになります。
$sample = new Sample(); でも良かったんですが、できるだけCodeIgniterの書式に沿った書き方をしたかったのでこうなりました。


■ Loaderの拡張

/application/models/BaseModels/Base--Data Table--.php が存在する場合に読み込む条件文を83-85行目に追加するだけです。

/application/core/MY_Loader.php
<?php
class MY_Loader extends CI_Loader
{
    /**
     * Model Loader
     *
     * This function lets users load and instantiate models.
     *
     * @access    public
     * @param    string    the name of the class
     * @param    string    name for the model
     * @param    bool    database connection
     * @return    void
     */
    function model($model, $name = '', $db_conn = FALSE)
    {
        if (is_array($model))
        {
            foreach ($model as $babe)
            {
                $this->model($babe);
            }
            return;
        }

        if ($model == '')
        {
            return;
        }

        $path = '';

        // Is the model in a sub-folder? If so, parse out the filename and path.
        if (($last_slash = strrpos($model, '/')) !== FALSE)
        {
            // The path is in front of the last slash
            $path = substr($model, 0, $last_slash + 1);

            // And the model name behind it
            $model = substr($model, $last_slash + 1);
        }

        if ($name == '')
        {
            $name = $model;
        }

        if (in_array($name, $this->_ci_models, TRUE))
        {
            return;
        }

        $CI =& get_instance();
        if (isset($CI->$name))
        {
            show_error('The model name you are loading is the name of a resource that is already being used: '.$name);
        }

        $model = strtolower($model);

        foreach ($this->_ci_model_paths as $mod_path)
        {
            if ( ! file_exists($mod_path.'models/'.$path.$model.EXT))
            {
                continue;
            }

            if ($db_conn !== FALSE AND ! class_exists('DB'))
            {
                if ($db_conn === TRUE)
                {
                    $db_conn = '';
                }

                $CI->load->database($db_conn, FALSE, TRUE);
            }

            if ( ! class_exists('Model'))
            {
                load_class('Model', 'core');
            }
            
            if(file_exists($mod_path.'models/BaseModels/'.'Base'.ucfirst($model).EXT))
            {
                require_once($mod_path.'models/BaseModels/'.'Base'.ucfirst($model).EXT);
            }

            require_once($mod_path.'models/'.$path.$model.EXT);

            $class = ucfirst($model);

            $CI->$name = new $class($model);

            $this->_ci_models[] = $name;
            return;
        }

        // couldn't find the model
        show_error('Unable to locate the model you have specified: '.$model);
    }

}



■ サンプル

/application/controller/welcome.php
<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');

class Welcome extends MY_Controller {

 /**
  * Index Page for this controller.
  *
  * Maps to the following URL
  *   http://example.com/index.php/welcome
  * - or -  
  *   http://example.com/index.php/welcome/index
  * - or -
  * Since this controller is set as the default controller in 
  * config/routes.php, it's displayed at http://example.com/
  *
  * So any other public methods not prefixed with an underscore will
  * map to /index.php/welcome/<method_name>
  * @see http://codeigniter.com/user_guide/general/urls.html
  */
 public function index()
 {
  $this->load->view('welcome_message');
 }
    
    public function generate()
    {
        $this->load->database();
        $this->load->helper('orm');
        $this->load->helper('url');
        generate_models();
        
        redirect('/');
    }
    
    public function insert()
    {
        $this->load->database();
        $this->load->model('sample');
        
        $sample = $this->sample;
        // set data
        $sample->field1 = 'aaaaa';
        $sample->field2 = 'bbbb';
        // insert
        $sample->save();
    }
    
    public function update()
    {
        $this->load->database();
        $this->load->model('sample');
        
        // retrieve data by id = 1
        $sample = $this->sample->find(1);
        // set data
        $sample->field1 = 'cccc';
        $sample->field2 = 'bbbb';
        // update
        $sample->save();
    }
}

/* End of file welcome.php */
/* Location: ./application/controllers/welcome.php */

□ モデルファイル作成
http://localhost/CodeIgniter_2.1.3/index.php/welcome/generate

実行すると (1) /application/models/sample.php(2) /application/models/BaseModels/BaseSample.php のファイルが作成されます。

(1)は拡張用のモデルです。CodeIgniterからロードされるファイルはこちらのファイルです。
(2)がデータテーブルに対応した情報を持つ拡張用のモデル専用ファイルです。データテーブルが変更されると上書きされるのであまり手を入れないほうがいいでしょう。ファイル名やクラス名がCodeIgniterの形式に沿っていないのでCodeIgniterからは直接ロードできません。


□ insert
http://localhost/CodeIgniter_2.1.3/index.php/welcome/insert
実行するとsampleテーブルに一行データが追加されます。

□ update
http://localhost/CodeIgniter_2.1.3/index.php/welcome/update
実行すると上で追加したデータのidが1の行を変更します。


■ 最後に

ライブラリ化や機能の追加などは予定していません。
使う人が改善、拡張していって使いやすいようにしてください。(使う人が居れば、ですけど・・・)

俺自身、あまりサイズが大きくて多機能なORMは覚えるのが面倒な上、ほとんどの機能は必要なかったので簡単な拡張で済ませました。
普段使いそうな機能を追加すれば便利に使えると思います。

・・・だったらいいな(笑)

0 件のコメント:

コメントを投稿