Caution: You are browsing the legacy symfony 1.x part of this website.

День 13: Пользователь

1.4 / Propel
Symfony version
1.2
Language ORM

Вчерашний день был буквально напичкан новой информацией. Всего несколько строк PHP кода, и генератор админки Symfony позволяет разработчику создать бэкэнд-интерфейс за несколько минут.

Сегодня мы исследуем, как Symfony управляет постоянными (persistent) данными между HTTP запросами. Как Вы возможно знаете, протокол HTTP не хранит состояние, что означает, что каждый запрос не зависит от всех предшествующих и последующих. Современные веб-сайты нуждаются в способе сохранения данных между запросами, чтобы повысить качество взаимодействия с пользователем.

Сессия пользователя может быть отслежена при помощи куки (cookie). В Symfony разработчик не должен управлять сессией напрямую, вместо этого он использует объект sfUser, который предоставляет все сведения о текущем пользователе приложения.

Пользовательские мгновенные сообщения (flashes)

Мы уже видели использование объекта пользователя с мгновенными сообщениями.
Они представляют собой временно хранящиеся в пользовательской сессии сообщения, которые будут автоматически удалены при последующих запросах. Это очень удобно, когда Вам надо отобразить сообщения для пользователя после перенаправления его на другую страницу. Генератор админки использует эти сообщения очень активно для отображения результатов сохранения, удаления или продления срока действия вакансии.

Flashes

Сообщение может быть добавлено при помощи метода setFlash() объекта sfUser:

// apps/frontend/modules/job/actions/actions.class.php
public function executeExtend(sfWebRequest $request)
{
  $request->checkCSRFProtection();
 
  $job = $this->getRoute()->getObject();
  $this->forward404Unless($job->extend());
 
  $this->getUser()->setFlash('notice', sprintf('Your job validity has been extended until %s.', $job->getExpiresAt('m/d/Y')));
 
  $this->redirect($this->generateUrl('job_show_user', $job));
}

Первый аргумент - это идентификатор сообщения, а второй - текст для отображения. Вы можете определять любые типы сообщений, какие Вам нужны, но notice и error это два наиболее используемых типа (они активно используются в генераторе админки).

Теперь от разработчика требуется включить вывод мгновенных сообщений в шаблоны. В Jobeet они выводятся в главном шаблоне layout.php:

// apps/frontend/templates/layout.php
<?php if ($sf_user->hasFlash('notice')): ?>
  <div class="flash_notice"><?php echo $sf_user->getFlash('notice') ?></div>
<?php endif; ?>
 
<?php if ($sf_user->hasFlash('error')): ?>
  <div class="flash_error"><?php echo $sf_user->getFlash('error') ?></div>
<?php endif; ?>

В шаблонах объект пользователя доступен через специальную переменную $sf_user.

note

Некоторые объекты Symfony всегда доступны в шаблонах, без необходимости устанавливать их в контроллере: $sf_request, $sf_user и $sf_response.

Атрибуты пользователя

К сожалению, пользовательские истории Jobeet не содержат требований, включающих сохранение чего-то в пользовательской сессии. Давайте добавим новое требование: чтобы облегчить просмотр вакансий, последние три вакансии, просматривавшиеся пользователем ранее, должны быть отображены в меню в виде ссылок. чтобы можно было вернуться на предыдущие страницы.

Когда пользователь заходит на страницу вакансии, отображаемый объект вакансии должен быть добавлен в историю посещений и сохранен в сессии:

// apps/frontend/modules/job/actions/actions.class.php
class jobActions extends sfActions
{
  public function executeShow(sfWebRequest $request)
  {
    $this->job = $this->getRoute()->getObject();
 
    // fetch jobs already stored in the job history
    $jobs = $this->getUser()->getAttribute('job_history', array());
 
    // add the current job at the beginning of the array
    array_unshift($jobs, $this->job->getId());
 
    // store the new job history back into the session
    $this->getUser()->setAttribute('job_history', $jobs);
  }
 
  // ...
}

note

Мы могли бы просто добавить объекты JobeetJob непосредственно в сессию. Но это не рекомендуется, поскольку переменные сессии сериализуются между запросами. И когда сессия загружается, объекты JobeetJob десериализуются и могут оказаться неактуальными, если они были изменены или удалены в промежутке между запросами.

getAttribute(), setAttribute()

Получив идентификатор атрибута, метод sfUser::getAttribute() извлекает его значение из пользовательской сессии. И наоборот, метод setAttribute() сохраняет любую PHP переменную в сессию под заданным именем.

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

note

