Контент


Представьтесь!

Настало время продолжить разговор о работе с модулем Auth, который мы начали вчера. Пользователя мы зарегистрировали, теперь надо пустить его на сайт под своим логином/паролем, а также позволить выйти («разлогиниться»). Кроме того, пользователь должен видеть сайт несколько по-другому, нежели анонимный гость (как минимум, имя пользователя и ссылка на выход должны присутствовать). Этим мы и займемся в данной статье.

Входит и выходит… Замечательно выходит!

Итак, после прошлой статьи у нас имеется контроллер Auth_Controller. Давайте добавим методы login и logout:

  public function login() {
    $errors = array();
    if ($this->input->post()) {
      // введены данные формы
      if (Auth::instance()->login($_POST['username'], $_POST['password'])) {
        url::redirect(url::base(FALSE).'auth/profile');
      }
      else $errors['login'] = 'login failed!';
    }
    $this->template->content = new View('auth/login');
    $this->template->title = 'Авторизация пользователя';
    $this->template->content->errors = $errors;
  }
 
    public function logout() {
      if (Auth::instance()->logout()) {
        url::redirect(url::base(FALSE).'auth/login');
      }
    }

Все довольно просто. Проверяем входные данные (массив $_POST), если есть — пробуем войти. Используются методы login() и logout() объекта Auth (доступ к которому мы получаем через Auth::instance(), налицо реализация паттерна Singleton). В методе login() используется представление application/views/auth/login.php:

<?php defined('SYSPATH') OR die('No direct access allowed.'); ?>
<form action='<?=url::base(FALSE)?>auth/login' id='login' method='post'>
<fieldset>
  <legend>Авторизация пользователя</legend>
  <div id='errors'><?foreach($errors as $key=>$val):?><p><?=$key.":".$val?></p><? endforeach?></div>
  <ol>
    <li><label for='username'>login </label><input type='text' name='username' id='username' size="20" maxlength="32" /></li>
    <li><label for='password'>password </label><input type='password' name='password' id='password' size="20" maxlength="50" /></li>
    <li><input type="submit" value="Войти" /></li>
  </ol>
  <a href='/auth/register'>Зарегистрироваться</a>
</fieldset>
</form>

Все вроде стандартно. Но попробуйте войти под правильной комбинацией логина и пароля — нас перенаправит на страницу профиля… и все. Мы даже не можем определить, вошли мы или нет. Вывод: надо еще на каждой странице определять статус пользователя и отображать его где-то (я предпочитаю в «шапке» сайта). Давайте подумаем, как это можно сделать.

  • Во-первых, объект Auth после логина пользователя сохраняет в сессии экземпляр модели User_Model (в случае ORM), имя ключа указывается в параметре $config['session_key'] (т.е. по умолчанию получаем $_SESSION['auth_user']). Для любителей самостоятельно поковырять исходники — смотрите файл modules/auth/libraries/drivers/auth.php, метод complete_login().
  • Во-вторых, поскольку необходимо иметь доступ к данным пользователя на каждой странице сайта, проще всего создать переменную в таком контроллере, от которого будут наследоваться все остальные контроллеры страниц. Давайте назовем его Web_Controller (файл application/controllers/web.php):

    <?php defined('SYSPATH') OR die('No direct access allowed.');
     
    class Web_Controller extends Template_Controller {
      public $template = 'index';
      public $user = FALSE;
     
      public function __construct() {
        parent::__construct();
        $this->template->login = 'test';
        if(!Auth::instance()->logged_in('login')) {
          $this->template->login = new View('header/guest');
        }
        else {
          $this->user = $_SESSION['auth_user'];
          $this->template->login = new View('header/logged');
          $this->template->login->login = $this->user->username;
        }
        $this->template->menus = array
          (
              array
              (
                  'title'   =>  'Главная страница блога',
                  'link'    =>  url::base(FALSE).'blog/',
              ),
          );
      }
     
    }

Итак, мы вынесли в базовый контроллер проверку, вошел ли пользователь, результат хранится в переменной $this->user. В зависимости от этого мы в шапку сайта вставляем предложение войти либо информацию о текущем пользователе и пару ссылок («Профиль» и «Выйти»). Попутно закинули инициализацию массива ссылок для меню правой части нашего шаблона (на самом деле элементы интерфейса могут зависеть от текущего контроллера/метода, но это уже оффтоп). Для работы кода понадобятся представления.

