CakePHP 1.2でできたアプリケーションをを1.3へマイグレーションした話を書きます。

えー、3じゃないのーという話なのですが、大人には事情(お金)がある(ない)のです。 :sweat:

なにかお役に立てる話が書けたらいいなと思ってますが、たぶんほとんど自分のアプリに特化した内容になりそうです。 コーヒー片手に薄目で見てやってください。 :tea:

参考情報:
1.2から1.3への移行ガイド
http://book.cakephp.org/1.3/ja/The-Manual/Appendices/Migrating-from-CakePHP-1-2-to-1-3.html

:cake: さくっとcakeフォルダを入れ替え

まずは既存アプリからcakeフォルダを消して、CakePHP 1.3のcakeフォルダをコピーしました。

:cake: index.phpファイルをコピー

CakePHP 1.3のapp/webroot/index.phpファイルを既存アプリへコピーしました。

自前で変更している箇所があるので、それも反映しました。

2か所です。

index.php

    if (!defined('ROOT')) {
        define('ROOT', dirname(dirname(__FILE__)));
    }

    if (!defined('APP_DIR')) {
        define('APP_DIR', 'app');
    }

:cake: app/config/core.phpファイルを編集

logという設定とSession.modelという設定が足りてないので付け足しました。

app/config/core.php

    Configure::write('log', true);

    Configure::write('Session.model', 'Session');

:cake: log4phpを使うには……。

CakePHP付属のCakeLogクラスを自前のものに置き換えて、log4phpを呼ぶようにしていました。

自前のCakeLogクラスは、app/cake_log.phpファイルに置き、app/config/core.phpファイルの中からrequire_onceで読み込んでいました。

app/cake_log.phpの中でApp::importでlog4phpを読み込んでいましたが、ここがなぜか動きませんでした。

CakePHP 1.3ではログエンジンを入れ替えられるらしいので、とりあえずコメントアウトして後回しにしました。 :rabbit: :rabbit: :rabbit:

ぼちぼちエラーメッセージが出ますが、とりあえずはこれで画面が見えるようになりました。 :wink:

:cake: Smarty Viewをアップグレード

CakePHP 1.3対応版Smarty Viewを https://github.com/tsak/glitch-auto-sell/blob/master/app/views/smarty.php からダウンロードして置き換えます。

Smarty 2.6の場合(?)、assignByRefが無いのでassign_by_refを使うようにします。書き換え箇所は2つあります。

app/views/smarty.php

$this->Smarty->assign_by_ref($camelBackedHelper, ${$camelBackedHelper});
//$this->Smarty->assignByRef($camelBackedHelper, ${$camelBackedHelper});

/* (snip) */

$this->Smarty->assign_by_ref('view', $this);
//$this->Smarty->assignByRef('view', $this);

viewの下にsmartyディレクトリを作っていましたので、$this->subDir = 'smarty'.DS; としました。

app/views/smarty.php

$this->subDir = 'smarty'.DS;

この辺りはもともとコメントアウトしてあるコードをイキにするだけです。親切ぅ~ :smile:

:cake: Sessionヘルパーを読み込み

SessionコンポーネントとSessionヘルパーは自動的に読み込まれなくなりました。

Sessionヘルパーは自動で読み込まれなくなったそうで、AppController$helpersに追加しろとのことです。

app/app_controller.php

  var $helpers = array('Html', 'Form', 'Javascript', 'Session');

:cake: Htmlヘルパー使用箇所の修正

HTMLエスケープするかどうかを指定する専用の引数があったのですが、なくなってしまいました。

HtmlHelper::link()$escapeTitle引数は削除されました。代わりに$options['escape']を使用してください。

:cake: EmailComponentを継承したクラスを変更

EmailComponentを継承してmb_send_mailを使うクラスを作っていました。

app/controllers/components/mb_email.php

class MbEmailComponent extends EmailComponent {
  var $delivery = 'mb_mail';

  function __wrap($message) { /* ... */ }
  function __mb_mail() { /* ... */ }
  function __wordwrap($str, $width = 75, $break = "\n", $cut = false) { /* ... */ }
}

EmailComponent内にあるメソッドの頭のアンダースコア2つが1つになったので、それぞれメソッド名を変更しました。
__wrap_wrap__mb_mail_mb_mail

たぶんprivate扱いだから、ドキュメントにも書いてない話です。そんなもんオーバーライドしてるのが悪いんです。はい。

:cake: Modelの__existsプロパティがなくなった

これも多分またprivate扱いのやつです。

次のように書き換えました。


$this->exists()

:cake: コミット漏れを修正

CakePHP 1.2のころはトランザクションを開始してコミットしなくても、データベースが更新されていました。

