Розділи

Навчальний посібник - Vökuró


Vökuró

Vökuró is a sample application, showcasing a typical web application written in Phalcon. This application focuses on:

  • User Login (security)
  • User Signup (security)
  • User Permissions
  • User management

NOTE: You can use Vökuró as a starting point for your application and enhance it further to meet your needs. By no means this is a perfect application, and it does not fit all needs.

NOTE: This tutorial assumes that you are familiar with the concepts of the Model View Controller design pattern. (see References at the end of this tutorial)

NOTE: Note the code below has been formatted to increase readability

Встановлення

Завантаження

In order to install the application, you can either clone or download it from GitHub. Ви можете відвідати сторінку GitHub, завантажити додаток і потім розпакувати його в каталог на вашому комп’ютері. Крім того, ви можете використовувати git clone:

git clone https://github.com/phalcon/vokuro

Розширення

Для запуску Vökuró необхідно виконати певні умови. You will need to have PHP >= 7.2 installed on your machine and the following extensions:

  • ctype
  • curl
  • dom
  • json
  • iconv
  • mbstring
  • memcached
  • opcache
  • openssl
  • pdo
  • pdo_mysql
  • psr
  • session
  • simplexml
  • xml
  • xmlwriter

Phalcon повинен бути встановлений. Перейдіть на сторінку встановлення, якщо вам потрібна допомога з встановленням Phalcon.

Нарешті, вам також потрібно буде переконатися, що ви оновили пакети композера (див. розділ нижче).

Старт

Якщо всі вищезазначені вимоги задоволені, ви можете запустити додаток за допомогою локального PHP веб-сервера, виконавши таку команду в терміналі:

php -S localhost:8080 -t public/ .htrouter.php

Ця команда запустить сайт для localhost з портом 8080. Ви можете змінити ці налаштування відповідно до ваших потреб. Крім того, ви можете налаштувати свій сайт в Apache або nginX за допомогою віртуального хоста. Будь ласка, зверніться до відповідної документації, щоб налаштувати віртуальний хост для цих веб-серверів.

Docker

У папці resources ви знайдете Dockerfile, який дозволяє швидко налаштувати середовище і запустити програму. Щоб використовувати Dockerfile нам потрібно визначити назву нашого докеризованого додатка. Для цілей цього посібника ми використаємо phalcon-tutorial-vokuro.

З кореню додатка ми повинні компілювати проект (вам потрібно це робити лише раз):

$ docker build -t phalcon-tutorial-vokuro -f docker/Dockerfile .

а потім запустіть його

$ docker run -it --rm phalcon-tutorial-vokuro bash

Це дозволить нам отримати доступ до докеризованого середовища. Щоб перевірити версію PHP:

[email protected]:/code $ php -v

PHP 8.1.8 (cli) (built: Jul 12 2022 08:28:43) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.1.8, Copyright (c) Zend Technologies
    with Xdebug v3.1.5, Copyright (c) 2002-2022, by Derick Rethans

та Phalcon:

[email protected]:/code $ php -r 'echo Phalcon\Version::get();'

4.0.0

Тепер ви маєте докеризоване середовище з усіма необхідними компонентами, щоб запустити Vökuró.

Nanobox

У теці resources ви також знайдете файл boxfile.yml, що дозволяє використовувати nanobox для швидкого налаштування середовища. Вам потрібно просто скопіювати файл в кореневий каталог і запустити nanobox run php-server. Після налаштування додатка ви зможете перейти за IP-адресою, що відображається на екрані та працювати з цим додатком.

For more information on how to set up nanobox, check our [Environments Nanobox][environments-nanobox] page as well as the Nanobox Guides page

NOTE: In this tutorial, we assume that your application has been downloaded or cloned in a directory called vokuro.

Structure

Погляньте на структуру додатка:

vokuro/
    .ci
    configs
    db
        migrations
        seeds
    public
    resources
    src
        Controllers
        Forms
        Models
        Phalcon
        Plugins
        Providers
    tests
    themes
        vokuro
    var
        cache
            acl
            metaData
            session
            volt
        logs
    vendor
Каталог Description
.ci Файли, необхідні для налаштування служб для CI
configs Файли конфігурації
db Містить файли міграції для бази даних
public Точка входу в додаток, місце зберігання файлів css, js та зображень
resources Файли Docker/nanobox для налаштування додатка
src Місце розташування всіх основних файлів (контролери, форми тощо)
src/Controllers Контролери
src/Forms Форми
src/Models Моделі бази даних
src/Plugins Плагіни
src/Providers Постачальники: налаштування сервісів у контейнері DI
tests Тести
themes Теми/подання для легкого налаштування
themes/vokuro Тема додатку за замовчуванням
var Різні допоміжні файли
var/cache Файли кешу
var/logs Журнали
vendor Бібліотеки сторонній постачальників/композера

Configuration

.env

Vökuró uses the popular Dotenv library by Vance Lucas. Бібліотека використовує .env файл розташований в кореневій теці, який містить параметри конфігурації, такі як сервер бази даних, ім’я користувача, пароль тощо. Там є файл .env.example, який постачається з Vökuró, який можна скопіювати та перейменувати на .env а потім відредагувати його у відповідності до умов вашого середовища. Ви повинні зробити це спочатку, щоб ваш додаток міг працювати належним чином.

Доступні варіанти:

Option Description
APP_CRYPT_SALT Random and long string that is used by the Phalcon\Encryption\Crypt component to produce passwords and any additional security features
APP_BASE_URI Зазвичай /, якщо ваш веб-сервер спрямовує запити безпосередньо у каталог Vökuró. Якщо ви встановили Vökuró в підкаталозі, ви можете відкоригувати базовий URI
APP_PUBLIC_URL Публічний URL додатку. Використовується для електронних листів.
DB_ADAPTER Адаптер бази даних. Доступні адаптери: mysql, pgsql, sqlite. Будь ласка, переконайтеся, що в вашій системі встановлені відповідні розширення бази даних.
DB_HOST Хост бази даних
DB_PORT Порт бази даних
DB_USERNAME Ім’я користувача бази даних
DB_PASSWORD Пароль бази даних
DB_NAME Ім’я бази даних
MAIL_FROM_NAME Ім’я FROM при надсиланні електронної пошти
MAIL_FROM_EMAIL FROM email при надсиланні електронної пошти
MAIL_SMTP_SERVER Сервер SMTP
MAIL_SMTP_PORT SMTP порт
MAIL_SMTP_SECURITY Безпека SMTP (наприклад, tls)
MAIL_SMTP_USERNAME Ім’я користувача SMTP
MAIL_SMTP_PASSWORD Пароль до SMTP
CODECEPTION_URL Сервер Codection для випробувань. Якщо ви запускаєте тести локально, це має бути 127.0.0.1
CODECEPTION_PORT Порт Codeception

