Bookmarker Tutorial, часть 2



Перед тем как продолжить работу с приложением закладок (первая часть данной статьи), убедитесь, что оно открывается у вас на локальном сервере по адресу http://yousite.loc/bookmarks. Если все корректно работает: пользователи, закладки и теги добавляются, удаляются, редактируются и просматриваются, то можно двигаться дальше. Дополнительно, напоминаю, что данный материал является переводом тотуриалов по работе с CakePHP, размещенных на официальном сайте разработчиков фреймворка https://book.cakephp.org. И так, приступим к добавлению аутентификации в наше приложение.

Добавление формы входа

В CakePHP аутентификация обрабатывается одним из компонентов контроллеров (Components). Компоненты можно рассматривать как способы создания многократно используемых контроллерами блоков кода, связанных с определенной функцией или концепцией. Компоненты могут также подключаться не ко всему контроллеру, а к определенному жизненному циклу его события и таким образом взаимодействовать с вашим приложением. Для начала добавим AuthComponent (компонент, отвечающий за аутентификацию) в наше приложение. Так как, мы хотим, чтобы каждый метод любого контроллера требовал аутентификации, AuthComponent будет прописан в главном контроллере - AppController:

// Редактируем файл src/Controller/AppController.php
namespace App\Controller;

use Cake\Controller\Controller;

class AppController extends Controller
{
    public function initialize()
    {
        $this->loadComponent('Flash');
        $this->loadComponent('Auth', [
            'authenticate' => [
                'Form' => [
                    'fields' => [
                        'username' => 'email',
                        'password' => 'password'
                    ]
                ]
            ],
            'loginAction' => [
                'controller' => 'Users',
                'action' => 'login'
            ],
            'authError' => 'Не авторизированные пользователи не имеют разрешения для просмотра данного контента!',
            'unauthorizedRedirect' => $this->referer() // В случае успешной авторизации возвращаем пользователя на страницу, с которой он был перенаправлен на форму входа
        ]);
    }
}

Мы только что сказали CakePHP, что хотим загрузить компоненты Flash и Auth. Кроме того, мы произвели настройку конфигурации AuthComponent, поскольку наша таблица users использует электронную почту в качестве имени пользователя. Теперь, если вы перейдете по любому URL-адресу приложения, вас переадресует на страницу /users/login, которая покажет сообщение об ошибке, поскольку мы еще не написали нужный код. Итак, давайте в контроллере пользователей (UsersController) создадим соответствующее действие (action) для входа:

// Делаем запись в файле src/Controller/UsersController.php
public function login()
{
    if ($this->request->is('post')) {
        $user = $this->Auth->identify();
        if ($user) {
            $this->Auth->setUser($user);
            return $this->redirect($this->Auth->redirectUrl());
        }
        $this->Flash->error('Имя пользователя или пароль введены не корректно.');
    }
}

А так же создадим файл шаблона src/Template/Users/login.ctp с формой входа:

<div class="users form large-9 medium-8 columns content">
    <h1>Форма входа</h1>
    <?= $this->Form->create() ?>
    <fieldset>
        <legend><?= __('Пожалуйста, введите данные для авторизации') ?></legend>
            <?= $this->Form->control('email') ?>
            <?= $this->Form->control('password', [
                'label' => 'Пароль'
            ]) ?>
    </fieldset>
    <?= $this->Form->button('Войти') ?>
    <?= $this->Form->end() ?>
</div>

Внимание! Элемент control() доступен начиная с версии 3.4. Для предыдущих версий вы можете использовать функцию input().

Теперь, когда у нас есть простая форма для входа, мы можем войти в систему под именем одного из пользователей с хэшированным (в базе данных) паролем.

Внимание! Если ни один из ваших ранее сохраненных пользователей не имеет хэшированного пароля, закомментируйте строку loadComponent ('Auth'). Затем перейдите и отредактируйте пользователя, сохранив для него новый пароль.

Добавление выхода

