Инструкция по началу работы

Лучший способ изучить CakePHP - это сесть и создать что-нибудь с его помощью. Для начала мы создадим простое приложение Менеджер Закладок

Пример Менеджер Закладок

Это руководство проведет вас через создание простого приложения для создания закладок (Менеджер Закладок). Для начала мы установим CakePHP, создадим нашу базу данных, и используем инструменты CakePHP для быстрой развертки нашего приложения.

Вот что вам понадобится:

  1. Сервер БД. Мы собираемся использовать в этом руководстве сервер MySQL. Вы должны знать достаточно об SQL, чтобы суметь создать базу данных: в остальном можно положиться на CakePHP. Поскольку мы используем MySQL, убедитесь также, что у Вас включено расширение pdo_mysql в PHP.

  2. Базовые знания PHP.

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

php -v

Убедитесь, что Вы используете версию PHP не ниже 5.5.9 (CLI). Версия PHP вашего веб-сервера также должна быть не ниже 5.5.9 и лучше всего должна совпадать с версией CLI. Если вы хотите увидеть полностью рабочее приложение, проверьте cakephp/bookmarker. Давайте приступим!

Получение CakePHP

Простейший путь установить CakePHP - это использование Composer. С помощью Composer Вы с легкостью установите фреймворк через командную строку или терминал. Сначала скачайте и установите Composer если у Вас его еще нет. Если у Вас установлен cURL, можете использовать следующую команду:

curl -s https://getcomposer.org/installer | php

Или Вы можете скачать composer.phar с веб-сайта Composer.

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

php composer.phar create-project --prefer-dist cakephp/app:^3.8 bookmarker

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

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

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

/cake_install
    /bin
    /config
    /logs
    /plugins
    /src
    /tests
    /tmp
    /vendor
    /webroot
    .editorconfig
    .gitignore
    .htaccess
    .travis.yml
    composer.json
    index.php
    phpunit.xml.dist
    README.md

Теперь настало самое подходящее время для ознакомления с файловой структурой приложения: проверьте раздел документации Структура папок CakePHP.

Проверка нашей установки

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

bin/cake server

Примечание

Для Windows, команда должна быть bin\cake server (с обратным слешем).

Это запустит встроенный веб-сервер PHP на порте 8765. Откройте http://localhost:8765 в вашем веб-браузере, чтобы увидеть страницу приветствия. Все проверки на странице должны быть пройдены, за исключением проверки подключения к базе данных. Если же это не так, возможно вам следует установить еще какие-то расширения PHP или установить права доступа к папкам.

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

Следующим шагом давайте настроим необходимую базу данных для нашего Менеджера Закладок. Если вы еще этого не сделали - создайте пустую базу данных для работы вашего приложения, с любым удобным именем, например cake_bookmarks. Вы можете выполнить следующий SQL-запрос, для создания необходимых таблиц:

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    email VARCHAR(255) NOT NULL,
    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 поддерживает составные первичные ключи почти везде, делая легче процесс создания мультиарендных приложений.

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

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

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

Настройка должна показаться довольно легкой: просто замените значения в массиве Datasources.default в файле config/app.php на нужные вам. В результате у вас должно получиться что-то вроде этого:

return [
    // More configuration above.
    'Datasources' => [
        'default' => [
            'className' => 'Cake\Database\Connection',
            'driver' => 'Cake\Database\Driver\Mysql',
            'persistent' => false,
            'host' => 'localhost',
            'username' => 'cake_blog',
            'password' => 'AngelF00dC4k3~',
            'database' => 'cake_bookmarks',
            'encoding' => 'utf8',
            'timezone' => 'UTC',
            'cacheMetadata' => true,
        ],
    ],
    // More configuration below.
];

Как только вы сохраните ваш файл config/app.php, на приветственной странице CakePHP вы увидите сообщение, что База данных обнаружена и подключение к ней прошло успешно.

Примечание

Копия файла с настройками по умолчанию может быть найдена в config/app.default.php.

Генерирование шаблонного кода

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

// В Windows пишите bin\cake.
bin/cake bake all users
bin/cake bake all bookmarks
bin/cake bake all tags

Это сгенерирует код Контроллеров, Моделей, Видов и т.д. для наших ресурсов users, bookmarks, tags. Если вы остановили работу вашего веб-сервера, перезапустите его и перейдите по адресу http://localhost:8765/bookmarks.

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

Примечание

Если у вас отображается 404 ошибка, убедитесь, что модуль Apache mod_rewrite загружен.

Хеширование паролей