application/views/header/logged.php

<?php defined('SYSPATH') OR die('No direct access allowed.'); ?>
Вы - <strong><?=$login?></strong>. <a href='<?=url::base(FALSE)?>auth/profile'>Профиль</a>. <a href='<?=url::base(FALSE)?>auth/logout'>Выйти</a>

application/views/header/guest.php

<?php defined('SYSPATH') OR die('No direct access allowed.');?>
<a href='<?=url::base(FALSE)?>auth/login'>Войти</a>

Не забываем, что теперь надо все используемые контроллеры наследовать не от Template_Controller, как раньше, а от Web_Controller. Ну и по-хорошему надо бы анализировать предыдущий URL, с которого пользователь выходил на страницы с login/logout, чтобы перенаправлять его обратно.

Далее еще немного подумаем. Если пользователь залогинен, то ему бессмысленно попадать на страницу авторизации, поэтому добавим в самое начало метода login() такую строчку:

$this->user AND url::redirect(url::base(FALSE).'auth/profile');

Редирект произойдет только если переменная $this->user содержит информацию о текущем пользователе. Добавим такую же проверку в метод register(), а также по аналогии изменим logout():

$this->user OR url::redirect(url::base(FALSE).'auth/login');

Заполняем анкету

Конечно, страница профиля смотрится бедненько, точнее совсем не смотрится. Предлагаю дать возможность пользователю ввести дополнительную информацию о себе (ФИО, телефон и т.д.). Для этого надо создать отдельную таблицу user_details. Почему я не хочу расширить существующую таблицу users? Дело в том, что анкетные данные — информация скорее справочная, и так часто использоваться не будет. А вот из users данные будут браться чаще (как минимум при login/logout), да и хранить в сессии лишние данные (ведь у нас там User_Model) как-то неохота. Поэтому решено — создаем отдельную таблицу:

DROP TABLE IF EXISTS `user_details`;
CREATE TABLE  `user_details` (
  `user_id` INT(10) UNSIGNED NOT NULL,
  `first_name` VARCHAR(127) DEFAULT NULL,
  `last_name` VARCHAR(127) DEFAULT NULL,
  `middle_name` VARCHAR(127) DEFAULT NULL,
  `interests` VARCHAR(127) DEFAULT NULL,
  `about` VARCHAR(255) DEFAULT NULL,
  `icq` VARCHAR(10) DEFAULT NULL,
  `jabber` VARCHAR(127) DEFAULT NULL,
  `website` VARCHAR(127) DEFAULT NULL,
  `city` VARCHAR(127) DEFAULT NULL,
  PRIMARY KEY  (`user_id`),
  CONSTRAINT `FK_user_details_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Создали несколько полей, связали с таблицей `users` внешним ключом (т.к. связь один-к-одному, то user_id является и первичным ключом). Все поля, кроме user_id, являются необязательными.

Какой следующий этап? Вспоминаем, какие компоненты MVC используются для предоставления данных из БД — правильно, модели. Надо создать модель (конечно, это будет ORM-модель), предоставляющую возможность работать с таблицей user_details. Все это несложно:

файл application/models/User_Detail.php

<?php defined('SYSPATH') OR die('No direct access allowed.');
 
class User_Detail_Model extends ORM {
 
    // Relationships
    protected $has_one = array('user');
 
    protected $primary_key = 'user_id';
 
   /**
	 * Validates and optionally saves a new user record from an array.
	 *
	 * @param  array    values to check
	 * @param  boolean  save the record when validation succeeds
	 * @return boolean
	 */
	public function validate(array & $array, $save = FALSE)
	{
		$array = Validation::factory($array)
			->pre_filter('trim')
      ->add_massive_rules(array('first_name', 'last_name', 'middle_name'),
          'length[2,127]', 'chars[\pL -\']')
      ->add_massive_rules(array('interests', 'city'), 'length[2,127]', 'standard_text')
      ->add_rules('website', 'url', 'length[2,127]')
      ->add_rules('icq', 'length[2,10]', 'chars[\pN-]')
      ->add_rules('about', 'length[2,255]', 'standard_text')
      ->add_rules('jabber', 'length[2,127]', 'email');
		return parent::validate($array, $save);
	}
}

Разбираемся. В ORM-моделях необходимо описывать связи с другими таблицами в виде свойств ($has_one, $has_many, $belongs_to, $has_and_belongs_to_many), имена которых говорят сами за себя. Так как связь с таблицей users один-к-одному, то мы вписываем имя таблицы в свойство $has_one. Таким образом, информация о пользователе из данной таблицы будет доступно через «магическое» свойство users (например, $userdata->users). Идем дальше. Одно из стандартных требований ORM — первичные ключи должны иметь имя `id`. Но есть и обходной путь — свойство $primary_key позволяет нам задать другое имя для первичного ключа, чем мы и пользуемся.

Из методов необходимо только описать валидацию данных, т.к. в ней описываются требования к вводимым данным. Обратите внимание на использование нашего «напильникового» метода add_massive_rules (мы его создавали в этой статье). Также стоит пристальнее глянуть на правила вида ‘chars[\pL -\']‘ — о том, как я к ним пришел, можно почитать здесь.

Далее, надо создать метод контроллера для редактирования анкеты пользователя. Назовем его change_profiile():

public function change_profile() {
      $this->user OR url::redirect(url::base(FALSE).'auth/login');
      $errors = $data = array();
      $userdata = ORM::factory('user_detail', $this->user->id);
      if ($this->input->post()) {
        // введены данные формы
        $data = array(
          'first_name' =>   '',
          'last_name' =>    '',
          'middle_name' =>  '',
          'city' =>         '',
          'interests' =>    '',
          'about' =>        '',
          'website' =>      '',
          'icq' =>          '',
          'jabber' =>       '',
        );
        $data = arr::overwrite($data, $_POST);
        if (!$userdata->validate($data)) {
          $errors = $data->errors();
        }
        else {
          $userdata->user_id = $this->user->id;
          if ($userdata->save())
            $errors['Result'] = 'Change_profile.Succesfull';
          else $errors['Result'] = 'Change_profile.Error';
        }
      }
      $this->template->content = new View('auth/edit_profile');
      $this->template->title = 'Ваша анкета';
      $this->template->content->userdata = arr::overwrite(arr::merge($this->user->as_array(), $userdata->as_array()), $data);
      $this->template->content->errors = $errors;
    }

В самом начале скрипта проверяем, вошел ли пользователь (т.к. у гостя профиля нет и редактировать ему нечего). Для заполнения массива $data только нужными нам значениями использовали функцию arr::overwrite(), которая позволила скопировать из $_POST только значения полей ,перечисленных в $data. Далее создается модель User_Detail через метод ORM::factory(). Для выборки данных передается первичный ключ — идентификатор текущего пользователя. В случае успешного прохождения валидации состояние модели $userdata сохраняется с помощью метода save().

Обратите внимание, что поля, не участвующие в валидации, необходимо добавить в модель вручную! Так пришлось поступить с первичным ключом user_id.

Для вывода результата работы скрипта заполняются переменные шаблона content: $userdata (общая информация о пользователя, включая некорректно введенные в форму данные) и $errors (ошибки и сообщения валидации).

Ну и напоследок приведу текст шаблона:
Файл application/views/auth/edit_profile.php

<?php defined('SYSPATH') OR die('No direct access allowed.');?>
<h2>Информация о пользователе <?=$userdata['username']?></h2>
<form action="<?=url::base(FALSE)?>auth/change_profile" id="login" method="post">
<div id="errors"><?foreach($errors as $key=>$val):?><p><?=$key.":".$val?></p><? endforeach?></div>
<fieldset>
  <legend>Личные данные</legend>
  <ol>
    <li>
      <label for="first_name">Фамилия </label>
      <input type="text" name="first_name" id="first_name" size="20" maxlength="127" value="<?=$userdata['first_name']?>" />
    </li>
    <li>
      <label for="last_name">Имя </label>
      <input type="text" name="last_name" id="last_name" size="20" maxlength="127" value="<?=$userdata['last_name']?>" />
    </li>
    <li>
      <label for="middle_name">Отчество </label>
      <input type="text" name="middle_name" id="middle_name" size="20" maxlength="127" value="<?=$userdata['middle_name']?>" />
    </li>
    <li>
      <label for="city">Город </label>
      <input type="text" name="city" id="city" size="20" maxlength="127" value="<?=$userdata['city']?>" />
    </li>
  </ol>
</fieldset>
<fieldset>
  <legend>Контакты</legend>
  <ol>
    <li>
      <label for="website">Сайт </label>
      <input type="text" name="website" id="website" value="<?=$userdata['website']?>" size="20" maxlength="127" />
    </li>
    <li>
      <label for="icq">ICQ </label>
      <input type="text" name="icq" id="icq" value="<?=$userdata['icq']?>" size="20" maxlength="10" />
    </li>
    <li>
      <label for="jabber">Jabber </label>
      <input type="text" name="jabber" id="jabber" value="<?=$userdata['jabber']?>" size="20" maxlength="127" />
    </li>
  </ol>
</fieldset>
<fieldset>
  <legend>Дополнительно</legend>
  <ol>
    <li>
      <label for="interests">Интересуюсь </label>
      <input type="text" name="interests" id="interests" value="<?=$userdata['interests']?>" size="20" maxlength="127" />
    </li>
    <li>
      <label for="about">О себе </label>
      <textarea name="about" id="about" rows="3" ><?=$userdata['about']?></textarea>
    </li>
    <li><input type="submit" value="Сохранить" /></li>
  </ol>
</fieldset>
</form>

Думаю, стоит сделать небольшой перерыв, дабы все это проверить и осмыслить ;)

Update. Конечно, постоянно хранить в сессии весь ORM-объект совсем необязательно. В принципе, нам потребуется id и username (его мы показываем в «шапке»), а уже если в какой-то момент потребуется проверить уровень доступа, тогда на основании id конструировать модель и проверять наличие нужной роли через метод has().

Update2. Как правильно заметил Алексей (Alex_XS), теперь после регистрации пользователей можно их перенаправлять сразу на редактирование анкеты (т.е. url::redirect(‘auth/change_profile’)).

Update3. Выложил архив с классами по итогам двух уроков с модулем Auth. Обратите внимание, что мы переопределили процесс валидации пользователя (models/auth_user.php) и создали новый метод add_massive_rules для объекта Validation (файл libraries/MY_Validation.php).

Google Bookmarks Digg Reddit del.icio.us Ma.gnolia Technorati Slashdot Yahoo My Web News2.ru БобрДобр.ru RUmarkz Ваау! Memori.ru rucity.com МоёМесто.ru Mister Wong

Опубликовано в учебник.

Теги: , , .


Комментарии (20)

Будьте в курсе обсуждения, подпишитесь на RSS ленту комментариев к этой записи.

  1. cwer.livejournal.com/ пишет:

    а выложите код архивом :)

  2. BIakaVeron пишет:

    Выложил. Не забудьте настроить доступ к БД.

  3. Dark Preacher пишет:

    Спасибо за цикл замечательных статей, всё доступно, понятно, легко и с интересом читается!
    ps — в архиве, в папке «views» остались папки «.svn»

  4. BIakaVeron пишет:

    Да не за что.
    Архив подправил, спасибо, что заметили.

  5. Slaver пишет:

    У Kohana оказалась ужасная проверка на «залогиненность» — 3 запроса к БД, которые легко можно объединить в один. Плюс ошибка в auth_user.php. Придётся ещё много допиливать :)

  6. BIakaVeron пишет:

    Вообще модуль носит скорее ознакомительный характер, поэтому его практически каждому придется допиливать под себя.
    Посмотрел профайлером — таки да, три запроса. Но ведь на самом деле каждый из них по отдельности можно будет кэшировать и уже после авторизации как таковых запросов к БД не будет…

  7. Slaver пишет:

    3 запроса идут на Auth::instance()->logged_in(‘login’):

    SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 ORDER BY `users`.`id` ASC LIMIT 0, 1
    SELECT `roles`.* FROM `roles` WHERE `roles`.`name` = 'login' ORDER BY `roles`.`id` ASC LIMIT 0, 1
    SELECT `role_id` AS `id` FROM `roles_users` WHERE `roles_users`.`user_id` = 1

  8. BIakaVeron пишет:

    Да. Это создание ORM-объектов `user` и `role`, а также проверка $this->user->has($role). Повторюсь, поскольку эта проверка будет происходить при каждом переходе на сайте, кэширование включить придется.

  9. prophet пишет:

    Приветствую!
    Скажите, пожалуйста, в чем может быть проблема: после успешного логина модель User_Detail сохраняется в сессии с ключем auth_user, а после редиректа на редактирование профиля ключ auth_user и его значениче полностью пропадает.
    что может быть?

  10. BIakaVeron пишет:

    Здравствуйте.
    Для начала попробуйте использовать другой драйвер сессии (файл config/session.php), например ‘native’ вместо ‘cookies’.
    Если не поможет, приведите более подробную информацию (ОС, версия php и Kohana, проверяемые браузеры и т.д.).
    Если владеете английским, почитайте данную тему на форуме.

  11. prophet пишет:

    Благодарю!!! Совет помог, ссылка тоже, нужно было подправить файл config/session.php
    указал значение переменных
    session.regenerate = 0
    ession.expiration = 460000
    заработало. Но не все. Были проблемы с капчей, они после изменений решились, а аутентификация пока не работает. Проблему определил, она в модуле Auth.
    что именно пока не нашел, но после строк i
    f (Auth::instance()->login($_POST['username'], $_POST['password'])) {
    url::redirect(url::base(FALSE).’auth/profile’);
    }
    сессии пропадают

  12. BIakaVeron пишет:

    Собственно говоря, у некоторых в том топике были аналогичные проблемы именно после редиректа. Попробуйте без него (вручную header послать или вообще не редиректить, а переходить по ссылке). Протестируйте тот же код на другом сервере.

  13. prophet пишет:

    Благодарю!!!
    Все разрешилось! Но, как оказалось был виноват сам,
    в файле config/cookie.php
    значение $config['domain'] оставалось с локального сервера ))

    зы. Классный блог, как, говорится, пишите еще!

  14. mazneff пишет:

    Приветствую!!!
    Реально очень очень полезные статьи — спасибо!!! Сам я новичок в Кохане (к сожалению и php тоже), но программистом работаю более 20 лет. И у меня возникла проблема при выполнении примеров. Конкретно, при выполнении строки функции change_profile исходник auth.php
    $this->template->content->userdata = arr::overwrite(arr::merge($this->user->as_array(), $userdata->as_array()), $data);
    ругается

    Z:/home/localhost/www/kohana/system/helpers/arr.php [247]:

    array_intersect_key() [function.array-intersect-key]: Argument #1 is not an array
    Stack Trace

    * system\helpers\arr.php [247]:

    array_intersect_key( Validation Object
    (
    [pre_filters:protected] => Array

    как исправить проблему — я не понял, хотя и промучался пару часов. Пока заменил уту строку на
    $this->template->content->userdata = arr::merge($this->user->as_array(), $userdata->as_array());
    т.е. убрал overwrite — вроде как работает нормально. Но хотелось разобраться, что не так и как сделать правильно.

    Заранее благодарен!!!

  15. BIakaVeron пишет:

    Добро пожаловать!

    Гм, странно, класс Validation должен свободно распознаваться как массив… Попробуйте $data->as_array() или просто $_POST.

  16. mazneff пишет:

    Да нет, прикол в том, что ругается на то, что возвращает функция merge — менял параметры в overwrite местами, что бы понять, от нуля нумерует в сообщении, или от 1. Оказалось, от 1.

  17. BIakaVeron пишет:

    Судя по всему, ругается именно на параметр $data (в методе arr::overwrite() они подставляются в array_intersect() в обратной последовательности, да и в дебаге показан объект Validation).

  18. faost пишет:

    После строчки

    if (!$userdata->validate($data)) {

    $data превращяется в Validation Object, поэтому после условия надо поставить

    $data = $data->as_array();

  19. Олег пишет:

    Как на меня, то перед валидацией данных лучше продублировать массив $data, к примеру $data_to_validate = $data; и все манипуляции проводить с новым массивом. Массив переводить в объект, а объект в массив — это не лучшее решение.

  20. biakaveron пишет:

    А в чем смысл? Старый массив может отличаться от нового — часть данных отсеется, т.к. в модели может не быть таких полей, часть значений может поменяться из-за фильтров.

    PS. На самом деле статья несколько устарела (прошло полтора года, неудивительно!). Сейчас ORM автоматом подхватывает правила модели, нужды в «напиленном» massive_rules() нет…



Можно включить подсветку кода: <code><pre lang="">...</pre></code>
Разрешены некоторые HTML теги

или используйте trackback.