Posted on

イベントシステムについて今更勉強した件

CakePHP4の使い方をあれこれ探っているときに、上司から「論理削除したとき、deletedカラムにもタイムスタンプが立つようにして」と言われたのが事の発端でした。
CakePHPには前からsoftDeleteなるプラグインが存在していて、それがタイムスタンプを立てることは知ってたんです。そんでも、プラグインをやたら増やしたくないという方針もあったため、何とか今あるものだけでdeletedカラムにタイムスタンプを立てようとしておりました。そうなると、TimeStampBehaviorの出番だろうと思い至ったわけです。今まで既存のコールバック(beforeSave)に対してタイムスタンプを置いていたわけだから、どうやって他のタイムスタンプを立てるタイミングを決めるのやら結構勉強が必要でした。

逆に言うと、勉強の時間が大半で、数行いじったらすぐできてしまったことにびっくりしているんです。
イベントをTimeStampビヘイビアに発行するナニカが必要なだけだったというオチで、最初にイベントリスナから組む長い長い道のりを覚悟していて肩透かし。

イベント発行って、こんだけでいいんですね

$event = new Event('Model.beforeDelFlag',$this,['entity' => $entity]);
$this->getEventManager()->dispatch($event);

TimeStampは内部的にイベントを全部観測していて、イベント名が設定のキーと同じ名前のものが発行されたとき、その下に設定されたタイムスタンプに適切に現在時刻を立てる…こういうことだったんだなと。

イベント名の命名規則は、層.(クラス.)イベント という具合だとか、リスナの書き方とか、まだイベントシステムを完全に理解したわけではないけれど、これを論理削除メソッドに埋め込んだらタイムスタンプ押してくれるようになった、という具合。これを他でどう使うかはまだイベントリスナ含め思いついていないですけれども、もしかしたらこれがまた仕事場の常識を塗り替えるような改善を生み出す可能性は無きにしも非ず…。

とりあえず今のところは、イベントの発行方法だけでも分かったことでよし、と思ってます。

ディスパッチという言い方がわかりにくいのねん…「発行」とか「発令」と訳語を当ててくれたらええのに…。

Posted on

Hashはいいぞ Hash::map編

気が付いたら前の記事から1年半以上空いてしまいました。それだけみんな忙しかったといえばそうなのですが、ようやっと時間ができたので、何を書こうかなと思ったとき、そういえば近くの人にHash::map教えてないなって思い出したんです。

これ、最初のうちはかなり読みにくい印象あると思うんです。ラムダ関数慣れが要るという感じで…

ここまでくるとforeachと大差ないっちゃ大差ないところもあります。
Hash::reduceともなるとさらに差がなくて、正直どっちで書けばいいか自分でも迷います。

それでも、配列を「ループで一つ一つ確認する」というforeachの見え方と、
配列を「各要素にまとめて適用する」というmapの見え方ってやっぱり違うと思うんです。

// Model->find('all') したときありがちな配列
$data = [
    o => [
        'Color' => [
            'id'=>'1',
            'name'=>ゴールド',
            'red'=>'255',
            'green'=>'215',
            'blue'=>'0',
        ]
    ],
    1 => [
        'Color'=>[
            'id'=>'2',
            'name'=>'シルバー',
            'red'=>'192',
            'green'=>'192',
            'blue'=>'192',
        ]
    ],
    2 => [
        'Color'=>[
            'id'=>'3',
            'name'=>'ブロンズ',
            'red'=>'154',
            'green'=>'98',
            'blue'=>'41',
        ]
    ]
];

//色名と16進コードの配列を作る
$result = Hash::map($data,'{n}.Color',function($item){
    return [ 
        'name' => $item['name'],
        'hex_code' => sprintf("#%02x%02x%02x",$item['red'],$item['green'],$item['blue'])
    ];
});

/**

$result = [
    0 => [
        'name' => 'ゴールド',
        'hex_code' => '#ffd700',
    ],
    1 => [
       'name' => 'シルバー',
       'hex_code' => '#c0c0c0',
    ],
    2 => [
       'name' => 'ブロンズ',
       'hex_code' => '#9a6229',
    ],
];

*/