Після того, як файл конфігурації буде збережено, перехід у браузері за цією IP-адресою відобразить щось схоже на це:

База данних

Також потрібно ініціалізувати базу даних. Vökuró uses the popular library Phinx by Rob Morgan (now the Cake Foundation). Бібліотека використовує власний файл конфігурації (phinx.php), але для Vökuró вам не потрібно змінювати будь-які параметри, оскільки phinx.php читає файл .env, щоб отримати налаштування конфігурації. Це дозволяє вам встановити параметри конфігурації в одному місці.

Тепер нам потрібно буде розпочати міграцію. Щоб перевірити статус нашої бази даних:

/app $ ./vendor/bin/phinx status

Ви побачите цей екран:

Щоб ініціалізувати базу даних, нам потрібно запустити міграції:

/app $ ./vendor/bin/phinx migrate

Екран відображатиме дію:

А команда status тепер покаже всі зелені:

Config

acl.php

Заглянувши у папку config/, ви помітите чотири файли. Вам не потрібно змінювати ці файли, щоб запустити додаток, але якщо ви хочете їх змінити, то це саме те місце, де вони розташовані. The acl.php file returns an array of routes that controls which routes are visible to only logged-in users.

Поточне налаштування вимагає, щоб користувач увійшов у систему, якщо хоче отрисати доступ до таких маршрутів:

  • users/index
  • users/search
  • users/edit
  • users/create
  • users/delete
  • users/changePassword
  • profiles/index
  • profiles/search
  • profiles/edit
  • profiles/create
  • profiles/delete
  • permissions/index

If you use Vökuró as a starting point for your own application, you will need to modify this file to add or remove routes to ensure that your protected routes are behind the login mechanism.

NOTE: Keeping the private routes in an array is efficient and easy to maintain for a small to medium application. Як тільки ваш додаток почне зростати, ви можете розглянути іншу техніку зберігання своїх приватних иаршрутів, наприклад: база даних з механізмом кешування.

config.php

Цей файл містить всі параметри конфігурації, які потрібно Vökuró. Usually you will not need to change this file, since the elements of the array are set by the .env file and Dotenv. Однак, ви можете захотіти змінити місцезнаходження своїх журналів чи інші шляхи, вирішите змінити структуру каталогів.

Одним з елементів, які ви можете захотіти змінити у роботі з Vökuró на своїй локальній машині є useMail та встановити його на false. Це вкаже Vökuró, не намагатися підключатися до поштового сервера, щоб надіслати повідомлення при реєстрації користувача на сайті.

providers.php

Цей файл містить всіх постачальників, які потрібні Vökuró. Це список класів нашого додатку, що реєструє певні класи у контейнері DI. Якщо вам потрібно зареєструвати нові компоненти у контейнері DI, ви можете додати їх до масиву цього файлу.

routes.php

У цьому файлі містяться маршрути, які розуміє Vökuró. Роутер уже зареєстрував маршрути за замовчуванням, тому будь-які маршрутизатори, визначені в routes.php є специфічними і нетиповими. You can add any non-standard routes you need, when customizing Vökuró, in this file. На всякий випадок нагадаємо маршрути за замовчуванням:

/:controller/:action/:parameters

Постачальники

Як було зазначено вище, Vökuró використовує класи під назвою Providers для реєстрації послуг у контейнері DI. Це один зі способів реєстрації послуг в контейнері DI, ніщо не заважає вам помістити всі ці реєстрації в один файл.

Для Vökuró ми вирішили використовувати окремі файли для кожного сервісу, та файл providers.php (див. вище) в якості реєстраційного масиву конфігурації для цих сервісів. Це дозволяє нам мати набагато менші фрагменти коду, організовані в окремих файлах для різних сервісів, а також масив який дозволяє нам реєструвати чи відключати службу без видалення файлів. Все, що нам потрібно - це змінити масив providers.php.

Класи постачальників розташовані в src/Providers. Each of the provider classes implements the Phalcon\Di\ServiceProviderInterface interface. Для отримання додаткової інформації дивіться нижче у розділі завантажувача.

Composer

Vökuró uses composer to download and install supplemental PHP libraries. Бібліотеки, що використовуються:

Глянувши у composer.json, бачимо, що необхідні такі пакунки:

"require": {
    "php": ">=7.2",
    "ext-openssl": "*",
    "ext-phalcon": "~4.0.0-beta.2",
    "robmorgan/phinx": "^0.11.1",
    "swiftmailer/swiftmailer": "^5.4",
    "vlucas/phpdotenv": "^3.4"
}

Якщо це нова установка, ви можете запустити

composer install

or if you want to upgrade the existing installations of the above packages:

composer update

For more information about composer, you can visit their documentation page.

Завантажувач

Точка входу

Вхідною точкою нашого додатку є public/index.php. У цьому файлі міститься необхідний код, який збирає і завантажує додаток. Він також служить єдиною точкою входу до нашого додатка, спрощує нам відловлювання помилок, захист файлів тощо.

Давайте поглянемо на код:

<?php

use Vokuro\Application as VokuroApplication;

error_reporting(E_ALL);
$rootPath = dirname(__DIR__);

try {
    require_once $rootPath . '/vendor/autoload.php';

    Dotenv\Dotenv::create($rootPath)->load();

    echo (new VokuroApplication($rootPath))->run();
} catch (Exception $e) {
    echo $e->getMessage(), '<br>';
    echo nl2br(htmlentities($e->getTraceAsString()));
}