Когда вы создали ваших пользователей (посетив адрес http://localhost:8765/users), вы вероятно были оповещены, что пароли сохранены в виде простого текста. Это очень плохо с точки зрения безопасности, давайте исправим это.

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

К примеру, хеширование паролей выполняется индивидуально для каждой записи, таким образом внедрим это поведение в объект сущности (entity). Так как мы хотим хешировать пароль каждый раз, когда он устанавливается, мы будем использовать метод мутатор/сеттер. CakePHP будет вызывать основанные на соглашениях методы-сеттеры каждый раз, когда свойство будет установлено на одной из сущностей. Давайте добавим сеттер для пароля. В файле src/Model/Entity/User.php добавьте следующий код:

namespace App\Model\Entity;

use Cake\Auth\DefaultPasswordHasher; //добавьте эту строку
use Cake\ORM\Entity;

class User extends Entity
{

    // Код от bake.

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

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

Примечание

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

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

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

В идеале у нас должны быть адреса наподобие такого http://localhost:8765/bookmarks/tagged/funny/cat/gifs. Подобный адрес даст нам возможность найти все закладки с тегами „funny“, „cat“ или „gifs“. Прежде чем начать, нам нужно добавить новые маршруты. Ваш файл config/routes.php должен выглядеть примерно так:

<?php
use Cake\Routing\Route\DashedRoute;
use Cake\Routing\Router;

Router::defaultRouteClass(DashedRoute::class);

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

Router::scope('/', function ($routes) {
    // Connect the default home and /pages/* routes.
    $routes->connect('/', [
        'controller' => 'Pages',
        'action' => 'display', 'home'
    ]);
    $routes->connect('/pages/*', [
        'controller' => 'Pages',
        'action' => 'display'
    ]);

    // Connect the conventions based default routes.
    $routes->fallbacks();
});

Приведенный выше код определяет новый „маршрут“, соединяющий путь /bookmarks/tagged/ с экшеном BookmarksController::tags(). Объявляя маршруты, мы можем отделить то, как URL-адреса выглядят, от того, как они формируются. Если бы мы попытались перейти по адресу http://localhost:8765/bookmarks/tagged, мы бы увидели сообщение об ошибке от CakePHP, говорящее о том, что нужный экшен отсутствует в Контроллере. Давайте теперь добавим нужный метод. В файле src/Controller/BookmarksController.php добавьте следующее:

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

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

    // Передаем переменные в Вид.
    $this->set([
        'bookmarks' => $bookmarks,
        'tags' => $tags
    ]);
}

Для получения более подробной информации о запросах посмотрите раздел Запрос.

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

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

// Аргумент $query это экземпляр класса конструктора запросов.
// Массив $options будет содержать опцию 'tags' переданную нами
// в find('tagged') в экшене нашего Контроллера.
public function findTagged(Query $query, array $options)
{
    $bookmarks = $this->find()
        ->select(['id', 'url', 'title', 'description']);

    if (empty($options['tags'])) {
        $bookmarks
            ->leftJoinWith('Tags')
            ->where(['Tags.title IS' => null]);
    } else {
        $bookmarks
            ->innerJoinWith('Tags')
            ->where(['Tags.title IN ' => $options['tags']]);
    }

    return $bookmarks->group(['Bookmarks.id']);
}

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

Создание Вида

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

<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>

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

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

Вы можете заметить, что мы могли использовать переменные $tags и $bookmarks в нашем Виде. Используя метод set() в нашем Контроллере, мы определяем переменные, которые должны быть доступны в Виде. Вид сделает все переданные переменные доступными в качестве локальных.

Теперь вы можете например перейти по URL-адресу /bookmarks/tagged/funny, и увидеть все закладки с тегом „funny“.

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

Теперь вы можете продолжить перейдя во вторую часть руководства Пример Менеджер Закладок Часть 2 для дальнейшей разработки приложения, или же погрузиться в изучение документации чтобы узнать больше о том, что CakePHP может сделать для вас.

Пример Менеджер Закладок Часть 2

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

Вход пользователя

В CakePHP аутентификация обрабатывается Компоненты. Компоненты можно представить как повторно используемые куски кода Контроллера для реализации какой-либо функциональности. Компоненты также могут цепляться к жизненному циклу событий Контроллера и взаимодействовать с вашим приложением таким способом. Для начала мы добавим Компонент Auth в наше приложение. Мы в достаточной степени хотим, чтобы каждый метод требовал аутентификацию, так что, мы добавим 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'
            ],
            'unauthorizedRedirect' => $this->referer() // Если не авторизованы, то возвращаем на страницу, где только что были
        ]);

        // Разрешение экшена display, чтобы наш контроллер pages
        // продолжал работать.
        $this->Auth->allow(['display']);
    }
}