Теперь, когда люди могут войти в систему, вы, вероятно, захотите предоставить способ выхода из неё . Для этого в UserController необходимо добавить следующий код:

public function initialize()
{
    parent::initialize();
    $this->Auth->allow(['logout']);
}

public function logout()
{
    $this->Flash->success('Вы успешно вышли.');
    return $this->redirect($this->Auth->logout());
}

Верхняя часть кода добавляет функцию выхода logout в список исключений, при обращении к которым аутентификация не требуется. Код, расположенный ниже реализует метод выхода. Теперь вы можете обратиться к /users/logout, чтобы выйти из системы, после чего, приложение перенаправит вас на страницу с формой входа.

Разрешение регистрации

Сейчас, если не войти в систему и попытаться посетить /users/add, приложение перенаправит вас на страницу входа. Необходимо это исправить, поскольку мы хотим разрешить людям регистрироваться на нашем сайте. Для этого в UserController добавим следующий код:

public function initialize()
{
    parent::initialize();
    // помимо 'logout' в список разрешений добавляем функцию контроллера 'add'.
    $this->Auth->allow(['logout', 'add']);
}

Выше мы сказали компоненту AuthComponent, что действие add() не требует аутентификации или авторизации. Вы можете захотеть потратить время на очистку Users/add.ctp и удалить вводящие в заблуждение ссылки, или сразу перейти к следующему разделу.

Ограничение доступа к закладкам

Теперь, когда пользователи могут войти в систему, давайте ограничим их доступ только к своим закладкам. Мы сделаем это с помощью адаптера авторизации. Для начала, мы скажем компоненту AuthComponent, как наше приложение будет действовать при авторизации. В свой AppController добавьте следующее:

public function isAuthorized($user)
{
    return false;
}

Кроме того, добавьте в конфигурацию для Auth в AppController следующее:

'authorize' => 'Controller',

Метод initialize() в AppController теперь должен выглядеть так:

public function initialize()
{
    $this->loadComponent('Flash');
    $this->loadComponent('Auth', [
        'authorize'=> 'Controller', //добавлена эта строка
        'authenticate' => [
            'Form' => [
                'fields' => [
                    'username' => 'email',
                    'password' => 'password'
                ]
            ]
        ],
        'loginAction' => [
            'controller' => 'Users',
            'action' => 'login'
        ],
        'authError' => 'Не авторизированные пользователи не имеют разрешения для просмотра данного контента!',
        'unauthorizedRedirect' => $this->referer()
    ]);

     // Доступ к главной странице приложения оставляем открытым 
     // для любого пользователя (метод "display" контроллера PagesController)
    $this->Auth->allow(['display']);
}

Мы по умолчанию полностью закрываем доступ и поэтапно предоставляем его там, где это имеет смысл. Сначала мы добавим логику авторизации для закладок. В свой BookmarksController добавьте следующее:

public function isAuthorized($user)
{
    $action = $this->request->getParam('action');

    // Доступ к действиям add и index всегда разрешен.
    if (in_array($action, ['index', 'add', 'tags'])) {
        return true;
    }
    // Все другие действия требуют идентификатор id.
    if (!$this->request->getParam('pass.0')) {
        return false;
    }

    // Проверяем, что закладка принадлежит текущему пользователю.
    $id = $this->request->getParam('pass.0');
    $bookmark = $this->Bookmarks->get($id);
    if ($bookmark->user_id == $user['id']) {
        return true;
    }
    return parent::isAuthorized($user);
}

Теперь, если вы попытаетесь просмотреть, отредактировать или удалить закладку, которая не принадлежит вам, то будете перенаправлены обратно на страницу, с которой пришли и увидите сообщение об ошибке ( 'authError' => 'Не авторизированные пользователи не имеют разрешения для просмотра данного контента!'). Если сообщение об ошибке не отображается, то добавьте в шаблон макета следующее:

// Файл src/Template/Layout/default.ctp
<?= $this->Flash->render() ?>