Перш за все, ми пересвідчуємось, що маємо повноцінне звітування про помилки. Звісно, ви можете змінити це, якщо бажаєте, або переписати код, щоб звітування про помилки контролювалось через записи у вашому .env файлі.

Блок try/catch згортає усі операції. Це гарантує, що на екрані з’являться всі помилки.

NOTE You will need to rework the code to enhance security. Якщо зараз станеться помилка бази даних, код catch виведе на екран технічну інформацію щодо доступу до бази даних з інформацією про помилку. This code is intended as a tutorial not a full scale production application

Ми впевнені, що маємо доступ до всіх підтримуваних бібліотек, завантажуючи автозавантажувач композера. У composer.json ми також визначили запис autoload, що забезпечує автозавантаження будь-яких класів з простору імен Vokuro з теки src.

"autoload": {
    "psr-4": {
        "Vokuro\\": "app/"
    },
    "files": [
        "app/Helpers.php"
    ]
}

Потім ми завантажуємо змінні середовища, визначені у нашому файлі .env, викликаючи

Dotenv\Dotenv::create($rootPath)->load();

І нарешті ми запускаємо нашу програму.

Application

Вся логіка програми загорнута в клас Vokuro\Application. Давайте подивимося, як це робиться:

<?php
declare(strict_types=1);

namespace Vokuro;

use Exception;
use Phalcon\Application\AbstractApplication;
use Phalcon\Di\DiInterface;
use Phalcon\Di\FactoryDefault;
use Phalcon\Di\ServiceProviderInterface;
use Phalcon\Mvc\Application as MvcApplication;

class Application
{
    const APPLICATION_PROVIDER = 'bootstrap';

    /**
     * @var AbstractApplication
     */
    protected $app;

    /**
     * @var DiInterface
     */
    protected $di;

    /**
     * @var string
     */
    protected $rootPath;

    /**
     * @param string $rootPath
     *
     * @throws Exception
     */
    public function __construct(string $rootPath)
    {
        $this->di       = new FactoryDefault();
        $this->app      = $this->createApplication();
        $this->rootPath = $rootPath;

        $this->di->setShared(self::APPLICATION_PROVIDER, $this);

        $this->initializeProviders();
    }

    /**
     * @return string
     * @throws Exception
     */
    public function run(): string
    {
        return (string) $this
            ->app
            ->handle($_SERVER['REQUEST_URI'])
            ->getContent()
        ;
    }

    /**
     * @return string
     */
    public function getRootPath(): string
    {
        return $this->rootPath;
    }

    /**
     * @return AbstractApplication
     */
    protected function createApplication(): AbstractApplication
    {
        return new MvcApplication($this->di);
    }

    /**
     * @throws Exception
     */
    protected function initializeProviders(): void
    {
        $filename = $this->rootPath 
                 . '/configs/providers.php';
        if (!file_exists($filename) || !is_readable($filename)) {
            throw new Exception(
                'File providers.php does not exist or is not readable.'
            );
        }

        $providers = include_once $filename;
        foreach ($providers as $providerClass) {
            /** @var ServiceProviderInterface $provider */
            $provider = new $providerClass;
            $provider->register($this->di);
        }
    }
}

Конструктор класу спочатку створює новий контейнер DI та зберігає його в локальній власності. Ми використовуємо Phalcon\Di\FactoryDefault, який містить багато сервісів уже зареєстрованих для нас.

Потім ми створюємо новий Phalcon\Mvc\Application та зберігаємо його також у власність. Ми також зберігаємо кореневий шлях, тому що він потрібний кругом у додатку.

Потім ми реєструємо цей клас ( Vokuro\Application) у контейнері Di, використовуючи ім’я bootstrap. Це дає нам доступ до цього класу з будь-якої частини нашого застосунку через контейнер DI.

Останнє, що ми робимо - це реєструємо всіх постачальників. Хоча об’єкт Phalcon\Di\FactoryDefault має багато сервісів, які вже зареєстровані для нас, нам все ще треба реєструвати постачальників, які відповідають потребам нашої програми. As mentioned above, each provider class implements the Phalcon\Di\ServiceProviderInterface interface, so we can load each class and call the register() method with the Di container to register each service. Для цього ми спочатку завантажимо масив конфігурації config/providers.php, а потім зв’яжемо записи і зареєструємо кожного провайдера.

Доступні постачальники:

Постачальник Description
AclProvider Права доступу
AuthProvider Authentication
ConfigProvider Значення конфігурації
CryptProvider Шифрування
DbProvider Доступ до бази даних
DispatcherProvider Диспетчер, який використовує контролер для переходу за URL-адресою
FlashProvider Флеш-повідомлення для забезпечення зворотного зв’язку з користувачем
LoggerProvider Реєстратор помилок та іншої інформації
MailProvider Підтримка пошти
ModelsMetadataProvider Метадані для моделей
RouterProvider Маршрути
SecurityProvider Безпека
SessionBagProvider Дані сесії
SessionProvider Дані сесії
UrlProvider Обробка URL
ViewProvider Подання та двигун, що його формує

run() тепер запустить REQUEST_URI, обробить його і поверне вміст назад. Внутрішньо програма вираховує маршрут на основі запиту, координує відповідний контролер і подання перед поверненням результату цієї операції назад користувачеві у якості відповіді.

База данних

Як зазначено вище, Vökuró можна встановити з MariaDB/MySQL/Aurora, PostgreSql або SQLite в якості сховища баз даних. Для цілей цього посібника ми використовуємо MariaDB. Таблиці, які використовує програма:

Таблиця Description
email_confirmations Підтвердження електронною поштою для реєстрації
failed_logins Невдалі спроби входу
password_changes Коли було змінено пароль і ким
permissions Матриця дозволів
phinxlog Міграційна таблиця Phinx
profiles Профіль для кожного користувача
remember_tokens Remember Me functionality tokens
reset_passwords Таблиця токенів скидання паролів
success_logins Успішні спроби входу
users Користувачі

Моделі

Following the Model-View-Controller pattern, Vökuró has one model per database table (excluding the phinxlog). The models allow us to interact with the database tables in an easy object-oriented manner. Моделі розташовані в каталозі /src/Models, і кожна модель визначає відповідні поля вихідної таблиці та будь-які зв’язки між моделлю та іншими об’єктами. Деякі моделі також втілюють правила перевірки для забезпечення належного збереження даних у базі даних.

