GANCHIKU.com

conditionalCacheFilterをより一般的に考えてみる

2009年10月24日

はじめに

symfonyでアプリケーションを作ろうとする際にキャッシュを使っていますか?キャッシュと一言に言ってもいろんなレイヤーでキャッシュを実現する方法があるので、なんとも答えに困る質問ですね。しかし、webアプリを作る際に言われているキャッシュであれば、

  1. apcなどでphpスクリプトをキャッシュ
  2. ページ出力内容を静的ファイルもしくは、メモリ上にキャッシュ
  3. クライアントのブラウザ側に残してもらうキャッシュ

等が一般的に考えられるのではないかと思います。おそらく、opcodeに関しては、多くの人はapcなどを使うとしても、チューニングの設定等を意識する必要はあると思いますが、実際に中でどう動いているのかを考えることが少ないと思います。「普通のやつらの下を行く」ことを目的とするユーザ(下のレイヤーで勝負するという意味)はそちらを調べてもらうとして、Webアプリ作成屋としては一番腕の見せ所は2番目ではないかと思います。つまり、どの出力をキャッシュ化して、どの出力をキャッシュ化しない、といったようなことです。そして、キャッシュを絶妙のタイミングでクリアさせることです。これによって、データベースへの接続の際のクエリーが激減させることができますので、大幅なパフォーマンスの改善が望めるからです。

今回のポストでは、この2番目の内容をもう少し詳細まで扱おうと思います。

問題

symfonyでは、cacheをサポートしており、cache.ymlで指定させることによって、次に上げるようなページキャッシュが可能となっています。

  1. layoutを含むキャッシュなのか、そうでないのか、という指定
  2. キャッシュの有効期間の指定
  3. partial, component等のページ全体ではなくて、一部のみのキャッシュの指定

これでだいたいの場合はキャッシュの使用として解決ができると思います。しかし実際にアプリケーションを作成していると、もう少し詳細なキャッシュ機構があったらいいな、というときがあります。たとえば、Jobeetのfeedの章であったようなことです。つまり、同じactionを使用するが、sf_formatの値によって、出力をatomにするのか、htmlにするのか、切り分ける場合です。atomの出力とhtmlの出力で同じキャッシュの指定をしたいですか?もし、同じでいいのであればいいのですが、私は分けたいと思いました。htmlの方ではログインしているかどうかも関係してくるのですが、atomの場合は、ログインしていようがいまいが同じ出力を返すからです。

同様に、ログインしているかどうかによって切り分けるページもあるでしょう。例えば、ログインしていると、ヘッダに、「hogehogeさん」って名前が出てくるようなものです。見せるコンテンツはログインしていようがいまいが同じだけども、ログインしていない際にはページ全体をキャッシュさせたいと思いますし、ログインしていたら、キャッシュ化させる内容は分けたいと思います。現在は、同じページでwith_layoutとwithout_layoutで切り分けることはできません。なので、ログインしてない際にはキャッシュを有効にさせ、ログインしている際にはページを無効にさせるという方法を取ります。

これらの内容は、cache.ymlだけでは実現が不可能ですので、他の方法が必要となります。

方法

symfonyにおいて、キャッシュを使う方法はいくつか用意されています。一番大きな方法としては、cache.ymlで指定することです。次にviewの中でキャッシュを使用するかどうかを指定することでしょう。さらにcacheManagerを通してキャッシュのロジックを変更することができます。キャッシュを自分の好きなタイミングでクリアしたり、有効にしたりすることができます。このcacheManagerを通してキャッシュを使用するには、filterが一番相性がいいようです。クリアするのは、データの変更後などになりますので、actionに書くことになってしまいますが、requestによってキャッシュを使用するしないを変更するにはfilterを使うことになります。The Definitive Guilde to symfonyのcacheの章のconditionalCacheFilterのような方法です。今回のポストでは、このconditionalCacheFilterをもう少し拡張してみることにします。

ソースコード

