第49回PHP勉強会@関東で発表してきました

第49回PHP勉強会@関東で、久々の参加&発表してきました。

第49回PHP勉強会@関東
http://events.php.gr.jp/events/show/88

発表資料

サンプルコードはこちらから

発表は相変わらず...でしたが、なんとかDatasourceの面白さは伝えられたかなぁと思います。

lithiumは、とても刺激的ですね。私も2,3日ほど前に落として色々中を見ていますが、PHP5.3を今まで全く使っていないので苦戦していたので、安藤さんの発表はとても良いヒントになりました!

Schema ShellでDB設定を切り替える方法

忘れそうなのでメモ。

今開発中の案件では、DBのスキーマ情報をCakeSchemaで管理しています。
本番環境に反映時する際、DB設定の切り替え方がわからなかったので調べてみました。

connectionというパラメータを指定すると切り替えられます。以下の例だと「production」というのがdatabase.phpで設定された設定名になります。

php cake/console/cake.php schema run create -connection production

CakeLogをスマートにカスタマイズする方法

CakePHPのログ出力処理は結構簡易なもので今ひとつ使いにくいです。また、「内部的に flock してるからアクセスが多いと遅いので実運用では使わない方が良いよ!」という話も聞いていたので、うちではカスタマイズしたものを使っています。

ROOT/cake/libs/cake_log.phpを書き換えてしまえばそれでも良いのですが、COREのコードを書き換えずにカスタマイズする方法を見つけたのでまとめました。

ログ出力処理は、objectクラスのメソッドとして実装されていて実際のコードは以下のようになっています。

<?php
// cake/libs/cake_log.php
...
	function log($msg, $type = LOG_ERROR) {
		if (!class_exists('CakeLog')) {
			uses('cake_log');
		}
		if (is_null($this->_log)) {
			$this->_log = new CakeLog();
		}
		if (!is_string($msg)) {
			$msg = print_r($msg, true);
		}
		return $this->_log->write($type, $msg);
	}
...

上記のコードの通り、このコードが実行される前に CakeLog というクラスを定義してしまえば差し替えが出来そうです。

で、どこにその設定を書けば良いか探るために起動時にどんな順番で処理が遷移するか主なファイルにログを仕込んでみました。
取得した結果は以下のとおり。

1. ROOT/cake/bootstrap.php
2. ROOT/app/config/core.php
3. ROOT/cake/libs/cake_log.php
4. ROOT/cake/config/config.php
5. ROOT/app/config/bootstrap.php
6. ROOT/cake/dispatcher.php

ということで、cake_log.php より前に呼ばれているAPP側のファイルは core.phpだけなのでここになにか仕込めば良さそうです。

実際のログ出力は、LOG4PHPを使うなりsyslogを使うなり適宜実装し、APP/app_log.phpに以下のように CakeLogクラスを定義します。

# LOG4PHPを使う方法は以下がとでも参考になりました。

<?php
// app/app_log.php
class CakeLog {
  function CakeLog() {
    $args = func_get_args();
    if (method_exists($this, '__destruct')) {
      register_shutdown_function (array(&$this, '__destruct'));
    }
  }
  
  function __destruct() {
...
  }
  
  function write($type, $msg) {
...
  }

で、APP/config/core.phpの一番頭に以下のように記述すれば、上記クラスが使用されます。

<?php
require_once(dirname(dirname(__FILE__)).DS.'app_log.php') ;
...

COREに手を入れずにカスタマイズできてなかなか良いのではないかと思います。一度試してみてはいかがでしょうか?

sfMobileIPPluginをCakePHPに移植してみた

btoさんが作られたSymfony用のプラグインsfMobileIPPluginをCakePHPに移植しました。

以前から移植したものを利用していたのですが、今開発中の案件でPluginとして抜き出しやすいように書き直したので公開します。

使い方

<?php
class HogeController extends AppController
{
  var $components = array('mobileip.MobileIp') ;
  
  function index()
  {
    pr($this->MobileIp->carrier()) ;
  }
}

READMEにも少し書きましたが、IPアドレスの取得を独自のModelで置き換えられるようにしてあるので、
以下のようなモデルを書けばIPアドレスをDBで管理できます。

<?php
App::import('Model', 'mobileip.MobileIp') ;
class Ipaddress extends MobileIp
{
  var $name = 'Ipaddress';
  var $useTable = 'ipaddresses' ;

  function afterSave($created) 
  {
    $last_update_cache = $this->getCacheDir() . DS . 'lastupdated';
    @touch($last_update_cache) ;
  }
  
