07 События модели
События и управление событиями
Модели позволяют реализовать события, которые будут срабатывать при выполнении вставки/обновления/удаления. Другими словами, события помогают определить бизнес-логику для определенной модели. Ниже приведены события, поддерживаемые Phalcon\Mvc\Model и порядок их выполнения:
| Операция | Название | Может остановить операцию? | Пояснение | 
|---|---|---|---|
| Вставка | afterCreate | Нет | Выполняется после выполнения требуемой операции над системой базы данных только при выполнении операции вставки | 
| Удаление | afterDelete | Нет | Выполняется после выполнения операции удаления | 
| Обновление | afterUpdate | Нет | Выполняется после выполнения требуемой операции в системе базы данных только при выполнении операции обновления | 
| Вставка/Обновление | afterSave | Нет | Выполняется после выполнения требуемой операции в системе базы данных | 
| Вставка/Обновление | afterValidation | Да | Выполняется после проверки полей на наличие не пустых / пустых строк или внешних ключей | 
| Вставка | afterValidationOnCreate | Да | Выполняется после проверки полей на наличие ненулевых / пустых строк или внешних ключей при выполнении операции вставки | 
| Обновление | afterValidationOnUpdate | Да | Выполняется после проверки полей на наличие ненулевых / пустых строк или внешних ключей при выполнении операции обновления | 
| Вставка/Обновление | beforeValidation | Да | Выполняется перед проверкой полей на наличие не пустых строк / нулей или внешних ключей | 
| Вставка | beforeCreate | Да | Выполняется перед требуемой операцией в системе базы данных только при выполнении операции вставки | 
| Удаление | beforeDelete | Да | Выполняется до выполнения операции удаления | 
| Вставка/Обновление | beforeSave | Да | Запускается до требуемой операции над системой базы данных | 
| Обновление | beforeUpdate | Да | Выполняется перед требуемой операцией в системе базы данных только при выполнении операции обновления | 
| Вставка | beforeValidationOnCreate | Да | Выполняется перед проверкой полей на наличие не пустых строк / нулей или внешних ключей при выполнении операции вставки | 
| Обновление | beforeValidationOnUpdate | Да | Выполняется до проверки поля на не нулевую/пустую строку или на внешние ключи при выполнении операции обновления | 
| Вставка/Обновление | onValidationFails | Да (уже остановлено) | Выполняется после сбоя средства проверки целостности | 
| Вставка/Обновление | validation | Да | Выполняется до проверки поля на не нулевую/пустую строку или на внешние ключи при выполнении операции обновления | 
Реализация событий в классе модели
Простой способ заставить модель реагировать на события — это реализовать метод с тем же именем события в классе модели:
<?php
namespace Store\Toys;
use Phalcon\Mvc\Model;
class Robots extends Model
{
    public function beforeValidationOnCreate()
    {
        echo 'Это выполняется перед созданием робота!';
    }
}
События могут быть полезны для присвоения значений перед выполнением операции, например:
<?php
use Phalcon\Mvc\Model;
class Products extends Model
{
    public function beforeCreate()
    {
        // Установить дату создания
        $this->created_at = date('Y-m-d H:i:s');
    }
    public function beforeUpdate()
    {
        // Установить дату модификации
        $this->modified_in = date('Y-m-d H:i:s');
    }
}
Использование пользовательского менеджера событий
Кроме того, этот компонент интегрируется с Phalcon\Events\Manager, это означает, что мы можем создать слушателей, которые запускаются при срабатывании события.
<?php
namespace Store\Toys;
use Phalcon\Mvc\Model;
use Phalcon\Events\Event;
use Phalcon\Events\Manager as EventsManager;
class Robots extends Model
{
    public function initialize()
    {
        $eventsManager = new EventsManager();
        // Добавляем анонимную функцию в качестве слушателя для событий "model"
        $eventsManager->attach(
            'model:beforeSave',
            function (Event $event, $robot) {
                if ($robot->name === 'Scooby Doo') {
                    echo "Scooby Doo isn't a robot!";
                    return false;
                }
                return true;
            }
        );
        // Устанавливаем менеджер событий для события
        $this->setEventsManager($eventsManager);
    }
}
В приведенном выше примере диспетчер событий действует только как мост между объектом и прослушивателем (анонимная функция). События будут запускаться в прослушиватель при сохранении robots:
<?php use Store\Toys\Robots; $robot = new Robots(); $robot->name = 'Scooby Doo'; $robot->year = 1969; $robot->save();
Если мы хотим, чтобы все объекты, созданные в нашем приложении использовали один и тот же EventsManager, то мы должны назначить его менеджеру модели:
<?php
use Phalcon\Events\Event;
use Phalcon\Events\Manager as EventsManager;
// Регистрация сервиса modelsManager
$di->setShared(
    'modelsManager',
    function () {
        $eventsManager = new EventsManager();
        // Добавляем анонимную функцию в качестве
        // слушателя для событий "model"
        $eventsManager->attach(
            'model:beforeSave',
            function (Event $event, $model) {
                // Перехватываем события, производимые моделью Robots
                if (get_class($model) === 'Store\Toys\Robots') {
                    if ($model->name === 'Scooby Doo') {
                        echo "Scooby Doo isn't a robot!";
                        return false;
                    }
                }
                return true;
            }
        );
        // Устанавливаем EventsManager по умолчанию
        $modelsManager = new ModelsManager();
        $modelsManager->setEventsManager($eventsManager);
        return $modelsManager;
    }
);
Если слушатель возвращает false, то это прерывает выполняемую операцию.
Логирование низкоуровневых SQL запросов
При использовании компонентов абстракции высокого уровня, таких как Phalcon\Mvc\Model, для доступа к базе данных, трудно понять, какие операции, в конечном итоге, посылаются базе данных. Phalcon\Mvc\Model поддерживается изнутри Phalcon\Db. Phalcon\Logger взаимодействует с Phalcon\Db, обеспечивая возможность ведения логов на уровне абстракции базы данных, таким образом, позволяя нам логировать SQL запросы.
<?php
use Phalcon\Logger;
use Phalcon\Events\Manager;
use Phalcon\Logger\Adapter\File as FileLogger;
use Phalcon\Db\Adapter\Pdo\Mysql as Connection;
$di->set(
    'db',
    function () {
        $eventsManager = new EventsManager();
        $logger = new FileLogger('app/logs/debug.log');
        // Слушаем все события базы данных
        $eventsManager->attach(
            'db:beforeQuery',
            function ($event, $connection) use ($logger) {
                $logger->log(
                    $connection->getSQLStatement(),
                    Logger::INFO
                );
            }
        );
        $connection = new Connection(
            [
                'host'     => 'localhost',
                'username' => 'root',
                'password' => 'secret',
                'dbname'   => 'invo',
            ]
        );
        // Назначаем EventsManager экземпляру адаптера базы данных
        $connection->setEventsManager($eventsManager);
        return $connection;
    }
);
Как только модель взаимодействует с соединением, все SQL запросы, которые передаются в базу данных, будут сохранены в файле:
<?php
use Store\Toys\Robots;
$robot = new Robots();
$robot->name       = 'Robby the Robot';
$robot->created_at = '1956-07-21';
if ($robot->save() === false) {
    echo 'Не удалось сохранить робота';
}
Упомянутый выше файл app/logs/db.log будет содержать что-то вроде этого:
[Mon, 30 Apr 12 13:47:18 -0500][DEBUG][Resource Id #77] INSERT INTO robots(name, created_at) VALUES ('Robby the Robot', '1956-07-21')
Профилирование SQL запросов
Благодаря Phalcon\Db, основе компонента Phalcon\Mvc\Model, возможно профилировать SQL запросы, генерируемые ORM, в целях анализа производительности операций с базой данных. При этом вы можете диагностировать проблемы производительности и выявлять узкие места.
<?php
use Phalcon\Db\Profiler as ProfilerDb;
use Phalcon\Events\Manager as EventsManager;
use Phalcon\Db\Adapter\Pdo\Mysql as MysqlPdo;
$di->set(
    'profiler',
    function () {
        return new ProfilerDb();
    },
    true
);
$di->set(
    'db',
    function () use ($di) {
        $eventsManager = new EventsManager();
        // Получаем общий экземпляр DbProfiler
        $profiler = $di->getProfiler();
        // Слушаем все события базы данных
        $eventsManager->attach(
            'db',
            function ($event, $connection) use ($profiler) {
                if ($event->getType() === 'beforeQuery') {
                    $profiler->startProfile(
                        $connection->getSQLStatement()
                    );
                }
                if ($event->getType() === 'afterQuery') {
                    $profiler->stopProfile();
                }
            }
        );
        $connection = new MysqlPdo(
            [
                'host'     => 'localhost',
                'username' => 'root',
                'password' => 'secret',
                'dbname'   => 'invo',
            ]
        );
        // Назначаем EventsManager экземпляру адаптера базы данных
        $connection->setEventsManager($eventsManager);
        return $connection;
    }
);
Профилирование некоторых запросов:
<?php
use Store\Toys\Robots;
// Отправим некоторый SQL запрос в базу данных
Robots::find();
Robots::find(
    [
        'order' => 'name',
    ]
);
Robots::find(
    [
        'limit' => 30,
    ]
);
// Получим данные профилировщика
$profiles = $di->get('profiler')->getProfiles();
foreach ($profiles as $profile) {
   echo 'SQL запрос: ', $profile->getSQLStatement(), "\n";
   echo 'Начальное время: ', $profile->getInitialTime(), "\n";
   echo 'Конечное время: ', $profile->getFinalTime(), "\n";
   echo 'Общее истекшее время: ', $profile->getTotalElapsedSeconds(), "\n";
}
Каждый генерируемый профиль содержит продолжительность выполнения каждого запроса в миллисекундах, а также сами сгенерированные SQL запросы.
