суббота, 13 февраля 2010 г.

Форма AJAX-авторизации

Задача: разместить форму авторизации, которая поставляется в комплекте с sfDoctrineGuardPlugin, на каждой странице сайта. А также обеспечить AJAX-авторизацию.

Пропустим установку и настройку sfDoctrineGuardPlugin, она хорошо описана в документации к плагину. Наша задача — вынести форму авторизации в отдельный компонент. Для определенности будем считать, что все действия мы делаем в приложении frontend. Сделать компонент очень просто: создаем класс sfGuardAuthComponents, пишем контроллер компонента signin, и три шаблона - вспомогательный signin, который принимает решение о том, что выводить пользователю - форму авторизации, или меню авторизованного пользователя (Здравствуйте, %username%! Выход.), и два основных - собственно форма авторизации и меню авторизованного пользователя. Итак, начнем.

Создадим файл apps/frontend/modules/sfGuardAuth/actions/components.class.php со следующим содержимым:

<?php
class sfGuardAuthComponents extends sfComponents
{
  function executeSignin()
  {
    $class = sfConfig::get('app_sf_guard_plugin_signin_form', 'sfGuardFormSignin'); 
    $this->form = new $class();
  }
}
?>

Контроллер компонента готов. Теперь нужно сделать три шаблона.
apps/frontend/modules/sfGuardAuth/templates/_signin.php

<?php if ($sf_user->isAuthenticated()): ?>
  <?php include_partial('sfGuardAuth/signin_auth_yes') ?>
<?php else: ?>
  <?php include_partial('sfGuardAuth/signin_auth_no',array('form'=>$form)) ?>
<?php endif ?>

Этот шаблон всего лишь решает, что вывести пользователю, в зависимости от того, авторизован тот или нет. Теперь сделаем шаблон _signin_auth_no.php, который выводит форму авторизации:
<script>
  $(function(){
    $("#signinForm").ajaxForm({
      url: '<?php echo url_for('@sf_guard_signin') ?>',
      type: 'post',
      dataType: 'json',
      beforeSubmit: function(){
        $("#signinSubmit").attr("disable",true);
        $("#signinLoader").show();
      },
      success: function(data) {
        $("#signinSubmit").attr("disable",false);
        $("#signinLoader").hide();
        if (data=='error')
        {
          //выводим сообщение об ошибке
          alert('Неверное имя пользователя или пароль');
        }
        else if (data=='ok')
        {
          //Заменям форму авторизации на меню авторизованного пользователя
          //
          
          //Или просто обновляем страницу
          document.location.reload();
        }
        else
        {
          alert('Неизвестная ошибка. Обратитесь к администрации');
        }
      }
    })
  })
</script>

<form action="<?php echo url_for('@sf_guard_signin') ?>" method="post" id="signinForm">
<a href="<?php echo url_for('@sf_guard_password') ?>">Забыли пароль?</a>
<?php echo $form->renderHiddenFields()?>
<?php echo $form['username']->renderError() ?>
<?php echo $form['password']->renderError() ?>

<span>Логин</span>

  <?php echo $form['username']->render() ?>
            
<span>Пароль:</span>
            
  <?php echo $form['password']->render() ?>

  <input name="" class="buton" value="" type="submit" id="signinSubmit"/>
  <img src="/images/loader.gif" style="padding-left:10px;display:none;" id="signinLoader">
</form>

Этот шаблон нуждается в подробном изучении. В шаблоне мы подготовили клиентскую часть для AJAX-авторизации: AJAX-изировали форму авторизации, написали обработчики для удачной и неудачной авторизации. Хочу обратить внимание на jQuery-плагин, который я здесь использую — jQuery Form Plugin. Этот плагин позволяет очень просто модифицировать любые формы (в том числе, содержащие input file) таким образом, что они отправляются с помощью XmlHttpRequest, и обрабатывать результат отправки. Вкратце остановлюсь на использовании плагина. Делаем совершенно обычную форму, затем вызываем $('форма').ajaxForm(опции). Вуаля! Форма готова к AJAX-отправке. Здесь не забываем добавить подключение jQuery-плагина в файл apps/frontend/config/view.yml

default:
  javascripts:    [jquery/jquery_forms.js]