<?php
declare(strict_types=1);

namespace Vokuro\Models;

use Phalcon\Mvc\Model;

class SuccessLogins extends Model
{
    /**
     * @var integer
     */
    public $id;

    /**
     * @var integer
     */
    public $usersId;

    /**
     * @var string
     */
    public $ipAddress;

    /**
     * @var string
     */
    public $userAgent;

    public function initialize()
    {
        $this->belongsTo(
            'usersId', 
            Users::class, 
            'id', 
            [
                'alias' => 'user',
            ]
        );
    }
}

У моделі вище ми визначили всі поля таблиці як публічні властивості для спрощення доступу:

echo $successLogin->ipAddress;

NOTE: If you notice, the property names map exactly the case (upper/lower) of the field names in the relevant table.

У методі initialize() ми також визначили зв’язок між цією моделлю і Users. Ми призначили поля (local/remote), а також alias для цього зв’язку. Таким чином, ми можемо отримати доступ до користувача, що має відношення до запису цієї моделі наступним чином:

echo $successLogin->user->name;

NOTE: Feel free to open each model file and identify the relationships between the models. Check our documentation for the difference between various types of relationships

Контролери

Again following the Model-View-Controller pattern, Vökuró has one controller to handle a specific parent route. Це означає, що AboutController обробляє маршрут /about. Всі контролери знаходяться в каталозі /src/Cotnrollers.

Контролер за замовчуванням це IndexController. Всі класи контролерів мають суфікс Controller. Кожен контролер має методи з суфіксамитAction, а дія за замовчуванням - indexAction. Therefore, if you visit the site with just the URL, the IndexController will be called and the indexAction will be executed.

Після цього, якщо ви не реєстрували певні специфічні маршрути, маршрути за замовчуванням (автоматично зареєстровані) намагатимуться прив’язувати:

/profiles/search

to

/src/Controllers/ProfilesController.php -> searchAction

Доступні контролери, дії та маршрути для Vökuró:

Controller Action Маршрут Description
About index /about Показує сторінкуПро проект
Index index / Типова дія - головна сторінка
Права доступу index /permissions Перегляд/зміна дозволів для рівня профілю
Privacy index /privacy Перегляд сторінки конфіденційності
Profiles index /profiles Переглянути сторінку за замовчуванням профілів
Profiles create /profiles/create Створити профіль
Profiles delete /profiles/delete Видалити профіль
Profiles edit /profiles/edit Редагувати профіль
Profiles search /profiles/search Пошук профілів
Session index /session Дія сесії за замовчуванням
Session forgotPassword /session/forgotPassword Забули пароль
Session login /session/login Вхід
Session logout /session/logout Вихід
Session signup /session/signup Зареєструватися
Terms index /terms Переглянути сторінку з правилами
UserControl confirmEmail /confirm Підтвердіть електронну пошту
UserControl resetPassword /reset-password Скидання пароля
Користувачі index /users Екран за замовчуванням для користувачів
Користувачі changePassword /users/changePassword Змінити пароль користувача
Користувачі create /users/create Створити користувача
Користувачі delete /users/delete Видалити користувача
Користувачі edit /users/edit Редагувати користувача

Views

The last element of the Model-View-Controller pattern is the views. Vökuró використовує Volt як генератор подання для його відображень.

NOTE: Generally, one would expect to see a views folder under the /src folder. Однак, Vökuró використовує трохи інший підхід, зберігаючи всі файли подань у /themes/vokuro.

Каталог подань містить теки, що відповідають кожному контролеру. Всередині кожної з цих тек є файли .volt, що створені для відображення результатів кожної окремої дії. Наприклад, маршрут:

/profiles/create

веде до:

ProfilesController -> createAction

і у поданнях розташований:

/themes/vokuro/profiles/create.volt

Доступні подання:

Controller Action Вигляд Description
About index /about/index.volt Показує сторінкуПро проект
Index index /index/index.volt Типова дія - головна сторінка
Права доступу index /permissions/index.volt Перегляд/зміна дозволів для рівня профілю
Privacy index /privacy/index.volt Перегляд сторінки конфіденційності
Profiles index /profiles/index.volt Переглянути сторінку за замовчуванням профілів
Profiles create /profiles/create.volt Створити профіль
Profiles delete /profiles/delete.volt Видалити профіль
Profiles edit /profiles/edit.volt Редагувати профіль
Profiles search /profiles/search.volt Пошук профілів
Session index /session/index.volt Дія сесії за замовчуванням
Session forgotPassword /session/forgotPassword.volt Забули пароль
Session login /session/login.volt Вхід
Session logout /session/logout.volt Вихід
Session signup /session/signup.volt Зареєструватися
Terms index /terms/index.volt Переглянути сторінку з правилами
Користувачі index /users/index.volt Екран за замовчуванням для користувачів
Користувачі changePassword /users/changePassword.volt Змінити пароль користувача
Користувачі create /users/create.volt Створити користувача
Користувачі delete /users/delete.volt Видалити користувача
Користувачі edit /users/edit.volt Редагувати користувача

Файл /index.volt містить основну схему сторінки, включаючи посилання на стилі, javascript і т. д. The /layouts directory contains different layouts that are used in the application, for instance a public one if the user is not logged in, and a private one for logged-in users. Окремі подання вкладаються у макети та будують кінцеву сторінку.

Компоненти

У Vökuró є кілька компонентів, функціонал яких ви використовуємо по всьому додатку. Всі ці компоненти знаходяться в каталозі /src/Plugins.

Acl

Vokuro\Plugins\Acl\Acl is a component that implements an Access Control List for our application. ACL контролює, до яких ресурсів має доступ окремий користувач. Більше про ACL можна прочитати на нашій окремій сторінці.

In this component, We define the resources that are considered private. Вони зберігаються у внутрішньому масиві з контролером як ключом та дією як значенням, та визначають, які контролери/дії потребують аутентифікації. It also holds human-readable descriptions for actions used throughout the application.

Компонент використовує наступні методи:

