‘PHP’ カテゴリーのアーカイブ

sfWidgetFormInputCheckboxの正負を反転させる その2

前回のエントリで、モデルを拡張して読み出し時と保存時に正負反転させる方法を紹介しましたが、カラム名と実際の動作が逆になってしまって紛らわしいので、widgetとvalidatorで反転させることにしました。Symfony 1.4+Doctrineです。

既存のsfWidgetFormInputCheckboxとsfValidatorBooleanを継承して実装します。

lib/widget/sfWidgetFormInputCheckboxInverse.class.php

<?php

class sfWidgetFormInputCheckboxInverse extends sfWidgetFormInputCheckbox
{
    public function render($name, $value = null, $attributes = array(), $errors = array())
    {
        
        if (!(null !== $value && $value !== false))
        {
            $attributes['checked'] = 'checked';
        }

        if (!isset($attributes['value']) && null !== $this->getOption('value_attribute_value'))
        {
            $attributes['value'] = $this->getOption('value_attribute_value');
        }

        return parent::render($name, null, $attributes, $errors);
    }
}

lib/validator/sfValidatorBooleanInverse.class.php

<?php

class sfValidatorBooleanInverse extends sfValidatorBoolean
{

    protected function configure($options = array(), $messages = array())
    {
        parent::configure($options, $messages);
        $this->setOption('empty_value', true);
    }

    protected function doClean($value)
    {
        if (in_array($value, $this->getOption('true_values')))
        {
          return false;
        }

        if (in_array($value, $this->getOption('false_values')))
        {
          return true;
        }

        throw new sfValidatorError($this, 'invalid', array('value' => $value));
    }

}

ほぼ、逆にしたメソッドでオーバーライドしただけです。チェックボックスにチェックを入れなかった場合の挙動がデフォルトではfalseなので、trueに変更しています。

これをformクラスから

        $this->widgetSchema['show_flag'] = new sfWidgetFormInputCheckboxInverse();
        $this->validatorSchema['show_flag'] = new sfValidatorBooleanInverse();

のように定義してやればいいだけです。

例によってカラムのタイプがbooleanになっていることを確認してください。なっていないと0でも1でもtrueと判断されてしまうので。

        $this->hasColumn('show_flag', 'boolean', null, array(
             'type' => 'boolean',
             'notnull' => true,
             'default' => 0,
             ));


sfWidgetFormInputCheckboxの正負を反転させる その1

symfony 1.4にて。データベースの定義は「表示」フラグなのに、フォームでは「非表示」時にチェックを入れさせたい場合などがありまして。

データベースのカラム自体の意味を変えるのは、逆にフォームの意味が今後変わったときにどうするんだという話なので、今回はモデルでの入出力時点で値を変更する方法を試してみました。

actAsで各モデルの好きなカラムを反転できる拡張にしてみました。

lib/InvertBoolean.class.php

<?php

class InvertBoolean extends Doctrine_Template
{
    protected $_options = array(
        'columns' => array()
    );
    
    public function setTableDefinition()
    {
        $this->addListener(new InvertBooleanListener($this->_options));
    }
    
    public function setUp()
    {
    }
}

lib/model/invert_boolean/InvertBooleanListener.class.php

<?php

class InvertBooleanListener extends Doctrine_Record_Listener
{
    protected $_options;
    
    public function __construct(array $options)
    {
        $this->_options = $options;
    }

    public function postHydrate(Doctrine_Event $event)
    {
        $obj = $event->data;
        foreach ($this->_options['columns'] as $column) {
            $obj->$column = !($obj->$column);
        }
        $event->set('data', $obj);
    }

    public function preSave(Doctrine_Event $event)
    {
        foreach ($this->_options['columns'] as $column) {
            $value = !($event->getInvoker()->get($column));
            $event->getInvoker()->set($column, $value);
        }
    }
}

postHydrateでデータをデータベースから読み出してきてハイドレートした後に、preSaveで保存する前にそれぞれデータを反転しています。

カラムのタイプはあらかじめbooleanにしておく必要があります。schema.ymlからデータベース生成した場合は問題ないかと思います。そうでないなら、モデルのsetUpメソッドにでも

        $this->hasColumn('show_flag', 'boolean', null, array(
             'type' => 'boolean',
             'notnull' => true,
             'default' => 0,
             ));

など。

これを使用できるようにするために、同じくモデルのsetUpメソッドに

$this->actAs('InvertBoolean', array('columns'=>array('show_flag')));

とすればOKです。actAsのcolumnsオプションに複数カラム名設定すれば複数カラムを一括で指定できます。

これで、sfWidgetFormInputCheckboxを使ったチェックボックスにチェックを入れたときにデータベースに0が保存され、チェックを外すと1が保存されるカラムができあがりました。

問題点として、カラム名と反対の動きをすることになるので、うっかりする可能性が高まります。

なので、動作は確認しましたが、結局この案は使ってません。次のエントリーで紹介するカスタムウィジェットの方法を使っています。