こんな感じにトランザクション開始するコードだけがあり、コミットするコードがないという元バグなのですが、1.2では問題なかったのです。 :sweat_smile:


$db =& ConnectionManager::getDataSource($this->useDbConfig);
$db->begin($this);

1.3ではちゃんとコミット$db->commit($this)しないといけないです。

あるいはsaveメソッドのオプションにatomicを渡すほうがよさそうです。


$myModel->save($myData, array('atomic' => true));

:cake: Model::bindModel()したアソシエーションをModel::saveAll()で保存できない :interrobang:

CakePHPでモデル間のアソシエーションを設定するとき、モデルクラスに直接プロパティを設定すると、毎回DBから読み込んでくるので困ってしまいます。

そんなわけで、ふつうはModel::bindModel()で動的にアソシエーションを設定しますよね?よね?ね?

1.3では、Model::saveAll()の中でModel::resetAssociations()を呼んでいるため、bindModelしたアソシエーションが消えてしまいます。

従って、アソシエーションしたモデルを保存できないという。。。 :astonished:

仕方がないのでbindModelするときにリセットしないようにしました。第2引数にfalseを渡します。


$myModel->bindModel($associatin, false);

:cake: Helper::valueの挙動が変わった

これまたアンドキュメンテッドな話です。

1.2までは次のようなコードが動いていました。


$formHelper->value('Model.field.time'); /* Model.fieldの値が返ります */

1.3では、こういう書き方ではModel.fieldの値が返りません。

幸いなことにSmartyプラグインの中で使っていたので、プラグイン側を書き換えて解決しました。 :rabbit:

:cake: Log4phpを使う

いよいよLog4phpを使えるようにしなくちゃいけなくなりました。

CakeLogの中から使えるようにラッパクラスを作ります。

app/libs/log/Log4phpWrapper.php

<?php
/** 
 * Small log4php wrapper for CakePHP 1.3
 *
 * @package app
 * @subpackage app.libs.log
 * @version $Revision$
 * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
 */
if (!defined('LOG_FATAL')) {
    define('LOG_FATAL', 1);
}

App::import(array('type' => 'Vendor',
                  'name' => 'log4php',
                  'file' => 'log4php'.DS.'Logger.php',
                  ));

class Log4phpWrapper {
    var $methodMap =
      array(
            LOG_DEBUG =>   'debug',
			LOG_INFO =>    'info',
			LOG_WARNING => 'warn',
			LOG_ERR =>     'error',
			LOG_ERROR =>   'error',
            LOG_FATAL =>   'fatal',
            );

    /** 
     * Constructor
     * 
     */
    public function __construct($options = array()) {
        Logger::configure(CONFIGS.'log4php.php');
    }

    /** 
     * Write log messsage
     * 
     * @param string $type
     * @param string $message
     */
    public function write($type, $message) {
        if (is_int($type) && isset($this->methodMap[$type])) {
            $type = $methodMap[$type];
        }

        if ($type === 'warning') {
            $type = 'warn';
        }

        /* when unknown method provided, fallback to 'info' */
        if (array_search($type, $this->methodMap) === false) {
            $type = 'info';
        }

        if (strpos($message, '|') !== false) {
            list($name, $message) = explode('|', $message, 2);
        }
        else {
            $backtrace = debug_backtrace(false);
            $caller = count($backtrace) > 3
              ? $backtrace[3] : $backtrace[0];
            $name = isset($caller['class'])
              ? $caller['class'] : $caller['function'];
        }
            
        $logger = Logger::getLogger($name);

        $logger->{$type}($message);
    }
}

作成したクラスを使うようにbootstrap.phpのなかで指示します(app/config/core.phpの中でこれをやろうとして、クラスが読み込まれなくて悩みました)。

app/config/bootstrap.php

CakeLog::config('log4php', array('engine' => 'Log4phpWrapper'));

log4php自体の設定はPHP方式にしています。

app/tmp/logsの下にログが落ちるようにしています。

app/config/log4php.php

return array(
    'rootLogger' => array(
        'appenders' => array('default'),
    ),
    'appenders' => array(
        'default' => array(
            'class' => 'LoggerAppenderDailyFile',
            'layout' => array(
                'class' => 'LoggerLayoutSimple'
            ),
            'params' => array(
            	'file' => LOGS.'%s.log'
            )
        )
    )
);

known issue :disappointed:

LoggerLayoutPatternで、呼び出し元を記録するとLog4phpWrapperが記録される。

ほんとは$this->logを呼び出したところが記録されて欲しい。

これを実現するにはlog4phpのLoggerLoggingEventクラスを変更すればよいのだけれど、dirty hackでやだなぁと悩み中。 :unamused:

:cake: おしまい

CakePHP 1.2アプリを1.3へマイグレートしたときに詰まった話を書きました。

また何かあれば追記します。