Method Результат Description
getActionDescription($action) string Повертає опис дії відповідно до її спрощеного імені
getAcl() об'єкт ACL Повертає список ACL
getPermissions(Profiles $profile) масив Повертає дозволи, надані профілю
getResources() масив Повертає всі доступні ресурси та їх дії
isAllowed($profile, $controller, $action) bool Перевіряє, чи дозволено поточному профілю отримати доступ до ресурсу
isPrivate($controllerName) bool Перевіряє, чи є контролер приватним або ні
rebuild() об'єкт ACL Перебудовує список доступу у файл

Auth

Vokuro\Plugins\Auth\Auth - це компонент, який керує автентифікацією і здійснює управління ідентифікацією в Vökuró.

Компонент використовує наступні методи:

Method Description
check($credentials) Перевіряє облікові дані користувача
saveSuccessLogin($user) Створює налаштування середовища “пам’ятати мене” - пов’язані файли cookie і генерування токенів
registerUserThrottling($userId) Імплементує тротлінг імені користувача. Знижує ефективність атак грубого підбору пароля
createRememberEnvironment(Users $user) Створює налаштування середовища “пам’ятати мене” - пов’язані файли cookie і генерування токенів
hasRememberMe(): bool Перевірте, чи має сесія куки з міткою “пам’ятати мене”
loginWithRememberMe(): Response Logs in using the information in the cookies
checkUserFlags(Users $user) Перевіряє, чи користувач заблокований/неактивний/тимчасово заморожений
getIdentity(): array / null Повертає поточну ідентифікацію
getName(): string Повертає ім’я користувача
remove() Видалення інформації про особистість користувача з сесії
authUserById($id) Автентифікація користувача за його ідентифікатором
getUser(): Users Отримує об’єкт, пов’язаний з користувачем у активній особистості
findFirstByToken($token): int / null Повертає токен поточного користувача
deleteToken(int $userId) Видаляє токен поточного користувача з сесії

Пошта

Vokuro\Plugins\Mail\Mail is a wrapper to Swift Mailer. Вона повертає два методи send() і getTemplate(), які дозволяють вам отримати шаблон з подань і заповнити його даними. Отриманий HTML може використовуватись в методі send() разом з отримувачем та іншими параметрами для відправлення повідомлення електронною поштою.

NOTE: Note that this component is used only if useMail is enabled in your .env file. Також потрібно буде забезпечити коректність SMTP сервера та облікових даних.

Реєстрація

Controller

Щоб отримати доступ до всіх областей Vökuró, вам потрібно мати обліковий запис. Vökuró дозволяє зареєструватися на сайті, натиснувши кнопку Створити обліковий запис.

Це переспрямує вас до URL-адреси /session/signup, яка у свою чергу викличе SessionController та signupAction. Подивимося, що відбувається в signupAction:

<?php
declare(strict_types=1);

namespace Vokuro\Controllers;

use Phalcon\Flash\Direct;
use Phalcon\Http\Request;
use Phalcon\Mvc\Dispatcher;
use Phalcon\Security;
use Phalcon\Mvc\View;
use Vokuro\Forms\SignUpForm;
use Vokuro\Models\Users;

/**
 * @property Dispatcher $dispatcher
 * @property Direct     $flash
 * @property Request    $request
 * @property Security   $security
 * @property View       $view
 */
class SessionController extends ControllerBase
{
    public function signupAction()
    {
        $form = new SignUpForm();

        // ....

        $this->view->setVar('form', $form);
    }
}

Робочий процес додатку:

  • Відвідуємо /session/signup
    • Створення форми, відправлення форми до подання, візуалізація форми
  • Надання даних (без відправки)
    • Форма ще раз відображається, але нічого більше не відбувається
  • Надання даних (відбувається відправка)
    • Помилки
      • Якщо валідатори форми виявили помилки, то при повторному відображенні форми виводиться інформація про них
    • Помилок немає
      • Дані знешкоджено (приведено у відповідність до шаблону)
      • Нову модель створено
      • Дані збережено в базі даних
        • Помилка
          • Показ повідомлення на екрані і оновлення форми
        • Успіх
          • Запис збережено
          • Показ підтвердження на екрані
          • Надсилання пошти (якщо доступно)

Форма

In order to have validation for user supplied data, we are utilizing the Phalcon\Forms\Form and Phalcon\Filter\Validation* classes. Ці класи дозволяють нам створювати HTML елементи та додавати до них валідатори. Потім форма передається до подання, де HTML елементи виводяться на екран.

Коли користувач відправляє інформацію, ми відправляємо надіслані дані назад до форми, де відповідні валідатори перевіряють коректність введення та повертають будь-які потенційні повідомлення про помилки.

NOTE: All the forms for Vökuró are located in /src/Forms

Спочатку ми створили об’єкт SignUpForm. У цьому об’єкті ми визначаємо всі HTML-елементи, які нам потрібні із їх відповідними валідаторами:

<?php
declare(strict_types=1);

namespace Vokuro\Forms;

use Phalcon\Forms\Element\Check;
use Phalcon\Forms\Element\Hidden;
use Phalcon\Forms\Element\Password;
use Phalcon\Forms\Element\Submit;
use Phalcon\Forms\Element\Text;
use Phalcon\Forms\Form;
use Phalcon\Validation\Validator\Confirmation;
use Phalcon\Validation\Validator\Email;
use Phalcon\Validation\Validator\Identical;
use Phalcon\Validation\Validator\PresenceOf;
use Phalcon\Validation\Validator\StringLength;

