5分で分かるCakePHPの基礎
PHPのFWを比較することを試みようと思っている。PHPの4大FWと言われる、CakePHP,Symphoney,Zend,Codeigniterは少しずつ勉強を進めよう。まずは軽量かつ覚えやすいというCakePHPについて記述する。※コードの追跡を行ったのはCakePHP version1.3.9。現時点での最新Version。
特徴や規則
- RoRの影響を受けて作られている。2006年頃から存在し、PHP4でも使える。
- MVC(Model View Controller)。Viewを純粋なPHPでかける。
- 習得が比較的楽。
- ViewHelperが利用可能。ViewHelperで自動的に出力をsanitize。
- Action/FilterChainをサポート。
- Validatorサポート。
- 関数の短縮表記。
- 簡単な処理の流れを説明すると、entrypointからDispatcherが呼び出すControllerを決定し、ControllerはModelとデータのやり取りをして取得結果を整形してViewに渡す という構成。
- ファイル名の設定にはアンダースコア、クラス名の設定にはキャメル記法を用いる。(例)my_sample_class.phpというファイル名にMySampleClassというクラスを定義する事ができる。
- ファイルやDBの命名規則が厳格。
- Controllerクラスの中でメソッド名の接頭辞としてアンダースコアを付けるとURLからはアクセス不可能なメソッドとして扱う事ができる。
- Modelクラスの定義もキャメル記法。呼び出されるdbのテーブル名はアンダースコア表記。名前は複数形とする。
- 呼び出し方は http://Cakeのパス/コントローラ/メソッド(アクション)/パラメータ。内部のrewrite処理によりapp/webroot/index.phpが起動されコントローラ、メソッド、パラメータを解釈。
- キャッシュ機構も備えている。(apc,memcache,xcache)。Viewのキャッシュも可能。
Directory Tree
主要なフォルダ構成は次の通り
- app : 作成したアプリケーションを設置する場所
- config: DB接続やbootstrap、coreの設定ファイルを入れる
- controllers : controllerとcomponentを入れる
- libs : ファーストパーティ用のライブラリ
- locale : 国際化のための文字ファイル
- models : models,behaviors,datasourcesを入れる
- plugins : pluginパッケージを入れる
- tests : testケースのコードを入れる
- tmp : ログ、セッション情報、データの一時保存場所
- vendors : サードパーティ用のライブラリ
- views : 表示用のファイルを設置
- webroot : アプリケーションのエントリポイントなどのドキュメントルート
- cake : coreライブラリ。中身の変更は禁止。
- vendors : 作成したライブラリを設置できる。
処理の流れ詳細
entrypoint
entrypointのphpは以下の処理を行っている。
- filepathは- entrypointはapp/webroot/index.php
- 各必要定数のdefineとbootstrapファイルのinclude。
- dispatcherの呼び出し、dispatch処理。
<?php ----- 略 if (!defined('DS')) { define('DS', DIRECTORY_SEPARATOR); } /** * These defines should only be edited if you have cake installed in * a directory layout other than the way it is distributed. * When using custom settings be sure to use the DS and do not add a trailing DS . */ /** * The full path to the directory which holds "app", WITHOUT a trailing DS. * */ if (!defined('ROOT')) { define('ROOT', dirname(dirname(dirname(__FILE__)))); } ----- 略 if (!include(CORE_PATH . 'cake' . DS . 'bootstrap.php')) { trigger_error("CakePHP core could not be found. Check the value of CAKE_CORE_INCLUDE_PATH in APP/webroot/index.php. It should point to the directory containing your " . DS . "cake core directory and your " . DS . "vendors root directory.", E_USER_ERROR); } if (isset($_GET['url']) && $_GET['url'] === 'favicon.ico') { return; } else { $Dispatcher = new Dispatcher(); $Dispatcher->dispatch(); }
bootstrap
- 定数の設定
- エラーの種類の設定
- 各種ファイルのrequire
<?php if (!defined('PHP5')) { define('PHP5', (PHP_VERSION >= 5)); } if (!defined('E_DEPRECATED')) { define('E_DEPRECATED', 8192); } error_reporting(E_ALL & ~E_DEPRECATED); require CORE_PATH . 'cake' . DS . 'basics.php'; $TIME_START = getMicrotime(); require CORE_PATH . 'cake' . DS . 'config' . DS . 'paths.php'; require LIBS . 'object.php'; require LIBS . 'inflector.php'; require LIBS . 'configure.php'; require LIBS . 'set.php'; require LIBS . 'cache.php'; Configure::getInstance(); require CAKE . 'dispatcher.php';
dispatcher
dispatcherは以下の処理を行う。
- URL、parameterの特定。
- 呼び出すcontrollerの特定。
<?php if (is_array($url)) { $url = $this->__extractParams($url, $additionalParams); } else { if ($url) { $_GET['url'] = $url; } $url = $this->getUrl(); $this->params = array_merge($this->parseParams($url), $additionalParams); } $this->here = $this->base . '/' . $url; if ($this->asset($url) || $this->cached($url)) { return; } $controller =& $this->__getController();
- action、メソッドの特定。
- controllerにデータを設定。
<?php $controller->base = $this->base; $controller->here = $this->here; $controller->webroot = $this->webroot; $controller->plugin = isset($this->params['plugin']) ? $this->params['plugin'] : null; $controller->params =& $this->params; $controller->action =& $this->params['action']; $controller->passedArgs = array_merge($this->params['pass'], $this->params['named']);
- invokeメソッド内部でcontroller処理の起動
- controllerの初期化。
- controllerの呼び出し、実行。
- viewのrender。
- controllerのshutdown処理を行う。
- viewの表示。
<?php $controller->constructClasses(); $controller->startupProcess(); $output = call_user_func_array(array(&$controller, $params['action']), $params['pass']); -------略 if ($controller->autoRender) { $controller->output = $controller->render(); } elseif (empty($controller->output)) { $controller->output = $output; } $controller->shutdownProcess(); if (isset($params['return'])) { return $controller->output; } echo($controller->output);
サンプルプログラム設置
- DBの定義。
- Controller,Model,Viewの設置。
DBの定義
DB設定ファイルをコピーして、書き換える
login,password,databaseなどを書き換え。
<?php class DATABASE_CONFIG { var $default = array( 'driver' => 'mysql', 'persistent' => false, 'host' => 'localhost', 'login' => 'user', 'password' => '', 'database' => 'todo', 'prefix' => '', );DBのschema作成SQLは次の通り。Modelの名前の複数形として定義する。
CREATE TABLE `members` ( `id` int(11) unsigned NOT NULL auto_increment, `name` varchar(255) NOT NULL, `created` datetime NOT NULL, `modified` datetime NOT NULL, PRIMARY KEY (`id`), KEY `created` (`created`), KEY `modified` (`modified`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 | INSERT INTO `members` (`name`, `created`, `modified`) VALUES ('yahoo', '2011-08-20 00:00:00', '2011-08-21 01:00:00'), ('google', '2011-08-21 02:00:00', '2011-08-21 02:00:00'), ('facebook', '2011-08-21 05:00:00', '2011-08-21 05:00:00');controllerの定義
- app/controllers/members_controller.phpとして定義。
- classは必ずAppControllerを継承する。
- 利用Model、アクションを定義。var $uses = array('Member'); $this->set('members', $this->Task->findAll(null, null, 'Member.created ASC'));
- Viewに渡すデータを設定。
<?php // app/controllers/members_controller.php class MembersController extends AppController { var $name = 'Members'; var $uses = array('Member'); function index() { $this->set('members', $this->Member->findAll(null, null, 'Member.created ASC')); } }Modelの定義
<?php // app/model/member.php class Member extends AppModel { var $name = 'Member'; }Viewの定義
- controllerでsetにより渡されてきたmembersを展開できる。
- hという関数はcake/basics.phpに登録されているhtmlspecialcharsを行う短縮関数。
<table> <tr> <th>Id</th> <th>登録内容</th> <th>作成日</th> </tr> <?php foreach ($members as $member) { ?> <tr> <td><?php echo h($member['Member']['id']) ?></td> <td><?php echo h($member['Member']['name']) ?></td> <td><?php echo h($member['Member']['created']) ?></td> </tr> <?php } ?> </table>
SimpleXMLElement Objectの参照
内容
久しぶりにxmlをパースする処理を書いていたんだけど、SimpleXMLElement Objectの参照方法を忘れていたのでメモをしておく。(20120416追記) 今まではサンプルプログラム中のforeachで$resultに添字$iでインクリメントして値を代入していましたが、$iを使わずに配列の[]を使って右辺のarray型を代入するように修正しました。
xmlサンプル
<?xml version="1.0" encoding="Shift-Jis" ?> <venture> <company> <name>ソフトバンク株式会社</name> <url>http://www.softbank.co.jp/</url> </company> <company> <name>楽天株式会社</name> <url>http://www.rakuten.co.jp/info/</url> </company> </venture>
simplexml_load_string関数
PHPの関数によりSimpleXMLElement ObjectとしてParseを行う。SimpleXMLElement Objectを更にPHP配列に整形してゆく。
<?php $xml = <<<XML <?xml version="1.0" encoding="Shift-Jis" ?> <venture> <company> <name>ソフトバンク株式会社</name> <url>http://www.softbank.co.jp/</url> </company> <company> <name>楽天株式会社</name> <url>http://www.rakuten.co.jp/info/</url> </company> </venture> XML; print( $xml . "\n" ); $data = simplexml_load_string( $xml ); print_r( $data );
simplexml_load_string関数の実行結果
xmlがSimpleXMLElement Objectに変換されていることが分かる。
SimpleXMLElement Object ( [company] => Array ( [0] => SimpleXMLElement Object ( [name] => ソフトバンク株式会社 [url] => http://www.softbank.co.jp/ ) [1] => SimpleXMLElement Object ( [name] => 楽天株式会社 [url] => http://www.rakuten.co.jp/info/ ) ) )
SimpleXMLElement Objectの値参照
SimpleXMLElement ObjectのNodeはアロー演算子で参照可能。
<?php (略) $data = simplexml_load_string( $xml ); print_r( $data ); foreach( $data->company as $value ) { $result[] = array( 'name' => $value->name, 'url' =>$value->url ); } print_r( $result );
アロー演算子で参照した値
下はまだSimpleXMLElement Objectの参照になっている。完全なphp配列にしたい。
Array ( [0] => Array ( [name] => SimpleXMLElement Object ( [0] => ソフトバンク株式会社 ) [url] => SimpleXMLElement Object ( [0] => http://www.softbank.co.jp/ ) ) [1] => Array ( [name] => SimpleXMLElement Object ( [0] => 楽天株式会社 ) [url] => SimpleXMLElement Object ( [0] => http://www.rakuten.co.jp/info/ ) ) )
castを使って値を格納する
配列に格納する場合はSimpleXMLElement ObjectのNodeをcastさせる。上のphpプログラムを次のように変更する。
<?php (略) $data = simplexml_load_string( $xml ); print_r( $data ); foreach( $data->company as $value ) { $result[] = array( 'name' => (string)$value->name, 'url' => (string)$value->url ); } print_r( $result );
castの実行結果
これで純粋なphp配列に置き換えることができた。
Array ( [0] => Array ( [name] => ソフトバンク株式会社 [url] => http://www.softbank.co.jp/ ) [1] => Array ( [name] => 楽天株式会社 [url] => http://www.rakuten.co.jp/info/ ) )
超簡単なインスタンスコンテナ 動的編
概要
以前のエントリで超簡単なインスタンスコンテナというタイトルで記事を書きましたが、
コンテナに格納できるインスタンスが静的に呼び出されるものだったので、
動的に呼び出されるように修正してみました。以前のエントリは以下のものです。
http://d.hatena.ne.jp/yutakikuchi/20100907/1283877423
改良したインスタンスコンテナ
<?php class Container { static private $instance_ = array(); private function __construct() {} static public function get( $class, $path = null ) { if( isset( self::$instance_[ $path ] ) ) { return self::$instance_[ $path ]; } if( !is_readable( $path ) ) { throw new Exception(); } require_once $path; $instance = new $class; self::add( $path, $instance ); return $instance; } static public function add( $path, $instance ) { if( !isset( self::$instance_[ $path ] ) ) { self::$instance_[ $path ] = $instance; } } }
Containerに格納したいクラス名、ファイルパスを指定してgetメソッドを呼び出すと
Containerが唯一のインスタンスを保持してくれます。
コンテナに格納するクラス
sample.php
<?php class Sample { private $count_ = 0; public function __construct() { $this->count_++; } public function call() { echo "sample method \n"; } public function getCount() { return $this->count_; } }
test.php
<?php class Test { public function call() { echo "test method \n"; } }
clientコード
実際に一回の実行で複数回コンテナからgetにてインスタンスを取得したときに
何回コンストラクタが呼び出されているのかを見てみます。
client.php
<?php require_once './Container.php'; require_once './client2.php'; $test = Container::get( 'test', './test.php' ); $test->call(); $sample = Container::get( 'sample', './sample.php' ); $sample->call(); client2::execute();
client2.php
<?php require_once "./Container.php"; class client2 { static public function execute() { $sample = Container::get( 'sample', './sample.php' ); echo $sample->getCount() . "\n"; } }
SQLのQUERY生成でsprintfを使いたくない
内容
- sprintfは見た長い文字列を書き足しやすくするための関数で、SQLの生成ではできるだけ使いたくない。
- sprintfは型の指定が入るので、特にnullなどの扱いに気をつけないといけない。
- sprintf + LIKE文を作ろうとすると %を多用するので、LIKE "%%%s%%"といった指定になり、見づらい。
例
<?php $sql = sprintf( "INERT INTO %s.%s (%s,%s) value(%s,%s) WHERE id = %s ", "mysql", "test", "name", "key", null, null, 7 ); echo $sql . "\n"; $sql = sprintf( "SELECT * FROM %s.%s WHERE id = %d AND name LIKE '%%%s%%' ", "mysql", "test", 1, "hoge" ); echo $sql . "\n";
serializeとjson関数の比較
概要
serializeとjson_encodeのどちらの利用が望ましいかを調べます。理由はWebAPIを作成するときにどのレスポンス形式が最適なのかを検証する必要がでてきたためです。事前に上がった意見としては”json_encodeの方がデータ容量少なくなるから速いっしょ”、”serializeの方がphpをそのまま扱っているんだから変換処理コストが少ないはず”などの意見がありましたが、どれが正確か分からないので実際に試してみます。
サンプルするデータ
50回実行して処理時間の平均値をサンプリングします。
エンコード ソースコード
<?php $data = array(); for( $i=0; $i<=10000; $i ++ ) { $data[] = "test"; //ここを適宜変えます。 } $start = microtime(); //PHP serialize $string = serialize( $data ); //JSON encode //$string = json_encode( $data ); $end = microtime(); echo $end - $start . "\n";デコード ソースコード
<?php $data = array(); for( $i=0; $i<=10000; $i ++ ) { $data[] = "test"; //ここを適宜変えます。 } //PHP serialize $string = serialize( $data ); //JSON encode //$string = json_encode( $data ); $start = microtime(); $data = unserialize( $string ); //json_decode( $string ); $end = microtime(); echo $end - $start . "\n";
エンコードの処理時間結果
php配列からのエンコード処理ではjson_encodeが速い/span>
形式 シングルバイト マルチバイト serialize 0.00371922 0.0040066 json_encode 0.00123026 0.00314484
エンコードbyte数比較
php配列からのエンコード処理ではjson_encodeの方が生成Stringのバイト数が少なく済みそう
形式 シングルバイト マルチバイト serialize 178919 248926 json_encode 70008 230024
TemplateMethod パターン
概要
親クラスに共通処理の流れを記述し、子クラスで各処理の具体的な実装を行うパターンです。
メリットとしては共通処理を一箇所にまとめることができるのと、子クラスで具体的な実装が組めるのでやりたい処理をクラスごとに変更することができます。
処理の方が決まっているクラス、例えばAPIクラス(パラメータ取得して、DBに接続して、結果を返してなど)などの定義では必ずと言っていいほど
使われているような気がします。
ハリウッドの原則
これはちょっと面白い話です。TemplateMethodパターンは通常とは"逆"のメソッド呼び出しをします。というのも抽象クラスから子クラスが
呼び出されるためです。この状態を示す英文として[ Don't call us. We'll call you ] 我々を呼び出すな、必要なときは我々が君を呼び出す という言葉があるようです。
ここでの我々は抽象クラス、君は子クラスという意味になりますね。誰が考えたのかわかりませんが、ユーモアを感じますね。
抽象クラス
<?php abstract class APIModel { protected $_params = array(); protected $_method = null; protected function __construct() {} protected function __destruct() {} protected function prepare() { //DBの初期化処理を記述 ※別クラス化 } protected function getParams() { $this->_params = $_REQUEST; } public function fetch() { $this->getParams(); $this->validate(); $this->prepare(); $this->execute(); $result = $this->parse(); $this->tearDown(); return $result; } protected function tearDown() { //DBの開放処理などを記述 } }
共通処理の流れをfetchに書きました。これにより処理の流れを定義できます。その他のprotected関数については子クラスで詳細定義を行ないます。
継承クラス
<?php class TestAPI extends APIModel { public function __construct() {} public function __destruct() {} protected function prepare() { //初期化処理を行ないます。 parent::prepare(); } protected function validate() { //パラメータのvalidateを行ないます。 $ret = true; if( $ret === false ) { throw new Exception(); } } protected function execute() { //DBに接続するなど実行を行ないます。 $this->_data = array( 'Test' ); } protected function parse() { //結果の整形 array_push( $this->_data, 'API'); return $this->_data; } protected function tearDown() { //後処理を行ないます。※DBの開放など } } class SampleAPI extends APIModel { public function __construct() {} public function __destruct() {} protected function prepare() { //初期化処理を行ないます。 parent::prepare(); } protected function validate() { //パラメータのvalidateを行ないます。 echo "aaaa \n"; } protected function execute() { //DBに接続するなど実行を行ないます。 $this->_data = array( 'Sample' ); } protected function parse() { //結果の整形 array_push( $this->_data, 'API'); return $this->_data; } protected function tearDown() { //後処理を行ないます。※DBの開放など } }
親クラスで抽象化されていたメソッドの詳細定義を行っています。クライアントから利用される時は親クラスのfetchを呼び出します。
client
<?php //client try { $test = new TestAPI(); $data = $test->fetch(); print_r( $data ); $sample = new SampleAPI(); $data = $sample->fetch(); print_r( $data ); } catch( Exception $e ) { exit; }
インスタンス化するクラスを切り替えて実行します。それぞれのクラスで詳細定義したメソッドを抽象クラスが呼び出しているので、処理の流れが共通ですが
結果が異なります。
実行結果
Array ( [0] => Test [1] => API ) aaaa Array ( [0] => Sample [1] => API )
Strategyパターン
概要
このパターンはアルゴリズムをクラス化してクライアントからクラスの切り替えで処理を行うパターンになります。
クライアントからのアクセス用の共通メソッド(API)を用意してあげて、別々のクラスのメソッドにアクセスをして
完全に処理クラスに委譲することができます。
またこのパターンを利用するとContext内部でif else分などの条件文をすっきりさせることができます。
Strategyクラス
<?php //Strategy interface interface Strategy { public function calculate( array $array ); } //Strategy interfaceの実装 class SumCalculator implements Strategy { //計算の実行 public function calculate( array $data ) { $sum = 0; foreach( $data as $value ) { $sum += $value; } return $sum; } } //Strategy interfaceの実装 class MultiplyCalculator implements Strategy { //計算の実行 public function calculate( array $data ) { $sum = array_shift( $data ); foreach( $data as $value ) { $sum *= $value; } return $sum; } }
このように共通Interfaceを用いて計算処理を行う個別クラスにアルゴリズムを記述します。
Contextクラス
<?php // Strategyインスタンスを実行するContext class Subject { private function __construct(){} public static function calculate( Strategy $strategy, array $data ) { return $strategy->calculate( $data ); } }
ストラテジークラスのcalculate処理を呼び出すContextクラスを用意します。クライアントからはContextクラスを呼び出します。
client
<?php // client $data = array( 1, 2, 3, 4, 5); $val = Subject::calculate( new SumCalculator(), $data ); echo "$val \n"; $val = Subject::calculate( new MultiplyCalculator(), $data ); echo "$val \n";
クライアントからは何の処理を実行するかという情報を与えてあげるだけで済みます。
実行結果
15 120
その他使用例
以前紹介したvalidatorの処理が似ているかもしれません。
http://d.hatena.ne.jp/jogriko/20100923