  function find($conditions = null, $fields = array(), $order = null, $recursive = null)
  {
    if ( $conditions != 'range' )
      return parent::find($conditions, $fields, $order, $recursive) ;
  
    $cacheDir = $this->getCacheDir();
    $folder = new Folder($cacheDir);
    $folder->create($cacheDir, 0777);

   $last_update_cache = $this->getCacheDir() . DS . 'lastupdated';

    $cacheFile = $this->getCacheFile();
    if (file_exists($cacheFile) &&
        ($this->_getLastModified($last_update_cache) <= filemtime($cacheFile))) {
      return include($cacheFile);
    }

    $ipaddresses = $this->find('all') ;
    if ( !is_array($ipaddresses) ) 
      return false ;

    $mobile_ips = array() ;
    foreach( $ipaddresses as $ip ) {      
      $mobile_ips[$ip['Ipaddress']['type']][] = long2ip($ip['Ipaddress']['ip']) . "/{$ip['Ipaddress']['mask']}" ;
    }

    $data = $this->_get_ranges($mobile_ips) ;
   
    $file = new File($cacheFile, true);
    $file->write($data);
    $file->close();

    return include($cacheFile);
  }
}

このModelを使うにはこんな感じ。

 <?php
class HogeController extends AppController
{
   var $components = array(
        'mobileip.MobileIp' => array(
          'ModelName' => 'Ipaddress'
        )
      ) ;
  
