フィルタチェーンが気持ち悪いのでActiveRecordをオーバーライドする

Akelos使ってるとバリデータの流れが気持ち悪いと思うことがある。
ActiveRecordでsaveを行うと順々にコールバックメソッドを評価してレコードが登録されるわけだが、メソッド名から想像する順序と実際に実行される順序が違いすぎて困る。

ちなみにソースコード中のコメントには以下のような感じでコールバックメソッドの実行順序が記述されている。

# * - (-) save()
# * - (-) needsValidation()
# * - (1) beforeValidation()
# * - (2) beforeValidationOnCreate() / beforeValidationOnUpdate()
# * - (-) validate()
# * - (-) validateOnCreate()
# * - (4) afterValidation()
# * - (5) afterValidationOnCreate() / afterValidationOnUpdate()
# * - (6) beforeSave()
# * - (7) beforeCreate() / beforeUpdate()
# * - (-) create()
# * - (8) afterCreate() / afterUpdate()
# * - (9) afterSave()
# * - (10) afterDestroy()
# * - (11) beforeDestroy()

想像してた流れはこのコメントのとおりの順番で、この通り動いてくれれば何ら問題はないんだけど、ざっと実際の流れを追ってみると以下のような感じになっている。

+ ---------save()-------------------------------------------------------------+
|                                                                             |
|     (1) beforeSave()  → false                                              |
|           ↓ true                                                           |
| +--------createOrUpdate() ---------------------------------------+          |
| |                                                                |          |
| | +------isValid() -----------------------------------+          | → false |
| | |                                                   |          |          |
| | |  (2) beforeValidation()   → false                | → false |          |
| | |       ↓ true                                     |          |          |
| | |      validate()                                   |          |          |
| | |                                                   |          |          |
| | |  (3) afterValidation()                            |          |          |
| | |                                                   |          |          |
| | |  (4) beforeValidationOn[Create|Update]() → false |          |          |
| | |       ↓ true                                     |          |          |
| | |      validateOn[Create|Update]()                  |          |          |
| | |                                                   |          |          |
| | |  (5) afterValidationOn[Create|Update]()           |          |          |
| | +---------------------------------------------------+          |          |
| |         ↓ true                                                |          |
| | +--- _create() or _update() ---------------+                   |          |
| | |                                          | → false          |          |
| | |  (6) before[Create|Update]() → false    |                   |          |
| | |       ↓ true                            |                   |          |
| | |  (7) after[Create|Update]()  → false    |                   |          |
| | +------------------------------------------+                   |          |
| +----------------------------------------------------------------+          |
|           ↓ true                                                           |
|      (8) afterSave()   → false                                             |
+-----------↓----------------------------------------------------------------+
          [commit]

見てのとおり気持ち悪い。とりあえずafterとかbeforeとか名前つけてるんならちゃんと順番通りやってほしい。
ちなみにsaveと並んでレコード操作によく使われるcreateとupdateについては内部でsaveを呼び出しているため基本的にこの流れになるが、createOnUpdateでレコードの操作を行うとbeforeSaveとafterSaveは通ってくれない。呼ばれたくないならprivateっぽく書けばいいのに。

ついでにafterValidation[Update|Create]についてはfalseを返してもチェーンは中断されない。
これは意図したものかどうかわからないが、他のメソッドと挙動がちがうのはちょっと止めてほしい。

しかも、致命的なことにbeforeValidationでfalseが返るとisValidはtrueを返すためvalidateはスルーされてそのまま登録処理に走るというよく分からない状態になっている。

とにかくこれらをしっかり把握しておかないと、本来便利であるメソッドチェーンがバグの温床になったりするので要注意。

とはいえ、どうみてもバグっぽい仕様なので素直にこの流れに沿ってロジックを書いてると、いつの間にか本家で修正されて涙目という状況になる可能性が高い。

結局shared_model.phpに書くメソッドをオーバーライドしてしまうのが一番手っ取り早い気がしたので勢いでやってしまった。後悔はしていない。

ということで、save,createOrUpdate,isValidをオーバーライドしたshared_model.phpです。

<?php

require_once(AK_LIB_DIR.DS.'AkActiveRecord.php');

