GANCHIKU.com

Playing with sfForm with Askeet Part 1

It took time to write a new post. :( My current project had a tight schedule, and it is very important for me to work on this project. Sorry. ;)

So, in this post, I will explain some tips for sfForm using the code I implemented in Askeet. I will explain these two cases for form handling:

  • user login / registration switching
  • question posting with tags and tags posting(next post)

To explain the code, I will use following files.

  • lib/form/UserLoginForm.class.php
  • lib/form/UserMixForm.class.php
  • apps/frontend/modules/user/actions/actions.class.php(executeNew, executeLogin, executeCreate
  • apps/frontend/templates/layout.php
  • apps/frontend/modules/user/actions/components.class.php
  • apps/frontend/modules/user/templates/login.php
  • apps/frontend/modules/user/templates/newSuccess.php

Before I explain the code I need to clarify the specification of user login and registration process in Askeet. There are two ways to login Askeet:

  • For unauthorized users, when some actions require authentication, such as posting questions and answers, voting for interest, moderating and etc, the login form dynamically appears on the page. Like following image.
  • Following signin/register link lead you the page you can login and register. When login failed by both ways this page is used as well. Like following image.

O.K, let’s start from the first one. Because I’m using symfony1.2, I should use sfForm instead writing every input tags or tag_helper in the view template. I didn’t know how to implement such form, but I got a good idea from Exgear’s tutorial, and I followed the instruction.
Here, I wrote some code in UserLoginForm.class.php, layout.php, components.class.php, and its template, _login.php. The code are follows:

UserLoginForm.class.php

class UserLoginForm extends BaseUserForm
{
  public function configure()
  {
    unset(
      $this['id'],
      $this['created_at'],
      $this['first_name'],
      $this['last_name'],
      $this['email'],
      $this['sha1_password'],
      $this['salt'],
      $this['ask_interest_list'],
      $this['ask_relevancy_list']
    );
    $this->validatorSchema['nickname'] = new sfValidatorString(
      array('min_length' => 5),
      array('required' => 'your nickname is required', 'min_length' => 'nickname must be 5 or more characters')
    );

    $this->widgetSchema['password'] = new sfWidgetFormInputPassword();
    $this->validatorSchema['password'] = new sfValidatorString(
      array(),
      array('required' => 'your password is required')
    );
    $this->widgetSchema['referer'] = new sfWidgetFormInputHidden();
    $this->validatorSchema['referer'] = new sfValidatorPass();
    $request = sfContext::getInstance()->getRequest();
    if ($request->isMethod('get')) {
      $this->setDefault('referer', $request->getReferer());
    } else {
      $this->setDefault('referer', $request->getParameter($this->getName().'[referer]'));
    }

    // check valid user
    $this->validatorSchema->setPostValidator(new sfValidatorCallback(array(
      'callback' => array($this, 'validUser'),
    )));

    $this->validatorSchema->setOption('allow_extra_fields', true);
  }

  public function validUser($validator, $values)
  {
    if (empty($values['nickname']) or empty($values['password'])) {
      return;
    }
    $nickname = $values['nickname'];
    $c = new Criteria();
    $c->add(UserPeer::NICKNAME, $nickname);
    $user = UserPeer::doSelectOne($c);

    if (is_null($user) || sha1($user->getSalt().$values['password']) != $user->getSha1Password()) {
      throw new sfValidatorError($validator, 'no user available');
    }
    $this->object = $user;

    return $values;
  }
}

Well, this code is straightforward and not many tricks.If I have to say something, I would say using sfValidatorCallback to authorizing user request as post validator. Actually this idea is also from exgear’s site. They are really good. :)
BTW, some might think why I put allow_extra_fields there? Well, I will explain this later. As it is, extra fields are expecting for UserLoginForm.

So, create apps/frontend/modules/user/actions/components.class.php, and render the form object of the class.

components.class.php

class userComponents extends sfComponents
{
  public function executeLogin(sfWebRequest $request)
  {
    $this->form = new UserLoginForm();
  }
}

Simple! Huh! :)

Now its view file, apps/frontend/modules/users/templates/_login.php

_login.php

renderLabel() ?> renderLabel() ?> 0.5))) ?>