今回使用したsymfonyのバージョンは、1.3.0-ALPHA2です。1.2系は同じようにできると思いますが、1.0系はおそらく実装が異なると思います。

/apps/your_apps/config/filters.yml

rendering: ~
security:  ~

# insert your own filters here
conditionalCacheFilter:
  class: conditionalCacheFilter
  param:
    pages:
#      - { module: post, action: index, format: [atom,xml] }
#      - { module: post, action: index, format: [xml], enabled: false }
      - { module: post, format: [xml], enabled: true , is_authenticated: false }

cache:     ~
execution: ~

/apps/your_app/lib/conditionalCacheFilter.class.php

class conditionalCacheFilter extends sfFilter
{
  protected
    $cacheManager    = null,
    $request         = null,
    $user            = null,
    $uri             = null,
    $defaultLifetime = null;

  public function initialize($context, $parameters = array())
  {
    parent::initialize($context, $parameters);

    $this->cacheManager    = $context->getViewCacheManager();
    $this->request         = $context->getRequest();
    $this->user            = $context->getUser();
    $this->uri             = $context->getRouting()->getCurrentInternalUri();

    $lifetime = (isset($this->cacheManager)) ? $this->cacheManager->getLifeTime($this->uri) : 0;
    $this->defaultLifetime = ($lifetime > 0) ? $lifetime : 86400;
  }

  public function execute($filterChain)
  {
    if ((!isset($this->cacheManager)) or !($this->getParameter('pages', false))) {
      $filterChain->execute();
      return;
    }

    foreach ($this->getParameter('pages') as $page) {

      $module = isset($page['module']) ? $page['module'] : null;

      $action = isset($page['action']) ? $page['action'] : $this->request->getParameter('action');

      $format = isset($page['format']) ? $page['format'] : array();
      if (!is_array($format)) {
        $format = array($format);
      }

      $is_authenticated = isset($page['is_authenticated']) ? $page['is_authenticated'] : null;

      // maybe better to throw exception?
      if (is_null($module)) {
        continue;
      }
      // skip
      if ($module !== $this->request->getParameter('module')) {
        continue;
      }
      // skip
      if (!empty($format) and !in_array($this->request->getParameter('sf_format', 'html'), $format)) {
        continue;
      }

      if (is_null($is_authenticated) or ($is_authenticated === $this->user->isAuthenticated())) {
        // remove
        if (isset($page['enabled']) and $page['enabled'] === false) {
          $this->cacheManager->remove($this->uri);
          continue;
        }

        //add
        $this->cacheManager->addCache($module, $action, array(
          'lifeTime' => isset($page['lifetime']) ? $page['lifetime'] : $this->defaultLifetime
        ));
      }
    }

    $filterChain->execute();
  }
}

解説

filters.ymlにて、cache filterが通る前にconditionalCacheFilterを呼びます。filterに渡すことのできるパラメターとしては、The Definitive Guilde to symfonyのcacheの章にあるようにpagesとします。そして、そこに配列として指定したいモジュール名とアクション(ない場合はそのモジュールに属すすべてのアクション)format, enabled, is_authenticated, lifetimeなどのパラメターを指定できるようにしました。is_authenticatedに関しては3つのパターンがありますね。つまり、認証済みの場合、非認証の場合、認証しているかどうかは関係のない場合です。

また、symfony1.3からは、yamlのバージョンの扱いが1.2になりますので、on/off, yes/noは使えなくなりますので、気をつけてください。true/falseで切り分けるようにしましょう。

そして、conditionalCacheFilter.class.phpにおいては、それぞれのパラメターによってキャッシュの使用について分けるようにしました。cacheManagerに関しましては、キャッシュを有効化していないとnullを持ちますので、そのチェックが必要です。デフォルトではenableとなっているページもこのfilters.ymlでenabledをfalseに指定することにより、毎回キャッシュをクリアするようにできますので、より柔軟にキャッシュの変更ができるようになっています。

まとめ