/**
* This file is application-wide model file. You can put all 
* application-wide model-related methods here.
* 
* Add your application-wide methods in the class below, your models
* will inherit them.
*
* @package ActiveRecord
* @subpackage Base
*/
class ActiveRecord extends AkActiveRecord 
{
   
   function save($validate = true)
   {
       return $this->createOrUpdate($validate);
   }
   
   function createOrUpdate($validate = true)
   {
       if($this->isFrozen()){
           return false;
       }
       
       if($validate && !$this->isValid()){
           $this->transactionFail();
           return false;
       }
       
       $this->transactionStart();
       
       while(true)
       {
           if(!$this->beforeSave() || !$this->notifyObservers('beforeSave')) break;
         
           $result = $this->isNewRecord() ? $this->_create() : $this->_update();
         
           if($result && !$this->transactionHasFailed() && $this->afterSave() && $this->notifyObservers('afterSave')){
                $this->transactionComplete();
                return true;
           }
           break;
       }
       $this->transactionFail();
       return false;
   }
   
   function isValid()
   {
        $this->clearErrors();
        
        while(true)
        {
          if ($this->isNewRecord()){
              $beforeValidation = 'beforeValidationOnCreate';
              $afterValidation  = 'afterValidationOnCreate';
              $validate = 'validateOnCreate';
          }else{
              $beforeValidation = 'beforeValidationOnUpdate';
              $afterValidation  = 'afterValidationOnUpdate';
              $validate = 'validateOnUpdate';
          }
          
          if(!$this->beforeValidation() || !$this->notifyObservers('beforeValidation')) break;
          if(!$this->$beforeValidation() || !$this->notifyObservers($beforeValidation)) break;
          
          if($this->_set_default_attribute_values_automatically){
              //$this->_setDefaultAttributeValuesAutomatically();
          }
          
          $this->validate();
          $this->$validate();
          
          if($this->_automated_validators_enabled){
              //$this->_runAutomatedValidators();
          }
          
          if(!$this->afterValidation() || !$this->notifyObservers('afterValidation')) break;
          if(!$this->$afterValidation() || !$this->notifyObservers($afterValidation)) break;
          
          return !$this->hasErrors();
        }
        return false;
    }
}

?>

一応メソッドチェーンは期待通り動くし、それぞれでfalseが返ればしっかり中断してくれるようになった。
本家のアップデートを追いかけつつ微妙に修正は入れていかないといけないけど、精神的にはこっちの方がうれしい。

IE7 わけがわからない

IE7特有で発生するワケわからん症状をみつけた。現時点で再現はできるけど原因と対策は不明。見当もつかない。

追記:今のところXP+IE7ではそれほど・・・という感じのような気がする。Vista+IE7だったり、もっと特有の環境で発生するのかも。