admin generatorの編集後のリダイレクト先変更

admin generatorのeditアクションで、編集後に再度編集画面ではなく、リストに飛ばしたい要件があったのでメモ。例によってSymfony 1.4+Doctrine+admin generator。

class hogeActions extends autoHogeActions
{
    public function executeUpdate(sfWebRequest $request)
    {
        $this->forward404Unless($request->isMethod('put'));
        
        $this->hoge = $this->getRoute()->getObject();
        $this->form = $this->configuration->getForm($this->hoge);
        
        if ($this->form->bindAndSave($request->getParameter($this->form->getName()), $request->getFiles($this->form->getName()))) {
            $this->redirect('hoge');
        }
        
        $this->setTemplate('edit');
    }
}

executeUpdateをオーバーライドすればOKのようです。ここではisMethodがputなことに注意。


Symfonyでのsave時に独自の処理を挟む

Symfony 1.4+Doctrineで、admin generatorな環境です。editなど、自動生成なので、どこをオーバーライドすれば目的の動作になるのかわかりにくいことがありますね。

例えば、あるモデルのステータスを変更すると同時に、他のモデルに対しても操作したい場合があるとします。

まぁ、mergeFormとかembedFormとか使ってもいいのですが、今回は他テーブルのプライマリキーをフォームから選択させたかったので、汎用性のあるdoSaveでの処理を追加してみました。

symfony Forms in Action | 第11章 – Doctrine との統合 | symfony | Web PHP Framework

class BackendHogeForm extends HogeForm
{
    public function configure()
    {
        parent::configure();

        // 使用するフィールドを指定
        $this->useFields(array('status'));
        
        // 別モデルのIDリストを選択するフォームを作成する
        $choices = array(100 => '100:value1', 101 => '101:value2');
        $this->widgetSchema['other_model_id'] = new sfWidgetFormSelect(array('choices' => $choices));
        $this->validatorSchema['other_model_id'] = new sfValidatorChoice(array('choices' => array_keys($choices)));
    }

    protected function doSave($con = null)
    {
        if ($this->getValue('status') == 'DONE') {
            Doctrine::getTable('OtherModel')->find($this->getValue('other_model_id'))
                ->setIsSelected(TRUE)
                ->save();
        }
        return parent::doSave($con);
    }
}

基本的にはdoSaveをオーバーライドするだけです。ちなみにuseFieldsメソッドを使うと必要なフィールド以外を全部unsetしてくれるので便利です。


PHPのDateTimeの結果が-0001-11-30 00:00:00になる現象について

mySQLなどで、日時を0000-00-00 00:00:00としてデータベースに格納しておくことがありますが、これを読み込んでそのままPHPのDateTimeオブジェクトに渡すと、出力が-0001-11-30 00:00:00になってしまいます。

$a = new DateTime('0000-00-00 00:00:00');
echo $a->format('Y-m-d H:i:s');
// Output: -0001-11-30 00:00:00

PHP :: Bug #42971 :: DataTime::format(): not well formated data ‘0000-00-00 00:00:00’

で、これはバグではないと言われているので、どういうことかと考えてみると、0000-00-00は存在しない0月0日を指定しているので、0月は繰り下がって-1年12月0日、さらに0日も繰り下がって-1年11月30日、となるわけですね。

データベースとPHPの文化の違い、というところでしょうか。

ちなみにDateTime型、コンストラクタにNULLを渡すと現在時刻のインスタンスが生成されるので、データベースの値をNULLにしておくと、現在時刻になってしまいます。うーん…symfonyのDoctrineでNULL判定したい場合はどうすればいいんだ…

$row->getDateTimeObject('deleted_at')

みたいなことがやりたいのですが。

sfDoctrineRecordも

    $type = $this->getTable()->getTypeOf($dateFieldName);
    if ($type == 'date' || $type == 'timestamp')
    {
      return new DateTime($this->get($dateFieldName));
    }

こうなってるからオーバーライドするしかないのかな。


SymfonyのAdmin generatorでリストにフィルタを表示させない方法

Admin generatorを使っていて、リスト表示をするときにフィルタが不要な場合があります。例えば行が少ないとか、すでに決まったフィルタが別にある場合など。フィルタのフォーム自体を消したい場合は以下の方法で。

apps/backend/modules/MODULE_NAME/templates/

_filters.php
を空で作成すればいいだけです。

admin generatorでの開発とはキャッシュを読む作業と見つけたり…

追記

コメントで教えていただいた方法で、もっと簡単に実現できました。filterのclassをfalseにするだけでOKです。

generator:
  class: sfDoctrineGenerator
  param:
    config:
      filter:
        class: false


Symfonyでメール送信のデバッグをする

Symfony1.4で、メールを送信するコードで、開発中にテンプレートの動作確認をしたい場合があります。Swift Mailerを使っているなら、すべての送信メールのあて先を上書きして特定のメールアドレスにすることができるので、やり方メモ。

The More with symfony book | メール | symfony | Web PHP Framework