class SignUpForm extends Form
{
    /**
     * @param string|null $entity
     * @param array       $options
     */
    public function initialize(
        string $entity = null, 
        array $options = []
    ) {
        $name = new Text('name');
        $name->setLabel('Name');
        $name->addValidators(
            [
                new PresenceOf(
                    [
                        'message' => 'The name is required',
                    ]
                ),
            ]
        );

        $this->add($name);

        $email = new Text('email');
        $email->setLabel('E-Mail');
        $email->addValidators(
            [
                new PresenceOf(
                    [
                        'message' => 'The e-mail is required',
                    ]
                ),
                new Email(
                    [
                        'message' => 'The e-mail is not valid',
                    ]
                ),
            ]
        );

        $this->add($email);

        $password = new Password('password');
        $password->setLabel('Password');
        $password->addValidators(
            [
                new PresenceOf(
                    [
                        'message' => 'The password is required',
                    ]
                ),
                new StringLength(
                    [
                        'min'            => 8,
                        'messageMinimum' => 'Password is too short. ' .
                                            'Мінімум 8 символів',
                    ]
                ),
                new Confirmation(
                    [
                        'message' => "Пароль не збігається " .
                                     "confirmation",
                        'with'    => 'confirmPassword',
                    ]
                ),
            ]
        );

        $this->add($password);

        $confirmPassword = new Password('confirmPassword');
        $confirmPassword->setLabel('Confirm Password');
        $confirmPassword->addValidators(
            [
                new PresenceOf(
                    [
                        'message' => 'The confirmation password ' .
                                     'is required',
                    ]
                ),
            ]
        );

        $this->add($confirmPassword);

        $terms = new Check(
            'terms', 
            [
                'value' => 'yes',
            ]
        );

        $terms->setLabel('Accept terms and conditions');
        $terms->addValidator(
            new Identical(
                [
                    'value'   => 'yes',
                    'message' => 'Terms and conditions must be ' .
                                 'accepted',
                ]
            )
        );

        $this->add($terms);

        $csrf = new Hidden('csrf');
        $csrf->addValidator(
            new Identical(
                [
                    'value'   => $this->security->getRequestToken(),
                    'message' => 'CSRF validation failed',
                ]
            )
        );
        $csrf->clear();

        $this->add($csrf);

        $this->add(
            new Submit(
                'Sign Up', 
                [
                    'class' => 'btn btn-success',
                ]
            )
        );
    }

    /**
     * @param string $name
     *
     * @return string
     */
    public function messages(string $name)
    {
        if ($this->hasMessagesFor($name)) {
            foreach ($this->getMessagesFor($name) as $message) {
                return $message;
            }
        }

        return '';
    }
}

У методі initialize ми встановлюємо всі необхідні HTML-елементи. Це такі елементи:

Елемент Type Description
name Text Ім’я користувача
email Text Електронна адреса для облікового запису
password Password Пароль до облікового запису
confirmPassword Password Підтвердження пароля
terms Check Поставити галочку щодо прийняття умов користування
csrf Hidden Елемент захисту CSRF
Реєстрація Submit Кнопка надсилання

Додавання елементів здійснюється поступово:

<?php
declare(strict_types=1);

$email = new Text('email');
$email->setLabel('E-Mail');
$email->addValidators(
    [
        new PresenceOf(
            [
                'message' => 'The e-mail is required',
            ]
        ),
        new Email(
            [
                'message' => 'The e-mail is not valid',
            ]
        ),
    ]
);

$this->add($email);

Спочатку ми створюємо об’єкт Text та задаємо в якості його назви email. Ми також встановили мітку елемента на E-Mail. Після цього ми прикріплюємо різні валідатори до елемента. Вони будуть викликані після того, як користувач відправить дані, які будуть передані у формі.

Як ми бачили вище, ми прикріпляємо валідатор PresenceOf на елемент email із повідомленням Потрібно вказати e-mail. Валідатор перевіряє, чи користувач надав дані, коли натиснув на кнопку відправки повідомлення, та відображає повідомлення в разі помилки. Валідатор перевіряє переданий масив (як правило, $_POST), а для цього конкретного елементу він перевірить $_POST['email'].

Ми також прикріпляємо валідатор Email, відповідальний за перевірку дійсної адреси електронної пошти. Як ви бачите, валідатори розміщуються у масиві, тож ви легко можете прикріпити до будь якого елемента стільки валідаторів, скільки вам потрібно.

Останнє, що ми зробимо, це додамо елемент у форму.

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

Особлива увага до елементів password та confirmPassword. Ви побачите, що обидва елементи мають тип Password. Ідея полягає в тому, що ви повинні ввести пароль двічі, і такі паролі повинні збігатися для того, щоб уникнути помилок.

Поле password має два валідатори для вмісту: PresenceOf, що є обов’язковим, і StringLength: для того, щоб пароль був більше, ніж 8 символів. Ми також прикріплюємо третій валідатор, що називається Confirmation. Цей спеціальний валідатор зв’язує елемент password з елементом confirmPassword. Коли він доданий для перевірки, то перевірить вміст обох елементів, і якщо вони не ідентичні - буде відображено повідомлення про помилку, наприклад про те, що перевірка буде невдала.

Вигляд

Тепер, коли у нас є все, що ми передаємо через форму, ми відправлчємо форму поданню для відображення:

$this->view->setVar('form', $form);

Our view now needs to render the elements:


