Tetsujin’s blog

http://d.hatena.ne.jp/Tetsujin

MySQLのCLIのプロンプトをカスタマイズするzshのfunctionを書いた

いままで、mysqlのプロンプトの変更は $HOME/.my.cnf で、

[mysql]
prompt="^[[01;31m\U[\d]-[\D]^[[0m\nmysql> "

みたいにしていましたが、

  • 接続元のサーバ情報が出せない。(出し方を知らないだけかも)
  • 本番と開発環境など、同一の設定ファイルで環境に応じてプロンプトを柔軟に変更できない。
  • 色をつけるにはエスケープシーケンスを直書きしないといけない。

といった点が、ちょっと不満でした。

なのでコマンドオプションをパースして、環境変数$MYSQL_PS1を組み立ててから
mysqlコマンドを実行する、zshのfunctionを作ってみました。

https://github.com/tetsujin/zsh-function-mysql

ちょっとわかりにくいですが、下記の例では

  • 接続元のサーバ情報を表示、ホスト名によって色分け。
  • 接続先のMySQLサーバのユーザ名、ホスト名で色分け。

をしています。

https://raw.github.com/tetsujin/zsh-function-mysql/master/doc/img/sample1.png
https://raw.github.com/tetsujin/zsh-function-mysql/master/doc/img/sample2.png

インストール

zshが読み込める場所にfunctionを追加します。
この例ではホームディレクトリに追加します。

$ cd /path/to
$ git clone git://github.com/tetsujin/zsh-function-mysql.git
$ mkdir -p ~/.zsh.d/functions
$ ln -s /path/to/zsh-function-mysql/mysql ~/.zsh.d/functions

$HOME/.zshrcへ設定を追記

インストールしたfunctionと、色設定で使うcolorsをautoloadします。