//やってることは以下のforeachとあんまり差はない
$result_foreach = [];
foreach($data as $color){
    $result_foreach[] = [
        'name' => $color['Color']['name'],
        'hex_code' =>  sprintf("#%02x%02x%02x",$item['Color']['red'],$item['Color']['green'],$item['Color']['blue'])
    ];
}

foreachとあんまり変わらないなら、なぜこういう書き方をするかと考察したとき、いくつか強みっぽいところはあります

深い階層からもパスで引っ張ってきてくれる
深い深い配列の奥にあっても、Hashのパス記法を使えば、何重foreachを使わずとも。
引っ張ってくる個数と結果の配列は要素個数が同じで、キーは0始まり連番
foreachだとソースを読まないとそういう結果の予想は付かない
ラムダ関数の中は独自スコープ
外側と変数名がかぶることを気にしなくていい。外側の変数を使いたいときはfunction($item)use($outside_var)みたいな感じで

なんだかんだ、使ってると手になじんでくるものです。

Posted on

Hashはいいぞ Hash::combine編

去年からだいぶHash::なんとかを使い続けて、だいぶ手に馴染んできました。foreachのほうが分かりやすい場合は、素直にforeachを回したらいいんですが、select用のリストをfind結果の複数のカラムから作る、みたいなことにHash::combineを使えると非常に見通しが良いです。なんてったって「配列のこれをキーとして、これを値とする配列を作る」ですもの。

// Model->find('all') したときありがちな配列
$data = [
    o => [
        'Color'=>[
            'id'=>'960018',
            'name'=>'カーマイン',
            'red'=>'150',
            'green'=>'2',
            'blue'=>'24',
        ]
    ],
    1 => [
        'Color'=>[
            'id'=>'00896B',
            'name'=>'ビリジアン',
            'red'=>'0',
            'green'=>'136',
            'blue'=>'53',
        ]
    ],
    2 => [
        'Color'=>[
            'id'=>'434DA2',
            'name'=>'ウルトラマリン',
            'red'=>'67',
            'green'=>'77',
            'blue'=>'162',
        ]
    ]
];

// [id => 色名]
$result_1 = Hash::combine($data,'{n}.Color.id','{n}.Color.name');
/**
$result_1 = [
    '960018'=>'カーマイン',
    '00896B'=>'ビリジアン',
    '434DA2'=>'ウルトラマリン',
];
*/

// [id => 'Rスペース詰め3桁,Gスペース詰め3桁,Bスペース詰め3桁']
$result_2 = Hash::combine($data,'{n}.Color.id',['%3d,%3d,%3d','{n}.Color.red','{n}.Color.green','{n}.Color.blue']);
/**
$result_2 = [
    '960018'=>'150,  2, 24',
    '00896B'=>'  0,136, 53',
    '434DA2'=>' 67, 77,162',
];
※ 配列のキー側引数も配列でsprintf記法指定できる
*/

どっちも、フォームの選択肢を作るとき凄い役に立ちます。
パス記法に慣れると、いくつも組み合わせてprintfの指定をすれば配列のキーも値も自在に組めます。
もうforeachでぐるぐる検索結果を回しては文字列をこねくり回して…みたいなことはしなくていい!

単純に「これとこれとこれを取り出して配列を作りたい!」とき、使ってください。

Posted on

Hashはいいぞ

誰もが言っていました。配列をいじるならHashを使えと。

Hash::get()だけは結構使っていました。配列に存在しない引数を引いてもnotice出さずにnullを返してくれる。
それでも、勉強するのは面倒で、ついついforeachをぶん回していろいろやっていました。

この冬、ふとHash::insertを使おうと思い立ったんです。
「たったこれだけのためにforeach書くのめんどい」という状況が増えていたのです。

誰もが言っていた通り、Hashは良いものでした。

