[CakePHP]paginateオプションをモデルで生成する

Posted under CakePHP by uechoco on 木曜日 23 12月 2010 at 00 : 32 : 26

ネットサーフィンしていて、hiromi2424さんのPaginateオプションをモデルに移行する - 24時間CakePHPという記事を見つけて衝動的に記事を書いています。

私も普段CakePHPでpaginateを使用しているときは、モデル側でpaginateオプションを生成しています。この方針は上記記事とは変わりありません。ただ私の場合はpaginateTypeといった指標を持たずに、getPaginateStandard($options, $data)という関数を作って、パラメータを返すようにして、以下のように呼んでいます。

php:
  1. function index(){
  2.         $options = array('limit'=>10, /* 省略 */);
  3.         $this->paginate = $this->Group->getPaginateStandard($options,$this->data);
  4.         $this->set('groups', $this->paginate('Group'));
  5.     }

先程の例と違うのは、

  1. $this->paginateオプションはcontrollerに持たせている
  2. $optionsや$dataを与えることで、関数内でページネーションを拡張できる

$this->paginateオプションは先の例のように完全にモデル側に持たせるのも有りだとは思うのですが、CakePHPの標準的な仕様はController側が先に持っていて、それを処理してModel側に渡しています。出来る限りCakePHPの標準的な仕様に合わせたほうが、プログラムが複雑になっていったときに有利になってくるし、仕様が分かりやすいのではないかと思っています。(もしかしたら他の外部ライブラリと整合性が取れなくなったりするかも知れませんし)

もう1つ、$optionsや$dataを与えているのは、ページネーションの初期値があった上で、POSTやGET、namedなどのデータを元にページネーションを拡張していきたい場合に便利だからです。もちろん、先程の例でもできなくはないのですが、配列できっちり決まっているものを、別のタイミングで後からいじるのは私はあまり好きではなく、最初から関数だったら弄りやすいかなっという感覚の違いだと思います。あと、そもそもページネーションの内容が外的要因でコロコロ変わるプログラムだからという違いなのかも知れません。

ちなみに、なぜ「getPaginateStandard」という名前にしているのか気になるところだと思いますが、これは「(モデル内で)標準的なパラメータを取得する」という意味でこういう名前になっています。実際に作りたいものにも寄るとは思いますが、私が最近つくっているプログラムでは、モデル内にfindXxxYyyZzz()やgetListXxxYyyZzz的な関数をロジック毎に作っていますが、ほとんどのSELECT系関数は

php:
  1. $params = array(/* その関数内での独自の定義 */);
  2. return $this->find('all', $this->getPaginateStandard($params));

みたいな感じで、パラメータを作る部分を1つの関数に集約しています。1回直すと全箇所に影響が出るというふうに思われるかも知れませんが、1つの関数を直せば全箇所に適用可能というメリットのほうを取っています。(わりと複雑な関連付けロジックを様々な箇所に使うので、共通で直せると嬉しいんです)。

今思えば「Paginate」って単語は余計だった(別のものにしたかった)と思っていますが、そのうちリファクタリングでもして直しますかね。


[CakePHP]AppViewクラスの作成

Posted under CakePHP by uechoco on 木曜日 16 12月 2010 at 11 : 33 : 14

AppControllerとかAppModelはよく聞くけど、AppViewはあんまり聞きません。あんまり拡張する必要性がないってのが一番の理由ですが、やっぱり変えたいときはあります。

AppViewは作れるようです。

  1. app.phpをviewsフォルダ直下に作成
  2. AppControllerでViewクラス名を変更

app.phpをviewsフォルダ直下に作成

/.../views/app.php

php:
  1. <?php
  2. class AppView extends View
  3. {
  4. }

AppControllerでViewクラス名を変更

/...//app_controller.php

php:
  1. <?php
  2.  
  3. class AppController extends Controller
  4. {
  5.     /**
  6.      * Viewクラスの定義
  7.      *
  8.      * @var string
  9.      * @access public
  10.      */
  11.     var $view = 'App'; // AppView
  12. }

こんな感じです。意外と簡単でした。


[Symfony2]core.requestイベントは2つ以上登録できる?

