Zend_Filter_Input で重複のチェック

unique なカラムのチェックをバリデート時に行いたい。

Table scheme

CREATE TABLE `page` (
  `id`      int PRIMARY KEY,
  `user_id` int,
  `url`     varchar(255),
  UNIQUE (`user_id`, `url`)
);

My/Validate/Unique.php

<?php

class My_Validate_Unique extends Zend_Validate_Abstract
{

    const DUPLICATE = 'duplicate';

    protected $_messageTemplates = array(
        self::DUPLICATE => "already exists"
    );
    protected $_messageVariables = array();


    private $table;

    public function __construct($table) {
        $this->table = $table;
    }

    /**
     * @param $fields array  field の連想配列。
     *                       primary key, 重複チェック対象 field ...
     */
    public function isValid($fields) {
        $table = new $this->table;

        $select = $table->select();
        reset($fields);
        $select = $this->addWhere($select, key($fields), current($fields), false);  // 自身は除外
        array_shift($fields);
        foreach ($fields as $field => $value) {
            $select = $this->addWhere($select, $field, $value);     // 重複チェック対象
            $this->_messageVariables[$field] = $field;
            $this->$field = $value;
        }

        if ($row = $table->fetchRow($select)) {
            $this->_error(self::DUPLICATE);
            return false;
        }

        return true;
    }

    private function addWhere($select, $field, $value,  $match = true) {
        if (!is_null($value))
            $select = $select->where($field . ($match ? ' = ?' : ' != ?'), $value);
        return $select;
    }

}

My/Filter/Input.php

<?php

class My_Filter_Input extends Zend_Filter_Input
{

    public function __construct($filterRules, $validatorRules, array $data = null, array $options = null) {
        parent::__construct($filterRules, $validatorRules, $data, $options);
        $this->addValidatorPrefixPath('My_Validate', 'My/Validate');
        $this->setDefaultEscapeFilter(new My_Filter_Ident());   // DB にいれるのでエスケープはしない
    }

    public function getMessages() {
        // <field>-n は <field> とみなす
        // (fields 指定ありなしを混在させるために)
        $messages = parent::getMessages();
        foreach ($messages as $field => $strs) {
            if (preg_match('/^(.*)-\d+$/', $field, $matches)) {
                $originField = $matches[1];
                if (array_key_exists($originField, $messages))
                    $strs = array_merge($message[$originField], $strs);
                $messages[$originField] = $strs;
                unset($messages[$field]);

            }
        }
        return $messages;
    }

}

// 何もしない filter
class My_Filter_Ident implements Zend_Filter_Interface
{
    public function filter($value) { return $value; }
}

以上のものを使う。
Filter_Input 設定ファイルは ini だと表現力が足りないし XML だと面倒なので http://wadslab.net/2008/05/zend_config-3/ を使って YAML で。
conf/page_filter.yml

filters:
  *: StringTrim

validators:
  url:
    -
      - StringLength
      - 1
      - 255
  # url のエイリアス
  # fields なしの validator も利用するためにやむなく
  url-2:
    -
      - Unique
      - Page
    - fields:
      - id
      - user_id
      - url
    - messages:
      - URL "%url%" は既に使用されています。

ActionController でみせているけど rails みたいにモデルでやるほうが楽でいい気がする。

<?php

class PageController extends Zend_Controller_Action
{

   const FILTER_INPUT_CONFIG = 'page_filter.yml';

   private function getFilterInput() {
       $config = new My_Config_Yaml(APPLICATION_ROOT . '/conf/' . self::FILTER_INPUT_CONFIG);
       return new My_Filter_Input($config->filters->toArray(), $config->validators->toArray());
   }

    public function createAction()
    {
       $req = $this->getRequest();
       $params = $req->getPost();
       unset($params['id']); // id をセットされるとチェックを抜けられる
       $params['user_id'] = $req->getParam('id');  // どこかから

       $input = $this->getFilterInput();
       $input->setData($params);
       if ($input->isValid()) {
          ...

うーん。Zend_Form だともっと簡単にできたりするんだろうか。あれもなんかいまいちな雰囲気が。Zend Framework って基本の構成はできてるけど使いやすくするための部分がまだまだという感じがしますねぇ。ライブラリーの疎結合を謳っているのでそこが阻害要因になっているというのもあるかもしれません。