今回のポストでは、symfonyのキャッシュフレームワークを使用する際に役に立つtipsとしてconditionalCacheFilterを一般的な使用方法で使えるように拡張しました。この拡張によって、次の2つが可能となりました。

  1. sf_formatによってキャッシュをするかどうかを切り分ける
  2. ユーザが認証済みかどうかでキャッシュをするかどうかを切り分ける

sf_formatによってキャッシュをするかどうかを切り分けることによって、例えば、feedのようなxml形式のものはある程度キャッシュをしつつ、アプリケーションとしてwebで表示されるviewであるhtml形式のものはキャッシュを使用しなくするということが可能となります。xml以外にも考えられるものはjsonなどのAPI提供です。これらは特別な理由がない限りキャッシュ化されたものを返すのがいいと考えています。

また、ユーザが認証済みかどうかでキャッシュをするかどうかを切り分けることができることにより、認証をしていないユーザに返すレスポンスに速くなることでしょう。これはWebサイトに訪問してくれたユーザのみならず、Googleなどの検索エンジンのクローラーにも言えることです。クローラーによってアクセスさせるものに毎回DBアクセスをさせるのではなく、キャッシュを返して、定期的にそのキャッシュを更新させることによって、負荷を下げましょう。

拡張しようと思えば、その余地を残してくれているのがsymfonyフレームワークです。今回のconditonalCacheFilterは、特定のアプリケーションに依存するというよりは、より一般的なアプリケーションに必要な機能だと考えていますので、plugin化して、置いておくのもいいかもしれません。もちろんバグがあることもあると思いますので、それに関しては教えてください。

still working on routing cache issue

I’m getting know routing cache problem now, but still struggling how to fix this. :( In my last post, I introduced lazy_routes_deserialize is the way to avoid this problem. Well, I believe some is true, but not everything. After I fixed my factories.yml configuration for sfPatterRouting, it seems that the performance of the application got a lot better, and I no longer across the error messages which I was frustrated very much. :)

However, I ran the application for a couple days, and found different problem. As the symfony blog mentioned , it generates too many routing cache files!!!!! It was more than 600000. 600000 in one directory is not very healthy. So, I changed my idea to use lazy_routes_deserialize. Instead, I decided to use sfAPCCache. The application was using APC anyway, so I thought put every routing cache in it. It worked fine, but I came across the exact same problem again, which is the size of routing cache gets huge! It ate almost 150M in 3 days, and it seems it is going to consume more in the near future. For now, I just run apache reload task one in a day, so it is working great. However, I don’t like the idea I’m using now. I have to find other strategies for this routing cache.

I think the base of this problem is my routing configuration. It gets so huge, and I need to find out better way to avoid it. The more specific URL you have, the bigger your routing cache gets. I understand it, but it seems the number it grows exponentially! :( It is not fair!

Is there any way to optimize my routing cache issue?

O.K, I found an answer. Do not use routing cache. Set sfNoCache instead.

symfony routing cache and memory error

2009年7月19日

I was struggling with this trouble for almost 2 months!!!! My current project uses symfony as always, but I had a big problem with memory issue.

I am engaging this project for almost a year. This application was first created with symfony1.1, but now it is compatible with 1.2. Currently it uses many only 1.2 features such as routing, cache, and etc. I was very satisfied with symfony1.2, except this memory problem.

There were many out of memory error lines in my httpd error log.

 PHP Fatal error:  Allowed memory size of 100663296 bytes exhausted (tried to allocate 5926568 bytes) in /xxxxx/cache/frontend/prod/config/config_core_compile.yml.php on line 3366

Because of this error, I increased memory_limit to 96M in php.ini. However, it was not enough. Plus, because my server was a bit old and has only 512M, it always uses swap, and gets really bad sometimes. I was so frustrated with this problem, and googled this a lot. It is still a guess, and I don’t know the solution I did was the exact one. BUT, It is working very good so far.

After I googled this issue, I found a couple similar topics in symfony forum as follows:

The first one was the problem with symfony1.2.4, and it said routing cache files size gets huge and takes too much memory to serialize it. At first, I did not know this post relates with my problem, because I was too lazy to look inside of line 3366 in config_core_compile.yml.php. After checking line 3366, I found it was the same problem. Too much memory consumption exceeded allowed memory size. One of this fix is using SQLite to handle this cache, but in my understanding, symfony1.2.7 provides different way to solve this problem, which you can find symfony 1.2.7 – more power.

all:
  routing:
    class: sfPatternRouting
    param:
      generate_shortest_url:            true
      extra_parameters_as_query_string: true
      lazy_routes_deserialize:          true
      lookup_cache_dedicated_keys:      true

I checked how this fix affects. This fix generate many routing cache files in cache/app_name/prod/config/routing/. The file size was not huge, but number of files is huge, as the symfony blog says. I might need to use different storage for this cache handling, but now the memory error is disappeared. I am very happy with it. :)

