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.
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:
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.
// 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.
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.
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.
// 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.