設定の仕方は簡単で、アプリケーションのconfig\factories.ymlにdelivery_strategy: single_addressを指定すればOK。同時にdelivery_addressに、あて先メールアドレスを記述します。

dev:
  mailer:
    param:
      delivery_strategy: single_address
      delivery_address: dev@localhost

これで開発環境のローカルユーザdevにメールが送られます。

元々のヘッダ指定 toccbcc のどれに送られたメールなのかを確認できるよう、それぞれ X-Swift-ToX-Swift-CcX-Swift-Bcc というヘッダが追加されます。

あとはローカルのメールをMewとかで読めばOKです。

もちろんdelivery_addressを外部のメールアドレスに指定してもいいですよ。

PayPalの開発環境みたいにメールのSandBoxとか統合されないかなぁ。


複数の入力欄にまたがるValidationのエラーを特定の入力欄に表示させたい場合

sfFormでSymfony 1.4のお話。

PostValidatorのエラーメッセージをどこに出せばいいんだ?という状況で使用できます。

class SomethingInputForm extends BaseForm
{
    public function configure()
    {
        // ...
        $this->validatorSchema->setPostValidator(
            new sfValidatorCallback(array('callback' => array($this, 'myCallbackFunc')))
        );
    }

    public function myCallbackFunc($validator, $values) {
        // 何かバリデーション
        if (!($values['input1'] == $values['input2'] == $values['input3'])) {
            $error = new sfValidatorError($validator, 'error message ...');
            throw new sfValidatorErrorSchema($validator, array('input1' => $error)); 
        }
        
        return $values;
    }
}

こんな感じで、input1のエラーとして出力できるので、テンプレートのエラー表示が楽になります。


Doctrine_Collectionをループする

Symfony 1.4+Doctrine
やたら不便(だと個人的には思っている)なDoctrineの、SELECTに関するメモ。

やりたかったことは、ある条件で抽出した複数行に1行ずつ処理を加えて書き戻すというフロー。
fetchArray()とかでは配列しか返ってこないので意味がなく、fetchOne()では先頭行しか返ってこない。
fetchOne()を複数回実行すればいいのかと思ったら無限ループに陥った。

findByではorderByをするのにフックを使わないといけないらしいのでパス。

一応、以下の方法で解決しました。

$collection = Doctrine_Query::create()
    ->select('u.*')
    ->from('Users u')
    ->where('u.flag = ?', '1')
    ->orderBy('u.id')
    ->execute();

var_dump(get_class($collection)); // Doctrine_Collection

$iter = $collection->getIterator();
var_dump(get_class($iter)); // ArrayIterator

while ($record = $iter->current()) {
    var_dump(get_class($record)); // Users
    var_dump($record->getId());
    $iter->next();
}

next()呼ばないといけないのは面倒だなと思ったら、foreachで使えたらしい。

$collection = Doctrine_Query::create()
    ->select('u.*')
    ->from('Users u')
    ->where('u.flag = ?', '1')
    ->orderBy('u.id')
    ->execute();

var_dump(get_class($collection)); // Doctrine_Collection

$iter = $collection->getIterator();
var_dump(get_class($iter)); // ArrayIterator

foreach ($iter as $record) {
    var_dump(get_class($record)); // Users
    var_dump($record->getId());
}

これでかなりマシになった。

最終的にはこれで。

$collection = Doctrine_Query::create()
    ->select('u.*')
    ->from('Users u')
    ->where('u.flag = ?', '1')
    ->orderBy('u.id')
    ->execute();

var_dump(get_class($collection)); // Doctrine_Collection

foreach ($collection->getIterator() as $record) {
    var_dump(get_class($record)); // Users
    var_dump($record->getId());
}

あとはループ内で$record[‘data’] = ‘hoge’;$record->save();とかしておけばOK。

追記:2010/03/30

全然違った。もっと簡単にできました。getIterator不要でした。

foreach ($collection as $record) {
    $record['data'] = 'hoge';
    $record->save();
}

慣れてくると意外と便利な気がしなくもないDoctrine。結合系と特に。


CakePHPのPaginator逆ルーティング設定をビューで行う

CakePHP 1.2で、/hoge/Controller/page:1とかにしたページングがしたかったので調べた。

超参考になったのはこちらの記事。スライドの26~27ページあたり。逆ルーティング。

極める routes.php (CakePHP 1.2) : akiyan.com

routes.phpでゴチャゴチャにしたURLに対してもPaginatorにOptionsを渡してやればできるという話だったので実験。

Controllerのvar $paginateに書いてみたのだけどなぜか効かない…

色々試した末、viewに書くことで解決。

<?php
$paginator->options(array('url'=>array(
    'controller' => 'pages',
    'action' => 'display',
    'target' => 'hoge',
)));
?>

routesはこれ。

Router::connect('/hoge/pages/*', array('controller' => 'pages', 'action' => 'display', 'target' => 'hoge'));

これはこれで使い勝手いいかもしれんね。