Man, i should have noticed symfony1.2.7 fix. :(

BUT, it did not fix, but it is working now, The answer is “Do not use routing cache. Set sfNoCache instead.”

Don’t want .html file extention for my link!

To make the symfony1.1 base application compatible with symfony1.2 is not very difficult. Because symfony1.2 considers backward compatibility. If you look inside of symfony, you will find many strange implements, such as link_to1, link_to2. In this case, link_to1 is for symfony1.1 base application, and link_to2 is for symfony1.2.

It made us easy to move on to symfony1.2. And I also upgrade the application I developed with symfony1.1 to symfony1.2. One of the new symfony1.2 feature is routing. Yeah! When I did Jobeet tutorial, it took a bit time to understand the new routing framework. I understand it after I implemented askeet with symfony1.2. :) When I developed askeet with symfony1.2, I used pure symfony1.2, so the generated codes are symfony1.2 base, and did not have any problems. However, an application I’m currently working on was developed with symfony1.1 first. It works with symfony1.2 now , but routing ruling is still syfmony1.1 base, and there are other symfony1.1 base code as well. :(

One of the routing features which added in symfony1.2 is sfPropelRouteCollection. Before the emergence of the useful class, I had to write many CRUD and more rule in routing.yml, but sfPropelRouteCollection helped me a lot to implement model base module. I was trying my symfony1.1 base application to upgrading routing rule for symfony1.2. After adding sfPropelRouteCollection in routing.yml, it seemed it is working great, and result of app:routes command is just what I was expected. :) However, urls displayed converted with link_to or url_for are not what I wanted. :( These helper functions add “.html” for urls. I don’t want them! :( I struggled to removing this for a bit. Well, It was easy, but it took me two hours to find this fix. When I found the solution, I was so happy that I could not write post related with my problems.

The problem is generate_shortest_url and extra_parameters_as_query_string values in factories.yml. Well, if you used symfony1.2 to generate application using generate:app, you do not have this problem, because symfony1.2 generate factories.yml with this values. However my application was generated with symfony1.1 almost half year ago, and generate_shortest_url and extra_parameters_as_query_string was not in factories.yml.

As you find in symfony1.2 base application’s factories.yml, you need following values in your symfony1.1 base factories.yml.

all:
  routing:
    class: sfPatternRouting
    param:
      generate_shortest_url: true
      extra_parameters_as_query_strings: true

O.K, that’s it! :) It is very simple, if you know the reason.

Playing with sfForm with Askeet Part 2

It’s another post related with sfForm. After using symfony1.2 several months, I found mastering sfForm is crucial for symfony developers. :) I’m still learning it, too. :)

In this post, I will explain QuesionForm, QuestionTagForm. Maybe I should also explain BackendQuestionForm, because QuestionForm uses tag form with auto complete, and BackendQuestionForm does not, and you might want to choose whether you use it or not. Well, I do! ;)

O.K, let’s clarify the specification of posting question and tagging. In askeet, you can only post a question from question posting page. Yeah, as it is. Look at the image below. This is the question posting form.

