Y's note

Web技術・プロダクトマネジメント・そして経営について

本ブログの更新を停止しており、今後は下記Noteに記載していきます。
https://note.com/yutakikuchi/

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 : 作成したライブラリを設置できる。

処理の流れ詳細

  • URLとしてのエントリポイント呼び出し(app/webroot/index.php)
  • dispatcherの呼び出し。使用するControllerを決定する。
    • dispatcherのパスは cake/dispatcher.php
  • Controllerのメソッドであるアクションを実行。アクションからModelを経由してDBにアクセスし、データを取得。
    • 例 /user/list/5 というリクエストURIだとしたら UserControllerクラスのlistメソッドが呼び出される。
  • Controllerは取得した結果を整形し、Viewにデータを渡して表示。

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設定ファイルをコピーして、書き換える

app/config# sudo cp database.php.default database.php

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>
アクセスして閲覧できる事を確認

http://localhost/cake/members

サンプルプログラムの設定は以上です。

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";
    }

}

実行結果

上のclient.phpを実行してみます。以下は実行結果です。

test method 
sample method 
1

最後の数字がsample.classのコンストラクタ呼び出し回数です。
1回のみ呼び出されています。2回目のgetではコンテナに格納されている
インスタンスが返されていることが分かります。

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";

出力SQL

INERT INTO  mysql.test (name,key) value(,) WHERE id = 7;
SELECT * FROM mysql.test WHERE id = 1 AND name LIKE '%hoge%' ;
  • 1の例は本来ならばvalue( 'a', 'test_key' )のように文字列が入ってくるところ処理の過程で誤ってnullが入ってしまった場合。
  • 2の例はソースコード上に%が多数含まれて、逆に見づらい。

serializeとjson関数の比較

概要

serializeとjson_encodeのどちらの利用が望ましいかを調べます。理由はWebAPIを作成するときにどのレスポンス形式が最適なのかを検証する必要がでてきたためです。事前に上がった意見としては”json_encodeの方がデータ容量少なくなるから速いっしょ”、”serializeの方がphpをそのまま扱っているんだから変換処理コストが少ないはず”などの意見がありましたが、どれが正確か分からないので実際に試してみます。

サンプルするデータ

50回実行して処理時間の平均値をサンプリングします。

  • php配列から各エンコード処理を施した時の処理コスト
    • シングルバイト文字列を含む配列を変換するコスト
    • マルチバイト文字列を含む配列を変換するコスト
  • エンコードしたデータからphp配列への復元処理コスト
    • シングルバイト文字列を含む配列エンコードしたデータをデコード
    • マルチバイト文字列を含む配列をエンコードしたデータをデコード

エンコード ソースコード

<?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

デコード処理時間結果

エンコード文字列からのデコード処理ではunserializeが速い。
※この検証は元々のデータ形式が異なるものを扱うため、比較が適正ではないかもしれません。

形式 シングルバイト マルチバイト
unserialize 0.0007774 0.00080212
json_decode 0.00382754 0.00643018

まとめ

エンコードではjson_encode、デコードではunserializeが処理として速いことが分かった。
エンコード、デコード単体で処理の速さを比較することも大事だが、処理全体のtotalコストとしての見積が必要。
・個人的にはjson_encode,json_decodeを利用するのが良いと思う。(APIだと転送データ容量も減らせるので)

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