Значение по умолчанию, принимаемое методом getAttribute(), это ярлык (shortcut) для:

if (!$value = $this->getAttribute('job_history'))
{
  $value = array();
}

Класс myUser

Чтобы лучше осмыслить разделение понятий, давайте переместим код в класс myUser. Класс myUser расширяет стандартный Symfony-класс sfUser, реализуя поведение, специфичное для приложения:

// apps/frontend/modules/job/actions/actions.class.php
class jobActions extends sfActions
{
  public function executeShow(sfWebRequest $request)
  {
    $this->job = $this->getRoute()->getObject();
 
    $this->getUser()->addJobToHistory($this->job);
  }
 
  // ...
}
 
// apps/frontend/lib/myUser.class.php
class myUser extends sfBasicSecurityUser
{
  public function addJobToHistory(JobeetJob $job)
  {
    $ids = $this->getAttribute('job_history', array());
 
    if (!in_array($job->getId(), $ids))
    {
      array_unshift($ids, $job->getId());
 
      $this->setAttribute('job_history', array_slice($ids, 0, 3));
    }
  }
}

Код так же был модифицирован, чтобы учесть все требования:

  • !in_array($job->getId(), $ids): Вакансия не может быть сохранена в историю дважды

  • array_slice($ids, 0, 3): Отображаются только последние три вакансии, просмотренные пользователем

Внесите в основной шаблон следующий код перед тем, как значение переменной $sf_content будет выведено:

// apps/frontend/templates/layout.php
<div id="job_history">
  Recent viewed jobs:
  <ul>
    <?php foreach ($sf_user->getJobHistory() as $job): ?>
      <li>
        <?php echo link_to($job->getPosition().' - '.$job->getCompany(), 'job_show_user', $job) ?>
      </li>
    <?php endforeach ?>
  </ul>
</div>
 
<div class="content">
  <?php echo $sf_content ?>
</div>

Шаблон использует новый метод getJobHistory() для получения текущей истории:

// apps/frontend/lib/myUser.class.php
class myUser extends sfBasicSecurityUser
{
  public function getJobHistory()
  {
    $ids = $this->getAttribute('job_history', array());
 
    return JobeetJobPeer::retrieveByPKs($ids);
  }
 
  // ...
}

Метод getJobHistory() использует метод Propel retrieveByPKs() для получения нескольких объектов JobeetJob в одном вызове.

История вакансий

sfParameterHolder

Чтобы завершить API истории вакансий, давайте добавим метод для очистки истории:

// apps/frontend/lib/myUser.class.php
class myUser extends sfBasicSecurityUser
{
  public function resetJobHistory()
  {
    $this->getAttributeHolder()->remove('job_history');
  }
 
  // ...
}

Атрибуты пользователя управляются объектом класса sfParameterHolder. Методы getAttribute() и setAttribute() это методы-заместители (proxy) для методов getParameterHolder()->get() и getParameterHolder()->set(). Поскольку метод remove() не имеет заместителя в классе sfUser, Вам придется использовать объект-хранитель параметров непосредственно.

note

Класс sfParameterHolder также используется классом sfRequest для хранения его параметров.

Обеспечение безопасности приложения

Аутентификация

Так же, как и многие другие возможности Symfony, безопасность (security) управляется через YAML файл security.yml. Например, Вы можете найти конфигурацию по умолчанию для приложения backend в директории config/:

# apps/backend/config/security.yml
default:
  is_secure: false

Если Вы переключите запись is_secure на true, все бэкэнд-приложение будет требовать, чтобы пользователь был аутентифицирован.

Защищенная страница

tip

В YAML файле, логическая величина может быть выражена как true и false.

Если Вы заглянете в логи на панели отладки, Вы заметите, что метод executeLogin() класса defaultActions вызывается для каждой страницы, к которой Вы пытаетесь получить доступ.

Панель отладки

Когда неаутентифицированный пользователь пытается получить доступ к защищенному действию, Symfony перенапрявляет запрос на действие login, настроенное в файле settings.yml:

all:
  .actions:
    login_module: default
    login_action: login

note

Защитить действие login нельзя, чтобы не создать бесконечную рекурсию.

tip

Как мы видели в дне 4, один и тот же конфигурационный файл может присутствовать в нескольких местах. Это относится и к файлу security.yml. Чтобы защитить или открыть единственное действие или целый модуль, создайте файл security.yml в директории config/ этого модуля:

index:
  is_secure: false
 
all:
  is_secure: true

По умолчанию, класс myUser расширяет класс sfBasicSecurityUser, а не sfUser. sfBasicSecurityUser обеспечивает дополнительные методы для управления аутентификацией и авторизацией пользователей.