However, tagging is different. You can set tags when you post a question as an author of the question. Plus, if you are authenticated user, you can set/add tag for the questions no matter who post the question. So, there are two ways for tagging. The one you see above, which is tagging with posting question, and the form you found in sidebar when you logined askeet and go to the question detail page as a below image:

To explain how to do it, I use following codes:

  • lib/form/QuestionForm.class.php
  • lib/form/QuestionTagForm.class.php
  • lib/form/BackendQuestionForm.class.php
  • lib/form/sfWidgetFormProtoculousAutocompleter.class.php
  • apps/frontend/modules/question/actions/actions.class.php(executeNew, executeCreate)
  • apps/frontend//modules/question/templates/newSuccess.php
  • apps/frontend/modules/tag/actions/actions.class.php(executeCreate, executeAutocomplete)
  • apps/frontend/modules/tag/components/components.class.php
  • apps/frontend/modules/tag/templates/_add.php
  • apps/frontend/modules/tag/templates/autocompleteSuccess.php

O.K, let’s start with the posting question form.

lib/form/QuestionForm.class.php

class QuestionForm extends BaseQuestionForm
{
  protected $questionTagForm = null;
  public function configure()
  {
    unset(
      $this['created_at'],
      $this['updated_at'],
      $this['interested_users'],
      $this['reports'],
      $this['user_id'],
      $this['html_body'],
      $this['stripped_title'],
      $this['report_question_list'],
      $this['question_tag_list'],
      $this['interest_list']
    );

    $this->widgetSchema['title'] = new sfWidgetFormInput( array('label' => 'question:')
    );
    $this->validatorSchema['title'] = new sfValidatorString(
      array(),
      array('required' => 'You must give a title to your question')
    );

    $this->widgetSchema['body'] = new sfWidgetFormTextarea(
      array('label' => 'describe it:'),
      array('cols' => 40, 'rows' => 10)
    );
    $this->validatorSchema['body'] = new sfValidatorString(
      array('min_length' => 10),
      array(
        'required' => 'You must provide a brief context for your question',
        'min_length' => 'Please, give some more details'
      )
    );

    $this->setQuestionTagForm();
    $this->mergeForm($this->questionTagForm);
  }

  public function updateObject($values = null)
  {
    $object = parent::updateObject($values);
    if (is_null($object->getUserId())) {
      $userId = sfContext::getInstance()->getUser()->getSubscriberId();
      $object->setUserId($userId);
    }
    $interest = new Interest();
    $interest->setUserId($userId);
    $object->addInterest($interest);

    return $object;
  }

  public function doSave($con = null)
  {
    parent::doSave($con);

    $this->taintedValues['question_id'] = $this->object->getId();
    $this->questionTagForm->bind($this->taintedValues);
    $this->questionTagForm->doSave($con);
  }

  protected function setQuestionTagForm()
  {
    $this->questionTagForm = new QuestionTagForm(
      null,
      array('url' => sfContext::getInstance()->getController()->genUrl('tag_autocomplete'))
    );
  }
}

Check out doSave method. It is a bit tricky, because we need to save posted tags as well. When tagging, you are not going to set only one tag. You might want to tag more than one phrases. Also, using mergeForm does not call doSave for the merged form, so you have to override doSave method here. I need to bind the question_id for QuestionTagForm

In the end of configure method, I called setQuestionTagForm and mergeForm, because I will override setQuestionTagForm in BackendQuestionForm, not to use auto completing feature. See the BackendQuestionForm now.

lib/form/BackendQuestionForm.class.php

class BackendQuestionForm extends QuestionForm
{
  public function configure()
  {
    parent::configure();
    $this->widgetSchema['user_id'] = new sfWidgetFormInputHidden();
    $this->validatorSchema['user_id'] = new sfValidatorPropelChoice(array(
      'model' => 'User', 'column' => 'id', 'required' => false
    ));
  }

  protected function setQuestionTagForm()
  {
    $this->questionTagForm = new QuestionTagForm(null);
  }