{# ... #}
{% 
    set isEmailValidClass = form.messages('email') ? 
        'form-control is-invalid' : 
        'form-control' 
%}
{# ... #}

<h1 class="mt-3">Sign Up</h1>

<form method="post">
    {# ... #}

    <div class="form-group row">
        {{ 
            form.label(
                'email', 
                [
                    'class': 'col-sm-2 col-form-label'
                ]
            ) 
        }}
        <div class="col-sm-10">
            {{ 
                form.render(
                    'email', 
                    [
                        'class': isEmailValidClass, 
                        'placeholder': 'Email'
                    ]
                ) 
            }}
            <div class="invalid-feedback">
                {{ form.messages('email') }}
            </div>
        </div>
    </div>

    {# ... #}
    <div class="form-group row">
        <div class="col-sm-10">
            {{ 
                form.render(
                    'csrf', 
                    [
                        'value': security.getToken()
                    ]
                ) 
            }}
            {{ form.messages('csrf') }}

            {{ form.render('Sign Up') }}
        </div>
    </div>
</form>

<hr>

{{ link_to('session/login', "&larr; Back to Login") }}

Змінна, яку ми встановили у поданні нашого об’єкта SignUpForm називається form. Тому ми використовуємо її напряму і викликаємо її методи. Синтаксис в Volt дещо відрізняється. In PHP, we would use $form->render() whereas in Volt we will use form.render().

Подання містить спочатку результати перевірки, чи мають місце якісь помилки у нашій формі, і якщо так, то додається клас CSS is-invalid до відповідного елемента. Цей клас додає гарну червону рамку до елемента, виділяючи помилку та відображаючи повідомлення.

Після цього ми маємо звичайні HTML-теги із відповідними стилями. Для того, щоб відобразити HTML-код кожного елементу, нам потрібно викликати render() у form із зазначенням імені відповідного елемента. Також зверніть увагу, що ми водночас викликаємо form.label() з таким самим іменем елемента, щоб створити відповідні теги <label>.

У кінці подання ми візуалізуємо приховане поле CSRF, а також кнопку Зареєструватися.

Відправка

As mentioned above, once the user fills the form and clicks the Sign Up button, the form will self post i.e. it will post the data on the same controller and action (in our case /session/signup). Відповідній дії тепер необхідно обробити ці надіслані дані:

<?php
declare(strict_types=1);

namespace Vokuro\Controllers;

use Phalcon\Flash\Direct;
use Phalcon\Http\Request;
use Phalcon\Mvc\Dispatcher;
use Phalcon\Security;
use Phalcon\Mvc\View;
use Vokuro\Forms\SignUpForm;
use Vokuro\Models\Users;

/**
 * @property Dispatcher $dispatcher
 * @property Direct     $flash
 * @property Request    $request
 * @property Security   $security
 * @property View       $view
 */
class SessionController extends ControllerBase
{
    public function signupAction()
    {
        $form = new SignUpForm();

        if (true === $this->request->isPost()) {
            if (false !== $form->isValid($this->request->getPost())) {
                $name     = $this
                    ->request
                    ->getPost('name', 'striptags')
                ;
                $email    = $this
                    ->request
                    ->getPost('email')
                ;
                $password = $this
                    ->request
                    ->getPost('password')
                ;
                $password = $this
                    ->security
                    ->hash($password)
                ;

                $user = new Users(
                    [
                        'name'       => $name,
                        'email'      => $email,
                        'password'   => $password,
                        'profilesId' => 2,
                    ]
                );

                if ($user->save()) {
                    return $this->dispatcher->forward([
                        'controller' => 'index',
                        'action'     => 'index',
                    ]);
                }

                foreach ($user->getMessages() as $message) {
                    $this->flash->error((string) $message);
                }
            }
        }

        $this->view->setVar('form', $form);
    }
}

If the user has submitted data, the following line will evaluate, and we will be executing code inside the if statement:

if (true === $this->request->isPost()) {

Тут ми перевіряємо запит, що надійшов від користувача, чи це POST. Тепер, коли це так, ми маємо використати валідатори перевірки форми і переконатись, чи немає ніяких помилок. Об’єкт Phalcon\Http\Request дозволяє нам отримати ці дані легко через використання:

$this->request->getPost()

Тепер нам потрібно передати ці опубліковані дані у формі і викликати isValid. Це активує всі валідатори для кожного елемента і якщо якийсь із них зазнає невдачі, форма виведе повідомлення з внутрішньої колекції і поверне false

if (false !== $form->isValid($this->request->getPost())) {

Якщо все в порядку, ми використовуємо знову об’єкт Phalcon\Http\Request для отримання поданих даних, а також і для їх приведення у відповідність до встановлених нами вимог. Наведений приклад вирізає теги з надісланої стрічки name:

$name     = $this
    ->request
    ->getPost('name', 'striptags')
;

Зверніть увагу, що ми ніколи не зберігаємо точний текст паролів. Instead, we use the Phalcon\Security component and call hash on it, to transform the supplied password to a one way hash and store that instead. Таким чином, якщо хтось скомпрометує нашу базу даних, принаймні він не матиме доступу до конкретних текстів паролів.

$password = $this
    ->security
    ->hash($password)
;

Тепер нам потрібно зберегти надані дані в базі. Ми це зробимо, створивши нову модель Users, передавши до неї приведені у відповідність дані, а потім викликавши save:

$user = new Users(
    [
        'name'       => $name,
        'email'      => $email,
        'password'   => $password,
        'profilesId' => 2,
    ]
);

if ($user->save()) {
    return $this
        ->dispatcher
        ->forward(
            [
                'controller' => 'index',
                'action'     => 'index',
            ]
        );
}

Якщо $user->save() повертає true, користувач буде переспрямований на головну сторінку (index/index), і повідомлення про успіх з’явиться на екрані.

Модель

Зв’язки

Тепер ми маємо перевірити модель Users, оскільки там закладена певна логіка, зокрема, події afterSave і beforeValidationOnCreate.

Якщо бажаєте, можна додати базові налаштування через метод initialize. Це місце, де ми встановлюємо всі зв/’язки для цієї моделі. Для класу Users ми визначили кілька зв/’язків. Чому ви можете запитати зв/’язки? Phalcon пропонує простий спосіб отримання пов’язаних даних до певної моделі.

Якщо ми хочемо перевірити всі успішні логіни для конкретного користувача, ми можемо це зробити за допомогою наступного фрагмента коду:

<?php
declare(strict_types=1);

use Vokuro\Models\SuccessLogins;
use Vokuro\Models\Users;

$user = Users::findFirst(
    [
        'conditions' => 'id = :id:',
        'bind'       => [
            'id' => 7,
        ] 
    ]
);

$logins = SuccessLogin::find(
    [
        'conditions' => 'userId = :userId:',
        'bind'       => [
            'userId' => 7,
        ] 
    ]
);

Наведений вище код витягує дані користувача з ідентифікатором 7, а потім отримує усі успішні логіни з відповідної таблиці для цього користувача.

Використовуючи зв/’язки, ми можемо дозволити Phalcon виконато для нас всі складні прив’язки і підтягування даних. Отже, наведений вище код:

<?php
declare(strict_types=1);

use Vokuro\Models\SuccessLogins;
use Vokuro\Models\Users;

$user = Users::findFirst(
    [
        'conditions' => 'id = :id:',
        'bind'       => [
            'id' => 7,
        ] 
    ]
);

$logins = $user->successLogins;

$logins = $user->getRelated('successLogins');

Останні дві стрічки роблять одне і те ж. Це питання уподобань, який синтаксис ви хочете використовувати. Phalcon відкриє відповідну таблицю та профільтрує її дані за ідентифікатором користувача.

Для нашої таблиці Users ми визначили наступні зв/’язки:

Name Базове поле Пов/’язане поле Модель
passwordChanges id usersId PasswordChanges
profile profileId id Profiles
resetPasswords id usersId ResetPasswords
successLogins id usersId SuccessLogins
<?php
declare(strict_types=1);

namespace Vokuro\Models;

use Phalcon\Mvc\Model;
use Phalcon\Validation;
use Phalcon\Validation\Validator\Uniqueness;

class Users extends Model
{
    // ...

    public function initialize()
    {
        $this->belongsTo(
            'profilesId', 
            Profiles::class, 
            'id', 
            [
                'alias'    => 'profile',
                'reusable' => true,
            ]
        );

        $this->hasMany(
            'id', 
            SuccessLogins::class, 
            'usersId', 
            [
                'alias'      => 'successLogins',
                'foreignKey' => [
                    'message' => 'Користувача не може бути видалено через ' .
                                 'наявність у нього активності в системі',
                ],
            ]
        );

        $this->hasMany(
            'id', 
            PasswordChanges::class, 
            'usersId', 
            [
                'alias'      => 'passwordChanges',
                'foreignKey' => [
                    'message' => 'Користувач не може бути видалений через ' .
                                 'наявність у нього активності у системі',
                ],
            ]
        );

        $this->hasMany(
            'id', 
            ResetPasswords::class, 
            'usersId', [
            'alias'      => 'resetPasswords',
            'foreignKey' => [
                'message' => 'Користувач не може бути видалений через ' .
                             'наявність активності у системі',
            ],
        ]);
    }

    // ...
}

Як ви бачите у визначених зв/’язках, ми маємо belongsTo, а також три hasMany. Усі зв/’язки мають псевдоніми, щоб ми могли легше отримати до них доступ. Відношення belongsTo також має активний прапорець reusable. Це означає, що якщо у одному запиті викликається зв/’язок більш ніж один раз, Phalcon виконуватиме запит до бази даних лише перший раз і кешуватиме результат. Будь-які наступні запити використовуватимуть кешовані результати.

Also, notable is that we define specific messages for foreign keys. Якщо конкретні зв/’язки будуть порушуватись, то визначене повідомлення буде показане.

Events

Phalcon\Mvc\Model розроблений, щоб виконувати специфічні events. Ці методи подій можуть бути розташовані або в сервісі слухача подій, або в конкретній моделі.

Для моделі Users ми додаємо код для подій afterSave та beforeValidationOnCreate.

<?php
declare(strict_types=1);

namespace Vokuro\Models;

use Phalcon\Mvc\Model;
use Phalcon\Validation;
use Phalcon\Validation\Validator\Uniqueness;

class Users extends Model
{
    public function beforeValidationOnCreate()
    {
        if (true === empty($this->password)) {
            $tempPassword = preg_replace(
                '/[^a-zA-Z0-9]/', 
                '', 
                base64_encode(openssl_random_pseudo_bytes(12))
            );

            $this->mustChangePassword = 'Y';

            $this->password = $this->getDI()
                                   ->getSecurity()
                                   ->hash($tempPassword)
            ;
        } else {
            $this->mustChangePassword = 'N';
        }

        if ($this->getDI()->get('config')->useMail) {
            $this->active = 'N';
        } else {
            $this->active = 'Y';
        }

        $this->suspended = 'N';

        $this->banned = 'N';
    }
}

Подія beforeValidationOnCreate буде виконуватись щоразу, як ми матимемо новий запис (Create), перед тим, як буде здійснено будь-яку валідацію. We check if we have a defined password and if not, we will generate a random string, then hash that string using Phalcon\Security and storing it in the password property. Ми також встановлюємо прапорець для зміни пароля.

Якщо поле пароля не порожнє, то ми зазначаємо у полі mustChangePassword текст ні. Нарешті, ми встановили типові значення параметрів active, suspended або banned, залежно від статусу користувача. Це гарантує, що наш запис буде готовий до додавання у базу даних.

<?php
declare(strict_types=1);

namespace Vokuro\Models;

use Phalcon\Mvc\Model;
use Phalcon\Validation;
use Phalcon\Validation\Validator\Uniqueness;

class Users extends Model
{
    public function afterSave()
    {
        if ($this->getDI()->get('config')->useMail) {
            if ($this->active == 'N') {
                $emailConfirmation          = new EmailConfirmations();
                $emailConfirmation->usersId = $this->id;

                if ($emailConfirmation->save()) {
                    $this->getDI()
                         ->getFlash()
                         ->notice(
                            'A confirmation mail has ' .
                            'надіслано на ' . $this->email
                        )
                    ;
                }
            }
        }
    }
}

Подія afterSave виконується одразу після додавання у базу нового запису про користувача. У цій події ми перевіряємо чи електронні скриньки активні (дивіться файл.env налаштування useMail), і якщо активні, то створюємо новий запис у таблиці EmailConfirmations, після чого зберігаємо запис. Як тільки все буде зроблено, на екрані з’явиться сповіщення.

NOTE: Note that the EmailConfirmations model also has an afterCreate event, which is responsible for actually sending the email to the user.

Валідація

Модель також має метод validate, який дозволяє нам додати валідатордо будь-якої кількості полей нашої моделі. Для таблиці Users на потрібно, щоб email був унікальним. As such, we attach the Uniqueness validator to it. The validator will fire right before any save operation is performed on the model and the message will be returned if the validation fails.

<?php
declare(strict_types=1);

namespace Vokuro\Models;

use Phalcon\Mvc\Model;
use Phalcon\Validation;
use Phalcon\Validation\Validator\Uniqueness;

class Users extends Model
{
    public function validation()
    {
        $validator = new Validation();

        $validator->add(
            'email', 
            new Uniqueness(
                [
                    "message" => "The email is already registered",
                ]
            )
        );

        return $this->validate($validator);
    }
}

Підсумок

Vökuró це приклад додатку, який ми використовуємо для демонстрації деяких функцій, які пропонує Phalcon. Це, безумовно, не рішення, яке підійде для всіх потреб. However, you can use it as a starting point to develop your application.

References