symfony1.4.x + sfDoctrinePluginで失敗しないmigrationのやり方 - Symfony Advent Calendar JP 2011 day 19

0
    Symfony Advent Calendar JP 2011 19日目です。
    昨日は@co3kさんでした。


    doctrineのmigration機能、使ってますか?

    そもそもsymfony+doctrineのmigrationとは何かというと、一旦決めたDBスキーマが後から変更になったとき、テーブル作成・カラムの追加/変更などの作業を一元管理できる仕組みです。(RubyOnRails由来)
    例えばカラムが追加になった時、プロジェクト側で持っているスキーマ情報(schema.yml)を変更した時に、手動で(コマンドやphpmyadmin等から)実際のDBテーブルにもカラムの追加を反映するのはなかなか大変です。追加カラムが1個ならまだしも、何個もあったり、複数テーブルに渡っていたりするととても面倒です。
    面倒な作業=ミスが発生しやすい のでできるだけ手動でやるのは避けたいですよね。

    これを解決してくれるのがdoctrineのmigration機能です。
    スキーマ情報を変更するだけで、あとはコマンドを手順通り発行すれば実際のDBテーブルに変更を反映してくれます。

    既に使っている人にはもはや手放せないぐらい便利な(?)機能なのですが、あまり日本語ドキュメントがないので、この機会に書いてみることにしました。

    ■ケース1:DBスキーマを変更する(基本の使い方)
    下のようなプロジェクトの初期schema.ymlがあったとします。


    後からやっぱり商品説明もDB管理したいとなったとき、migrationの出番です。

    まず、schema.ymlを変更してdescriptionを追加します。


    スキーマの差分を自動生成するコマンドを実行します。
    ./symfony doctrine:generate-migrations-diff

    lib/migration/doctrineにmigrationクラスが自動的に生成されています。

    (※生成されたクラスの中身を見れば、別に自動生成に頼らずとも手動でも書こうと思えば書けるクラスだということはわかると思います→ケース3参照)

    いよいよmigrationを実行します。
    ./symfony doctrine:migrate 1
    →productテーブルの構造が変更され、descriptionカラムが追加されます。
    ※オプションの1はバージョン番号です。未指定の場合、lib/migration/doctrineにあるうち、最新のバージョンまで順番に全て実行されます。

    model,form,filterはdescriptionを扱えるように生成し直す必要があります。
    ./symfony doctrine:build --model --forms --filters
    (form,filterを使ってない時は--modelだけでOK)

    これでDBスキーマ変更が完了してしまいます!


    migrationのバージョン番号は、migration_versionというテーブルが勝手に作られ、その中のversionというカラムで管理されます。
    一旦descriptionを足した後で不具合等が確認され「やっぱりちょっとリリース待って!」という事態になった時は、バージョンを下げることができます。
    ./symfony doctrine:migrate 0
    →descriptionカラムが削除されます。modelの再生成をしてしまっていたら、再々生成の必要があります。
    ※オプションの0はバージョン番号です。

    もし一度追加したカラムを永久的に削除する(一旦下げるのではなく)場合は、バージョンを下げるのではなくカラム削除のmigrationを新たに行うほうが無難でしょう。



    なお、実行する環境と実行する人の手際によって、ほんのわずかの間ですがmodelと実際のDBの間に齟齬が発生します。そのため、あるはずのカラムがない状態になる等で運用中のシステム上でエラーが発生する可能性があります。
    が、万一ダウンタイムになっても、手動でDBを変更しながらmodelを再生成する場合に比べて最小限で済むと思われます。

    対策としては、
    ・カラムの削除だけを行うmigrationの場合は上記の手順を逆にして、先にmodelの再生成を行ってからdoctrine:migrateを実行する
    ・カラムの追加だけを行うmigrationの場合は先にdoctrine:migrateを実行してからmodelを再生成する
    ・カラムの追加と削除が同時に発生する要件でも、敢えてschema.ymlの編集を2回に分けて、カラム追加とカラム削除のmigrationの実行を分ける(カラム追加のmigrate→model再生成→カラム削除のmigrate→model再生成のような手順で行う)
    が考えられます。
    この回避策は一般的なものですので、個々のケースに合わせて考えて実行してみてください。


    ■ケース2:migrationが積み重なったプロジェクトを新規にセットアップし、さらにDBスキーマを変更する(実際のプロジェクトでの使い方)

    ケース1を経てリリースされたシステムをサーバー移転等で一から再構築した場合を考えてください。
    ./symfony doctrine:build --all
    で構築すると、最初から既に変更済みのschema.ymlでDBテーブルが作成されます。
    そのまま運用する場合には問題ないのですが、更にDBテーブルの変更が発生すると再びmigrationの出番です。
    descriptionだけでなく、社内用の備考memoもDB管理することになったとして考えてみましょう。

    まず、ケース1と同じようにschema.ymlを変更し、差分自動作成のコマンドを実行します。

    ./symfony doctrine:generate-migrations-diff

    lib/migration/doctrineにVersion2クラスが生成されます。(ここまでケース1と同じですね)


    さて、ここで問題になるのが、既にversion1までの内容が適用済みのDBに対して、バージョン1→バージョン2だけのmigrationを行いたいのですが、このサーバーではmigrationを行ったことがないため、migration_versionテーブルがまだありません。つまり、doctrineはバージョン0と認識してしてしまう状態です。

    が、心配は要りません。黙ってバージョン2へのmigrationを行いましょう。
    ./symfony doctrine:migrate 2

    やはり、予想通りに(?)エラーが出ます。

    - SQLSTATE[42S21]: Column already exists: 1060 Duplicate column name 'descrip
    tion'. Failing Query: "ALTER TABLE product ADD description TEXT"

    が、エラーになっても止まらずに指定したバージョンまで順に進めてくれるため、問題なくDBテーブルの変更自体は完了します。
    modelの再生成を行えば、DBテーブルの変更は完了となります。

    ケース2ではもう1個しておいた方が良い作業があります。
    この段階まで終わった状態でmigration_versionテーブルを見ると、versionカラムが空になっていると思います。
    versionカラムに適用済みにしたバージョン番号(このケースの場合だと2)を手動で(コマンド又はphpmyadmin等で)入れます。
    こうしておくと、更にもう1回以上DBスキーマ変更が発生した時でも、今度はバージョン2まで適用済みとして認識させることができます。
    ※これを怠ると次もバージョン0から実行されてしまい、doctrine:migrate実行時にSQLのエラーを見ることになります。


    影響がないとはいえ一時でもエラーを見たくない、という人は、migrationの前に自分でmigration_versionテーブルを作って、現在のバージョンを正しく認識させることでエラー表示を回避できます。
    migration_versionテーブルを作り、integerでversionカラムを作ります。index等は要りません。
    既に適用済のバージョン番号(このケースの場合だと1)を値としてversionに入れてから
    ./symfony doctrine:migrate 2
    を実行しましょう。今度はエラーなくmigrationが完了するはずです。


    ■ケース3:behaviorのmigration
    便利なdoctrineのbehaviorですが、デフォルトのbehavior(Timestampable,NestedSet等)でなく自作のものを使っている時に、behaviorで追加しているカラム自体が変更になった時のmigrationは注意が必要です。
    具体的には、doctrine:generate-migrations-diffコマンドではschema.ymlとmodelの間の差分のみを見るため、behaviorの変更分は自動でmigrationクラスを作ってくれないからです。

    このような場合、doctrineのmigrationを使ってDBテーブルに変更を反映するには、migrationクラスを手動で書く必要が出てきます。
    lib/migration/doctrineにversion(バージョン番号).phpファイルを作り、自分で内容を書きます。
    Version(バージョン番号)クラスは、Doctrine_Migration_Baseをextendsし、upとdownメソッドをpublicで定義すれば手動で書いてもちゃんと認識・実行されます。
    ケース1やケース2の自動生成されたVersion1,Version2クラスでカラムの追加・変更の書き方は出てきたと思いますが、他にもカラムの型を変更したりindexを足したりすることができます。
    一覧がhttp://www.doctrine-project.org/projects/orm/1.2/docs/manual/migrations/ja#%E3%83%9E%E3%82%A4%E3%82%B0%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%AF%E3%83%A9%E3%82%B9%E3%82%92%E6%9B%B8%E3%81%8F:%E5%88%A9%E7%94%A8%E5%8F%AF%E8%83%BD%E3%81%AA%E3%82%AA%E3%83%9A%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3にありますので、実際に書き始める前に一度チェックしてみてください。



    一度わかってしまえばすごく便利なdoctrine:migrate、symfony1.4で開発する時には是非使ってみてださい^^
    Symfony2ネタが続く中、空気読まずにsymfony1.4.xネタでお送りしました☆

    明日は@fivestrさんです!

    Silexでエラーページをカスタマイズする方法 : Symfony Advent Calendar 2011 - day 12

    0

      Symfony Advent Calendar 201112日目です。

      昨日は@itemanさんでした。

       


       

      Silexのデフォルトのエラー画面は
      デフォルトエラーページ
      こんな感じです。

      ちゃんと他の画面と同じレイアウトの中に埋め込みたいと思いませんか?(ヘッダーとかフッターとかメニューとか)

      そんなときはエラー設定をします。
      エラー設定
      errorもget,postもreturnするのは同じですから、twigをrenderしたものを返せばエラー画面として表示することができるのです。errorでちょっと違うのは、レスポンスコードをつけるために生のボディ(twigをレンダリングしたhtml)だけでなくSymfony¥Component¥HttpFoundation¥Responseのインスタンスを返していることです。
      勝手にerror.twigをレンダリングするべしと書いてしまったので、error.twigも書きます。
      layout.twig
      error.twig
      最初の行でlayout.twigをextendsすればちゃんと反映されます。エラーページでもヘッダーがつくようになりました。
      カスタムエラーページ
      余談ですが¥Exception $e->getMessage()したものはデバッグメッセージ的なものなのでむやみやたらと見せない方が良いです。$app['debug']に設定してある場合のみ表示するようにしました。(twigテンプレート側から$appはappで参照できます)

       


       

      SymfonyのAdvent Calendarなのに、twitterで後藤さんにSilexはOKですよ〜と言ってもらったのでv お言葉に甘えてSilexネタでした。

       

      明日は@bakorer さんです。


      第5回Symfony2勉強会に参加してLTしてきました!

      0
         12月4日(日)に第5回Symfony2勉強会に参加してLTしてきました!
        参加レポートです。

        ■到着
        道玄坂をえっちらおっちら登って、こんなキレイなビルに入るの怖いよーと半泣きになりながら8Fへ。
        受付で思わず本名を名乗ってしまったのですが、githubで顔出ししておいた成果で(?)受け付けの方から「77webさんですよね」と。

        ■ハンズオン
        後藤さんによるハンズオン。
        Symfony-standard2.0.6を使って掲示板(?)アプリのバンドルを作成するまでを丁寧に順を追って説明。
        2.0.0かbetaの頃に一度触ったきりだったのですが、色々と変わっているところ(楽になっているところ!)が多くてびっくりしました。

        ■TwigExtensionの作り方
        テンプレートを綺麗にできるのはすごくいいと思います。

        ■スイーツタイム
        お昼にプリン食べたばっかりだったのと、急いでLTの準備と帰る準備をしておかないとヤバかったので辞退しましたが、美味しそうなプリンでした☆

        ■LT「SymfonyComponentを使ってWordPressを作ろう」
        スライド
        http://www.slideshare.net/77web/symfony-componentwordpress

        サンプルコード
        https://github.com/77web/wp-sf-myplugin

        話し始める前は勝手に「Symfonyだし、自社の大規模システムやってる人が多そうだから全然うけなかったらどうしよう?」と心配してたのですが、受託の話のところで結構笑ってもらえたので安心しましたv

        gdgd色々言いましたが、私の中での結論は、「皆もっとSymfonyComponent使おうよ!Symfony普及させようよ!」ということです^^

        なお、話してるうちに何度もマイクの存在を忘れてしまって、会場の奥の方やustで視聴頂いた方には全然聞こえなかったと思われます。。ごめんなさい。


        ■全体通して
        当日朝に東京着の高速バスで上京して、17時半に東京発の高速バスで帰るという弾丸トラベラーだったため、LTの順番を最初にしてもらったり、LT途中でも現在時刻を示してもらったりと色々と配慮いただいて申し訳ないやらありがたいやら。
        後藤さん、前田さん、その他のスタッフの皆様も、本当にありがとうございました。

        普段Twitter上でお話している方々ともろくに挨拶もできず、次(!)はもう少し余裕のある日程で行きたいなと思いました。

        終わり。

        coreserverにOpenPNE3.6.0をインストール(速度はともかくxreaも同手順で可)

        0
          OpenPNE3.6がリリースされたので、記念にcoreserverへのインストール方法を詳細に書いておきます。時々公式SNSでインストールに躓いている方がいるので…。

          0.準備:データベースの設定

          coreserver,xreaのコンパネで設定するだけです。
          反映するまで少し時間がかかるので最初にやっておくと良いです。

          ※SSHの扱いに慣れていなくて大変な方は、下記1〜5の手順で作業せずに 一旦自分のPCにダウンロード→PC上で解凍→フォルダ名変更→設定ファイルコピー&編集→FTPソフトでアップロード でも大丈夫です。
          1.ダウンロード先URLを準備

          http://openpne.jpにアクセスし、ダウンロード>OpenPNE3.6.0に「zip版ダウンロード」というリンクがあるので、そこのリンク先URLをコピーしておきます。

          2.サーバーにzipをダウンロード

          SSHでサーバーにログインし、
          wget http://github.com/openpne/OpenPNE3/zipball/OpenPNE-3.6.0 --no-check-certificate
          
          を実行します。
          ホームディレクトリにOpenPNE-3.6.0という名前でzipファイルがダウンロードされるはずです。
          まだまだSSHを使うので閉じないでください。

          3.解凍

          ダウンロードしたOpenPNE-3.6.0のzipを解凍します。
          unzip OpenPNE-3.6.0
          
          を実行します。
          ダーーーっと実行中の表示が出て、openpne-OpenPNE3-xxxxxx(xxxxxxの部分はランダムっぽい英数字)という名前でOpenPNE3.6のソースコード一式が入ったフォルダが生成されます。

          4.フォルダ名の変更(面倒なら飛ばしてもOK)

          今後「openpne-OpenPNE3-xxxxxx」のままでは扱いづらいのでフォルダ名を変えます。
          mv openpne-OpenPNE3-xxxxxx openpne3.6
          
          を実行します(xxxxxxの部分は実際のフォルダについている名前を入れてください) 5.設定ファイルのコピー&編集

          config/OpenPNE.yml
          config/ProjectConfiguration.class.php
          を作ります。
          config/OpenPNE.yml.sample
          config/ProjectConfiguration.class.php.sample
          というファイルが入っているので、これをコピーすれば良いです。
          cp config/OpenPNE.yml.sample config/OpenPNE.yml
          cp config/ProjectConfiguration.class.php.sample config/ProjectConfiguration.class.php
          
          を実行します。
          本当ならここでOpenPNE.ymlを編集するのですが、内容は後からでも変更できるので一旦編集なしで先に進みます。

          6.インストールコマンドの実行

          いよいよインストールです。
          php symfony openpne:install
          
          を実行します。
          データベース設定を1個ずつ訊かれるので、手順0で作成したDBの情報を入力していきます。最後にIs it OK to start this task?と訊かれるので「y」と答えてスタートさせてください。

          インストールが完了するとcomplete!と出ます。

          ※処理時間が長くなると、途中でkilledとなって処理が止まってしまうことがあります。(xreaやcoreでは非常によくあります)

          もしkilledと言われて途中でインストールが中止されてしまったら、openpne:installのコマンドを実行し直せば、いつかは最後までインストールできます。

          …と言っても、何度もDB設定を手入力するのが面倒なので、簡単に再実行できる抜け道を作りましたv
          https://gist.github.com/897640のスクリプトをダウンロードして、openpne3.6/lib/taskにopenpneFastInstallTask.class.phpという名前で保存してから、openpne:installの代わりに
          php symfony openpne:fast-install --dbms=mysql --dbuser=DBユーザー名 --dbpassword=DBパスワード --dbname=DB名
          
          のようにして実行すれば、openpne:installと同じ処理をDB設定を1つずつ入力しなくてもインストールができます。(1つ前に実行したコマンドはキーボードの「↑」でもう一度呼び出せます)

          ※何度やっても完了まで行けない(途中でkilled)時は、最初にインストールするプラグインを減らしてみてください。プラグインは本体のインストールが終わった後でも個別にインストールができます。→インストールするプラグインの減らし方

          7.ドメインの設定

          coreserver,xreaのコンパネで設定するだけです。経験上、メイン(public_html)に設定するのは止めた方が良いです。メインはblankにして、サブに設定。
          ※既に設定済のドメインの下部ディレクトリでPNEを使いたい方はやらなくて良いです


          8.シンボリックリンクの作成

          シンボリックリンクはWindowsでいうショートカットみたいなものです。本来coreserverでは「public_html/ドメイン名」というフォルダをサブドメインのドメインルート(http://ドメイン名/ でアクセスした時に表示するフォルダ)とするところを、「public_html/ドメイン名」という名前でopenpne3.6/webへのシンボリックリンク(ショートカット)を作ってやることで、openpne3.6/webをhttp://ドメイン名/でアクセスした時に表示するフォルダにすることができます。
          ln -s $HOME/openpne3.6/web $HOME/public_html/PNEで使用したいドメイン名
          
          を実行します。

          ↑WinSCPでpublic_htmlに「PNEで使用したいドメイン名」の名前でリンクができたのを確認してみたところ。FTPソフトやcoreserverコンパネのファイルマネージャでも確認できます。

          9.htaccessの編集
          .htaccessを画像のようにcoreserverの環境に合わせて編集します。


          10.ログインして確認
          ブラウザで「http://PNEで使用するドメイン名」にアクセスして、ログインフォームが表示されればインストール成功です!


          OpenPNE3のプラグイン開発でハマリやすいところの個人的まとめ

          0
            ここ1年ぐらい、OpenPNE3のプラグインを色々作っているわけですが、
            http://plugins.openpne.jp/package/listMember?id=37

            個人的にハマりやすいと思ってる点を4点ほどまとめてみました。

            ■フォームのカスタマイズはconfigureじゃなくてsetupでやりましょう
            特にsymfony1.xでsfFormを扱ったことがある人は要注意。PNEプラグインの場合、path/to/pne/plugins/opXXXPlugin/lib/form/doctrine/PluginXXXForm.class.phpを弄ることになるのですが、そこでconfigureを定義してもPluginXXXForm.class.phpを継承しているpath/to/pne/lib/form/doctrine/XXXForm.class.phpに空っぽのconfigureが入ってるので、その空っぽの内容に上書きされてしまいます。

            ■既存プラグインをカスタマイズするとき、ディレクトリごと.oldとか.orgとか付けて残しておくのはNG。ディレクトリごと取っておくときはpath/to/pneの外に出すか、FugaHoge.class.php.oldのように.php以外の拡張子で残そう
            symfony1.x系に共通の話なのですが、autoloadが働く時lib/util.org/FugaHoge.class.phpとlib/util/FugaHoge.class.phpがあったらutil.orgのほうが読み込まれてしまっていつまでたっても変更した内容が反映されない場合があります。

            ■携帯のテンプレートで<table width="100%">等と入れるのがめんどくさい。
            表示内容をslotにつっこんでop_include_box使いましょう。便利。

            ■plugins.openpne.jpからインストールした時は良いが自SNS内に自分でプラグインを作った時、テーブル追加はopenpne:install --redoしかない(?)
            migrationのスクリプト書けばinstallやり直さなくても大丈夫な気もしますが、今のところ開発専用にSNSを1個確保してそこでinstall --redoしまくるのがいいかも


            Silexの使いどころ(妄想)

            0
              最近Silexにハマってます。面白いです。
              で、真面目に仕事での使いどころを考えてみました。

              ・DBを使わない小規模なスクリプトの時
              一昔前?は無料CGIで済ませたような、ペラ1枚のスクリプトを楽に書きたいときは向いてる気がします。
              メールフォームとか、単純な○○占いとか。
              あと、RSSや何かを取ってきて加工して表示するようなのを書くのに良いかもしれません。

              ・既存アプリでDBを操作しているものの携帯版のみ作るとき
              具体的にはWordPressやMovableType等の携帯向け表示を作るのにいいかも?と思ってます。もちろんktai_styleプラグインやMTの携帯用テンプレート利用などはできるわけですが、それでは足りない時に使えるかもと思ってます。


              現時点ではORMがバンドルされているわけでなく、Doctrine2のORMを使えるようにするExtensionも非公式なので、SQLをガリガリ書いても後でメンテできるぐらいの規模用のフレームワークということなんですよね。
              読み出しはほとんど問題ないと思いますが、symfonyとかでバリバリやってるようなDBにUPDATE,INSERTをどんどんかけて行くようなアプリを書くのは厳しいと思います。

              個人的には、django.contrib.adminのようなExtensionなり再利用可能なappが出てくれば、独自CMS用のフレームワークとして一気に飛躍できる!と思っているのですが…^^;

              GoogleAnalytics Export APIをPHPから使う GoogleAnalytics.class.php利用時の注意

              0
                 開発者さんのところからダウンロードしてくると、使えることは使えるんですが2ヶ所NOTICEが出ます。リリースしちゃえば(prodにしちゃえば)気にならないことなんですが、symfonyでdev版で開発してるとどうしても気になるので2ヶ所直して使っています。

                ■126行目で$dimsは未設定の変数というNOTICE
                見てみると
                $dims .= $dimension->getAttribute('value');

                どうやら1回目のループ実行時はまだ未設定の変数に対して.=を行うことになるのでNOTICEになる模様。
                ループの前に$dims = '';と1行足して解決。

                ■251行目でArray to String convertionのNOTICE
                見てみると
                250             $header[] = array("application/x-www-form-urlencoded");
                251            curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
                となっていた。
                curl_setoptのマニュアルを見るに第3引数は配列を渡せばいいらしい。
                ちゃんと$headerは配列になっているが、$header配列の値自体がarray("application/x-www-form-urlencoded")と配列になってしまっている。
                curl関数使ったことないので(汗)詳しいことはわからないが、配列がstringとして評価されてNOTICEが出てるってことなので、$header = array(...)のarray関数を外してみた。

                250             $header[] = "application/x-www-form-urlencoded";
                251            curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
                正直curl関数はまだよくわからないがこれでテスト実行してみてエラー出ず、開発サーバーで実行して見て画面からNOTICEも消えたので良しとする(笑)

                後でgapi.class.phpも試してみたい。どっちもPHP5なオブジェクト指向で書かれたlibですが、GoogleAnalyticsの公式に載っているのがgapi.class.phpのほうなので。

                Doctrine(1.x)でのLEFT OUTER JOIN実現方法

                0
                   公式のドキュメント
                  http://www.doctrine-project.org/projects/orm/1.2/docs/manual/dql-doctrine-query-language/ja#join%E3%81%AE%E6%A7%8B%E6%96%87:on%E3%82%AD%E3%83%BC%E3%83%AF%E3%83%BC%E3%83%89
                  によると、ONキーワードでリレーション条件がカスタム指定できるようなことが書いてありますが、実際には効きませんorz

                  試行錯誤の末、無理やりLEFT OUTER JOINぽいことを実行させることができたので忘れないようにメモ。
                  実験しながらエラーMSGでググると、symfony公式のフォーラムでも世界各国の方々がLEFT OUTER JOINができないと悩んでいた。

                  なんでONを自分で指定したいかと言うとモデルを次のように定義してあるとして、
                  Able:
                  columns:
                  id: { type: integer(1), primary: true }
                  Baker:
                  columns:
                  id: { type: integer(1), primary: true }
                  out_class: string(4)
                  out_id: integer(1)
                  Cat:
                  columns:
                  id: { type: integer(1) }
                  Bakerはout_classの値によりAbleかCatどちらかにJOIN先が変わります。
                  なんでこんな変な構造にするか詳細は聞かないでください(笑

                  【実験1】
                  まず公式のドキュメント通りに。
                  $q = Doctrine::getTable("Baker")->createQuery("b")->leftJoin("Able a ON a.id ON b.out_id");
                  ⇒...FROM Baker b, Able a...
                  ONどこいった??単にFROMにカンマ区切りで入れられると、SQL側でAble.idとBaker.idで勝手にJOINされちゃって都合が悪い。

                  【実験2】
                  公式ドキュメントにWITHというのも書いてあるので使ってみた。
                  $q = Doctrine::getTable("Baker")->createQuery("b")->leftJoin("Able a ON a.id =b.out_id");
                  ⇒...FROM Baker b, Able a...
                  WITHどこいった??

                  【実験3】
                  無理を承知でINNER JOINにしてみる。Linux系OSのMySQLで、MyISAMテーブル(=外部キー制約なし)だとBaker.out_idとA.idのINNER JOINとか普通に成立した記憶が。
                  $q = Dcotrine::getTable("Baker")->createQuery("b")->innerJoin("Able a ON a.id = b.out_id");
                  ⇒エラー。AbleとBakerの間にリレーションが定義されてないとか何とか。まあ当然。


                  ここで公式APIドキュメントを読みに行く。
                  Doctrine_Query_Abstruct::leftJoin($join, $params=array())
                  色々場合分けはあるが、要は最後に $this->dqlQueryPart['from']に 'LEFT JOIN '.$joinをセットする仕組みのよう。
                  【実験4】
                  とりあえずセットした後にちゃんとセットされているかどうかチェックしてみる。
                  $q = Doctrine::getTable("Baker")->createQuery("b")->leftJoin("Able a ON a.id = b.out_id");
                  var_dump($q->getDql());
                  ⇒FROM Baker b LEFT JOIN Able a ON a.id = b.out_id
                  ちゃんとDQLにはセットされていることが確認できた。
                  が、$q->getSqlQuery()すると相変わらず FROM Baker b, Able aのみ。どうやらDQLからSQLを生成する時にONが無視されてしまう様子。

                  ここでふと思い出した。Pear::DBを使ってSQLを勉強し始めた頃、FROMにテーブルを羅列してWHEREでJOIN条件書いてもほしいデータが取れてたなー。その後SQL実行速度の問題でちゃんとJOINを書くようになったんだけど。
                  【実験5】
                  だめもとで。
                  $q = Doctrine::getTable("Baker")->createQuery("b")->addFrom("Able a")->addWhere("b.out_id = a.id");
                  ⇒実験3と同じエラー。でも、$q->getSqlQuery()ではちゃんとSQLが出て来るし、それをコピペしてphpmyadminに実行させたらちゃんと想定通りのデータが出た。

                  【実験6】
                  実験5ではAbleのデータを入れるコンポーネント(プロパティ)がないよ!って怒られてる気がしてきた。同時にBakerとJOINするAbleまたはCatのデータはBakerに取得用のメソッドを作ってあるのでAbleのデータを最初のSELECTで取らなくてもいいよね、と考える。
                  $q = Doctrine::getTable("Baker")->createQuery("b")->addFrom("Able a")->addWhere("b.out_id = a.id")->select("b.*");
                  ⇒成功!!

                  ここまで来るのに休み休み(家事育児やりながら^^;)で4時間近くかかってしまった(汗
                  【結論】
                  スマートじゃない&MySQLエンジンがんばって!なSQLだけど、上記の方法でLEFT OUTER JOINっぽいことはできることがわかりました。


                  [OpenPNE3.x]op_include_yesnoヘルパーがすごい

                  0
                     超便利なヘルパーを見つけた(発掘した)ので感動が冷めないうちにまとめておきます。
                    何かに書いておかないとまた忘れちゃいそうなのでw

                    function op_include_yesno($id, $yesForm, $noForm, $options)
                    #定義はopPartsHelper内にあります。

                    $id…formを囲むdivに付けたいid名。HTMLのid名なので同一ページ内の他のidと干渉しなければ自由につけられます。例えばスケジュール削除確認の画面ならscheduleDeleteConfirmFormとか。まぁ好みで。

                    $yesForm…「はい」の場合に送信するformインスタンス。予めアクション側で作ってビューに渡しておく。⇒私はこのyesnoを主に<削除確認画面>で使ってるので、大抵の場合は単なるsfFormなのですが。何かのパラメータをhiddenで渡したいときにはhiddenFieldsを持ってるformを作って値をセットして渡せばOK。

                    $noForm…「いいえ」の場合に送信するformインスタンス。$yesFormと以下同文。

                    $options…オプションの連想配列。op_include_formを使ったことがあればほぼ同じオプション内容が使えます。ただし、送信先URLの指定がyes,no2つ分あるのでそれぞれ指定すべし。⇒前述の通り私は<削除確認画面>で使ってるので、yes_urlを削除実行のアクションのURL、no_urlを表示のアクションのURLにしてます。
                    yes_url…yesの場合($yesForm)の送信先URL
                    no_url…noの場合($noForm)の送信先URL
                    title…formを囲むdivのpartsHeading>h3に入るテキスト。無指定だとタイトルなしのブロックになる。⇒<削除確認画面>だと「予定削除の確認」とか。
                    body…yes,noボタンの上に表示したいテキスト。⇒<削除確認画面>なら「本当に削除しますか?」とか「削除します。よろしいですか?」とか。
                    yes_button…yesのボタンのvalue。「はい」とか「削除する」とか「出席する」とか。デフォルトは「はい」。
                    no_button…noのボタンのvalue。「いいえ」とか「中止する」とか「欠席する」とか。デフォルトは「いいえ」

                    op_include_formとの違いは、view.ymlによるカスタマイズの読み込み機構は付いてないところぐらい?まぁどうしても必要なら足せばいいけど。


                    すごい便利なのにバンドルされてるプラグインでもほとんど使われてないのが不思議。
                    皆さんもお試しあれ。

                    sfWidgetFormSelectCheckbox, sfWidgetFormSelectRadioの日本語value対応(何年ぶりのsf記事??

                    0
                      sfWidgetFormSelectCheckbox, sfWidgetFormSelectRadioでchoicesのvalue値(label値ではなく)としてマルチバイト文字列を使うと、いくつかの選択肢が表示されなくなる現象がありました。
                      原因としては、inputタグを作るとき(render?)にinputタグのid値をユニークなキーとして使うらしく、そのid値はnameとvalueを「_」で繋いで作るんだけど、sfWidgetForm内でマルチバイト文字列(まぁ日本語の文字列)は「_」に変更されてしまうので、偶々複数のchoicesのvalue値の中に文字列長が同じのがあると、1つにまとまってしまうみたい。
                      長々と文章で説明するより実例のほうがわかりやすいかな。

                      $choices = array("ほげ"=>"ほげ", "ふが"=>"ふが");
                      をcheckboxやradioのchoicesとして指定すると、出力は
                      checkboxなら □ふが <input type="checkbox" name="check" value="ふが" id="check___" />&nbsp;ふが
                      radioなら ○ふが <input type="radio" name="radio" value="ふが" id="radio___" />
                      のみになります。つまり最後の1個だけ。

                      で、これを回避するためにはid値の生成メソッド=sfWidgetForm::generateId()を下記のように変更してみました。
                      /lib/vender/symfony/lib/widget/sfWidgetForm.class.php
                      255行目〜オリジナル
                          // remove illegal characters
                          $name = preg_replace(array('/^[^A-Za-z]+/', '/[^A-Za-z0-9¥:_¥.¥-]/'), array('', '_'), $name);
                      255行目〜私の修正版
                          // remove illegal characters
                          //$name = preg_replace(array('/^[^A-Za-z]+/', '/[^A-Za-z0-9¥:_¥.¥-]/'), array('', '_'), $name);
                          $name = preg_replace('/^[^A-Za-z]+/', '', $name);
                          if(preg_match('/[^A-Za-z0-9¥:_¥.¥-]/', $name)>0)
                          {
                            $name = md5($name);
                          }
                      一見してわかるとおりマルチバイト文字を「_」×文字数に置換してしまうのではなく、マルチバイト文字列を含むname + _ + value をmd5ハッシュ化してしまっただけです。
                      デメリットはinputのid値を使ってCSSでデザインを指定したいときには厳しい(出力されたinputタグをみて頑張って><)ことぐらい?
                      まぁ個々のinputにデザイン決めたいぐらいの時はchoicesのkey(value値として渡す値)をアスキー文字だけにして使ってください。
                      今回の私の案件ように、運営者が自分でフォームの選択肢を編集する(しかも、運営者は、言語ファイル未アップでtitle, bodyとフォームのラベルが表示されたら「プログラムのコードが出ってます!!」と泡食って連絡してくるぐらい英語オンチ)とか、そういう特殊な状況じゃなければ多分使わないと思いますが。
                      #そういえば、mysqlのenumってマルチバイト文字使えたっけ。enumに日本語文字列を指定して、doctrineのschemaからフォームを自動生成させたらどうなるんだろう。そういう時この修正方法が生きるかも???誰か人柱お願いします(笑)


                      何年ぶりにsymfony記事を書いたんだろう、自分…。


                      PR

                      calendar

                      S M T W T F S
                      1234567
                      891011121314
                      15161718192021
                      22232425262728
                      293031    
                      << January 2012 >>

                      twitter

                      selected entries

                      categories

                      archives

                      recent comment

                      • coreserverにOpenPNE3.6.0をインストール(速度はともかくxreaも同手順で可)
                        kazu
                      • coreserverにOpenPNE3.6.0をインストール(速度はともかくxreaも同手順で可)
                        77web
                      • coreserverにOpenPNE3.6.0をインストール(速度はともかくxreaも同手順で可)
                        Tsukasa
                      • coreserverにOpenPNE3.6.0をインストール(速度はともかくxreaも同手順で可)
                        jun
                      • coreserverにOpenPNE3.6.0をインストール(速度はともかくxreaも同手順で可)
                        dk
                      • symfony+Ajaxでhierselectを実現!
                        ビギナーシンフォニアン
                      • HTMLの表(TABLE)のセル(TD)に斜線を引くjavascriptライブラリ slash.js 作っちゃいました
                        momo
                      • HTMLの表(TABLE)のセル(TD)に斜線を引くjavascriptライブラリ slash.js 作っちゃいました
                      • 復活^^;
                        もも
                      • pychart使いたいよう…(泣
                        もも

                      recent trackback

                      • HTMLの表(TABLE)のセル(TD)に斜線を引くjavascriptライブラリ slash.js 作っちゃいました
                        常山日記
                      • django対symfony 日本語メール送信(その1 symfony編)
                        CPA-LABテクニカル
                      • CodeIgniterでユーザー認証
                        されどLAMPな日々
                      • 久々にdjangoを最新版にしたらHTMLがエスケープされちゃった!!(解決済み)
                        常山日記
                      • FastCGIを諦めてmod_pythonを使う。Apacheのアップグレード
                        サーバー用語集
                      • さくらインターネット、sqlite3でdjango@CGI版を使う際の設定メモ
                        常山日記
                      • さくらインターネット スタンダードプランでdjango使ってる方、DBは?
                        mitszoの日記
                      • python多次元リストをsort(並べ替え)する方法?
                        mitszoの日記
                      • フォームから送信した値とrequest.POSTの挙動($_POST@PHPとの比較)
                        Humming Via Kitchen
                      • 日本語テキストをtruncate@django(Python全般にも??)
                        常山日記

                      recommend

                      links

                      profile

                      search this site.

                      others

                      mobile

                      qrcode

                      powered

                      無料ブログ作成サービス JUGEM