  public function updateDefaultsFromObject()
  {
    parent::updateDefaultsFromObject();
    $tag = implode(" ", $this->object->getTags());
    $values['tag'] = $tag;
    if ($this->isNew) {
      $this->setDefaults(array_merge($values, $this->getDefaults()));
    } else {
      $this->setDefaults(array_merge($this->getDefaults(), $values));
    }
  }
}

As I saild above, I overrided the setQuestionTagFrom for without auto completing function.

O.K, now it’s about time for tagging.

lib/form/QuestionTagForm.class.php

class QuestionTagForm extends BaseQuestionTagForm
{
  public function configure()
  {
    unset(
      $this['user_id'],
      $this['normalized_tag'],
      $this['created_at']
    );

    $this->widgetSchema['question_id'] = new sfWidgetFormInputHidden();
    $this->validatorSchema['question_id'] = new sfValidatorPropelChoice(array('model' => 'Question', 'column' => 'id', 'required' => false));

    $url = $this->getOption('url');
    if (!empty($url)) {
      $this->widgetSchema['tag'] = new sfWidgetFormProtoculousAutocompleter(array(
        'label' => 'tags:',
        'url' => $url,
        'use_style' => true
      ));
    } else {
      $this->widgetSchema['tag'] = new sfWidgetFormInput(array(
        'label' => 'tags:'
      ));
    }
    $this->widgetSchema->setNameFormat('question[%s]');
  }

  public function doSave($con = null)
  {
    $tags = Tag::splitPhrase($this->taintedValues['tag'] . (sfConfig::get('app_permanent_tag') ? ' '.sfConfig::get('app_permanent_tag') : '' ));
    $question = QuestionPeer::retrieveByPk($this->taintedValues['question_id']);

    foreach ($tags as $tag) {
      try {
        $questionTag = new QuestionTag();
        $questionTag->setQuestionId($question->getId());
        $questionTag->setUserId($question->getUserId());
        $questionTag->setTag($tag);
        $questionTag->save($con);
      } catch (PropelException $e) {
        // do nothing
      }
    }
    // hmm, i don't like this... :(
    $this->object = $question;
  }
}

To create auto complete widget, I made sfWidgetFormProtoculousAutocompleter. There was JavaScript helper for auto complete. However, I do not want to use it, because I prefer to use sfForm. :) Also symfony1.2 integrated some useful function with jQuery. :) jQuery is a wonderful library, but I hate use two different JavaScript library in one application. If I use jQuery, I only use jQuery, and askeet has a lot of prototype base code, so I created autocompletor widget for sfProtoculousPlugin. :) Well, it was easy anyway.
So, if there was url option when intiating QuestionTagForm, use sfWidgetFormProtoculousAutocompleter. Without option, use another sfWigetFormInput.