Теперь напишем обработчик AJAX-запроса (контроллер модуля sfGuardAuth, который примет AJAX-запрос, попытается авторизовать пользователя, и, либо отправит сообщение о неверной авторизации, либо о том, что авторизация прошла успешно. Javascript в шаблоне, который мы только что написали, получает ответ, и реагирует на него в зависимости от присланных данных (data). В этом примере реакция максимально примитивна - в случае успеха страница перезагружается (после перезагрузки будет показываться уже не форма авторизации, а меню авторизованного пользователя), а в случае неверного логина-пароля выскакивает alert(). Итак, контроллер, обрабатывающий AJAX-запрос на авторизацию. Придется скопировать и изменить соответствующий экшен модуля sfGuardAuth плагина sfDoctrineGuardPlugin. Создаем файл apps/frontend/modules/sfGuardAuth/actions.class.php со следующим содержимым:

<?php

require_once(dirname(__FILE__)."/../../../../../plugins/sfDoctrineGuardPlugin/modules/sfGuardAuth/lib/BasesfGuardAuthActions.class.php");

/**
 *
 * @package    symfony
 * @subpackage plugin
 * @author     Evgeny Babin <psylosss@gmail.com>
 * @version    
 */
class sfGuardAuthActions extends BasesfGuardAuthActions
{
  public function executeSignin($request)
  {
    $user = $this->getUser();
    if ($user->isAuthenticated())
    {
      return $this->redirect('@homepage');
    }

    $class = sfConfig::get('app_sf_guard_plugin_signin_form', 'sfGuardFormSignin'); 
    $this->form = new $class();

    if ($request->isMethod('post'))
    {
      $this->form->bind($request->getParameter('signin'));
      if ($this->form->isValid())
      {
        $values = $this->form->getValues(); 
        $this->getUser()->signin($values['user'], array_key_exists('remember', $values) ? $values['remember'] : false);

        if ($request->isXmlHttpRequest())
        {
          return $this->renderText(json_encode('ok'));
        }
        
        // always redirect to a URL set in app.yml
        // or to the referer
        // or to the homepage
        $signinUrl = sfConfig::get('app_sf_guard_plugin_success_signin_url', $user->getReferer($request->getReferer()));
        //$signinUrl = $user->getReferer($request->getReferer());

        return $this->redirect('' != $signinUrl ? $signinUrl : '@homepage');
      }
      else
      {
        if ($request->isXmlHttpRequest())
        {
          return $this->renderText(json_encode('error'));
        }      
      }
    }
    else
    {
      // if we have been forwarded, then the referer is the current URL
      // if not, this is the referer of the current request
      $user->setReferer($this->getContext()->getActionStack()->getSize() > 1 ? $request->getUri() : $request->getReferer());

      $module = sfConfig::get('sf_login_module');
      if ($this->getModuleName() != $module)
      {
        return $this->redirect($module.'/'.sfConfig::get('sf_login_action'));
      }

      $this->getResponse()->setStatusCode(401);
    }
  }
}

Мы несколько изменили логику поведения экшена. Теперь, если он вызывается методом post (отправлена форма), то экшен дополнительно проверяет, а не AJAX-ом ли запрошена авторизация? Если да, то выдает JSON-ответ: 'error', если форма невалидная, и 'ok', если форма валидная. Именно этот ответ видит и понимает javascript в шаблоне _signin_auth_no.php.

Осталось совсем чуть-чуть: написать шаблон авторизованного пользователя _signin_auth_yes.php:

Вы вошли как <b><?php echo $sf_user->getGuardUser()->username ?></b>
<br>
<a href="<?php echo url_for('@sf_guard_signout') ?>">Выход</a>

Вот и готово. Используем в любом шаблоне:

<?php include_component('sfGuardAuth','signin') ?>

Полезная ссылка: генератор gif-иконок AJAX-загрузки

4 комментария:

  1. Спасибо, отличный пост.

    Единственный нюанс - яваскрипт хорошо бы сделать unobtrusive. Но это уже придирки :)

    ОтветитьУдалить
  2. Теперь сделаем шаблон _signin_auth_no.yml

    nenuzhno li zdes _signin_auth_no.php ?

    ОтветитьУдалить
  3. @develop7, яваскрипт, безусловно, поддается шлифовке :)

    @Malas, спасибо, поправил :)

    ОтветитьУдалить
  4. Доброго времени суток! пожалуйста опишите более подробно(на пальцах:) для чайников:b)

    ОтветитьУдалить