Для управления аутентификацией используйте методы isAuthenticated() и setAuthenticated():

if (!$this->getUser()->isAuthenticated())
{
  $this->getUser()->setAuthenticated(true);
}

Авторизация

Когда пользователь аутентифицирован, доступ к некоторым действиям может быть дополнительно ограничен путем определения удостоверений (credentials). Пользователь должен иметь требуемые удостоверения для доступа к странице:

default:
  is_secure:   false
  credentials: admin

Система управления удостоверениями Symfony очень простая и в то же время мощная. Удостоверение может представлять что-то, что Вам нужно для описания системы безопасности приложения (например, группы пользователей и разрешения).

sidebar

Составные удостоверения

Запись credentials файла security.yml поддерживает логические операции для описания сложных взаимодействий удостоверений.

Если пользователь должен иметь удостоверения A и B, заключите удостоверения в квадратные скобки:

index:
  credentials: [A, B]

Если пользователь должен иметь удостоверение A или B, заключите их в две пары квадратных скобок:

index:
  credentials: [[A, B]]

Вы можете смешивать и сочетать скобки, чтобы описать любое логическое выражение с любым числом удостоверений.

Для управления удостоверениями пользователя, класс sfBasicSecurityUser предоставляет несколько методов:

// Добавление одного или нескольких удостоверений
$user->addCredential('foo');
$user->addCredentials('foo', 'bar');
 
// Проверка, что пользователь имеет удостоверение
echo $user->hasCredential('foo');                      =>   true
 
// Проверка, что пользователь имеет оба удостоверения
echo $user->hasCredential(array('foo', 'bar'));        =>   true
 
// Проверка, что пользователь имеет одно из удостоверений
echo $user->hasCredential(array('foo', 'bar'), false); =>   true
 
// Удаление удостоверения
$user->removeCredential('foo');
echo $user->hasCredential('foo');                      =>   false
 
// Удаление всех удостоверений (удобно в процессе выхода из приложения (logout))
$user->clearCredentials();
echo $user->hasCredential('bar');                      =>   false

Для бэкэнд-приложения Jobeet, мы не будем использовать удостоверения, поскольку для него используется только один профиль: администратор.

Plugins

Поскольку мы не любим изобретать велосипед, мы не будем разрабатывать действие login с нуля. Вместо этого мы установим плагины Symfony (plugins).

Одной из главных сильных сторон фреймворка Symfony является его система плагинов. Как мы увидим в следующих уроках, создать плагин очень просто. Плагин хорош еще и тем, что может содержать все - от конфигурации до модулей и стилей страниц.

Сегодня мы установим sfGuardPlugin для защиты бэкэнд-приложения:

$ php symfony plugin:install sfGuardPlugin

Задача plugin:install устанавливает плагин по имени. Все плагины после установки находятся в директории plugins/, каждый в своей директории, имя которой совпадает с именем плагина.

note

PEAR должен быть установлен, чтобы задача plugin:install работала.

Когда Вы устанавливаете плагин при помощи задачи plugin:install, Symfony устанавливает последнюю стабильную версию. Чтобы установить определенную версию плагина, используйте опцию --release.

Страница плагина содержит список всех доступных версий плагина, сгруппированный по версии Symfony.

Поскольку плагин содержится в самостоятельной директории, Вы также можете скачать дистрибутив с сайта Symfony и разархивировать его, либо использовать ссылку svn:externals на Subversion репозиторий плагина.

Задача plugin:install автоматически разрешает использование плагинов, обновляя файл ProjectConfiguration.class.php. Но если Вы установили плагин через Subversion или скачали архив с сайта, Вам нужно разрешить его вручную в ProjectConfiguration.class.php:

// config/ProjectConfiguration.class.php
class ProjectConfiguration extends sfProjectConfiguration
{
  public function setup()
  {
    $this->enablePlugins(array('sfPropelPlugin', 'sfGuardPlugin'));
  }
}

Защита бэкэнд приложения

Каждый плагин содежит README файл, который объясняет, как его использовать.

Давайте посмотрим, как настроить новый плагин. Поскольку плагин предоставляет несколько новых классов модели для управления пользователями, группами и разрешениями, Вам нужно пересобрать модель:

$ php symfony propel:build --all --and-load --no-confirmation

tip

Помните, что задача propel:build --all --and-load удаляет все существующие таблицы перед тем, как создать их снова. Чтобы этого избежать, Вы можете собрать отдельно модели, формы и фильтры, а затем создать новые таблицы, запустив соответствующие SQL-команды из файла, создавшегося в директории data/sql/.

