Bookmarker Tutorial



Перед тем как приступить к созданию приложения убедитесь, что у вас установлен локальный сервер и скачена актуальная версия самого фреймворка CakePHP. Так же убедитесь в том, что локальный сервер запущен, а заготовка вашего приложения открывается через адресную строку браузера (например: http://yousite.loc) в виде стартовой страницы фреймворка (вид на момент написания статьи): CakePHP-3, стартовая страница

Страница приветствия кейка, так же, выводит информацию о настройке расширений PHP, плагина DebugKit и информацию о подключении вашего приложения к базе данных. Если напротив соответствующей настройки фигурирует зеленый маркер, то проблем нет, в противном случае маркер будет красным и вам придется изменить необходимые настройки PHP (в статье по установке фреймворка CakePHP 3 это рассматривалось). На рисунке выше видно, что красный маркер стоит около настройки подключения к базе данных, которое мы ещё не успели произвести.

Создание базы данных

Далее, давайте создадим базу данных для нашего сайта закладок. Если вы еще не сделали этого, то создайте пустую базу данных, которую мы будем использовать в этом учебнике, с именем по вашему выбору, например, cake_bookmarks.

Внимание! При создании базы данных не забудьте указать нужную вам кодировку. В моем случае это будет utf8_general_ci.

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

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    email VARCHAR(255) NOT NULL UNIQUE,
    password VARCHAR(255) NOT NULL,
    created DATETIME,
    modified DATETIME
);

CREATE TABLE bookmarks (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    title VARCHAR(50),
    description TEXT,
    url TEXT,
    created DATETIME,
    modified DATETIME,
    FOREIGN KEY user_key (user_id) REFERENCES users(id)
);

CREATE TABLE tags (
    id INT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(255),
    created DATETIME,
    modified DATETIME,
    UNIQUE KEY (title)
);

CREATE TABLE bookmarks_tags (
    bookmark_id INT NOT NULL,
    tag_id INT NOT NULL,
    PRIMARY KEY (bookmark_id, tag_id),
    FOREIGN KEY tag_key(tag_id) REFERENCES tags(id),
    FOREIGN KEY bookmark_key(bookmark_id) REFERENCES bookmarks(id)
);

Обратите внимание, что в таблице bookmarks_tags (таблица для организации связи многие ко многим) используется составной первичный ключ. CakePHP поддерживает составные первичные ключи практически везде, что облегчает проектирование мульти-арендуемых (multi-tenanted) приложений.

Имена таблиц и столбцов, которые мы использовали, не были произвольными, а были сформированы на основании соглашений об наименованиях. Это позволяет нам эффективно использовать возможности CakePHP и избежать необходимости выполнять дополнительные настройки.

Конфигурация базы данных

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

Для выполнения подключения подставьте в массив Datasources.default конфигурационного файла config/app.php значения, которые применимы для вашей базы данных. Это username (имя пользователя, для локального сервера - это как правило root), password (пароль - для локального сервера как правило отсутствует) и database (имя базы, в нашем примере это cake_bookmarks). Готовый массив-пример конфигурации БД может выглядеть следующим образом:

return [
    //другие конфигурационные данные, расположенные выше.
    'Datasources' => [
        'default' => [
            'className' => 'Cake\Database\Connection',
            'driver' => 'Cake\Database\Driver\Mysql',
            'persistent' => false,
            'host' => 'localhost',
            'username' => 'root',
            'password' => '',
            'database' => 'cake_bookmarks',
            'encoding' => 'utf8',
            'timezone' => 'UTC',
            'cacheMetadata' => true,
        ],
    ],
    //другие конфигурационные данные, расположенные ниже.
];

После того, как вы сохраните файл config/app.php и обновите стартовую страницу CakePHP, красный маркер, рядом с информацией о подключении к базе данных, изменится на зеленый.

Внимание! Копия файла конфигурации CakePHP (конфигурация по умолчанию), находится в config/app.default.php. Поэтому, если вы что либо напортачили с конфигурацией, всегда можно исправить ситуацию, воспользовавшись указанной копией.

Генерация основного кода

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

// Если у вас Windows, то используйте обратный слэш (например bin\cake bake all users).
bin/cake bake all users
bin/cake bake all bookmarks
bin/cake bake all tags

Консоль фреймворка создаст необходимые контроллеры, модели и представления для наших пользователей (users), закладок (bookmarks) и тегов (tags). CakePHP, так же, учтет логику взаимодействия между указанными моделями (связи, которые мы прописывали при создании таблиц базы данных). Если вы остановили свой локальный сервер, запустите его и перейдите к http://yousite.loc/bookmarks.