親子関係のあるテーブルを想像してください。
parentsテーブルがあって、childrenテーブルにparent_idを持っている、よくある parents has many childrenという関係です。
都合によりモデルにリレーションを固定していないので、毎回保存のたびに親子関係idをくっつけておく必要がある、という前提で…

//トランザクション中
if(!$this->Parent->save($parent_data)){
    $this->Parent->rollback();
    return false;
}
$parent_id=$this->Parent->getID();

//childrenにとにかくparent_idだけ付けなおす
foreach($children as $key=>$child){
    $children[$key]['parent_id']=$parent_id;
}
//(後略)

この、ID付与の為だけのforeachを見飽きたんです。
Hashを使えるとここが…

    //整数キー直下の配列全部に”parent_id”を追加する
    Hash::insert($children,'{n}.parent_id',$parent_id);

一行。何よりもやってることがすっきり見える。任意の整数とマッチする{n}という書き方が強い。
foreachが出ると読み手はループを身構えるから、「任意の~全部に操作」という話が見えにくい。

やっていることは同じなのに、配列的バッチ操作として書けるのは本当に助かる。
他にもHashには非常に強力な機能はあるけれど、勉強が追い付いていない…。
また今度、追加します。

Posted on

EAVをなんとかし隊

これは平成最後の月。
CakePHP2.x系。入力チェックが通らないたび、フォームの内容が編集前に戻ってしまう不具合を直したときの話。

単純にまず、DBがイケてなかったんです。


INT id
INT type
VARCHAR(255) value

どこからどう見てもEAV。Entity Attribute Value。
何故よくないかのDB設計理論的な難しい説明は置いとく。
DB構造はその時動かす権限がなかった。(もし権限があったらその根本原因から直してしまいたかった)

で、その不具合が発生している画面を見て。

・DBから持ってきたデータ構造
・フォームへ渡すときのデータ構造
・フォームの初期値を表示するとき

これらが全部バラバラで往生した…。

DBからとってきたときの配列:

[
    0=> ['Model'=> [id => 1, type => 1, value => 5]],
    1=> ['Model'=> [id => 2, type => 2, value => "someaddress@example.com"]],
    2=> ['Model'=> [id => 3, type => 2, value => "anotheraddress@example.com"]]
]

これを加工して,typeをキーにしてビューに渡すけれど:

