[CodeIgniter2.x]Cacheクラスを継承するには


[CodeIgniter2.x]Cacheの利用について
[CodeIgniter]CodeIgniter2のCache機能をCodeIgniter1系でも利用するパッチ

前回、前々回とCodeIgniter2から提供されているCache機能についての記事を書いたのですが、今回、ある要件で既存のCacheクラスを継承する必要が出てきました。通常のlibraryとは異なり、1階層異なりますが、通常通り継承できるだろうと思い、普通にsystem/libraries/Cache/Cache.phpを継承したMY_Cache.phpをapplication/libraries/Cache/MY_Cache.phpに配置して動かしてみると見事に落ちました。

ということで、今回はそんなまだ拡張出来ないCacheクラス。もとい、CI_Driver_Libraryクラスを継承しているクラスの継承方法について書きたいと思います。

広告

とりあえず継承してみる

まず、CI_Driver_Libraryを継承しているクラスの< 継承については現時点(2.0.1)では考慮されていません。今後も考慮されるかどうかはわかりません。一応、CodeIgniter ReactorにPull Requestは行うつもりです。ので、今後、考慮されるか分からないので自己責任でお願いします。 最初に述べたように下記のようにCacheクラスを継承したものを配置してみます。ストレージは前回と同じようにmemcachedを利用します。

application/config/cache.php

< ?php
$config = array(
	'adapter' => 'memcached',
);

application/config/memcached.php

< ?php
$config = array(
	'cache1' => array(
		'hostname' => '127.0.0.1',
		'port' => 11211,
		'weight' => 1,
	),
);

application/libraries/Cache/MY_Cache.php

ただ、Cacheを継承しただけのMY_Cacheです。

< ?php
class MY_Cache extends Cache {
}

まずは、この状態で下記のようにwelcome.phpにキャッシュの利用をするコードを書いてみてアクセスしてみます。

application/controller/welcome.php

	public function index()
	{
		$this->load->driver('cache');
		$this->cache->save('hoge', 'foo', 3600);
		$hoge = $this->cache->get('hoge');
		var_dump($hoge);
 
		$this->load->view('welcome_message');
	}

アクセスしてみると。。。

An Error Was Encountered
Unable to load the requested class: Cache

足りなければ追加すればいいじゃない

エラーになってしまいましたね。このメッセージがどこで呼ばれているかというと、下記になります。

system/core/Loader.php