Posted under Symfony2 by uechoco on 日曜日 12 12月 2010 at 23 : 56 : 17

Symfony2でRequestオブジェクトの中身をいじりたいと思っています。入出力の文字エンコードをコントローラ実行の前後で自動的に変換するようなのを作りたいと思っています。RequestオブジェクトはDIコンテナ外のものですが、HttpKernelの内部で引きずり回されてところどころのイベントで使用できます。

ところでcore.requestイベントはEventDispatcher::notifyUntil()で実行されます。メソッド名から判断すると、リスナーが1つでも実行されたらそこで処理が終了してしまうような印象を受けますが、メソッドの説明には「Notifies all listeners of a given event until one returns a non null value.(リスナーの1つがnull以外の値を返すまですべてのリスナーにイベントを通知する)」て書いてあります。どうやらnullを返すリスナーがある(or値を返さないリスナーがある)と次のイベントも実行されるようです。

// /src/vendor/symfony/src/Symfony/Component/EventDispatcher/EventDispatcher.php

php:
  1. /**
  2.      * Notifies all listeners of a given event until one returns a non null value.
  3.      *
  4.      * @param  Event $event An Event instance
  5.      *
  6.      * @return Event The Event instance
  7.      */
  8.     public function notifyUntil(Event $event)
  9.     {
  10.         foreach ($this->getListeners($event->getName()) as $listener) {
  11.             if (call_user_func($listener, $event)) {
  12.                 $event->setProcessed(true);
  13.                 break;
  14.             }
  15.         }
  16.  
  17.         return $event;
  18.     }

実は'core.request'イベントに標準で登録されているRequestListnerは値を返しません。つまりnotifyUntil()で呼ばれてもイベントがisProceeded()状態になることはありません。ということは、自分で'core.request'イベントにリスナーを追加登録すれば実行出来るってことですね。

実際にHelloRequestListenerを登録してみた

ちなみに、こんなカンジで登録しています。以下のコードを動かす場合はAppKernelへの登録やconfig.ymlへの設定なども必要なので、そこら辺は[Symfony2]PEAR::Net_UserAgent_MobileをDIコンテナから呼び出すなどを参考にしてください。
// /src/Application/HelloBundle/DependencyInjection/HelloExtension.php

php:
  1. public function configLoad($config, ContainerBuilder $container)
  2.     {
  3.         $def = new Definition('Application\\HelloBundle\\HelloRequestListener');
  4.         $def->addTag('kernel.listener');
  5.         $def->addArgument(new Reference('service_container'));
  6.         $def->addArgument(new Reference('router'));
  7.         $def->addArgument(new Reference('logger', ContainerInterface::IGNORE_ON_INVALID_REFERENCE));
  8.         $container->setDefinition('hello.request_listener', $def);
  9.         /** 参考にしたもの:
  10.         <service id="request_listener" class="%request_listener.class%">
  11.             <tag name="kernel.listener" />
  12.             <argument type="service" id="service_container" />
  13.             <argument type="service" id="router" />
  14.             <argument type="service" id="logger" on-invalid="ignore" />
  15.         </service>
  16.          */
  17.     }

HelloRequestListenerのサンプル
// /src/Application/HelloBundle/HelloRequestListener.php

php:
  1. <?php
  2.  
  3. namespace Application\HelloBundle;
  4.  
  5. use Symfony\Bundle\FrameworkBundle\RequestListener as BaseRequestListener;
  6. use Symfony\Component\HttpKernel\Log\LoggerInterface;
  7. use Symfony\Component\HttpKernel\HttpKernelInterface;
  8. use Symfony\Component\HttpFoundation\Request;
  9. use Symfony\Component\EventDispatcher\Event;
  10. use Symfony\Component\Routing\RouterInterface;
  11. use Symfony\Component\DependencyInjection\ContainerInterface;
  12. use Symfony\Component\HttpFoundation\Response;
  13.  
  14. /*
  15.  * This source file is subject to the MIT license that is bundled
  16.  * with this source code in the file LICENSE.
  17.  */
  18.  
  19. /**
  20.  * HelloRequestListener.
  21.  *
  22.  * @author uechoco
  23.  */
  24. class HelloRequestListener extends BaseRequestListener
  25. {
  26.     public function __construct(ContainerInterface $container, RouterInterface $router, LoggerInterface $logger = null)
  27.     {
  28.         parent::__construct($container, $router, $logger);
  29.     }
  30.  
  31.     public function handle(Event $event)
  32.     {
  33.         $result = parent::handle($event);
  34.         $request = $event->get('request');
  35.         $master = HttpKernelInterface::MASTER_REQUEST === $event->get('request_type');
  36.  
  37.         $this->convertRequestParameters($request, $master);
  38.     }
  39.  
  40.     protected function convertRequestParameters($request, $master)
  41.     {
  42.         if (!$master) {
  43.             return;
  44.         }
  45.         echo "Ok";
  46.     }
  47. }