[
    1=> [
        0=> ['id'=>1, 'value'=>5]
    ],
    2=> [
        0=> ['id'=>2, 'value"=>"someaddress@example.com"],
        1=> ['id'=>3, 'value'=>"anotheraddress@example.com"]
    ]
]

ビューでさらに取り出す:
(Formヘルパに’value’で渡している)

$this->Form->input('Model.type_1.value',['type'=>'text', 'value'=> $this->request->data[1][0]['value']);
$this->Form->input('Model.type_2.0.value,['type'=>'text', 'value'=>$this->request->data[2][0]['value']]);
$this->Form->input('Model.type_2.1.value,['type'=>'text', 'value'=>$this->request->data[2][1]['value']]);

(あと、idをそれぞれhiddenフォームで送っていたりする)

フォームを送信すると、入ってくるのは…

[
    'Model'=> [
        'type_1'=> ['id'=> 1, 'value'=>5],
        'type_2'=> [
            0=> ['id'=> 2, 'value'=> 'someaddress@example.com],
            1=> ['id'=> 3, 'value'=> 'anotheraddress@example.com]]
	]
    ]
]

当然、DBからデータを取ってきたときとも最初フォームに渡したときとも違う構造。
このままではフォームの次の値が取れないから、前のコードではリクエスト関係なくDBからその都度データ読み込んで来ていた。
エラー値ならエラー値のままフォームに残しておく仕様なのに…。

とりあえず、その時はビューに渡すときの構造と受け取る時のデータ構造をそろえた。
(そうでないと変換が双方向に必要になるんで)
そろえれば使えなくはない。
Formヘルパに渡すvalue要素も要らなくなるし、入力チェックが通らないときフォームの初期値を作るためだけにDBを読みに行かなくていい。

でも、それでもやっぱりEAVはイケてない…。

この構造の時、まず絶対に1つしかないパラメータをこう…ひとつテーブルにして

INT model_id
INT type1_val

で、複数登録する必要がありそうな情報だけテーブルを分けてこう…。

INT model_mail_id
VARCHAR type2_mail

そしたら、DBからとってきた構造をパースしてフォームに渡す必要って本来それほどないはずなんです…。

僕がDB設計するときには、後輩に同じような愚痴を語らせないためにも、
あと、DB設計理論以上にそもそもコーディングがめちゃくちゃ往生するので、できることならEAVは避けていきたいという話でした。

Posted on

Cakeのpaginatorのソートを押して複数条件のソートを行う方法

あるシステムで日報の1日の予定を登録して頂いていて、
日付単位とその中の詳細の予定の2段階でのソートをする必要がありました。
第一ソート条件は日付で第二ソート条件が詳細予定の時間というソートを実現させる実装方法です。

まずviewでpaginatorのソートの記述を書きます。
Paginator->sort(‘report_date’, ‘訪問日時’); ?>

次にコントローラーで次のように書きます。

if (empty($this->passedArgs['sort'])) {
    $order = array('Report.report_date' => 'desc', 'Report.staff_id' => 'asc', 'ReportDetail.report_date' => 'asc');
}elseif ($this->passedArgs['sort'] == 'report_date'){
    $order = array('Report.report_date' => $this->passedArgs['direction'], 'Report.staff_id' => 'asc', 'ReportDetail.report_date' => 'asc');
}
$this->ReportDetail->setOrder($order);

最後にモデルで次のように書きます。

// デフォルトのソート条件は何もなし
var $order = array();

function setOrder($order) {
	// controllerから渡された$orderを変数に持っておく
	$this->order = $order;
}

function beforeFind($queryData) {
	// 実行中のfindの種別が'count'以外だった場合のみ、
	// ソート条件を追加し、$orderを初期化する
	if ($this->findQueryType != 'count' ) {
		array_unshift($queryData['order'], $this->order);
		$this->order = array();
	}
	return $queryData;
}

これでCakeのpaginatorのソートを押して複数条件のソートが行えました。
作成するに当たり下記サイトを参考にさせて頂きました。
http://d.hatena.ne.jp/atcorp/20100213/p1

Posted on

PDF出力で時間がかかる場合に「処理中...」表示する

CAKEPHP2で、PDF作成時にその作成している間だけ、処理中...を表示するサンプルを記入します。
まずDBを追加します。

CREATE TABLE IF NOT EXISTS `sessions` (
  `session_id` text
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

custom.css

・
・
・
div#loading {
    position : absolute;
    top : 20%;
    left : 50%;
}
・
・
・

bootstrap.php

・
・
・
define('ROOT_URL', preg_replace('/index\.php/', '', env('SCRIPT_NAME')));
・
・
・

default.ctp

・
・
・
    
・
・
・


・
・
・


xxxxController.php

    function detail($id, $step){
	if (isset($this->request->data['out'])) {
            $info = PDF出力する情報を取得処理を記述する。
            if (!empty($info)) {
                $this->autoRender = false;
                $this->layout = false;
                $this->set('info', $info);
                $this->render('pdf_out');
            } else {
                $this->モデル名->invalidate('error', "データがありません。");
                $this->autoRender = true;
                $this->layout = 'default';
            }
        }	
    }

    /**
     * セッション保存
     *
     * @note
     */
    function ajax_session_save() {
        Configure::write('debug', 0);
        $this->autoRender = false;

        $id = $this->Session->id();
        $sql = "INSERT INTO sessions (`session_id`) VALUE (:id);";
        $sqlArr['id'] = $id;
        $jsonResult = array();
        if (false === $this->モデル名->query($sql,$sqlArr)) {
            $jsonResult['RES'] = -1;
        } else {
            $jsonResult['RES'] = 0;
        }
        echo json_encode($jsonResult);
        exit;
    }

    /**
     * セッション確認
     *
     * @note
     */
    function ajax_session_get() {
        Configure::write('debug', 0);
        $this->autoRender = false;

        $id = $this->Session->id();
        $sql = "select * from sessions WHERE session_id = :id;";
        $sqlArr['id'] = $id;

        $jsonResult = array();
        $res = $this->モデル名->query($sql,$sqlArr);
        if (!empty($res)) {
            $jsonResult['RES'] = 0;
        } else {
            $jsonResult['RES'] = -1;
        }
        echo json_encode($jsonResult);
        exit;
    }

    /**
     * render直後の処理
     *
     * @note
     */
    public function afterFilter() {
        $id = $this->Session->id();
        $sql = "delete from sessions WHERE session_id = :id;";
        $sqlArr['id'] = $id;
        $res = $this->モデル名->query($sql,$sqlArr);
    }

detail.ctp

・
・
・
    
・
・
・

PDF作成処理はこのブログでは触れませんが、
結論から言うとsession_idがDBに保存されている間は、PDF出力処理中になるようにします。
出力ボタンが押下された時点で、本当に実行してよいかを問い、実行してよい場合は、当該のsession_idをDBに追加します。
renderで呼ばれた処理が終了すると、PDF出力が終わったことになり、CAKEPHP2では、
afterFilter()が呼ばれます。そこで、DBから該当のsession_idを消します。
表示VIEWでは、session_idが存在する間、処理中...を表示としておき、
DBからsession_idが削除されれば、処理中...を消すことで完成となります。

Posted on

cronでシェル登録しているファイルをテストで叩く

cronで動かすプログラムをURLから実行して動作確認をする方法です。
cronを動かす処理を書いて、それを実行して動作検証する時にテスト環境でいちいちcronを動くようにして試すのは面倒くさいのでURLを叩いてcronの結果が正しいのかを見たいという時のメモです。
cronを動かすのはCakePHP2系でCronを動かすのようにしています。
後はコントローラーのアクションで以下のように記述してあげればURLを叩いてcronが実行されます。

class XXXXController extends AppController

    function xxxxxx()
    {
	    App::uses('AppShell','Console/Command');
        App::uses('XXXXShell', 'Console/Command');
        $shell = new XXXXShell();
        $shell->action_name();
    }
}
Posted on

CakePHP2系でCronを動かす

今までCakePHP1系ではCronを動かした事はあったのですが
CakePHP2系でCronを動かすのが初めてで動くようにするのに
つまづいたので備忘録として残しておきます。

シェルファイルを作るのはCakePHP1系と同じなのですが内容は次のようになります。
(ファイルへのパスは今回設置した環境用になっていますので適時ご自身の環境に合わせて下さい)
1系だと

#!/bin/sh
/var/www/cakephp/cake/console/cake cron action_name -app /var/www/cakephp/app

2系だと

#!/bin/sh
/usr/bin/php /var/www/cakephp/lib/Cake/Console/cake.php Cron action_name -app /var/www/cakephp/app

/usr/bin/phpがPHPの実行ファイルを指定
作ったシェルファイルをパーミッション755にしておきます。
/var/www/cakephp/lib/Cake/Console/cake.phpをパーミッション755にします。

次に実際の処理内容なのですが
1系の場合は/cakephp/app/vendors/shells/cron.phpに書いていたのを
2系の場合は/cakephp/app/Console/Command/にXXXXShell.phpというファイルを作ります。

内容は

Staff->find('all'); // コントローラーから呼ぶようにModelを呼べる
    }
}
?>
Posted on

Cake1.Xで複数ファイルをUPする

以前「Cake2で複数ファイルをUPする」でCake2系での複数ファイルを上げる方法でつまづいたのですが、今回Cake1系の改修で複数ファイルを上げる際に引っかかったのでメモでブログします。
Cake2系だと$this->Form->input(‘Model.field.’, array(‘type’ => ‘file’, ‘multiple’));で行けたのですが1系だとピリオドの後にスペースがいるとの事でした。
というわけで$this->Form->input(‘Model.field. ’, array(‘type’ => ‘file’, ‘multiple’));と書いたら解決!!