Теперь сообщения об ошибках авторизации будет отображаться.

Дорабатываем список закладок и соответствующие формы

Хотя основной функционал нашего приложения работает, при удалении, редактировании и добавлении есть несколько нюансов, которые нужно поправить:

  • При добавлении закладки можно выбрать пользователя;
  • При редактировании закладки можно выбрать пользователя;
  • На странице списка показаны закладки других пользователей.
    • Для начала рассмотрим форму добавления закладки src/Template/Bookmarks/add.ctp, из которой необходимо удалить элемент управления ('user_id'). После этого давайте обновим действие add() в контроллере src/Controller/BookmarksController.php так, чтобы оно выглядело следующим образом:

      public function add()
      {
          $bookmark = $this->Bookmarks->newEntity();
          if ($this->request->is('post')) {
              $bookmark = $this->Bookmarks->patchEntity($bookmark, $this->request->getData());
              $bookmark->user_id = $this->Auth->user('id');
              if ($this->Bookmarks->save($bookmark)) {
                  $this->Flash->success('Закладка успешно сохранена.');
                  return $this->redirect(['action' => 'index']);
              }
              $this->Flash->error('Не удалось сохранить закладку. Пожалуйста, попробуйте еще раз.');
          }
          $tags = $this->Bookmarks->Tags->find('list');
          $this->set(compact('bookmark', 'tags'));
          $this->set('_serialize', ['bookmark']);
      }

      Устанавливая свойства объекту (entity) с помощью данных, полученных из формы, мы исключаем любую возможность изменения пользователем того, для кого предназначена сохраняемая закладка. То же самое сделаем для формы редактирования (убираем элемент управления 'user_id' в файле src/Template/Bookmarks/edit.ctp) и действия edit() в контроллере src/Controller/BookmarksController.php:

      public function edit($id = null)
          {
              $bookmark = $this->Bookmarks->get($id, [
              'contain' => ['Tags']
              ]);
              if ($this->request->is(['patch', 'post', 'put'])) {
                  $bookmark = $this->Bookmarks->patchEntity($bookmark, $this->request->getData());
                  $bookmark->user_id = $this->Auth->user('id');
                  if ($this->Bookmarks->save($bookmark)) {
                      $this->Flash->success('Закладка успешно сохранена.');
                      return $this->redirect(['action' => 'index']);
                  }
                  $this->Flash->error('Не удалось изменить закладку. Пожалуйста, попробуйте еще раз.');
              }
              $tags = $this->Bookmarks->Tags->find('list');
              $this->set(compact('bookmark', 'tags'));
              $this->set('_serialize', ['bookmark']);
          }

      Страница списка закладок

      Теперь давайте сделаем так, чтобы текущий пользователь видел только свои закладки. Для этого необходимо обновить их вызов в paginate(). Измените действие index() в контроллере src/Controller/BookmarksController.php следующим образом:

      public function index()
      {
          $this->paginate = [
              'conditions' => [
                  'Bookmarks.user_id' => $this->Auth->user('id'),
              ]
          ];
          $this->set('bookmarks', $this->paginate($this->Bookmarks));
          $this->set('_serialize', ['bookmarks']);
      }

      Также необходимо обновить действие tags() в контроллере src/Controller/BookmarksController.php и связанный с ним метод поиска, но это упражнение вы можете уже выполнить самостоятельно.

      Улучшение работы с тегами

      Прямо сейчас, добавление новых тегов - невозможный процесс, так как любой доступ к контроллеру TagsController закрыт. Вместо предоставления доступа, который вы можете уже сделать сами, по подобию, как мы это выполнили в контроллере BookmarksController, давайте лучше усовершенствуем пользовательский интерфейс выбора тегов, используя текстовое поле, разделенное запятыми. Это даст вам дополнительный опыт и позволит использовать более интересные функции в ORM.

      Добавление вычисляемого поля

      Поскольку нам нужен простой способ доступа к отформатированным тэгам для сущности Bookmark, мы можем добавить к ней виртуальное/вычисляемое поле. Для этого в файле src/Model/Entity/Bookmark.php добавьте следующее:

      use Cake\Collection\Collection;
      
      protected function _getTagString()
      {
          if (isset($this->_properties['tag_string'])) {
              return $this->_properties['tag_string'];
          }
          if (empty($this->tags)) {
              return '';
          }
          $tags = new Collection($this->tags);
          $str = $tags->reduce(function ($string, $tag) {
              return $string . $tag->title . ', ';
          }, '');
          return trim($str, ', ');
      }

      Это позволит получить доступ к вычисляемому свойству $bookmark->tag_string. Мы будем использовать это свойство в элементах управления позже. Так же, не забудьте добавить свойство tag_string в список $_accessible вашего объекта Bookmark, так как в дальнейшем мы будем его сохранить.

      В файле src/Model/Entity/Bookmark.php массив $_accessible будет выглядеть следующим образом:

      protected $_accessible = [
          'user_id' => true,
          'title' => true,
          'description' => true,
          'url' => true,
          'user' => true,
          'tags' => true,
          'tag_string' => true,
      ];

      Обновление представлений

      После обновления сущности мы можем добавить новый элемент управления для наших тегов. В src/Template/Bookmarks/add.ctp и src/Template/Bookmarks/edit.ctp замените существующий элемент управления tags._ids на следующий:

      echo $this->Form->control('tag_string', ['type' => 'text']);

      Сохранение строки тега

      Теперь, когда мы можем рассматривать существующие теги в виде строки, необходимо сделать так, чтобы эти данные сохранялись. Поскольку мы отметили tag_string как доступное свойство, ORM скопирует эти данные из запроса в нашу сущность. Мы можем использовать beforeSave() - метод ловушку (вызывается перед сохранением объекта) для разбора строки тега и поиска/создания связанных объектов. Добавьте следующий код в src/Model/Table/BookmarksTable.php:

      public function beforeSave($event, $entity, $options)
      {
          if ($entity->tag_string) {
              $entity->tags = $this->_buildTags($entity->tag_string);
          }
      }
      
      protected function _buildTags($tagString)
      {
          // Режем строку в массив тегов
          $newTags = array_map('trim', explode(',', $tagString));
          // Удаляем все пустые значения
          $newTags = array_filter($newTags);
          // Вырезаем все дублирующиеся теги
          $newTags = array_unique($newTags);
      
          $out = [];
          $query = $this->Tags->find()
              ->where(['Tags.title IN' => $newTags]);
      
          // Удаляем существующие теги из списка новых.
          foreach ($query->extract('title') as $existing) {
              $index = array_search($existing, $newTags);
              if ($index !== false) {
                  unset($newTags[$index]);
              }
          }
          // Добавляем существующие теги.
          foreach ($query as $tag) {
              $out[] = $tag;
          }
          // Добавляем новые теги.
          foreach ($newTags as $tag) {
              $out[] = $this->Tags->newEntity(['title' => $tag]);
          }
          return $out;
      }

      Хотя этот код немного сложнее, чем мы писали до сих пор, но он помогает продемонстрировать, насколько мощным является ORM в CakePHP. Вы можете манипулировать результатами запросов с помощью методов Collections (набор инструментов для работы с массивами или объектами Traversable) и обрабатывать сценарии, с легкостью создавая объекты, что называется «на лету».

      Заключение

      Если у вас что-то не получилось, или возникли дополнительные вопросы, о том как настроить доступ, например, к контроллеру тегов TagsController, то не отчаивайтесь. С моей странички на GitHub вы можете скачать исходники для созданного в этой статье приложения закладок. Всё что вам нужно будет сделать, это заменить папку src в корне установленного фреймворка CakePHP, а так же файлы app.php и routes.php в папке config, на скаченные исходники. Перед запуском приложения не забудьте создать базу данных (как это сделать, я рассказывал в первой части статьи) и проверить параметры подключения к ней в файле app.php.