As I am a programmer, I do not like to write every echo $form['blabla'] in view files, but I was too lazy to think other way right now. :( I might try better ways later if I had any chances. :) Anyway, this is very easy, so no explanation here either. Blinding up animation is there, because it was found in the Askeet tutorial, and I liked the cool animation. :) I did not write any error message in this template, you will see why later.

Now you need to include this components in layout.php

layout.php

// snip


// snip

Now you can see the form container is invisible, and use link_to_function to will change the status to visible. When actions need user authentication, echo the next helper function, and the login form will appear!

link_to_function('some action label', visual_effect('blind_down', 'login', array('duration' => 0.5)));

O.K, that’s one way to login for user. It was longer than I thought. :) We didn’t reach the tricky tips yet. Now you might think “what about login error page? How do users know if he or she typed right nickname and password? “, Well, those error messages are displayed signin /register page. So, when user login failed, you have different login page, which I will explain from now.

For easy user registration and login, many web applications use the same form to login or registering. Askeet also use this strategy. The default page of signin/register page only displays nickname and password fields, and a checkbox. Yes, this checkbox having label as “click here to create a new account”, switches this form action to be login or registering. Look at the image again.

If you check the checkbox, two more fields will appear in the page like following image:

O.K, finally! I had struggled a bit to implementing this with sfForm. Askeet tutorial was easy, because the tutorial write the input helper directly. I’m using sfForm framework, and I want to avoid this. To implement this form, I created UserMixForm.class.php for this page. UserLoginForm.class.php was for only login, but this form class is for registration mainly. Maybe naming is not good, though :(
Anyway, I copy all the source of UserMixForm.class.php here:

class UserMixForm extends BaseUserForm
{
  public function configure()
  {
    unset(
      $this['id'],
      $this['created_at'],
      $this['first_name'],
      $this['last_name'],
      $this['sha1_password'],
      $this['salt'],
      $this['ask_interest_list'],
      $this['ask_relevancy_list']
    );

    $this->widgetSchema['nickname'] = new sfWidgetFormInput(array('label' => 'Nickname:'));
    $this->validatorSchema['nickname'] = new sfValidatorString(
      array('min_length' => 5),
      array('required' => 'your nickname is required', 'min_length' => 'nickname must be 5 or more characters')
    );

    $this->widgetSchema['password'] = new sfWidgetFormInputPassword(array('label' => 'Password:'));
    $this->validatorSchema['password'] = new sfValidatorString(
      array(),
      array('required' => 'your password is required')
    );
    $controller = sfContext::getInstance()->getController();
    $this->widgetSchema['new'] = new sfWidgetFormInputCheckbox(array(
      'label' => 'click here to create a new account'
      ),
      array('style' => 'display: inline; float: none',
        'onclick' => "
      if (Element.visible('new_account')) {
        Effect.BlindUp('new_account');
        $('login_form').action = '" . $controller->genUrl('login') . "';
       } else {
        Effect.BlindDown('new_account');
        $('login_form').action = '" . $controller->genUrl('user_create') . "';
       }
    "));
    $this->validatorSchema['new'] = new sfValidatorPass();

    $this->widgetSchema['password_biz'] = new sfWidgetFormInputPassword(
      array('label' => 'confirm your password')
        );
    $this->validatorSchema['password_biz'] = new sfValidatorString(
      array(),
      array('required' => 'password confirmation is required')
    );

    $this->widgetSchema['email'] = new sfWidgetFormInput(
      array('label' => 'your email')
    );
    $this->validatorSchema['email'] = new sfValidatorEmail(
      array('min_length' => 5),
      array(
        'required' => 'your email is required',
        'invalid' => 'email address is invalid'
      )
    );

    $this->validatorSchema->setPostValidator(new sfValidatorAnd(array(
      new sfValidatorSchemaCompare(
        'password', '==', 'password_biz', array(),
        array('invalid' => 'password and confirmed password does not match')
      ),
      new sfValidatorPropelUnique(
        array('model' => 'User', 'column' => array('nickname')),
        array('invalid' => 'the nickname is already taken.')
      ),
      new sfValidatorPropelUnique(
        array('model' => 'User', 'column' => array('email')),
        array('invalid' => 'the email address is areadly registered.')
      )
    )));

    $this->widgetSchema['referer'] = new sfWidgetFormInputHidden();
    $this->validatorSchema['referer'] = new sfValidatorPass();
    $request = sfContext::getInstance()->getRequest();
    if ($request->isMethod('get')) {
      $this->setDefaults(array('referer' => $request->getReferer()));
    }
  }

  public function updateObject($values = null)
  {
    $object = parent::updateObject($values);
    $password = $this->taintedValues['password'];
    $object->setPassword($password);
    return $object;
  }

  public function mergeStatus($loginform)
  {
    if (isset($loginform)) {
      $nickname = $loginform->taintedValues['nickname'];
      $referer = $loginform->taintedValues['referer'];
      $this->errorSchema = $loginform->getErrorSchema();
      $this->setDefault('nickname', $nickname);
      $this->setDefault('referer',  $referer);
    }
  }
}

Wow, this is long, but the longest method, configure, does not need any explanation I think. The method, updateObject call setPassword, because there is no password field for user model. There are sha1_password and sha1 fields. So, setPassword of user model set both fields. Actually it is same as Askeet tutorial, so I skip this, too. I just need to update the field, so I overrided the method.

What about mergeStatus method? Well, this is something new. Hehehe. :) Because this is my original idea. I did not refer any site to implement this, and I believe this is a tricky part. As I mentioned above, UserMixForm is actually for user registration. However, this form also handles user login and displaying login error messages as well. To succeed the login failure messages and input values, this method was needed. You will see the reason soon.

So, the signin/register page is assigned to executeNew method in actions. executeCreate is for binding, validating, and saving the user registration form. Finally executeLogin binding, validating login authentication, and if the validation failed it will render the error messages as well. O.K, let’s see the code :)

// snip
  public function executeNew(sfWebRequest $request)
  {
    if ($this->getUser()->isAuthenticated()) {
      $this->redirect('homepage');
    }
    $this->form = new UserMixForm();
  }

  public function executeCreate(sfWebRequest $request)
  {
    $this->form = new UserMixForm();
    $this->form->bind($request->getParameter($this->form->getName()));
    if ($this->form->isValid()) {
      $this->form->save();
      $this->forward('user', 'login');
    }
    // use user registration form in login template
    $this->getRequest()->setAttribute('newaccount', true);
    $this->setTemplate('new');
  }
  public function executeLogin(sfWebRequest $request)
  {
    $loginform = new UserLoginForm();
    $loginform->bind($request->getParameter($loginform->getName()));
    if ($loginform->isValid()) {
      $this->getUser()->signIn($loginform->getObject());
      $this->redirect($request->getParameter($loginform->getName() .'[referer]', 'homepage'));
    }
    $this->form = new UserMixForm();
    $this->form->mergeStatus($loginform);
    $this->setTemplate('new');
  }

// snip

The method, executeNew does not need any explanation. It just renders the UserMixForm object. I don’t want authorized user to access this page, so let them forward to homepage route. O.K. the method, executeCreate is also straightforward. It is just another form transaction (bind, validate, save). In case the validation failed, set newaccount attribute for checkbox to be checked, and use the same template as executeNew.

O.K, finally! We are in tricky part, executeLogin. Because UserMixForm and UserLoginForm are children of BaseUserForm class, both share same fields, such as nickname, password. So, UserLoginForm require nickname, password, referer, and the for send the parameters. UserMixForm also send nickname, password, referer(and other fields, too). Well, yes! UserMixForm has other fields. That’s why allow_extra_fields is on in UserLoginForm. If the user authentication failed, this action uses newTemplate which means using UserMixForm. You need to render the form value, and you need to set the error messages and input values from the last request to the form as well. To do this, you have to pass them from userLoginForm to UserMixForm. Here is mergeStatus method. I don’t know if this name is proper, though. :(

Oops, I forgot to mention newSuccess.php.

newSuccess.php

renderGlobalErrors() ?>
renderLabel() ?> renderError() ?>
renderLabel() ?>renderError() ?>  
 renderLabelName() ?>

getAttribute('newaccount', false) ? '' : ' style="display: none"' ?>> renderLabel() ?> renderError() ?>
renderLabel() ?> renderError() ?>

It is another usual form. No explanation.

Because there are difficulties to explain in English (Probably it would be hard to write this topic in Japanese anyway), I may fix some words or explanation I posted today.

Anway, that’s it. Wow, this is a long post, I have to split the tips to two posts. Hmm. Next is Question and QuestionTag form handling. Have fun! :) :) :) :)

コメントをどうぞ

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

*

次のHTML タグと属性が使えます: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Shin Ohno 2003-2012