ちなみに、間違って'core.request'でreturn true;とかを返すと「'A "core.request" listener returned a non response object.'」エラーが起きるので注意しましょう。return true;が許されるのは、リスナーからいきなりResponseオブジェクトを生成できる場合(リスナー側でResponseオブジェクトを生成して$event->setReturnValue()に登録した場合)だけです。リクエストURLから判断してファイルキャッシュを取得していきなりResponseで返しちゃう的な感じですかね?

notifyUntilっていうから、絶対に1回しか実行されないのかと思ってたけど、そういう先入観は良くないですね。


[Symfony2]PEAR::Net_UserAgent_MobileをDIコンテナから呼び出す

Posted under Symfony2 by uechoco on 木曜日 9 12月 2010 at 00 : 01 : 18

この記事は、Symfony アドベントカレンダー 2010 に参加しています。

Symfony2も先日PR4バージョンがリリースされて、ベータ版のリリースも近くなってきているようです。Symfony2はphp 5.3.2以上を必要とする新しいフレームワークに生まれ変わりますが、まだまだphp4時代の遺産にはお世話になるんではないかと思っています。特にお世話になるのはPEARですよね。今回は、Symfony2でのPEARライブラリを使用してみる例として、日本人phperの誰もがお世話になってる(言い過ぎか?)PEAR::Net_UserAgent_MobileをSymfony2で扱う方法を模索してみます。「模索」と言っているのは、私自身がSymfony2歴が非常に浅いという意味と、エラーをどのように回避していったかの軌跡をしっかりとお見せしようと思っているためです。対象バージョンはgithubの最新ではなく、Symfony2 PR4のsandboxです。

それでは進めていきましょう。

PEARライブラリの配置

まずはPEAR::Net_UserAgent_MobileとPEAR.phpをダウンロードし、以下のように/src/vendorにpearディレクトリを作成して中に配置します。

pearディレクトリの配置場所

pearディレクトリの中身の様子

autoload.phpの編集

PEARライブラリに対してパスを通すため、/src/autoload.phpを編集します。

// /src/autoload.php の下部

php:
  1. $loader->registerPrefixes(array(
  2.     'Swift_' => $vendorDir.'/swiftmailer/lib/classes',
  3.     'Twig_'  => $vendorDir.'/twig/lib',
  4.     'Net_'   => $vendorDir.'/pear', // added prefix for PEAR::Net_XXX libraries
  5. ));
  6. $loader->register();
  7.  
  8. // hack for some PEAR libraries
  9. set_include_path(get_include_path().PATH_SEPARATOR.$vendorDir.'/pear');

上のコードでは、「Net_」という文字から始まるクラス名の場合の起点ディレクトリをregisterPrefixes()メソッドで指定しています。PEARライブラリには「PEARである」という接頭辞がないため、ちょっと気持ちが悪い指定の仕方になっています(本当は接頭辞をNet_と指定したらNetディレクトリを起点に登録するか、PEARまるごと起点ディレクトリを登録できると嬉しい)。

また、多くのPEARライブラリは、PEARの起点ディレクトリからのパスを用いてrequire_onceすることが多いため、起点ディレクトリをinclude pathに登録する必要があります(「require_once 'Net/UserAgent/Mobile/Error.php';」といったインクルードに対応するためです)。