767行目付近です。継承されるべきスーパクラスは、system/libraries/直下に存在しないといけないようです。

                        $subclass = APPPATH.'libraries/'.$subdir.config_item('subclass_prefix').$class.EXT;
 
                        // Is this a class extension request?
                        if (file_exists($subclass))
                        {    
                                $baseclass = BASEPATH.'libraries/'.ucfirst($class).EXT;
 
                                if ( ! file_exists($baseclass))
                                {    
                                        log_message('error', "Unable to load the requested class: ".$class);
                                        show_error("Unable to load the requested class: ".$class);
                                }

ここで少しおかしなコードが存在します。サブクラスは階層化されることを考慮しているにも関わらず、スーパクラスに関しては、階層化が考慮されていません。

                        $subclass = APPPATH.'libraries/'.$subdir.config_item('subclass_prefix').$class.EXT;

じゃあ、追加すればいいじゃん。

ということで、追加してみました。

                        $subclass = APPPATH.'libraries/'.$subdir.config_item('subclass_prefix').$class.EXT;
 
                        // Is this a class extension request?
                        if (file_exists($subclass))
                        {    
-                                $baseclass = BASEPATH.'libraries/'.ucfirst($class).EXT;
+                               $baseclass = BASEPATH.'libraries/'.$subdir.ucfirst($class).EXT;
                                if ( ! file_exists($baseclass))
                                {    
                                        log_message('error', "Unable to load the requested class: ".$class);
                                        show_error("Unable to load the requested class: ".$class);
                                }

この状態でもう一度アクセスしてみます。

An Error Was Encountered
Invalid driver requested: MY_Cache_memcached

良い傾向ですね。どうやらMY_Cacheは読み込まれたようです。ただ、まだまだ先は長そうです。。。

driversも継承してね

CodeIgniter 2のソースを見たことがある方はもう見えてきましたね。ただ、説明はまだまだ続きます。

system/libraries/Driver.php

先ほど出たエラーですが、コレは下記の一番下の方で発生しています。具体的にどういうものかというと、まず、__getメソッドですが、コレはマジックメソッドというもので、自身のフィールドでもないのに参照しようとした場合に呼ばれるメソッドです。Cache.phpを例にして説明すると、例えば下記のコードです。

        public function get($id)
        {
                return $this->{$this->_adapter}->get($id);
        }

さらに上記の部分のここです。

return $this->{$this->_adapter}->get($id);

実行時には{$this->_adapter}が展開されて実際には下記のようにアクセスすることになります。

return $this->memcached->get($id);

Cache.phpにはmemcachedなんてフィールドはありませんよね?なので、スーパクラスであるCI_Driver_Libraryの__getメソッドが呼ばれます。さらにこの時、重要なのが、__getの引数には、アクセスしたフィールド名が渡されます。先程の例の場合、「memcached」が下記の$child変数に入ることになります。

        function __get($child)
        {
                if ( ! isset($this->lib_name))
                {
                        $this->lib_name = get_class($this);
                }
 
                // The class will be prefixed with the parent lib
                $child_class = $this->lib_name.'_'.$child;
 
                if (in_array(strtolower($child_class), array_map('strtolower', $this->valid_drivers)))
                {
                        // check and see if the driver is in a separate file
                        if ( ! class_exists($child_class))
                        {
                                // check application path first
                                foreach (array(APPPATH, BASEPATH) as $path)
                                {
                                        // and check for case sensitivity of both the parent and child libs
                                        foreach (array(ucfirst($this->lib_name), strtolower($this->lib_name)) as $lib)
                                        {
                                                // loves me some nesting!
                                                foreach (array(ucfirst($child_class), strtolower($child_class)) as $class)
                                                {
                                                        $filepath = $path.'libraries/'.$this->lib_name.'/drivers/'.$child_class.EXT;
 
                                                        if (file_exists($filepath))
                                                        {
                                                                include_once $filepath;
                                                                break;
                                                        }
                                                }
                                        }
                                }
 
                                // it's a valid driver, but the file simply can't be found
                                if ( ! class_exists($child_class))
                                {
                                        log_message('error', "Unable to load the requested driver: ".$child_class);
                                        show_error("Unable to load the requested driver: ".$child_class);
                                }
                        }
 
                        $obj = new $child_class;
                        $obj->decorate($this);
                        $this->$child = $obj;
                        return $this->$child;
                }
 
                // The requested driver isn't valid!
                log_message('error', "Invalid driver requested: ".$child_class);
                show_error("Invalid driver requested: ".$child_class);
        }

CI_Driver_Libraryの__getメソッドで何をやっているかというと、アクセスしたフィールドの名称をもとにdrivers以下に配置された実態をインスタンス化してしまおうというものです。ただ、この時、適当な名前ではなく、下記のような名前である必要がある。

$this->lib_nameには、自身のインスタンス化されているクラス名。今回の場合はMY_Cacheが入ります。ので、サブクラスとして呼びだそうとしているのは、「MY_Cache_memcached」ということになります。

                if ( ! isset($this->lib_name))
                {
                        $this->lib_name = get_class($this);
                }
 
                // The class will be prefixed with the parent lib
                $child_class = $this->lib_name.'_'.$child;

また、MY_Cache_memcachedなど、対象のクラス名に関しては、MY_Cache.phpのフィールド「valid_drivers」に下記のようにクラス名を小文字化したものを指定する必要があります。

< ?php
class MY_Cache extends Cache {
        protected $valid_drivers = array(
                'my_cache_apc', 'my_cache_file', 'my_cache_memcached', 'my_cache_dummy'
        );
}

さらに下記のようにdrivers/配下にクラスを追加します。

application/libraries/Cache/drivers/MY_Cache_apc.php

< ?php
class MY_Cache_apc extends Cache_apc {
}

application/libraries/Cache/drivers/MY_Cache_file.php

< ?php
class MY_Cache_file extends Cache_file {
}

application/libraries/Cache/drivers/MY_Cache_memcached.php

< ?php
class MY_Cache_memcached extends Cache_memcached {
}

application/libraries/Cache/drivers/MY_Cache_dummy.php

< ?php
class MY_Cache_dummy extends Cache_dummy {
}

これで、やっと終わりですね。また、アクセスしてみましょう。

An Error Was Encountered
Unable to load the requested driver: MY_Cache_memcached

変わっていませんね。。。困った時はログ出力を最大にして何が表示されるか見てみましょう。

DEBUG - 2011-03-24 23:39:17 --> Loader Class Initialized
DEBUG - 2011-03-24 23:39:17 --> Controller Class Initialized
ERROR - 2011-03-24 23:39:17 --> Unable to load the requested driver: MY_Cache_memcached

どうやら、先ほどのCI_Driver_Libraryの__getメソッド内でMY_Cache_memcachedが見つからないと言われているようです。CI_Driver_Libraryにログを追加して読み込もうとしているファイルパスを出してみます。

                                                        $filepath = $path.'libraries/'.$this->lib_name.'/drivers/'.$child_class.EXT;
+                                                        log_message('debug', "load driver=$filepath");

ログを追加後アクセスすると、こんなログが出ました。

DEBUG - 2011-03-24 23:42:47 --> Controller Class Initialized
DEBUG - 2011-03-24 23:42:47 --> load driver=application/libraries/MY_Cache/drivers/MY_Cache_memcached.php
DEBUG - 2011-03-24 23:42:47 --> load driver=application/libraries/MY_Cache/drivers/MY_Cache_memcached.php
DEBUG - 2011-03-24 23:42:47 --> load driver=application/libraries/MY_Cache/drivers/MY_Cache_memcached.php
DEBUG - 2011-03-24 23:42:47 --> load driver=application/libraries/MY_Cache/drivers/MY_Cache_memcached.php
DEBUG - 2011-03-24 23:42:47 --> load driver=/home/tatsuya/workspace/hg/fukata-codeigniter-reactor/system/libraries/MY_Cache/drivers/MY_Cache_memcached.php
DEBUG - 2011-03-24 23:42:47 --> load driver=/home/tatsuya/workspace/hg/fukata-codeigniter-reactor/system/libraries/MY_Cache/drivers/MY_Cache_memcached.php
DEBUG - 2011-03-24 23:42:47 --> load driver=/home/tatsuya/workspace/hg/fukata-codeigniter-reactor/system/libraries/MY_Cache/drivers/MY_Cache_memcached.php
DEBUG - 2011-03-24 23:42:47 --> load driver=/home/tatsuya/workspace/hg/fukata-codeigniter-reactor/system/libraries/MY_Cache/drivers/MY_Cache_memcached.php
ERROR - 2011-03-24 23:42:47 --> Unable to load the requested driver: MY_Cache_memcached

想定していた

application/libraries/Cache/drivers/MY_Cache_memcached.php

ではなく、

application/libraries/MY_Cache/drivers/MY_Cache_memcached.php

を読み込んでいるようです。

これは、先ほどログを追加した直前のコードを見ればわかります。

$filepath = $path.'libraries/'.$this->lib_name.'/drivers/'.$child_class.EXT;

lib_nameはインスタンス化されているクラス名だったので、「MY_Cache」になりますね。よって、driversディレクトリを移動します。移動後のapplication/librariesディレクトリの中身はこのようになっています。

./
|-- Cache
|   `-- MY_Cache.php
|-- MY_Cache
|   `-- drivers
|       |-- MY_Cache_apc.php
|       |-- MY_Cache_dummy.php
|       |-- MY_Cache_file.php
|       `-- MY_Cache_memcached.php
`-- index.html

これで、もう一度アクセスします。

Fatal error: Class 'Cache_memcached' not found in /home/tatsuya/workspace/hg/fukata-codeigniter-reactor/application/libraries/MY_Cache/drivers/MY_Cache_memcached.php on line 2

サブクラス化が考慮されていないので、スーパクラスが読み込まれているわけがありませんね・・・。ということで、drivers/配下のそれぞれのファイルに汚いですが、require_onceを追加します。

application/libraries/MY_Cache/drivers/MY_Cache_apc.php

< ?php
require_once BASEPATH.'libraries/Cache/drivers/Cache_apc.php';
class MY_Cache_apc extends Cache_apc {
}

application/libraries/MY_Cache/drivers/MY_Cache_file.php

< ?php
require_once BASEPATH.'libraries/Cache/drivers/Cache_file.php';
class MY_Cache_file extends Cache_file {
}

application/libraries/MY_Cache/drivers/MY_Cache_memcached.php

< ?php
require_once BASEPATH.'libraries/Cache/drivers/Cache_memcached.php';
class MY_Cache_memcached extends Cache_memcached {
}

application/libraries/MY_Cache/drivers/MY_Cache_dummy.php

< ?php
require_once BASEPATH.'libraries/Cache/drivers/Cache_dummy.php';
class MY_Cache_dummy extends Cache_dummy {
}

これで、再度アクセスしてみます。

string(3) "foo"

やっと表示されました!!!これで、Cache.phpの拡張が出来ました。最後にCacheやCI_Driver_Libraryをスーパクラスに持つクラスのサブクラス化についてはまだ考慮されていないですし、考慮しようとした形跡が見られるコードも見当たりません。よって、今回のCacheクラスのサブクラス化については激しく自己責任でお願いします。

CodeIgniterの中の人がこの辺もサブクラス化を視野に入れているのかわかりませんが、実際の案件では要件としてサブクラス化したい事もあると思います。なので、もう少しスマートにサブクラス化できるように修正したものをPull Requestしたいと思います。

とりあえず、Issueに追加してみました。いつものように英語はGoogle翻訳のものをそのまま利用しているので見苦しい部分はあるかと思います。。。

ellislab / CodeIgniter Reactor / issues / #163 – extends Cache class — Bitbucket

関連記事