CakePHP 1.2アプリケーションを1.3へマイグレーションした話

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か所です。

```php: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``という設定が足りてないので付け足しました。

```php: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つあります。

```php: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;`` としました。

```php:app/views/smarty.php
$this->subDir = 'smarty'.DS;
```

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

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

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

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

```php: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を使うクラスを作っていました。

```php: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扱いのやつです。

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

```php:
$this->exists()
```

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

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

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

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

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

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

```php:
$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``を渡します。

```php:
$myModel->bindModel($associatin, false);
```

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

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

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

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

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

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


# :cake: Log4phpを使う

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

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

```php: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の中でこれをやろうとして、クラスが読み込まれなくて悩みました）。

```php:app/config/bootstrap.php
CakeLog::config('log4php', array('engine' => 'Log4phpWrapper'));
```

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

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

```php: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へマイグレートしたときに詰まった話を書きました。

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