実はこれだけでもうクラスパスが通ったので使えるようになりました。おめでとう!これにて一件落着!
...というところで終わってしまったらこの記事の価値は全くありません(笑 Symfony2らしく、DIコンテナからNet_UserAgent_Mobileクラスのインスタンスを取り出せるようにHelloBundleを拡張してみます。(ここからちょっとハードルが上がります)

HelloExtensionの作成

Symfony2のDI機構には、Extensionと呼ばれる仕組みがあり、バンドル毎にDIコンテナに対する初期設定などを行うことができます。HelloBundleであれば、以下のようにHelloBundleディレクトリ直下にDependencyInjectionディレクトリを作成しHelloExtension.phpを配置すると、設定ファイルに記載があれば自動的に読み込まれるようになっています。

// /src/Application/HelloBundle/DependencyInjection/HelloExtension.php

php:
  1. <?php
  2.  
  3. namespace Application\HelloBundle\DependencyInjection;
  4.  
  5. use Symfony\Component\DependencyInjection\Extension\Extension;
  6. use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
  7. use Symfony\Component\DependencyInjection\ContainerBuilder;
  8.  
  9. use Symfony\Component\DependencyInjection\Definition;
  10. use Symfony\Component\DependencyInjection\Reference;
  11.  
  12. /*
  13.  * This source file is subject to the MIT license that is bundled
  14.  * with this source code in the file LICENSE.
  15.  */
  16.  
  17. /**
  18.  * HelloExtension.
  19.  *
  20.  * @author uechoco
  21.  */
  22. class HelloExtension extends Extension
  23. {
  24.     /**
  25.      * Loads the Hello Bundle configuration.
  26.      *
  27.      * @param array            $config    An array of configuration settings
  28.      * @param ContainerBuilder $container A ContainerBuilder instance
  29.      */
  30.     public function configLoad($config, ContainerBuilder $container)
  31.     {
  32.         $def = new Definition('Net_UserAgent_Mobile');
  33.         $def->setFactoryMethod('factory');
  34.         $container->setDefinition('net.useragent.mobile', $def);
  35.     }
  36.  
  37.     /**
  38.      * Returns the base path for the XSD files.
  39.      *
  40.      * @return string The XSD base path
  41.      */
  42.     public function getXsdValidationBasePath()
  43.     {
  44.         return __DIR__.'/../Resources/config/schema';
  45.     }
  46.  
  47.     public function getNamespace()
  48.     {
  49.         return 'http://labs.uechoco.com/schema/dic/hello';
  50.     }
  51.  
  52.     public function getAlias()
  53.     {
  54.         return 'hello';
  55.     }
  56. }

このコードが何をしているのかを一言で言えば、DIコンテナがNet_UserAgent_Mobileを勝手に管理してくれるように登録しています。Net_UserAgent_Mobileクラスのサービスを定義し、ファクトリメソッドとして、factoryを設定し、net.useragent.mobileという識別子で登録しています。

また、getAlias()メソッドでHelloExtensionのエイリアスとしてhelloを指定していますが、これは次のconfig設定で使用されます。

config.ymlの修正

さきほど作成したHelloExtensionが自動的に読み込まれるには、/app/config/config.ymlにHelloExtensionの設定を記述する必要があります。

// /app/config.config.yml に追記

TEXT:
  1. # HelloBundle Configuration
  2. hello.config: ~

最後のチルダも忘れずに。

設定ファイルの読み込みの仕組みをここで詳しく説明はしませんが、例えばhello.configという記述があると、「hello」エイリアスのExtensionの、「config」Load()メソッドが自動的に呼び出される仕組みにあっています。これで先程のサービス定義のconfigLoad()メソッドが呼ばれるように成ります。

HelloContorllerから使ってみる

HelloControllerのindexActionを以下のように修正して、Net_UserAgent_Mobileクラスでモバイルかどうかを判定した真偽値をテンプレートに渡してあげます。

// /src/Application/HelloBundle/Controller/HelloController.php

php:
  1. public function indexAction($name)
  2.     {
  3.         $agent = $this->container->get('net.useragent.mobile');
  4.  
  5.         return $this->render('HelloBundle:Hello:index.twig', array('name' => $name, 'non_mobile' => $agent->isNonMobile()));
  6.  
  7.         // render a PHP template instead
  8.         // return $this->render('HelloBundle:Hello:index.php', array('name' => $name));
  9.     }

そして、twigテンプレートにも少し修正を加えます。

// /src/Application/HelloBundle/Resources/views/Hello/index.twig

TEXT:
  1. {% extends "HelloBundle::layout.twig" %}
  2.  
  3. {% block content %}
  4.     {% if non_mobile %}
  5.     <strong>non mobile!</strong>
  6.     {% else %}
  7.     <strong>mobile!</strong>
  8.     {% endif %}
  9.  
  10.     Hello {{ name }}!
  11. {% endblock %}

Webからアクセスしてみる

これで本来なら動くはずです。Webからアクセスしてみましょう。私の場合は以下のようなURLです。

http://sandbox-pr4.localweb02/app_dev.php/hello/uechoco

※uechocoの部分は、好きな英数字に置き換えても構いません。

すると、以下のようなエラーが発生して動きません。

「call_user_func() expects parameter 1 to be a valid callback, non-static method Net_UserAgent_Mobile::factory() should not be called statically」

原因を探る

困りました。何がいけなかったのか、エラーが発生したDIコンテナの該当部分を見てみます。

// /app/cache/dev/appDevDebugProjectContainer.php

php:
  1. protected function getNet_Useragent_MobileService()
  2.     {
  3.         return $this->services['net.useragent.mobile'] = call_user_func(array('Net_UserAgent_Mobile', 'factory'));
  4.     }

どうやらstatic宣言していないメソッドを静的に呼ぼうとしているのがいけないようです。php4自体のコードを引き継いでいるのでしょうがないですよね。static宣言をしていないメソッドを静的に呼び出すための一番容易な解決策は、エラー制御演算子付きで呼び出すことです。call_user_func()の前に@マークが付けられれば問題ないのですが、このコードはSymfony2の内部で自動的に生成されるコードなのですが、エラー制御演算子をつけるオプションはないようです。

※今回はエラー制御演算子の使用の是非の議論はしません。単純かつ分かりやすい解決策として採用しています。

Net_UserAgent_Mobile::factory()メソッドを呼び出すラッパークラスを作成

幸いにも、Symfony2のサービス定義には、別のサービスのメソッドを用いてインスタンスを生成するような指定方法があります。つまり、ラッパークラスを作ってエラー制御演算子付きでfactory()メソッドを呼び出すようなメソッドを持たせて、そのラッパークラス経由でNet_UserAgent_Mobileのインスタンスを生成するようにしてやります。言葉だと分かりにくいと思うので、ソースコードで語りますw

まずはラッパークラスを作成します。Utilディレクトリはつくってください。
// /src/Application/HelloBundle/Util/PEARClassLoader.php

php:
  1. <?php
  2.  
  3. namespace Application\HelloBundle\Util;
  4.  
  5. /*
  6.  * This source file is subject to the MIT license that is bundled
  7.  * with this source code in the file LICENSE.
  8.  */
  9.  
  10. /**
  11.  * class loader for PEAR classes
  12.  *
  13.  * @author uechoco
  14.  */
  15. class PEARClassLoader
  16. {
  17.     /**
  18.      * Load the Net_UserAgent_Mobile class.
  19.      *
  20.      * @return Net_UserAgent_Mobile
  21.      */
  22.     public function factoryNet_UserAgent_Mobile()
  23.     {
  24.         return @\Net_UserAgent_Mobile::factory();
  25.     }
  26. }

気をつけなければならないのは、Symfony2にとってNet_UserAgent_Mobileクラスはデフォルト名前空間で定義されています。このラッパークラスの名前空間とは異なりますので、エラー制御演算子とクラス名の間にバックスラッシュを入れるのを忘れないでください。

HelloExtensionの修正

続いて、DIコンテナがラッパークラスからインスタンスを生成するようにHelloExtensionのサービス定義を修正します。

php:
  1. /**
  2.      * Loads the Hello Bundle configuration.
  3.      *
  4.      * @param array            $config    An array of configuration settings
  5.      * @param ContainerBuilder $container A ContainerBuilder instance
  6.      */
  7.     public function configLoad($config, ContainerBuilder $container)
  8.     {
  9.         $def = new Definition('Application\\HelloBundle\\Util\\PEARClassLoader');
  10.         $def->setFile(__DIR__.'/../Util/PEARClassLoader.php');
  11.         $container->setDefinition('pear.classloader', $def);
  12.  
  13.         $def = new Definition('Net_UserAgent_Mobile');
  14.         $def->setFactoryService('pear.classloader');
  15.         $def->setFactoryMethod('factoryNet_UserAgent_Mobile');
  16.         $container->setDefinition('net.useragent.mobile', $def);
  17.     }

新たにラッパークラスのサービス定義を追加しています。またNet_UserAgent_Mobileクラスのサービス定義は、ラッパークラスのfactoryNet_UserAgent_Mobile()メソッドからインスタンスを作成するように変更しています。

ちなみに、どうやってこれらのメソッドを見つけ出したかというと、DIコンテナのphpファイルを吐き出すPhpDumperクラスとDefinitionクラスと交互に見て、どのように定義すればどのようにDIコンテナが吐き出されるのかを追って見つけ出しました。

再びWebからアクセスしてみる

これで動くはずです。Webからアクセスしてみましょう。私の場合は以下のようなURLです。

http://sandbox-pr4.localweb02/app_dev.php/hello/uechoco

※uechocoの部分は、好きな英数字に置き換えても構いません。

PCから見ると「non mobile」と表示されます

FirefoxでFireMobileSimulatorでP903iをシミュレートすると「mobie」と表示されます

おまけ:自動生成されたDIコンテナのサービス定義を見る

一応インスタンス生成部分のコードを再確認しておきましょう。

// /app/cache/dev/appDevDebugProjectContainer.php

php:
  1. /**
  2.      * Gets the 'pear.classloader' service.
  3.      *
  4.      * This service is shared.
  5.      * This method always returns the same instance of the service.
  6.      *
  7.      * @return Application\HelloBundle\Util\PEARClassLoader A Application\HelloBundle\Util\PEARClassLoader instance.
  8.      */
  9.     protected function getPear_ClassloaderService()
  10.     {
  11.         require_once '/mnt/hgfs/localweb02/sandbox-pr4/src/Application/HelloBundle/DependencyInjection/../Util/PEARClassLoader.php';
  12.  
  13.         return $this->services['pear.classloader'] = new \Application\HelloBundle\Util\PEARClassLoader();
  14.     }
  15.  
  16.     /**
  17.      * Gets the 'net.useragent.mobile' service.
  18.      *
  19.      * This service is shared.
  20.      * This method always returns the same instance of the service.
  21.      *
  22.      * @return Net_UserAgent_Mobile A Net_UserAgent_Mobile instance.
  23.      */
  24.     protected function getNet_Useragent_MobileService()
  25.     {
  26.         return $this->services['net.useragent.mobile'] = $this->get('pear.classloader')->factoryNet_UserAgent_Mobile();
  27.     }

ラッパークラスのインスタンス生成部分と、そのインスタンスを用いてfactoryNet_UserAgent_Mobile()メソッドを呼び出す部分がちゃんと動いていそうです。

まとめ

こんな感じでSymfony2でもPEARライブラリを使うことができそうです。使用するPEARライブラリが増えても、ある程度まではPEARClassLoaderのメソッドを増やして対応していくことで対応できそうです。

今回は既存のBundleに組み込む形で対応しました。PEARライブラリごとにBundleを作れば楽なんじゃないかという思いもあったのですが、それだとset_include_path()に登録するディレクトリが増えてしまいますので、PEARBundleとかでまとめてしまったほうが賢いかも知れません。

また繰り返しになりますが、私自身のSymfony2歴が浅いので、こんな方法もあるんだよというくらいで捉えてもらえると助かります。

次のアドベントカレンダーの記事は再びchobi_eさんの予定です。よろしくお願いします!

Symfony Advent 2010であなたの記事を公開してみませんか?

Symfony Advent 2010では12月1日から12月24日までを使って日替わりでsymfonyでイイなと思った小さなtipsから内部構造まで迫った解説などをブログ記事にし て公開していくイベントです。参加についてはATNDで参加表明の上、Google GroupのSymfony Advent 2010に追加リクエストを送信ください。Symfony Advent 2010チーム一同、あなたの参加をお待ちしております。

日本Symfonyユーザー会
Symfonyアドベントカレンダー 2010

※Syfony Advent 2010はsymfony好きな有志で集まったチームです。


[CakePHP]モデルに振られる

Posted under CakePHP by uechoco on 日曜日 5 12月 2010 at 00 : 31 : 21

この記事は、CakePHP Advent Calendar 2010に参加しています。この記事は5日目です。

こんばんわ。uechocoです。CakePHP歴は3ヶ月くらいのbakerです。Symfonyアドベントカレンダー 2010に参加したノリでCakePHP Advent Calendar 2010にも参加表明してみました。

4日目はremoreさんによるRe: Best Practices in MVC Design with CakePHPでした。英語とか!英語とか!モデルに重点を置いてコードを組むってのは納得ですね。そういえば3日目はshin1x1さんによるModelとの付き合い方でした。CakePHPにおいてモデルは大事な存在です。モデルをどう使うかによって付き合い方も変わってくるわけですよね。

この2つの記事を見て私が思ったのは・・・モデルといい関係でお付き合いをしたいけれど、やっぱりモデルに振られることもあるよねってことです。僕のような初心者はカリスマモデルとお付き合いなんてしてもらえないですかね。そういうわけで、3ヶ月の間に、こんな風なことしたらモデルに振られたけど、こんなカンジで寄りを戻したっていうお話を紹介させていただきます。

モデルとの思い出を全部消そうとしたが断られた(truncate文が発行できない)

当然です。こんなことをしたら振られちゃいますよね。でもやっぱりこちらの都合で思い出を全部消したいことも有りますよね。こっそりモデルの部屋に忍びこんでいつでも消せるようにしとくのがいいと思います。

(モデルにはなぜかtruncateに対応する命令がないんですが、DataSource側にはちゃんとあるようです。AppModelにtruncate()ラッパー関数をつくると、データが抹消できるようになります。)

// AppModel

php:
  1. /**
  2.      * モデルで正しくTRUNCATE(truncate)を行う
  3.      *
  4.      * @return void
  5.      */
  6.     function truncate()
  7.     {
  8.         $db =& ConnectionManager::getDataSource($this->useDbConfig);
  9.         $db->truncate($this);
  10.     }

モデルとのデート中に下手こいたのでリセットしたかったが断られた(トランザクション命令が発行できない)

お付き合いはゲームではないのでリセットなんでできないんです。でもデートはその都度一発勝負なんて草食系の僕には怖いんです。こっそりモデルの部屋に忍び込んで時計の針を戻してやればリセット出来ますかね。

(モデルはトランザクションにも対応していないみたいなんです。でもトランザクション無しって困る場合もあります。DataSource側は対応しているようなのでAppModelにトランザクション用のラッパー関数を作ると、うまく動くようです。)

// AppModel

php:
  1. /**
  2.      * モデルで正しくTRUNCATE(truncate)を行う
  3.      *
  4.      * @return void
  5.      */
  6.     function truncate()
  7.     {
  8.         $db =& ConnectionManager::getDataSource($this->useDbConfig);
  9.         $db->truncate($this);
  10.     }
  11.    
  12.     /**
  13.      * モデルで正しくトランザクション(begin)を行う
  14.      *
  15.      * @return void
  16.      * @link http://d.hatena.ne.jp/bobchin/20080805/1217913768
  17.      */
  18.     function begin()
  19.     {
  20.         $db =& ConnectionManager::getDataSource($this->useDbConfig);
  21.         $db->begin($this);
  22.     }
  23.    
  24.     /**
  25.      * モデルで正しくトランザクション(commit)を行う
  26.      *
  27.      * @return void
  28.      * @link http://d.hatena.ne.jp/bobchin/20080805/1217913768
  29.      */
  30.     function commit()
  31.     {
  32.         $db =& ConnectionManager::getDataSource($this->useDbConfig);
  33.         $db->commit($this);
  34.     }
  35.    
  36.     /**
  37.      * モデルで正しくトランザクション(rollback)を行う
  38.      *
  39.      * @return void
  40.      * @link http://d.hatena.ne.jp/bobchin/20080805/1217913768
  41.      */
  42.     function rollback()
  43.     {
  44.         $db =& ConnectionManager::getDataSource($this->useDbConfig);
  45.         $db->rollback($this);
  46.     }

(これらのメソッドは、CakePHP 1.2 でトランザクション - bobchinの日記を参考にしています。)

モデルとのデートを頭の中で整理しながら思い出させたら何回デートしたか曖昧になってた(paginateでgroup byしたときにcount値が変になる

デートの思い出を振り返っているとき、動物園には1回行って、遊園地には1回行って・・・あれ、ホントに1回だっけ?って言われた時、悲しいですよね。ホントは動物園は3回だし、遊園地は2回だし。こんな時は、うまくフォローしてあげるのもいいお付き合いの秘訣だと思います。

(paginateの時にgroup byが含まれているようなパラメータを与えているとき、paginateCount()がどうも値がずれていることがありました。そんな時はpaginateCount()関数をAppModelにつくってうまく動くようにすれば直ります。)

// AppModel

php:
  1. /**
  2.      * group by に対応した pagenateCount
  3.      *http://d.hatena.ne.jp/aroundthedistance/20090728/1248784179
  4.      *
  5.      * @params array $conditions 検索条件
  6.      * @params integer $recursive 再帰階層数
  7.      * @params array $extra 追加条件
  8.      */
  9.     function paginateCount($conditions = null, $recursive = 0, $extra = array())
  10.     {
  11.         $params = compact('conditions');
  12.         $this->recursive = $recursive;
  13.         $count = $this->find('count', array_merge($params, $extra));
  14.         if (isset($extra['group'])) {
  15.             $count = $this->getAffectedRows();
  16.         }
  17.         return $count;
  18.     }

(このpagenateCount()は、Group Byしている時にpaginator-&gt;number()が表示されない件 - Life is Really Short, Have Your Life!!という記事を参考にしています。記事名のとおり、group byしたときの個数がおかしくなってしまうので、正しい値を取るための工夫になっています。ちなみにpaginateCount()だけじゃなくて、paginate()というメソッドをモデルに作ってもページネーション時にコントローラからそのメソッドが呼ばれるようになります。)

Datumという名前のモデルと付き合ったら不幸な目にあった(Datumモデルをつくると途中でつまずいた

Datumっていう名前のモデルと付き合っていたことがあったんです。最初のうちはうまく付き合えていたんですけど、関係が続いていくうちに、なぜか急に言うことを聞いてくれなくなって、結局そのまま別れてしまいました。

("data"テーブルに対応する、"Datum"モデルを作っていたんです。dataの単数形はdatumなんですよ!とりあえず、find()とか発行できて問題なく動いていたんですが、いろいろといじっていたら、突然動かなくなりました。
Cakeのコードを追っていったらDataSourceの中か忘れましたがリレーション系の処理で実際のテーブル名で処理する場所があったんです。つまり、あるモデルにひもづいたDatumモデルを扱うときに、$Model->data->find()的な感じでアクセスされたんです。でも、Modelって$dataって変数持ってるじゃないですか。
つまり、$data変数と、Datumモデルの$dataインスタンスが混同してしまって動かなくなったんです。さすがにどうしようもなくて、テーブル名もモデル名も変更することで回避しました。)

まとめ

とりあえず、3ヶ月くらいでモデルに振られたり、喧嘩したりした特に思い出深い出来事を上げてみました。でもちゃんとうまく取り計らってモデルとはいいお付き合いを続けています。

なんだかんだいったって、モデルのこと好きなんです。愛してるよーーー\(^o^)/

できれば

モデルみたいにかわいい彼女募集しています\(^o^)/

最後に

だいぶ遊びすぎました^^;;ごめんなさい。俺はこんなふうに振られたぜ!とか、もっといい回避方法があるよ!とかそういう議論とかに発展すると嬉しです。ちなみにCakePHP 1.3系で動作確認しています。さてさて、CakePHP Advent Calendar 2010の6日目はtfmagicianさんです。まだまだモデルの話は続くのかな?楽しみです!


次ページへ »

Copyright © 2012 うえちょこ@ぼろぐ. WP Theme created by Web Top.