typeset -U fpath
fpath=(
    $HOME/.zsh.d/*(/N)
    $fpath
)
autoload -U colors; colors
autoload -U $(echo ~/.zsh.d/functions/*(:t))

下記のように設定を追記します。

# MySQLの接続元ホストのユーザ名($USER)によって色分けします。(自分は使ってません)
typeset -A mysql_prompt_style_client_user
mysql_prompt_style_client_user=(
    # 'root'     $fg_bold[red]
    # '*'        $fg_bold[green]
)
# MySQLの接続元ホスト名($HOST)によって色分けします。
# - .local.を含むローカル環境は緑
# - .dev.を含む開発環境では黄色
# - その他は赤
# としています。
typeset -A mysql_prompt_style_client_host
mysql_prompt_style_client_host=(
    '*.local.*'     "$fg_bold[green]"
    '*.dev.*'       "$fg_bold[yellow]"
    '*'             "$fg_bold[red]"
)
# MySQLの接続先ホストのユーザ名(-uオプション)によって色分けします。
# - rootは赤
# - その他は青
# としています。
typeset -A mysql_prompt_style_server_user
mysql_prompt_style_server_user=(
    'root'          "$bg_bold[red]$fg_bold[yellow]"
    '*'             "$fg_bold[blue]"
)
# MySQLの接続先ホスト名(-hオプション)によって色分けします。
# - masterを含む場合は赤
# - slaveを含む場合は黄色
# - その他は青色
# としています。
typeset -A mysql_prompt_style_server_host
mysql_prompt_style_server_host=(
    '*master*'      "$bg_bold[red]$fg_bold[yellow]"  # Master Server
    '*slave*'       "$bg[yellow]$fg[black]" # Slvae Server
    '*'             "$fg_bold[blue]"
)

# プロンプトを組み立てます。
# [注意] functionの実行直前に変数を評価するのでシングルクォートで定義してください。
mysql_prompt='${style_client_host}${USER}@${HOST}${fg_bold[white]} -> '
mysql_prompt=$mysql_prompt'${style_server_user}\u${reset_color}${fg_bold[white]}@${style_server_host}\h${reset_color}${fg_bold[white]}:${fg[magenta]}\d ${fg_bold[white]}\v\n'
mysql_prompt=$mysql_prompt'${fg_bold[white]}${bg_level}mysql${reset_color}> '


iTerm2とTerminal.appで軽く動作確認しただけなので、正しく表示できない環境もあるかもです。
視覚的にわかりやすくすることで、少しでもオペレーションミスが減れば嬉しいですね。


CoffeeScript + Guard::CoffeeScript + Emacs + Flymakeで「Errno::ENOENT: No such file or directory」が出ないように

最近、GuardでTitanium+CoffeeScriptの開発を快適に – ひげろぐを参考にGuard::CoffeeScriptを入れ、coffeescriptでflymakeする - Kentaro Kuribayashi's blogのFlymakeの設定を入れてCoffeeScriptでTitaniumでのアプリ作成を試してます。

Guard::CoffeeScriptでファイル変更の監視・コンパイルをしてると

ERROR: Guard::CoffeeScript failed to achieve its <run_on_change>, exception was:
Errno::ENOENT: No such file or directory - coffee/app_flymake.coffee

というエラーが発生して頻繁にGuardが落ちる。

Flymake用の一時ファイルをGuardが検知し、コンパイルしようとするが、すぐに削除されるのでエラーになっているようだ。

そのまま参考になるruby-modeのflymakeでguardが誤動作しないようにする - むしゃくしゃしてやったを参考に、 flymake-create-temp-inplace を flymake-create-temp-with-folder-structure に変更し、同一ディレクトリ内でなく、tempディレクトリにflymake用のファイルを作成するようにした。

以下、diff

(setq flymake-coffeescript-err-line-patterns
  '(("\\(Error: In \\([^,]+\\), .+ on line \\([0-9]+\\).*\\)" 2 3 nil 1)))

(defconst flymake-allowed-coffeescript-file-name-masks
  '(("\\.coffee$" flymake-coffeescript-init)))

(defun flymake-coffeescript-init ()
  (let* ((temp-file (flymake-init-create-temp-buffer-copy
+                     'flymake-create-temp-with-folder-structure))
-                     'flymake-create-temp-inplace))
         (local-file (file-relative-name
                      temp-file
                      (file-name-directory buffer-file-name))))
    (list "coffee" (list local-file))))

(defun flymake-coffeescript-load ()
  (interactive)
  (defadvice flymake-post-syntax-check (before flymake-force-check-was-interrupted)
    (setq flymake-check-was-interrupted t))
  (ad-activate 'flymake-post-syntax-check)
  (setq flymake-allowed-file-name-masks
        (append flymake-allowed-file-name-masks
                flymake-allowed-coffeescript-file-name-masks))
  (setq flymake-err-line-patterns flymake-coffeescript-err-line-patterns)
  (flymake-mode t))

(add-hook 'coffee-mode-hook 'flymake-coffeescript-load)

メモ

例えば/Users/foo/local/app/titanium/example/coffee/app.coffeeでエラーがある場合、エラー表示は以下のようになる。

[3] Error: In ../../../../../../../private/var/folders/OQ/OQ*************************/-Tmp-/Users/foo/local/app/titanium/example/coffee/app.coffee, Parse error on line 3: Unexpected 'TERMINATOR'

この/private/var/folders/..というのは何だろう?と見てみると、自分の環境では環境変数TMPDIR, TMP, TEMPを見ているようでmacではこうなってるみたい。

;; flymake.el -> flymake-create-temp-with-folder-structure
(defun flymake-create-temp-with-folder-structure (file-name prefix)
  (unless (stringp file-name)
    (error "Invalid file-name"))

  (let* ((dir       (file-name-directory file-name))
         ;; Not sure what this slash-pos is all about, but I guess it's just
         ;; trying to remove the leading / of absolute file names.
	 (slash-pos (string-match "/" dir))
	 (temp-dir  (expand-file-name (substring dir (1+ slash-pos))
                                      (flymake-get-temp-dir))))

    (file-truename (expand-file-name (file-name-nondirectory file-name)
                                     temp-dir))))

;; flymake.el -> flymake-get-temp-dir
(defalias 'flymake-get-temp-dir
  (if (fboundp 'temp-directory)
      'temp-directory
    (lambda () temporary-file-directory)))

;; files.el -> temporary-file-directory
(defcustom temporary-file-directory
  (file-name-as-directory
   (cond ((memq system-type '(ms-dos windows-nt))
	  (or (getenv "TEMP") (getenv "TMPDIR") (getenv "TMP") "c:/temp"))
	 (t
	  (or (getenv "TMPDIR") (getenv "TMP") (getenv "TEMP") "/tmp"))))
  "The directory for writing temporary files."
  :group 'files
  :initialize 'custom-initialize-delay
  :type 'directory)

Emacsのphp-modeでalignする その2

4年越しですが Emacsのphp-modeでalignする - てつじんにっき で書いていたものの挙動が気になったので修正し、requireして使うよう改良してphp-align.elという名称でgithubにあげてみました。
GitHub - tetsujin/emacs-php-align: Emacs's alignment configuration for PHP.


話が変わりますが最近php-modeは https://github.com/rradonic/php-mode を使っています。オリジナルに比べて色々と機能追加やバグ修正されているようです。*1

と、していたら https://github.com/ejmr/php-mode の方がEmacs同梱になるというのをブコメで知りました。


b:id:tomoya emacs, php
PHP5.4 対応版。php-mode-version-number は 1.5.1 になってる。と思ったら、あっという間に 1.6 へ。そして Emacs 同梱へ http://comments.gmane.org/gmane.emacs.devel/142507 2011/07/26

こちらも5.4対応など色々と機能追加されているようです。
乗り換えようかな。

*1:5.4のtraitも簡単なパッチを送ったらマージしてくれました

anything-project.elでSymfony2

anything-project.elにSymfony2用のプロジェクトを定義してみた。

(ap:add-project
 :name 'symfony2
 :look-for 'ap:symfony2-root-detector
 :grep-extensions '("\\.php" "\\.twig" "\\.yml")
 :exclude-regexp  '("/cache/" "/logs/"))
(defun ap:symfony2-root-detector (files)
  (ap:all-files-exist '("app" "bin" "src" "vendor" "web") files))

使ってみて、M-x anything-project-grepした時にcacheディレクトリなどが対象になって少し困ったので、ap:add-project-exという関数の追加とap:build-grep-commandを上書きして:grep-ignore-dirsというキーワード引数で除外するディレクトリを定義できるようにした。*1

最終的に以下のような感じ。

(require 'anything-project)

(defun* ap:add-project-ex (&key name look-for (include-regexp ".*") (exclude-regexp nil) (exclude-directory-regexp nil) (grep-extensions nil) (grep-ignore-dirs nil))
  (ap:add-project
   :name name
   :look-for look-for
   :include-regexp include-regexp
   :exclude-regexp exclude-regexp
   :exclude-directory-regexp exclude-directory-regexp
   :grep-extensions grep-extensions)
  (nconc (cdr (assq name ap:projects))
         (list (cons :grep-ignore-dirs grep-ignore-dirs))))

(defun ap:get-grep-extra-options (key)
  (let ((grep-ignore-dirs (ap:get-project-data key :grep-ignore-dirs)))
    (mapconcat 'identity (mapcar (lambda (dir)
                                   (concat "--ignore-dir=" dir))
                                 grep-ignore-dirs) " ")))

(defun ap:build-grep-command (key)
  (let ((grep-extensions (ap:get-grep-extensions key))
        (ack-command (ap:get-ack-command))
        (grep-extra-options (ap:get-grep-extra-options key))
        (xargs-command (ap:get-xargs-command))
        (egrep-command (ap:get-egrep-command)))
    (concat
     ack-command " -afG " grep-extensions
     " "
     grep-extra-options
     " | "
     xargs-command
     " "
     egrep-command " -Hin "
     "%s")))

;; PHP Symfony2
(ap:add-project-ex
 :name 'symfony2
 :look-for 'ap:symfony2-root-detector
 :grep-extensions '("\\.php" "\\.twig" "\\.yml")
 :grep-ignore-dirs '("cache" "logs")    ; これを新たに追加
 :exclude-regexp  '("/cache/" "/logs/"))
(defun ap:symfony2-root-detector (files)
  (ap:all-files-exist '("app" "bin" "src" "vendor" "web") files))


そしてsymfony2.elが欲しくなってきた。

*1:emacs lisp知識不足でdefunしたけどdefadviceで出来るものなのかな?

PHPer.jpでSymfony2のblogチュートリアルを動かすためにやったこと

http://phper.jp/http://docs.symfony.gr.jp/sf2-blog-tutorial/を動してみたので、雑なまとめですが、やった設定の備忘録。

最終的に下記のような構成に。

/path/to/approot/
 -  .phper/        # phper.jp用設定ファイル群
 -  Symfony/       # blogチュートリアルアプリケーション
 -  phpMyAdmin/    # 管理用phpMyAdmin

1. phper.jpに登録

http://phper.jp/quickstartを見てユーザ登録〜デプロイまで進める。
ドキュメントルートは Symfony/web にしておく。

2. phperコマンドのインストール

Ruby製のphperというCLIツールが用意されているので、http://blog.phper.jp/2011/03/07/94/を参考にRuby環境整えて

$ gem install phper

で、phperコマンドをインストール。

3. blogチュートリアルの作成

ローカルでblogチュートリアルを読み進めながら、/path/to/approot/Symfony下に作成。

4. phper.jp用に設定の修正

Symfony/web/app_dev.phpSymfony/web/config.phpのアクセス制限変更

http://blog.phper.jp/2011/03/05/28/の構成を見るとフロントにproxyが居るので、
$_SERVER['REMOTE_ADDR']だけじゃなくて、$_SERVER['HTTP_X_FORWARDED_FOR']も見てやるようにする。

# /path/to/approot/Symfony/web/app_dev.php
# /path/to/approot/Symfony/web/config.php
<?php
...
$remoteIpAddresses = array_merge(
    (array)@$_SERVER['REMOTE_ADDR'],
    array_map('trim', explode(',', @$_SERVER['HTTP_X_FORWARDED_FOR']))
);
$allowIpAddresses  = array(
    '127.0.0.1',
    '::1',
    'your-global-ip-address', // 自分の接続元グローバルIP
);
if (count(array_intersect($remoteIpAddresses, $allowIpAddresses)) === 0) {
    header('HTTP/1.0 403 Forbidden');
    die('You are not allowed to access this file. Check '.basename(__FILE__).' for more information.');
}
...
■ .gitignore

下記をgitの管理対象外にする。

# /path/to/approot/Symfony/.gitignore
*/logs/*
*/cache/*
app/config/parameters.ini

parameters.iniはローカル、phper.jp上で異なるのでgit管理対象外として、下記のようにした。

# /path/to/approot/Symfony/app/config/parameters.phper.ini
[parameters]
    database_driver   = pdo_mysql
    database_host     = db.phper.jp
    database_name     = yourname_appname
    database_user     = yourname
    database_password = password

mysqlの接続情報などは、phper infoで確認できる。

$ phper info
yourname-appname
--> gitosis@git.phper.jp:yourname/appname.git
--> mysql://yourname:password@db.phper.jp/yourname_appname

5. phpMyAdminを用意する

MySQLコマンドラインクライアントが使えないのでphpMyAdminを入れる。
http://www.phpmyadmin.net/home_page/downloads.phpからDownloadして/path/to/approot/phpMyAdminに設置する。


また、できればphpMyAdminにはhttpsでアクセスしたい。
http://blog.phper.jp/2011/03/05/28/を見ると、phper.jpではエッジレイヤのNginxがSSLアクセラレーションするようなので、
SSL-Proxy下でphpMyAdminSSLで動かす設定も入れておいた。


config.sample.inc.phpをコピーしてconfig.inc.phpを作成。下記を設定。

# /path/to/approot/phpMyAdmin/config.inc.php
<?php
..
// 書き換え
$cfg['Servers'][$i]['host'] = 'db.phper.jp';
// SSL-Proxy下でSSLで動かす設定。 http://wiki.phpmyadmin.net/pma/Config#PmaAbsoluteUri
$cfg['PmaAbsoluteUri'] = 'https://yourappname.phper.jp/pma/';

6. phper設定

プロジェクト直下の.phper/ディレクトリにいろいろとスクリプトや設定を記述できる。
詳しくはマニュアル参照。http://phper.jp/manual

■ deployの設定: /path/to/approot/.phper/deploy
#!/bin/sh
ROOT_DIR=$(cd $(dirname $0)/..; pwd)
# キャッシュの削除
rm -rf $ROOT_DIR/Symfony/app/cache/*
# parameters.iniのシンボリックリンク作成
ln -sf $ROOT_DIR/Symfony/app/config/parameters.phper.ini $ROOT_DIR/Symfony/app/config/parameters.ini
rsync除外設定: /path/to/approot/.phper/rsync_exclude.txt
Symfony/app/cache/*
Symfony/app/logs/*
アプリケーションサーバApacheの設定: /path/to/approot/.phper/httpd.conf

/pmaでphpMyAdminにアクセス。後、Digest認証も一応かけておく。

Alias /pma "/var/www/sites/yourname-appname/phpMyAdmin"
<Location pma>
    AuthType Digest
    AuthName "Secret Zone"
    AuthDigestDomain pma
    AuthDigestProvider file
    AuthUserFile  "/var/www/sites/yourname-appname/.phper/.htdigest"	
    Require valid-user
</Location>
$ htdigest -c /path/to/approot/.phper/.htdigest "Secret Zone" user
New password: ****
Re-type new password: ****

7. デプロイする

$ git push

8. phper.jp上のデータベースにschema定義を流しこむ

blogチュートリアルのschema定義をdumpして、phpMyAdminから流しこむ。

$ app/console doctrine:schema:create --dump-sql
ATTENTION: This operation should not be executed in an production enviroment.

CREATE TABLE Post (id INT AUTO_INCREMENT NOT NULL, title VARCHAR(255) NOT NULL, body LONGTEXT NOT NULL, createdAt DATETIME NOT NULL, updatedAt DATETIME NOT NULL, PRIMARY KEY(id)) ENGINE = InnoDB


後はブラウザから動作確認して動くか確認。


実際はこんなにすんなり行かずに、トライアンドエラーを繰り返しました。PaaS楽しい!!

*1:gitに登録するのも嫌だが他の方法が思いつかなかったので

symfony1.4+Doctrineでbuild-formsで生成されるコードに独自WidgetやValidatorをセットする

開発が進むに連れてフォームで

  • 表示するエラーメッセージを全体的に変更したい
  • 全角英数を半角英数に変換したい
  • その他色々と機能を拡張したい

などと、全体的な挙動を変えたいことが多くて、

$ symfony doctrine:build-forms

で生成される、lib/form/doctrine/base/Base***Form.class.php 内のWidget, Validatorを独自拡張したものに変更できないかなと思う事が多く、調べてみました。*1

$ symfony doctrine:build-forms --generator-class=myDoctrineFormGenerator

と、build-formsの--generator-classにクラス名を渡してGeneratorクラスでいろいろとやれば良いのですが、
オプション付けるのを忘れたり、他の人も触ることを考えるとデフォルト値を置き換えられないかなーと思って調べました。

既存taskを継承すれば良いかなと思い継承しましたが下記のようなエラーが。

$ cat lib/task/myDoctrineBuildFormsTask.class.php 
<?php

class myDoctrineBuildFormsTask extends sfDoctrineBuildFormsTask
{
}
$ symfony doctrine:build-forms
PHP Fatal error:  Uncaught exception 'sfCommandException' with message 'The task named "doctrine:build-forms" in "myDoctrineBuildFormsTask" task is already registered by the "sfDoctrineBuildFormsTask" task.' in /usr/local/apps/example/lib/vendor/symfony/lib/command/sfCommandApplication.class.php:142

...と、taskの正しい拡張方法がわかりませんでした。



前置きが長くなりましたが無理やりオプション書き換えてみました。BKっぽいですがメモ。

config/ProjectConfiguration.class.php

command.filter_optionsイベントを捕捉して、generator-classを置き換えます。

<?php

require_once dirname(__FILE__).'/../lib/vendor/symfony/lib/autoload/sfCoreAutoload.class.php';
sfCoreAutoload::register();

class ProjectConfiguration extends sfProjectConfiguration
{
  public function setup()
  {
    $this->dispatcher->connect(
      'command.filter_options',
      array($this, 'commandFilterOptionsEventHook')
    );
  }

  public function commandFilterOptionsEventHook(sfEvent $event, $options)
  {
    $task = $event->getSubject();
    $parameters = $event->getParameters();
    $commandManager = $parameters['command_manager'];
    if ($task->getFullName() === 'doctrine:build-forms') {
      $commandManager->getOptionSet()
        ->getOption('generator-class')
        ->setDefault('myDoctrineFormGenerator');
    }
    return $options;
  }
}

lib/generator/myDoctrineFormGenerator.class.php

  • $extendedWidgetFormClasses
  • $extendedValidatorClasses

のkey=>valueに置き換えたいクラスの定義を書きます。

<?php

class myDoctrineFormGenerator extends sfDoctrineFormGenerator
{
  protected $extendedWidgetFormClasses = array(
    'sfWidgetFormDate' => 'myWidgetFormTextDate', // 日付のinput text入力
  );

  protected $extendedValidatorClasses = array(
    'sfValidatorInteger' => 'myValidatorInteger', // 全角英数->半角英数
    'sfValidatorDate'    => 'myValidatorDate',    // 全角英数->半角英数
  );

  public function getWidgetClassForColumn($column)
  {
    return $this->getExtendedClass(
      parent::getWidgetClassForColumn($column),
      $this->extendedWidgetFormClasses
    );
  }

  public function getValidatorClassForColumn($column)
  {
    return $this->getExtendedClass(
      parent::getValidatorClassForColumn($column),
      $this->extendedValidatorClasses
    );
  }

  protected function getExtendedClass($class, $extendedClasses)
  {
    return isset($extendedClasses[$class]) ? $extendedClasses[$class] : $class;
  }
}

として、やるとsymfony doctrine:build-formsしてやると、生成されるソース内のWidget,Validatorが置き換わります。

Person:
  columns:
    name: string(255)
    date_of_birth: date
    allowance: integer(4)
  public function setup()
  {
    $this->setWidgets(array(
      'id'            => new sfWidgetFormInputHidden(),
      'name'          => new sfWidgetFormInputText(),
      'date_of_birth' => new myWidgetFormTextDate(),
      'allowance'     => new sfWidgetFormInputText(),
    ));

    $this->setValidators(array(
      'id'            => new sfValidatorChoice(array('choices' => array($this->getObject()->get('id')), 'empty_value' => $this->getObject()->get('id'), 'required' => false)),
      'name'          => new sfValidatorString(array('max_length' => 255, 'required' => false)),
      'date_of_birth' => new myValidatorDate(array('required' => false)),
      'allowance'     => new myValidatorInteger(array('required' => false)),
    ));
  ...

ちなみに今回、例として置き換えてみたWidgetとValidatorは以下です。参考までに。

lib/widget/myWidgetFormTextDate.class.php

<?php

class myWidgetFormTextDate extends sfWidgetFormDate
{
  protected function renderDayWidget($name, $value, $options, $attributes)
  {
    $attributes['size'] = '4';
    $attributes['maxlength'] = 2;
    return $this->renderWidgetFormInput($name, $value, $attributes);
  }
  protected function renderMonthWidget($name, $value, $options, $attributes)
  {
    $attributes['size'] = '4';
    $attributes['maxlength'] = 2;
    return $this->renderWidgetFormInput($name, $value, $attributes);
  }

  protected function renderYearWidget($name, $value, $options, $attributes)
  {
    $attributes['size'] = '6';
    $attributes['maxlength'] = 4;
    return $this->renderWidgetFormInput($name, $value, $attributes);
  }

  protected function renderWidgetFormInput($name, $value, $attributes) {
    $widget = new sfWidgetFormInput(array(), $attributes);
    return $widget->render($name, $value);
  }
}

lib/validator/myValidatorDate.class.php

<?php

class myValidatorDate extends sfValidatorDate
{
  protected function doClean($value)
  {
    if (is_string($value)) {
      $value = $this->filter($value);
    } else if (is_array($value)) {
      $value = array_map(array($this, 'filter'), $value);
    }
    return parent::doClean($value);
  }

  protected function filter($value)
  {
    return mb_convert_kana($value, 'a');
  }
}

lib/validator/myValidatorInteger.class.php

<?php

class myValidatorInteger extends sfValidatorInteger
{
  protected function doClean($value)
  {
    if ($value !== null) {
      $value = mb_convert_kana($value, 'a');
    }
    return parent::doClean($value);
  }
}

*1:デフォルトでセットされるsfValidatorString,sfValidatorInteger等を差し替えたい

symfony1.4+Doctrineで配列をカンマ区切りで保存する

最近チェックボックスの項目が大量あるフォームを実装していて、綺麗に正規化して実装するのも大変だなーと思って、valueを「,」区切りで文字列保存することにしたのでメモ。
valueに「,」が入る可能性などは考えてないのであくまでもシンプルに。

config/doctrine/scheme.yml

schemeの定義例は以下。interestに「,」区切りで保存する。

Question:
  columns:
    name: string(255)
    interest: string(255)

config/ProjectConfiguration.class.php

sfDoctrineRecordを継承して、myDoctrineRecordを定義するのでそちらをmodel生成で読み込むように。

<?php

require_once dirname(__FILE__).'/../lib/vendor/symfony/lib/autoload/sfCoreAutoload.class.php';
sfCoreAutoload::register();

class ProjectConfiguration extends sfProjectConfiguration
{
  public function setup()
  {
    $this->enablePlugins('sfDoctrinePlugin');
  }

  public function configureDoctrine(Doctrine_Manager $manager)
  {
    sfConfig::set('doctrine_model_builder_options', array(
      'baseClassName' => 'myDoctrineRecord',
    ));
  }
}

lib/doctrine/myDoctrineRecord.class.php

implode, explodeするメソッドを定義。

<?php

class myDoctrineRecord extends sfDoctrineRecord
{
  protected function _getExplode($key, $separator = ',', $load = true)
  {
    $value = $this->_get($key, $load);
    if ($value === '' || $value === null) {
      return array();
    }
    return array_map('trim', explode($separator, $value));
  }

  protected function _setImplode($key, $value, $separator = ',', $load = true)
  {
    if (is_array($value)) {
      $this->_set($key, implode($separator, array_map('trim', $value)), $load);
    } else {
      $this->_set($key, '', $load);
    }
  }
}

コマンドラインでモデルとCRUD画面生成

$ symfony doctrine:build --all
$ symfony doctrine:generate-module frontend question question

lib/model/doctrine/Question.class.php

interestのgetter,setterは前述のmethodを通すようにする。

<?php

class Question extends BaseQuestion
{
  public function getInterest()
  {
    return $this->_getExplode('interest');
  }

  public function setInterest($value)
  {
    $this->_setImplode('interest', $value);
  }
}

lib/model/doctrine/QuestionTable.class.php

interestの定義

<?php

class QuestionTable extends Doctrine_Table
{
  protected static $interests = array(
    '1' => 'Music',
    '2' => 'Sports',
    '3' => 'Programming',
  );

  public static function getInterests()
  {
    return self::$interests;
  }

  public static function getInstance()
  {
    return Doctrine_Core::getTable('Question');
  }
}

lib/form/doctrine/QuestionForm.class.php

intersetのwidget,validatorをQuestionTable::getInterests()にする。

<?php

class QuestionForm extends BaseQuestionForm
{
  public function configure()
  {
    $intersets = QuestionTable::getInterests();
    $this->setWidget('interest', new sfWidgetFormChoice(array(
      'choices'  => $intersets,
      'multiple' => true,
      'expanded' => true,
    )));
    $this->setValidator('interest', new sfValidatorChoice(array(
      'choices'  => array_keys($intersets),
      'multiple' => true,
    )));
  }
}

とするとこうなって、新規作成・編集することができる。


getter,setterをオーバライドしているので、form側で特に意識することなくデフォルト値のセット、データの保存ができて便利かなと思っています。

他のプロジェクトでも使えそうな気がするので、scheme.ymlに書いたら自動的にやってくれるようにDoctrineのBehaviorにしてみたい。