Actually I do not like my implementation of doSave. As I saild above, you need to set more than one tag same time when posting. So, I need to save all the tags. Because saving more than one tag, i don’t know what to return. I just return the question object here. :(

Good thing about this QuestionTagForm, you can use this both mergeForm and only itself. As I mentioned above, there are two ways to tag, and both are using this class.

lib/form/sfWidgetFormProtoculousAutocompleter.class.php

class sfWidgetFormProtoculousAutocompleter extends sfWidgetFormInput
{
  protected function configure($options = array(), $attributes = array())
  {
    $this->addRequiredOption('url');
    $this->addOption('config', '{ }');
    $this->addOption('use_style');

    parent::configure($options, $attributes);
  }

  public function render($name, $value = null, $attributes = array(), $errors = array())
  {
    $response = sfContext::getInstance()->getResponse();
    $response->addJavascript(sfConfig::get('sf_prototype_web_dir').'/js/prototype');
    $response->addJavascript(sfConfig::get('sf_prototype_web_dir').'/js/effects');
    $response->addJavascript(sfConfig::get('sf_prototype_web_dir').'/js/controls');

    if ($this->getOption('use_style')) {
      $response->addStylesheet(sfConfig::get('sf_prototype_web_dir').'/css/input_auto_complete_tag');
    }
    return parent::render($name, '', $attributes, $errors) .
      content_tag('div' , '', array('id' => $this->generateId($name) . '_autocomplete', 'class' => 'auto_complete')) .
      sprintf('',
        $this->generateId($name),
        $this->generateId($name . '_autocomplete'),
        $this->getOption('url'),
        $this->getOption('config')
      );
  }
}

I was a bit lazy, so above code is not well written. :( That’s all for form code, and let’s see the action and templates from here.

/apps/frontend/modules/questions/actions/actions.class.php

// snip
 public function executeNew(sfWebRequest $request)
  {
    $this->form = new QuestionForm();
  }
  public function executeCreate(sfWebRequest $request)
  {
    $this->form = new QuestionForm();
    $this->form->bind($request->getParameter($this->form->getName()));
    if ($this->form->isValid()) {
      $this->form->save();
      $this->redirect($this->generateUrl('question_show', $this->form->getObject()));
    }
    $this->setTemplate('new');
  }
// snip

executeNew and executeCreate is very simple, and probably, you don’t need any instruction with this. Good thing about mergeForm is you don’t need to know you are using tag form in action class, because we have already merge it in QuestionForm class. :) You just render the quetion form, and bind and save it.

apps/frontend/modules/question/template/newSuccess.php

renderError() ?> renderLabel() ?>
renderError() ?> renderLabel() ?>
renderError() ?> renderLabel() ?>

View file is also simple. Because we merge the QuestionTagForm, you can use $form['tag'], here! O.K, now you are able to post a question with tagging with auto complete feature!

O.K, it is about time to write about tag form that you find in sidebar. We will use the same QuestionTagForm here. :) To add tag, you have to be authenticated and go to the question/show page. In the component slot, _question.php, you include tag/add component. This component displays the tagging form.

/apps/frontend/modules/tag/actions/components.class.php

class tagComponents extends sfComponents
{
  public function executeAdd(sfWebRequest $request)
  {
    $questionTag = new QuestionTag();
    $questionTag->setQuestionId($this->question_id);
    $this->form = new QuestionTagForm($questionTag, array('url' => $this->generateUrl('tag_autocomplete')));
  }
}

In the action, executeAdd, we set QuestionTag object with question_id. Plus auto_complete url is also set when initialize form class. As you can see the template file, this form is ajax form.

/app/frontend/modules/tag/templates/_add.php

isAuthenticated()): ?>
'tag_create', 'update' => 'question_tags', 'complete' => '$("ask_question_tag_tag").value=""' )) ?>

This form request action goes tag_create rule, which is tag/executeCreate action.

apps/frontend/modules/actions/tag/actions/actions.class.php

// snip
 public function executeCreate(sfWebRequest $request)
  {
    $form = new QuestionTagForm();
    $form->bind($request->getParameter($form->getName()));
    if ($form->isValid()) {
      // xxx this form->save returns qustion object
      $this->question = $form->save();
    } else {
      $this->question = $form->getObject()->getQuestion();
    }
    $this->tags = $this->question->getTags();
  }
// snip

As I mentioned above QuestionTagForm’s doSave method returns not QuestionTag object, but Question Object, which is not really nice I think. :( Anyway, let’s render the tags value to the view file and return value for ajax request. In createSuccess.php, it is same as tag/question_tags partial. What you need to do here is just updating the question tags list which has just tagged by you.

apps/frontend/modules/actions/tag/actions/actions.class.php

// snip
 public function executeAutocomplete(sfWebRequest $request)
  {
    $this->tags = array();
    $form = new QuestionTagForm();
    $form->bind($request->getParameter($form->getName()));
    if ($form->isValid()) {
      $values= $form->getTaintedValues();

      $this->tags = QuestionTagPeer::getTagsForUserLike(
        $this->getUser()->getSubscriberId(),
        $values['tag'],
        10
      );
    }
  }
// snip
Shin Ohno 2003-2012