  function index()
  {
    pr($this->MobileIp->carrier()) ;
  }
}

CakePHPで携帯サイトを構築されている方は、ぜひ使ってみてください!

AuthComponentで軽くはまりました...

最近いろんなところではまってます...orz

id:cakephperさんのブログでTwitterでのつぶやきが取り上げられていたので、ちょっとまとめてみます。

現在、あるアプリの基盤部分を実装中なのですが、Authコンポーネントを使って管理ページの機能を作っています。
デバッグのために「TestsController」を作ってテストをしていたのですが、なんだか挙動が変です。認証処理が動いていません。
いろいろと試してもうまくいかないので、Authコンポーネントのコードを開いてみると...

<?php
// cake/controllers/components/auth.php
....
	function startup(&$controller) {
		$methods = array_flip($controller->methods);
		$action = strtolower($controller->params['action']);
		$allowedActions = array_map('strtolower', $this->allowedActions);

		$isErrorOrTests = (
			strtolower($controller->name) == 'cakeerror' ||
					// ↓ここに注目!
			(strtolower($controller->name) == 'tests' && Configure::read() > 0)
		);
		if ($isErrorOrTests) {
			return true;
		}
....

思わず「あーっっ!」と叫んでしまいましたとさ。

ContollerのTestのはまりどころ(redirect)

最近、テスト廚ぎみなわたなべです(^^;

ビジネスロジックは出来るだけModelやComponentに書く様にしているのでModelやComponenntなどのテストはそれなりに書いていたのですが、Controllerのテストは全く書いていませんでした。とはいえ、Controllerにも処理があるので、テストを書いてみようと試したときにはまった点と私なりの解決方法をまとめてみました。

Controllerをbakeすると自動で作られるControllerのTestCodeは以下のような感じです。

<?php 
/* SVN FILE: $Id$ */
/* ExampleController Test cases generated on: 2009-12-04 19:56:41 : 1259924201*/
App::import('Controller', 'Examples');

class TestExamples extends ExamplesController {
  var $autoRender = false;
}

class ExamplesControllerTest extends CakeTestCase {
  var $Examples = null;

  function startTest() {
    $this->Examples = new TestExamples();
    $this->Examples->constructClasses();
  }

  function testExamplesControllerInstance() {
    $this->assertTrue(is_a($this->Examples, 'ExamplesController'));
  }

  function endTest() {
    unset($this->Examples);
  }
}

で、以下のようなテストコードを書いてみました。

<?php
  // テストクラスの頭に以下のfixture定義を追加
  var $fixtures = array('example');
....
  function testIndex()
  {
    $result = $this->testAction('/admin/examples/', array('fixturize' => true, 'return'=>'vars'));

    $expected = array(
      array(
        'Example' => array(
          'id' => '1',
          'name'  => 'EXAMPLE1',
          'created'  => '2009-12-03 19:40:59',
          'modified'  => '2009-12-03 19:40:59'
        )
      ),
    );
    $this->assertEqual($results['examples'], $expected) ;
  }

textActionのPATHを見ていただければ分かる様に、このControllerは管理画面用のControllerで認証が通ってなかったり、権限がないとredirectしまくります。
テストを実行してみたところ、通常の遷移と同様にログイン画面に遷移してしまいました。

bakeされたコードにTest用のControllerのひな形があったので、このコントローラでredirectメソッドを書き換えちゃえばオッケー!と、思って試したところ撃沈...。

CakeTestCaseクラスの中を追いかけてみたのですが、Test用のControllerで置き換える様な仕掛けは見つけられませんでした。googleさんで調べてみた所、同じ様なところで皆さん苦労されているようで以下の方法にたどり着きました。

runkitは普段の開発環境のMacOSX上で動作させられなかったので、断念。
test.phpを書き換える方法は、Controllerにテスト用のコードが混ざってしまうのが気になるので避けたい...。

という事でTest用のDispatcherを弄ったりしてみたのですが、どうもしっくりきません。諦めかけていた所でひらめきました(^^!

「routingテーブルを書き換えちゃえば良いじゃん!」

あっさり動きました!以下がテストコードです。

<?php 
/* SVN FILE: $Id$ */
/* ExampleController Test cases generated on: 2009-12-04 19:56:41 : 1259924201*/
App::import('Controller', 'Examples');

// Test用のコントローラ名をbakeされた物(TestExamples)からTestExamplesControllerに変更
class TestExamplesController extends ExamplesController {
  var $autoRender = true;

    // redirect処理を上書き
  function redirect($url=NULL,$code=NULL)
  { 
    $this->lastRedirectUrl = $url;    
  }
}

class ExamplesControllerTest extends CakeTestCase {
  var $Examples = null;
  var $fixtures = array('example');

  function __construct()
  {
    parent::__construct() ;
      // ルーティングテーブルにテスト用コントローラを追加
    $Router = Router::getInstance() ;
    Router::connect('/admin/examples/:action/*', array('controller' => 'test_examples', 'admin' => true));    
  }

  function startTest() {
    $this->Examples = new TestExamplesController();
    $this->Examples->constructClasses();
  }

  function testExamplesControllerInstance() {
    $this->assertTrue(is_a($this->Examples, 'TestExamplesController'));
  }

  function endTest() {
    unset($this->Examples);
  }

  function testIndex()
  {
    $result = $this->testAction('/admin/examples/', array('fixturize' => true, 'return'=>'vars'));

    $expected = array(
      array(
        'Example' => array(
          'id' => '1',
          'name'  => 'EXAMPLE1',
          'created'  => '2009-12-03 19:40:59',
          'modified'  => '2009-12-03 19:40:59'
        )
      ),
    );
    $this->assertEqual($results['examples'], $expected) ;
  }
}

あとは、bakeのテンプレートを書き換えてしまえばいい感じになりそうです。

みなさん、Controllerのテストはどうしてますか?
「もっと簡単な方法があるよ!」とか「標準の機能でできるよ!」とかあれば是非教えてほしいです!

12/7 追記

CookbookのコントローラのTestのマニュアルページに対策が書いてありますね(^^;

CakeTestDispatcher::__loadControllerを書き換える方法なのです。
同じ様な方法で動作する事も確認していたのですが、,Coreのコードを触るのは出来るだけ避けたかったので、やめた方法でした(^^;

このページ見ていたはずですが、redirectを書き換える所だけ見て下の方は見てませんでした(;_;。

12/8 追記

$this->params['requested'] の有無で分岐する方法もとれそうですね。
いずれにせよテストしやすい様にする為に、Controller内でテストからの実行時に処理を分けるのは必要そうな感じ。

12/8 追記^2

結局、リダイレクト先のチェックをするのに実行されたControllerのインスタンスが必要だったので以下の様にCakeTestCaseを修正してみました。

<?php
  function startController(&$controller, $params = array()) {
    $this->___controller =& $controller ;
....  
  function &getController()
  {
    return $this->___controller ;  
  }

Cake Matsuri TOKYO 2009が終りました。

10/30〜31の二日にわたって開催された、CakeMatsuri TOKYO 2009が盛況のうちに終了しました。
今回は、参加者ではなく中の人として関わらせていただきました。

感想

初の試みが盛りだくさんで、色々と至らない部分もあったと思いますが、概ね満足頂けたようでほっとしています。
今回は「交流」というテーマを掲げていたので、休憩時間や懇親会で参加者同士で交流している様子をみて青年団一同にやにやしてしまいました(^^;。

また、この日は入門者コースの「Webサービスとの連携」の講師をさせていただきました。
実は講師自体初めてで、色々と手探りだったので満足頂ける講義を出来るか正直不安でした。
懇親会で参加された方とお話をしたときに、「楽しかった」、「良かった」と言っていただきかなりホッとしました。

進め方や、資料の構成など自分なりに反省点もあるので今後に生かしたいと思います。

これから

今は若干燃え尽き気味ですが、まだまだ"TAKE"の方が多いので、今後も引き続きCakePHPに関わりつつ、自分でも色々と発信できればと思っています。
そして、折角の機会なのに英語力不足で少ししかコアメンバとお話しできませんでした。来年への課題として、英語力を強化したいです。

皆さまおつかれさまでした!

# ワークショップの資料は、少し見直しをしてから公開します。少しお待ちくださいませ!