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()
-
{
-
$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->validatorSchema['title'] = new sfValidatorString(
-
);
-
-
$this->widgetSchema['body'] = new sfWidgetFormTextarea(
-
);
-
$this->validatorSchema['body'] = new sfValidatorString(
-
'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);
-
$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,
-
);
-
}
-
}
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();
-
'model' => 'User', 'column' => 'id', 'required' => false
-
));
-
}
-
-
protected function setQuestionTagForm()
-
{
-
$this->questionTagForm = new QuestionTagForm(null);
-
}
-
-
public function updateDefaultsFromObject()
-
{
-
parent::updateDefaultsFromObject();
-
$values['tag'] = $tag;
-
if ($this->isNew) {
-
} else {
-
}
-
}
-
}
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()
-
{
-
$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');
-
'label' => 'tags:',
-
'url' => $url,
-
'use_style' => true
-
));
-
} else {
-
'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
-
{
-
{
-
$this->addRequiredOption('url');
-
$this->addOption('config', '{ }');
-
$this->addOption('use_style');
-
-
parent::configure($options, $attributes);
-
}
-
-
{
-
$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')) .
-
$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
-
-
<div class="in_form">
-
<p>
-
<?php echo __('Have you looked for similar questions? Check if a related question already exists: The more interesting a question is, the more people will be willing to answer it.') ?>
-
</p>
-
<p>
-
<?php echo __('Be as accurate as you can when giving a title to your question. Keep it short and put the details in the question body.') ?>
-
</p>
-
</div>
-
-
<form action="<?php echo url_for('question') ?>" method="post" class="form">
-
<fieldset>
-
<br class="clearleft" />
-
-
<br class="clearleft" />
-
-
<br class="clearleft" />
-
-
</fieldset>
-
-
<div class="right">
-
<input type="submit" value="<?php echo __('ask it') ?>" />
-
</div>
-
</form>
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
-
<?php if ($sf_user->isAuthenticated()): ?>
-
'url' => 'tag_create',
-
'update' => 'question_tags',
-
'complete' => '$("ask_question_tag_tag").value=""'
-
)) ?>
-
<input type="submit" value ="<?php echo __('tag') ?>" />
-
</form>
-
</div>
-
<?php endif; ?>
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)
-
{
-
$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