В окне браузера вы должны увидеть простой, но функциональный интерфейс управления данными приложения, предоставляющий доступ к созданным таблицам базы данных. Используя интерфейс, добавьте по нескольку записей в каждую таблицу.

Внимание! Если вы видите сообщение, что страница не найдена: Not Found (404), убедитесь, что в Apache загружен модуль mod_rewrite.

Добавление метода хэширования паролей

При создании пользователей (по адресу http://yousite.loc/users), вы, вероятно, заметили, что пароли хранятся в виде обычного текста. Это очень плохо с точки зрения безопасности, так что давайте эту ситуацию исправим.

Так же, настало время, чтобы поговорить о слое модели. В CakePHP, существуют методы, которые управляют коллекциями объектов (например: пользователи, закладки, теги) и методы, относящиеся к единичному объекту (например: пользователь, закладка, тег). Методы, которые работают с коллекциями располагаются в классе таблицы, в то время как функции, принадлежащие к одной записи находятся в классе единичного объекта (Entity).

Например, хеширование пароля производится для отдельной записи (пользователя), поэтому оно реализуется в классе Entity (для одиночного объекта). Хеширование будет выполняться каждый раз, когда происходит присваивание значений свойствам объекта при его сохранении. Для реализации указанной задачи, внесем изменения в файле src/Model/Entity/User.php:

namespace App\Model\Entity;

use Cake\Auth\DefaultPasswordHasher; //подключаем необходимый класс
use Cake\ORM\Entity;

class User extends Entity
{

    // Вышестоящий код, сгенерированный Кейком

    protected function _setPassword($value)
    {
        $hasher = new DefaultPasswordHasher();
        return $hasher->hash($value);
    }
}

Теперь, при создании нового пользователя, либо при изменении пароля существующего пользователя, на странице http://yousite.loc/users вы уведите зашифрованные пароли, находящиеся в столбце password. По умолчанию, CakePHP осуществляет хэширование паролей согласно стандарту bcrypt. Так же, вы можете использовать другие алгоритмы хэширования, такие как sha1 или md5.

Внимание! Если у вас не происходит хэширование пароля, то убедитесь, что название функции соответствует названию столбца в базе данных. Например: если столбец password, то функция будет называться _setPassword, но если вы назвали столбец pass, то название функции будет _setPass.

Получение закладок по тэгам

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

В идеале нам нужен URL, который бы выглядел следующим образом: http://yousite.loc/bookmarks/tagged/funny/cat/gifs. Это позволит нам найти все закладки, которые имеют теги 'funny', 'cat' или 'gifs'. Итак, для того, чтобы продолжить, мы добавим новый маршрут. Для этого внесем соответствующие записи в конфигурационный файл маршрутизации config/routes.php, который будет выглядеть следующим образом:

<?php

use Cake\Core\Plugin;
use Cake\Routing\RouteBuilder;
use Cake\Routing\Router;
use Cake\Routing\Route\DashedRoute;


Router::defaultRouteClass(DashedRoute::class);

// Ниже мы добавляем новый маршрут для действия tagged.
// `*` означает, что после указанного маршрута
//могут быть добавлены параметры.
Router::scope(
    '/bookmarks',
    ['controller' => 'Bookmarks'],
    function ($routes) {
        $routes->connect('/tagged/*', ['action' => 'tags']);
    }
);

Router::scope('/', function (RouteBuilder $routes) {
   
    //подключение маршрута для стартовой 
    //страницы (список закладок)
    $routes->connect('/', [
        'controller' => 'Bookmarks', 
        'action' => 'index'
    ]);

    $routes->fallbacks(DashedRoute::class);
});

Plugin::routes();

И так, мы определили новый маршрут для пути /bookmarks/tagged/, который вызовет функцию tags() в контроллере BookmarksController (BookmarksController::tags()). Таким образом, прописывая маршруты, вы можете определять как будут выглядеть ваши URL адреса и как они будут реализовываться. Теперь, если вы посетите маршрут http://yousite.loc/bookmarks/tagged, то увидите страницу ошибки от CakePHP, информирующую о том, что действие контроллера tags() не существует. Давайте реализуем этот отсутствующий метод, путем внесения соответствующего изменения в контроллер src/Controller/BookmarksController.php:

public function tags()
{
    //Ключ 'pass' поддерживается в CakePHP и содержит 
    //все элементы URL пути, переданные в запросе (request)
    $tags = $this->request->params['pass'];

    // Используем BookmarksTable для поиска закладок по тэгам.
    $bookmarks = $this->Bookmarks->find('tagged', [
        'tags' => $tags
    ]);

    // Формируем переменные, передаваемые в шаблон представления.
    $this->set([
        'bookmarks' => $bookmarks,
        'tags' => $tags
    ]);
}

Более подробную информацию по получению доступа к другим данным запроса ($this->request), вы найдете, обратясь к разделу Request официального сайта Кейка.

Создание метода поиска

Для того, чтобы контроллеры нашего приложения не были перегружены кодом, большую часть логики в CakePHP принято размещать в моделях. Если вы посетите URL адрес /bookmarks/tagged, то увидите сообщение об ошибке, говорящее, что метод findTagged() еще не реализован. Чтобы это исправить, давайте внесем соответствующие изменения в файл src/Model/Table/BookmarksTable.php

//Аргумент $query является экземпляром конструктора запросов.
//Массив $options содержит в себе тэги, которые мы
//передали при вызове метода find('tagged') в контроллере (см.выше)
public function findTagged(Query $query, array $options)
    {
        $bookmarks = $this->find()
            ->select(['id', 'url', 'title', 'description']);
        if (empty($options['tags'])) {
            $bookmarks->innerJoinWith('Tags'); 
        } else {
            $bookmarks->innerJoinWith('Tags', function ($q) use ($options) {
                return $q->where(['Tags.title IN' => $options['tags']]);
            });
        }
        return $bookmarks->group(['Bookmarks.id']);
    }

Выше мы реализовали свой собственный (пользовательский) метод поиска find(). Это очень мощная концепция в CakePHP, которая позволяет создавать и повторно использовать разного рода запросы. Finder методы всегда получают объект Query Builder (объект запроса) и массив опций в качестве параметров. Искатели могут манипулировать запросами и добавлять любые необходимые условия или критерии, возвращая после своего завершения измененный объект запроса. В нашем поиске мы использовали метод innerJoinWith(), позволяющий находить различные закладки, которые относятся к соответствующему тегу (тегам). Метод innerJoinWith(), в качестве аргумента принимает анонимную функцию, определяющую условия запроса (внутри обратного вызова мы используем конструктор запросов, чтобы определить условия, согласно которым будут фильтроваться закладки, относящиеся к тегу поиска).

Создание вида (View)

Теперь, если вы посетите URL /bookmarks/tagged, CakePHP покажет ошибку, которая укажет на то, что не был создан файл представления. Далее, давайте создадим такой файл (src/Template/Bookmarks/tags.ctp) для нашего действия tags() и поместим в него следующее содержание:

<div class="bookmarks columns">
    <h1>
        Закладки, помеченные тегами
        <?= $this->Text->toList(h($tags)) ?>
    </h1>

    <section>
    <?php foreach ($bookmarks as $bookmark): ?>
        <article>
            <!-- Используется HtmlHelper для создания ссылки -->
            <h4><?= $this->Html->link($bookmark->title, $bookmark->url) ?></h4>
            <small><?= h($bookmark->url) ?></small>

            <!-- Используется TextHelper для форматирования текста -->
            <?= $this->Text->autoParagraph(h($bookmark->description)) ?>
        </article>
    <?php endforeach; ?>
    </section>
</div>

В приведенном выше коде используется HTML и Text помощники (helpers - хелперы), которые помогают упростить процесс создания внешнего вида нашего представления. В примере, так же, используется функция h - аналог php функции htmlspecialchars(). С целью исключения проблем, связанных с инъекциями, настоятельно рекомендуется пропускать все данные, получаемые от пользователей, через функцию h().

Файл tags.ctp, который мы только что создали, следует соглашениям CakePHP для файлов шаблонов представления. Соглашение состоит в том, что имя файла шаблона повторяет название экшена (tags()) контроллера, записанное в нижнем регистре.

Обратите внимание, что в шаблоне вида использовались переменные $tags и $bookmarks, которые были определены методом set() в контроллере и отправлены в представление. Слой View сделал их доступными в шаблоне как локальные переменные.

Теперь, если вы посетите URL адрес /bookmarks/tagged/funny, приложение выведет в браузер все закладки, относящиеся к тегу 'funny'.

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

На этом пока все, жду вас во второй части примера по созданию сайта закладок на основе фреймворка CakePHP (Bookmarker Tutorial, часть 2).