Настало время продолжить разговор о работе с модулем 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).
а выложите код архивом
Выложил. Не забудьте настроить доступ к БД.
Спасибо за цикл замечательных статей, всё доступно, понятно, легко и с интересом читается!
ps — в архиве, в папке «views» остались папки «.svn»
Да не за что.
Архив подправил, спасибо, что заметили.
У Kohana оказалась ужасная проверка на «залогиненность» — 3 запроса к БД, которые легко можно объединить в один. Плюс ошибка в auth_user.php. Придётся ещё много допиливать
Вообще модуль носит скорее ознакомительный характер, поэтому его практически каждому придется допиливать под себя.
Посмотрел профайлером — таки да, три запроса. Но ведь на самом деле каждый из них по отдельности можно будет кэшировать и уже после авторизации как таковых запросов к БД не будет…
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
Да. Это создание ORM-объектов `user` и `role`, а также проверка $this->user->has($role). Повторюсь, поскольку эта проверка будет происходить при каждом переходе на сайте, кэширование включить придется.
Приветствую!
Скажите, пожалуйста, в чем может быть проблема: после успешного логина модель User_Detail сохраняется в сессии с ключем auth_user, а после редиректа на редактирование профиля ключ auth_user и его значениче полностью пропадает.
что может быть?
Здравствуйте.
Для начала попробуйте использовать другой драйвер сессии (файл config/session.php), например ‘native’ вместо ‘cookies’.
Если не поможет, приведите более подробную информацию (ОС, версия php и Kohana, проверяемые браузеры и т.д.).
Если владеете английским, почитайте данную тему на форуме.
Благодарю!!! Совет помог, ссылка тоже, нужно было подправить файл 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’);
}
сессии пропадают
Собственно говоря, у некоторых в том топике были аналогичные проблемы именно после редиректа. Попробуйте без него (вручную header послать или вообще не редиректить, а переходить по ссылке). Протестируйте тот же код на другом сервере.
Благодарю!!!
Все разрешилось! Но, как оказалось был виноват сам,
в файле config/cookie.php
значение $config['domain'] оставалось с локального сервера ))
зы. Классный блог, как, говорится, пишите еще!
Приветствую!!!
Реально очень очень полезные статьи — спасибо!!! Сам я новичок в Кохане (к сожалению и 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 — вроде как работает нормально. Но хотелось разобраться, что не так и как сделать правильно.
Заранее благодарен!!!
Добро пожаловать!
Гм, странно, класс Validation должен свободно распознаваться как массив… Попробуйте $data->as_array() или просто $_POST.
Да нет, прикол в том, что ругается на то, что возвращает функция merge — менял параметры в overwrite местами, что бы понять, от нуля нумерует в сообщении, или от 1. Оказалось, от 1.
Судя по всему, ругается именно на параметр $data (в методе arr::overwrite() они подставляются в array_intersect() в обратной последовательности, да и в дебаге показан объект Validation).
После строчки
if (!$userdata->validate($data)) {
$data превращяется в Validation Object, поэтому после условия надо поставить
$data = $data->as_array();
Как на меня, то перед валидацией данных лучше продублировать массив $data, к примеру $data_to_validate = $data; и все манипуляции проводить с новым массивом. Массив переводить в объект, а объект в массив — это не лучшее решение.
А в чем смысл? Старый массив может отличаться от нового — часть данных отсеется, т.к. в модели может не быть таких полей, часть значений может поменяться из-за фильтров.
PS. На самом деле статья несколько устарела (прошло полтора года, неудивительно!). Сейчас ORM автоматом подхватывает правила модели, нужды в «напиленном» massive_rules() нет…