とりあえず、以下のようなよくあるリスト型メニューのHTMLを作ってIE7で開いてみる。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<style type="text/css">
ul.menu { list-style-type:none; }
ul.menu li a:hover { display:block;background-color:#ccc; }
</style>
</head>
<body>
<div style="width:200px">
<ul class="menu">
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
</ul>
<ul class="menu">
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
</ul>
<ul class="menu">
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
</ul>

<ul class="menu">
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
</ul>
<ul class="menu">
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
</ul>
<ul class="menu">
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
</ul>
<ul class="menu">
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
</ul>
<ul class="menu">
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
</ul>
<ul class="menu">
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
</ul>
<ul class="menu">
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
</ul>
<ul class="menu">
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
</ul>
<ul class="menu">
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
</ul>
<ul class="menu">
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
</ul>
<ul class="menu">
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
</ul>
<ul class="menu">
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
</ul>
<ul class="menu">
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
</ul>
<ul class="menu">
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
</ul>
<ul class="menu">
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
</ul>
<ul class="menu">
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
</ul>
<ul class="menu">
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
</ul>
<ul class="menu">
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
</ul>
<ul class="menu">
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
  <li><a href="#">メニュー1</a></li>
</ul>
</div>
</body>
</html>

当然、各リンクをにマウスを乗せると a:hover が適用されて要素の背景色が変わる。そしてIE7でももちろん変わる。

問題はその速度。なんかやたらと重い。もっさりしてる。

IE以外のモダンブラウザでは何ら問題なくサクサク背景色が変わるけど、IE7だとマウスの動きに遅れてついてくる感じになってしまう。
負荷を確認したところ、Pentium4 2.8GHzの環境で40%〜70%程度CPU負荷がかかっている。要は単純にCPU負荷の問題っぽいので最近のディアルコアの早いのならそんなに分からないかもしれない。


色々調べてたら、

  • ラップしているDIV要素のスタイルwidthを消したり、hoverさせる要素を少なくすると一気にCPU負荷は下がる
  • widthはラップしている要素に適用しようが、中に適用しようが結果は同じ。
  • widthじゃなくてheightでも同様の症状が出る。(marginやpaddingなどでは発生しない)

という事が分かった結果、ますますワケがわからない。


結局時間がなくて原因、回避方法の究明までいたらず気持ち悪いまま今日の調査は終了。

同じ症状を経験して回避策の分かる方いたら教えてください。

rm -rf /* が許されるのは小学生まで

昨日の話。

     ____  
   /      \
  /  ─    ─\ 
/    (●)  (●) \  さて、とりあえずソースの修正も終わったし
|       (__人__)    |   1回テストして寝るお。
/     ∩ノ ⊃  /
(  \ / _ノ |  |
.\ “  /__|  |  
  \ /___ /




         ____
       /      \
      /  ─    ─\    ここから新規投稿して通知メールが来たらおkだお。
    /    (●)  (●) \  
    |       (__人__)    | ________
     \      ` ⌒´   ,/ .| |          |
    ノ           \ | |          |
  /´                 | |          |
 |    l                | |          |
 ヽ    -一ー_~、⌒)^),-、   | |_________|
  ヽ ____,ノγ⌒ヽ)ニニ- ̄   | |  |




             (ヽ三/) ))
         __  ( i)))
        /⌒  ⌒\ \
      /( ●)  (●)\ )
    ./:::::: ⌒(__人__)⌒::::\ 通知メール一杯きてるお!
    |    (⌒)|r┬-|     |    やる夫にかかればこんなの楽勝だお!
    ,┌、-、!.~〈`ー´/   _/
    | | | |  __ヽ、   /
    レレ'、ノ‐´   ̄〉  |
    `ー---‐一' ̄







                   
   / ̄ ̄ ̄ \  ホジホジ    
  / ―   ― \
/   (●)  (●)  \    ?でもなんで1回しか登録してないのに 
|     (__人__)      |     何件もメールがくるのかお?
\   mj |⌒´     /        ・・・まぁいいか。
     〈__ノ





   ・・・5分後・・・





         ____
       /   u \
      /  ―    ─\    
    /  し (●)  (●) \ メール受信がとまらないお
    | ∪    (__人__)  J | (無限ループの予感がするお・・・・)
     \  u   `⌒´   / 






         ____
       /::::::::::  u\
      /:::::::::⌒ 三. ⌒\       送信ライブラリにバグがある・・・
    /:::::::::: ( ○)三(○)\           ループがbreakされてない
    |::::::::::::::::⌒(__人__)⌒  | ________
     \::::::::::   ` ⌒´   ,/ .| |          |
    ノ::::::::::u         \ | |          |
  /:::::::::::::::::      u       | |          |
 |::::::::::::: l  u             | |          |
 ヽ:::::::::::: -一ー_~、⌒)^),-、   | |_________|
  ヽ::::::::___,ノγ⌒ヽ)ニニ- ̄   | |  |




        ____
       /      \
      /  ─    ─\    まぁ、こんなときは焦らずプロセスkillして
    /    (●)  (●) \  メールボックスから直接削除だお。やる夫は冷静だお。
    |       (__人__)    | ________
     \      ` ⌒´   ,/ .| |          |
    ノ           \ | |          |
  /´                 | |          |
 |    l                | |          |
 ヽ    -一ー_~、⌒)^),-、   | |_________|
  ヽ ____,ノγ⌒ヽ)ニニ- ̄   | |  |






     ____
   /      \ ( ;;;;(
  /  _ノ  ヽ__\) ;;;;)   
/    (─)  (─ /;;/    一応「rm -rf」したけど
|       (__人__) l;;,´   10000件くらいメールきてるから時間かかるお
/      ∩ ノ)━・'/       暇だからとりあえずライブラリに修正いれておくか。
(  \ / _ノ´.|  |    
.\  "  /__|  |     
  \ /___ 




       / ̄ ̄ ̄\
     / ─    ─ \
    /  <○>  <○>  \.      ・・・?
    |    (__人__)    |     「cd: command not found」
    \    ` ⌒´    /       「vi: command not found」
    /              \







           ____
       /::::::::::::::::\
      /::::::─三三─\
    /:::::::: ( ○)三(○)\   「rm -rf /*」!?
    |::::::::::::::::::::(__人__)::::  |  ________
     \:::::::::   |r┬-|   ,/ .| |          |
    ノ::::::::::::  `ー'´   \ | |          |  
  /:::::::::::::::::::::             | |          |  
 |::::::::::::::::: l               | |          |







         ___
        /⌒  ⌒\         ━━┓┃┃
       /(  ̄)  (_)\         ┃   ━━━━━━━━
     /::::::⌒(__人__)⌒:::: \         ┃               ┃┃┃
    |    ゝ'゚     ≦ 三 ゚。 ゚                       ┛
    \   。≧       三 ==-
        -ァ,        ≧=- 。
          イレ,、       >三  。゚ ・ ゚
        ≦`Vヾ       ヾ ≧
        。゚ /。・イハ 、、    `ミ 。 ゚ 

ということをやった。

「rm -rf /*」をやった

ワザとじゃなくてミスで。
とりあえず、スレーブのHDDにcronで日次ミラーしてたので何とか復旧できたけども。。。
効果的なのは分かったので今後のためにもミラーリングシェルスクリプトと復旧手順を個人的メモであげておく。

※前準備としてスレーブHDDをマスタと同じ構成でFdiskしておくこと。

ミラーリングスクリプト

#!/bin/sh

echo "`date +%Y-%m-%d\ %k:%M:%S`: start syncing" >> /var/log/backup.log

/bin/mount -t xfs /dev/sdb3 /mnt/sdb 2>> /var/log/backup.log
/bin/mount -t ext3 /dev/sdb1 /mnt/sdb/boot 2>> /var/log/backup.log


/usr/bin/rsync -aH --delete \
    --exclude=/proc \
    --exclude=/sys \
    --exclude=/mnt \
    /* /mnt/sdb

if [ ! -d "/mnt/sdb/proc" ]; then
  mkdir /mnt/sdb/proc
fi
if [ ! -d "/mnt/sdb/sys" ]; then
  mkdir /mnt/sdb/sys
fi
if [ ! -d "/mnt/sdb" ]; then
  mkdir /mnt/sdb
  mkdir /mnt/sdb/boot
fi

/bin/umount /mnt/sdb/boot 2>> /var/log/backup.log
/bin/umount /mnt/sdb 2>> /var/log/backup.log

echo "`date +%Y-%m-%d\ %k:%M:%S`: end syncing" >> /var/log/backup.log

あとはcronなりでタスクに登録しておく。



やっちまったときは慌てずにマスターとスレーブを切り替えて、BootCDから起動。

そのあと、以下のような感じでGRUBをインストールして再起動すればおk。
※注:これはウチの環境の場合です。

$ mkdir /mnt/sda
$ mount -t xfs /dev/sda3 /mnt/sda
$ mount -t ext3 /dev/sda1 /mnt/sda/boot
$ grub-install --root-directory=/mnt/sda /dev/sda
$ shutdown -r now

人為的トラブルに備えてRAID以外のミラーはやっぱり必要だと心底思った。

繰り返し予定をデータベースで表現する

スケジューラとかを作ってていつも悩むのが繰り返し予定をRDBに保存する方法。
色々なWEBアプリのスケジューラを見た感じではiCalendarのrruleでやるのが多いみたいだけど、何となくDBとの相性が悪そうな気がしてならない。

繰り返し予定の要件としては、

  • 繰り返し期間を指定できる
  • 月を指定できる(例:1,3,5月)
  • 週を指定できる(例:月の1週目と3週目)
  • 曜日を指定できる(例1:月、火曜、例2:第2、第3土曜日)
  • 日を指定できる(例:1,11,21,31日)

くらいできれば組み合わせ次第で大体の繰り返し表現はできるんじゃないかと思う。

これをDBに保存する場合検索時の効率の良さを考えるとどうするのが一番良いんだろうか?その辺が悩みどころ。
とりあえず正解ではないかもしれないけど、自分なりにコレだと思うやり方でやってみることにする。

まず、それぞれの繰り返し表現をビットフラグに置き換える。
1,3,5月の第2月,火,水曜日の予定の場合は以下のようになる。

  • 月:101010000000(1,3,5月)
  • 週:11111(全ての週)
  • 曜日:0101010(月,火,水曜)
  • 曜日(X番目):01000(2番目)
  • 日:1111111111111111111111111111111(全ての日)

で、これらを10進数に置き換えてをDB(Mysql)に保存する。テーブルは以下のような感じで設計

CREATE TABLE `event_recurrence` (
  id integer primary key auto increment,
  title varchar(255) not null,
  content text,
  start date not null,
  until date,
  month integer unsigned not null default 0,
  week  intrger unsigned not null default 0,
  day   integer unsigned not null default 0,
  dnum  integer unsigned not null default 0,
  date  integer unsigned not null default 0,
  index (start,until,month,week,day,dnum,date)
);

今回は大して関係ないがunsingedないと整数範囲が狭くなるので一応つけておく。
準備ができたらそれぞれ10進数に変換してレコードを追加する。

INSERT INTO `event_recurrence` (title,content,start,until,month,wee,day,dnum,date)
VALUES ('test','test','2008-2-16','2009-2-15',2688,31,42,8,2147483647);

検索は対象となる日を同じようにビット->10進数に変換し、ビット演算で行う。
たとえば、2008年3月10日の場合、

  • 3月:001000000000 => 512
  • 3週目:00100 => 4
  • 月曜日:0100000 => 32
  • 2番目の曜日:01000 => 8
  • 10日:0000000001000000000000000000000 => 2097152

となるので以下のようなクエリを発行する

SELECT * FROM `event_recurrence` WHERE start <= '2008-3-10' AND until >= '2008-3-10'
AND (month & 512) = 512 AND (week & 4) = 4 AND (day & 32) = 32 AND (dnum & 8) = 8 AND (date & 2097152) = 2097152;

+----+-------+---------+---------------------+---------------------+------------+-----+------+------+-------+
| id | title | content | start               | until               | date       | day | dnum | week | month |
+----+-------+---------+---------------------+---------------------+------------+-----+------+------+-------+
|  1 |  test |    test | 2008-02-18 00:00:00 | 2009-02-17 00:00:00 | 2147483647 |  42 |    8 |   31 |  2688 |
+----+-------+---------+---------------------+---------------------+------------+-----+------+------+-------+
1 row in set (0.00 sec)

まぁ良い感じに検索できた。対象日をビットに変換するのなんてDBのインデックスの恩恵を考えれば大したことじゃないので割と実用的な気がしないでもない。
あー、でも対象日が複数の場合は1回のクエリで検索しようとすると困るかもしれない。どうしたものか。。
結局の所、繰り返し日をすべて展開してDBに放り込む方がいいのかもしれない。ふむ。

他にもっと良い方法があれば教えてください。

ちなみに今回変換したのプログラムは以下のような感じ。

<?php
/**
 *  month,week,day,dnum,date ot date to bit
 *
 *   [2008/3/10]
 *    -month: Mar => 001000000000 => 512
 *    -week:  3rd => 00100        => 4
 *    -day:   Mon => 0100000      => 32
 *    -dnum:  2nd => 01000        => 8
 *    -date:  10  => 000000000100000000000000000000 => 1048576
 *
 *  @param string $date   string of date (ex. 2008-3-10||2008/3/10)
 *
 *  @return object (ex. month=512, week=4, day=32, dnum=8, date=1048576)
 */
function date_to_bit($date_string, $week_start = 0)
{
    list($year,$month,$date,$day) = explode('-', date('Y-n-j-w', strtotime($date_string)));
    $first_day  = date('w', mktime(0,0,0,$month,1,$year));
    $week = ceil(($date+$first_day-$week_start)/7);
    $week = ($first_day<$week_start)?$week+1:$week;
    $dnum = ceil($date/7);

    $week  = bindec(substr_replace('00000', '1', $week-1, 1));
    $dnum  = bindec(substr_replace('00000', '1', $dnum-1, 1));
    $day   = bindec(substr_replace('0000000', '1', $day, 1));
    $month = bindec(substr_replace('000000000000', '1', $month-1, 1));
    $date  = bindec(substr_replace('0000000000000000000000000000000', '1', $date-1, 1));

    return (object)array('month'=>$month,'week'=>$week,'day'=>$day,'dnum'=>$dnum,'date'=>$date);
}

/**
 *  array or string to bitFlag => integer
 *
 *   -month: Jan,Feb,Dec => array(1,2,12)   => 110000000001 => 3073
 *   -week:  1st,3rd     => array(1,3)      => 10100        => 20
 *   -day:   Sun,Wed     => array(0,2)      => 1001000      => 72
 *   -dnum:  1st,3rd     => array(1,3)      => 10100        => 20
 *   -date:  11,21,31    => array(11,21,31) => 000000000010000000001000000001 => 524801
 *
 *  @param string        $type   month||week||day||dnum||date
 *  @param array||string $values array(1,3,5)||'1,3,5'
 *
 *  @return integer
 */
function array_to_bit($type, $values)
{
  if(is_string($values))
  {
    return array_to_bit($type, explode(',', $values));
  }
  elseif(is_array($values))
  {
    $values = array_map('trim',array_values($values));
    $bit = '';
    $start = $end = 1;
    switch($type)
    {
      case 'week':
      case 'dnum':  $end = 5; break;
      case 'month': $end = 12; break;
      case 'day':   $start = 0; $end = 6; break;
      case 'date':  $end = 31; break;
    }
    for($i=$start;$end>=$i;$i++){ $bit.=(in_array($i,$values))?'1':'0'; }
    return bindec($bit);
  }
  else
  {
    return 0;
  }
}

/**
 *  integer to bitFlag => array
 *
 *   -month: 3073 => 110000000001 => array(1,2,12)
 *   -week:  20   => 10100        => array(1,3)
 *   -day:   72   => 1001000      => array(0,2)
 *   -dnum:  20   => 10100        => array(1,3)
 *   -date:  524801 => 10000000001000000001 => array(11,21,31)
 *
 *  @param string  $type   month||week||day||dnum||date
 *  @param integer $int
 *
 *  @return array
 */
function bit_to_array($type, $int)
{
  $values = array();
  if(is_numeric($int))
  {
    $bit = (string)decbin((int)$int);
    $end = $num = 0;
    switch($type)
    {
      case 'week':
      case 'wnum':  $end = 5; $num = 1; break;
      case 'month': $end = 12; $num = 1; break;
      case 'wday':  $end = 7; break;
      case 'day':   $end = 31; $num = 1; break;
    }
    $bit = str_pad($bit, $end, '0', STR_PAD_LEFT);
    for($i=0;$end>$i;$i++){ if($bit[$i]=='1') $values[] = $i+$num; }
  }
  return $values;
}

Akelosでモジュールを使う

一般的にWEBアプリで管理画面を作る場合、通常のURLとは違い

http://〜/admin/[controller]/[action]

というURLを使いたくなる。Akelosでコレを実現する場合はとりあえずモジュールを使えばおk。

たとえば上記の例の場合、まずURLルーティングを行うためにconfig/routes.phpを編集する

config/routes.php

<?php

$Map->connect('/admin/:controller/:action/:id',array('module' => 'admin', 'controller' => COMPULSORY, 'action' => 'index'));

こんな感じで書けば「/admin/〜」へのアクセスはadminモジュールとして処理される。

モジュールを使う際の基本ルールは以下のような感じ

  • controllerファイル: app/controllers/[module_name]/[controller_name]_controller.php
  • controllerクラス名: [ModuleName]_[ControllerName]Controller
  • viewファイル: app/views/[module_name]/[controller_name]/[action_name].tpl

これに従って、adminモジュールでuserクラスを使用する場合はこうなる。

app/controllers/admin/user_controller.php

<?php
Admin_UserController extends ApplicationController
{
  function index(){}
}

app/views/admin/user/index.tpl

<h1>ユーザ管理画面</h1>
このページはユーザ管理画面です。

上記のようにすれば「http://〜/admin/user/index」にアクセスして利用することができるようになる。


ちなみにこの機能を使うと、view内の全てのurlヘルパーを使ったurl出力に「module」パラメータが付与される。
これがいやな場合は、コントローラに以下の記述を入れればよい。

<?php
Admin_UserController extends ApplicationController
{
  function index(){}

  function defaultUrlOptions($options)
  {
    $this->module_name = null;
    return parent::defaultUrlOptions($options);
  }
}