Поскольку класс sfGuardPlugin добавляет несколько новых методов к классу пользователя, Вам нужно заменить базовый класс для myUser на sfGuardSecurityUser:

// apps/backend/lib/myUser.class.php
class myUser extends sfGuardSecurityUser
{
}

sfGuardPlugin обеспечивает действие signin в модуле sfGuardAuth для аутентификации пользователей.

Отредактируем файл settings.yml, чтобы изменить действие по умолчанию, используемое для страницы входа в приложение:

# apps/backend/config/settings.yml
all:
  .settings:
    enabled_modules: [default, sfGuardAuth]
 
    # ...
 
  .actions:
    login_module:    sfGuardAuth
    login_action:    signin
 
    # ...

Поскольку все плагины доступны из любого приложения проекта, Вам нужно просто разрешить модули, которые Вы хотите использовать в каждом приложении, внеся их в строку enabled_modules.

Страница входа из плагина

Последний шаг - это создание пользователя с правами администратора:

$ php symfony guard:create-user fabien SecretPass
$ php symfony guard:promote fabien

tip

sfGuardPlugin предлагает задачи для управления пользователями, группами и разрешениями из командной строки. Используйте задачу list, чтобы вывести весь список задач из пространства имен guard:

$ php symfony list guard

Когда пользователь еще не аутентифицирован, мы должны скрыть панель меню:

// apps/backend/templates/layout.php
<?php if ($sf_user->isAuthenticated()): ?>
  <div id="menu">
    <ul>
      <li><?php echo link_to('Jobs', 'jobeet_job') ?></li>
      <li><?php echo link_to('Categories', 'jobeet_category') ?></li>
    </ul>
  </div>
<?php endif ?>

А если он аутентифицирован, то нам нужно добавить в меню ссылку logout:

// apps/backend/templates/layout.php
<li><?php echo link_to('Logout', 'sf_guard_signout') ?></li>

tip

Чтобы увидеть все маршруты, порожденные sfGuardPlugin, используйте задачу app:routes.

Чтобы еще улучшить бэкэнд-приложение Jobeet, давайте добавим новый модуль для управления пользователями. Замечательно, что sfGuardPlugin предоставляет такой модуль. Так же, как и для модуля sfGuardAuth, Вам надо разрешить его в файле settings.yml:

// apps/backend/config/settings.yml
all:
  .settings:
    enabled_modules: [default, sfGuardAuth, sfGuardUser]

Добавим ссылку в меню:

// apps/backend/templates/layout.php
<li><?php echo link_to('Users', 'sf_guard_user') ?></li>

Меню бэкэнд-приложения

Мы закончили!

Тестирование пользователя

Сегодняшний день не закончен, т.к. мы еще не поговорили о тестировании работы с пользователями. Поскольку объект браузера Symfony эмулирует куки, очень просто протестировать поведение пользователя, используя тестер sfTesterUser.

Давайте обновим функциональные тесты для пунктов меню, которые мы уже добавили сегодня. Добавьте следующий код в конец функционального теста для модуля job:

// test/functional/frontend/jobActionsTest.php
$browser->
  info('4 - User job history')->
 
  loadData()->
  restart()->
 
  info('  4.1 - When the user access a job, it is added to its history')->
  get('/')->
  click('Web Developer', array(), array('position' => 1))->
  get('/')->
  with('user')->begin()->
    isAttribute('job_history', array($browser->getMostRecentProgrammingJob()->getId()))->
  end()->
 
  info('  4.2 - A job is not added twice in the history')->
  click('Web Developer', array(), array('position' => 1))->
  get('/')->
  with('user')->begin()->
    isAttribute('job_history', array($browser->getMostRecentProgrammingJob()->getId()))->
  end()
;

Чтобы упростить тестирование, мы сначала перезагрузим начальные данные (fixtures) и браузер, чтобы начать с чистой сессии.

Метод isAttribute() проверяет установленные пользователю атрибуты.

note

Тестер sfTesterUser также предоставляет методы isAuthenticated() и hasCredential(), чтобы проверять аутентификацтю и авторизацию пользователя.

Увидимся завтра

Классы для работы с пользователями Symfony - это отличный способ абстрагироваться от управления PHP-сессией. Вместе с замечательной системой плагинов Symfony и плагином sfGuardPlugin, мы смогли сделать бэкэнд-приложение Jobeet защищенным за несколько минут. И мы даже создали понятный интерфейс для управлениями пользователями благодаря модулям, предоставленным плагином.