Мы только что сообщили CakePHP, что мы хотим загрузить компоненты Flash и Auth. В дополнение мы кастомизировали конфигурацию компонента Auth, таким образом, что наша таблица users использует email в качестве имени пользователя. Теперь, если вы попробуете перейти по любому URL, то будете переброшены наURL /users/login, который покажет ошибку, так как мы еще не написали необходимый код. Давайте создадим экшен login:

// В 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 добавьте следующее:

<h1>Вход</h1>
<?= $this->Form->create() ?>
<?= $this->Form->input('email') ?>
<?= $this->Form->input('password') ?>
<?= $this->Form->button('Войти') ?>
<?= $this->Form->end() ?>

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

Примечание

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

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

Выход пользователя

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

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

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

Этот код делает доступным экшен logout в качестве публичного и реализует метод logout. Теперь вы можете перейти по адресу /users/logout, чтобы разавторизоваться. После этого вы должны быть перенаправлены на страницу входа.

Добавление регистрации пользователей

Если вы не авторизованы, и попытаетесь посетить /users/add, то будете перенаправлены на страницу входа. Мы должны исправить это, так как мы хотим, чтобы у наших пользователей была возможность регистрации в нашем приложении. В UsersController добавьте следующее:

public function initialize()
{
    parent::initialize();
    // Добавили logout в список разрешенных экшенов.
    $this->Auth->allow(['logout', 'add']);
}

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

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

Теперь, когда пользователи могут авторизоваться, мы хотим разрешить им доступ только к их собственным закладкам. Мы сделаем это, используя адаптер „authorization“. Так как наши требования предельно просты, мы можем написать какой-нибудь простой код в нашем контроллере BookmarksController. Но перед этим мы бы хотели сказать компоненту Auth как наше приложение собирается авторизовывать экшены. В AppController добавьте следующий код:

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

Также добавьте следующее в настройки Auth в вашем AppController:

'authorize' => 'Controller',

Ваш метод initialize() должен выглядеть следующим образом:

public function initialize()
{
    $this->loadComponent('Flash');
    $this->loadComponent('Auth', [
        'authorize'=> 'Controller',//добавили эту строку
        'authenticate' => [
            'Form' => [
                'fields' => [
                    'username' => 'email',
                    'password' => 'password'
                ]
            ]
        ],
        'loginAction' => [
            'controller' => 'Users',
            'action' => 'login'
        ],
        'unauthorizedRedirect' => $this->referer()
    ]);

    // Разрешаем экшен display чтобы наш контроллер pages
    // продолжал работать.
    $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);
}

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

// В src/Template/Layout/default.ctp
<?= $this->Flash->render() ?>

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

Доработка форм и Вида списка закладок

В то время, как экшены view и delete работают, у экшенов edit, view и index имеются некоторыен проблемы:

  1. При добавлении закладки вы можете выбрать пользователя.

  2. При редактировании закладки вы можете выбрать пользователя.

  3. В списке выводятся закладки всех пользователей.

Давайте для начала разберемся с формой для добавления закладок. Удалите input('user_id') из шаблона src/Template/Bookmarks/add.ctp. Также нам нужно обновить экшен 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) из данных сессии, мы исключаем любую возможность изменения пользователем информации о том кому принадлежит закладка. Мы сделаем то же самое для формы и экшена edit. Ваш экшен 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() и соответствующий поисковый метод, но мы оставим вам данную задачу в качестве тренировки для самостоятельного решения.

Улучшение пользовательского опыта в тегах

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

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

Так как нам хочется простого способа получения доступа к отформатированным тегам объекта(entity), мы можем добавить виртуальное/вычисляемое поле к нему. В 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 в вашем объекте, так как нам понадобится „сохранять“ его в дальнейшем. This will let us access the $bookmark->tag_string computed property. We’ll use this property in inputs later on. Remember to add the tag_string property to the _accessible list in your entity, as we’ll want to „save“ it later on.

В src/Model/Entity/Bookmark.php добавьте tag_string в $_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->input('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, и реализуя сценарии, где вы создаете с легкостью объекты налету.

Заключение

Мы расширили наше приложение для закладок, реализовав сценарии аутентификации и базовой авторизации/контроля доступа. Мы также добавили некоторые милые улучения UX засчет использования хелпера FormHelper и возможностей ORM.

Спасибо за то, что уделили время изучению CakePHP. В довершение вы можете изучить Пример создания блога, узнать больше об Доступ к Базе Данных и ORM, или же можете пролистать Using CakePHP.