├── LICENSE.md ├── README.md ├── UPGRADE.md ├── composer.json ├── doc ├── ActiveRecord.md ├── MessageData.md ├── ViewModule.png ├── addLogs.md ├── clear.md ├── component.md ├── migrate.md └── viewModule.md ├── migrations └── m170427_214256_create_activity_log.php └── src ├── ActiveLogBehavior.php ├── LogCollection.php ├── LogInfoBehavior.php ├── Manager.php ├── ManagerInterface.php ├── MessageBuilder.php ├── MessageBuilderInterface.php ├── MessageEvent.php ├── console └── DefaultController.php ├── middlewares ├── EnvironmentMiddleware.php ├── Middleware.php ├── MiddlewarePipeline.php ├── UserInterface.php └── UserMiddleware.php ├── module ├── Module.php ├── controllers │ └── DefaultController.php ├── messages │ ├── config.php │ └── ru │ │ └── lav45 │ │ └── logger.php ├── models │ ├── ActivityLog.php │ ├── ActivityLogSearch.php │ ├── ActivityLogViewModel.php │ └── DataModel.php └── views │ └── default │ ├── _item.php │ └── index.php └── storage ├── ArrayStorage.php ├── DbStorage.php ├── DeleteCommand.php ├── MessageData.php └── StorageInterface.php /LICENSE.md: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017, LAV45 (Aleksey Loban) 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # yii2-activity-logger 2 | 3 | 4 | 5 | 8 | 13 | 14 |
6 | yii2-activity-logger 7 | 9 | Это расширение поможет вам отслеживать пользовательскую активность на сайте. 10 | Когда в админке над контентом работает больше двух человек, не всегда понятно кто, когда и зачем сделал изменения в описание статьи, убрал статью из публикации, добавил непонятного пользователя, удалил организацию. 11 | Для того чтобы была возможность поблагодарить автора за усердную работу и был разработан этот модуль. 12 |
15 | 16 | 17 | [![Total Downloads](https://poser.pugx.org/lav45/yii2-activity-logger/downloads)](https://packagist.org/packages/lav45/yii2-activity-logger) 18 | [![Test Status](https://github.com/lav45/yii2-activity-logger/workflows/test/badge.svg)](https://github.com/lav45/yii2-activity-logger/actions) 19 | [![Code Coverage](https://scrutinizer-ci.com/g/lav45/yii2-activity-logger/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/lav45/yii2-activity-logger/) 20 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/lav45/yii2-activity-logger/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/lav45/yii2-activity-logger/) 21 | [![License](https://poser.pugx.org/lav45/yii2-activity-logger/license)](https://github.com/lav45/yii2-activity-logger/blob/master/LICENSE.md) 22 | 23 | ## Установка расширения 24 | 25 | ```bash 26 | composer require lav45/yii2-activity-logger --prefer-dist 27 | ``` 28 | 29 | ## Подключение и настройка 30 | 31 | * [Миграции](doc/migrate.md) 32 | * [Компоненты](doc/component.md) 33 | * [ActiveRecord](doc/ActiveRecord.md) 34 | * [Отображение данных](doc/viewModule.md) 35 | * [MessageData](doc/MessageData.md) 36 | * [Очистки логов](doc/clear.md) 37 | * [Добавление логов](doc/addLogs.md) 38 | 39 | ## Тестирование 40 | 41 | ```bash 42 | ./build.sh 43 | ``` 44 | 45 | ```bash 46 | ./composer update --prefer-dist 47 | ``` 48 | 49 | ```bash 50 | ./composer phpunit 51 | ``` 52 | 53 | ## Поддерживаемые версии 54 | 55 | | Версия | Версия PHP | Статус | 56 | |--------|------------|--------------| 57 | | `4.x` | `>=8.4` | | 58 | | `3.x` | `>=8.1` | В разработке | 59 | | `2.x` | `>=7.4` | Активна | 60 | | `1.x` | `>=5.5` | Завершена | 61 | 62 | ## Лицензии 63 | 64 | Для получения информации о лицензии проверьте файл [LICENSE.md](LICENSE.md). 65 | -------------------------------------------------------------------------------- /UPGRADE.md: -------------------------------------------------------------------------------- 1 | Инструкция по обновлению ActivityLogger 2 | ======================================= 3 | 4 | Файл содержит примечания которые необходимо учесть при обновлении компонента на следующую версии. 5 | 6 | Обновление 2.2 7 | ------------------ 8 | 9 | * Удалены `\lav45\activityLogger\Manager::$user` 10 | * Удалены `\lav45\activityLogger\Manager::$userNameAttribute` 11 | * Добавлен `\lav45\activityLogger\middlewares\UserInterface` для `User` модели 12 | ```php 13 | Yii::$container->setDefinitions([ 14 | \lav45\activityLogger\middlewares\UserInterface::class => static fn() => Yii::$app->getUser()->getIdentity(), 15 | ]); 16 | ``` 17 | * Добавлен `\lav45\activityLogger\Manager::$middlewares` которые будут заполнять данные для `\lav45\activityLogger\MessageData` 18 | ```php 19 | return [ 20 | 'components' => [ 21 | 'activityLogger' => [ 22 | '__class' => \lav45\activityLogger\Manager::class, 23 | 'middlewares' => [ 24 | [ 25 | 'class' => \lav45\activityLogger\middlewares\UserMiddleware::class, 26 | ], 27 | [ 28 | 'class' => \lav45\activityLogger\middlewares\EnvironmentMiddleware::class, 29 | '__construct()' => [ 'env' => 'api' ], 30 | ] 31 | ], 32 | ], 33 | ], 34 | ]; 35 | ``` 36 | * Удалён `\lav45\activityLogger\DummyManager`. Вместо него добавлен `\lav45\activityLogger\storage\ArrayStorage`. 37 | * Удалён `\lav45\activityLogger\ManagerInterface::isEnabled()` 38 | * Переименован namespace `\lav45\activityLogger\modules` -> `\lav45\activityLogger\module` 39 | 40 | Обновление 2.1 41 | ------------------ 42 | 43 | * Переместил `\lav45\activityLogger\DbStorage` -> `\lav45\activityLogger\storage\DbStorage` 44 | * Переместил `\lav45\activityLogger\StorageInterface` -> `\lav45\activityLogger\storage\StorageInterface` 45 | * Переместил `\lav45\activityLogger\DeleteCommand` -> `\lav45\activityLogger\storage\DeleteCommand` 46 | * Переместил `\lav45\activityLogger\MessageData` -> `\lav45\activityLogger\storage\MessageData` 47 | * Удалён `\lav45\activityLogger\ManagerTrait` 48 | * Необходимо настроить DI container 49 | ```php 50 | Yii::$container->setDefinitions([ 51 | \lav45\activityLogger\ManagerInterface::class => static fn() => Yii::$app->get('activityLogger'), 52 | \lav45\activityLogger\storage\StorageInterface::class => static fn() => Yii::$app->get('activityLoggerStorage'), 53 | ]); 54 | ``` 55 | * Вместо свойства `\lav45\activityLogger\Manager::$enabled` следует использовать 56 | `\lav45\activityLogger\Manager::isEnabled()` 57 | * Если необходимо отключить логирование, используйте компонент заглушку `\lav45\activityLogger\DummyManager` 58 | * Из консольного контроллера `logger/clean` был удалён параметр `--old-than=1y`, который был выставлен по умолчанию! 59 | 60 | Обновление 2.0 61 | ------------------ 62 | 63 | * Переименован `lav45\activityLogger\LogMessageDTO` -> `lav45\activityLogger\MessageData` 64 | * Доработан `lav45\activityLogger\DeleteCommand` 65 | * `lav45\activityLogger\MessageData::$createdAt` указывается сразу при инициализации. 66 | * Удалёно свойство `lav45\activityLogger\Manager::$userIdPrefix`. Вместо этого можете настроить 67 | `\lav45\activityLogger\ActiveLogBehavior::$getEntityId` 68 | * php >= 7.4 69 | 70 | Обновление 1.8 71 | ------------------ 72 | 73 | * В классе `\lav45\activityLogger\modules\models\ActivityLogSearch` удалены методы `setEntityMap()`, `getEntityMap()`, 74 | `getEntityNameList()` 75 | * Доработана `src/modules/views/default/_item.php` 76 | * Удалён `src/modules/views/default/_search.php` 77 | * Доработан `\lav45\activityLogger\StorageInterface` и `\lav45\activityLogger\DbStorage` 78 | * Переименован и доработан класс `\lav45\activityLogger\LogMessage` => `LogMessageDTO` 79 | * Удалено свойство `\lav45\activityLogger\Manager::$messageClass` настройки можно передать через `Yii::$container` 80 | ```php 81 | Yii::$container->set(\lav45\activityLogger\LogMessageDTO::class, [ 82 | 'env' => 'console', // Окружение из которого производилось действие 83 | 'userId' => 'console', 84 | 'userName' => 'Droid R2-D2', 85 | ]); 86 | ``` 87 | 88 | Обновление 1.7 89 | ------------------ 90 | 91 | * Удалено свойство `\lav45\activityLogger\Manager::$deleteOldThanDays`. Вместо него можно использовать параметр 92 | `--old-than=30d` консольного контроллера `logger/clean` 93 | * Удалено свойство `\lav45\activityLogger\ActiveLogBehavior::$actionLabels`. Изменения коснулись только стандартных 94 | действий если вы использовали произвольные имена действий то они будут отображаться как есть. 95 | 96 | Обновление 1.6 97 | ------------------ 98 | 99 | * Доработан метод `\lav45\activityLogger\ActiveLogBehavior::beforeSaveMessage()` и событие 100 | `\lav45\activityLogger\ActiveLogBehavior::EVENT_BEFORE_SAVE_MESSAGE` 101 | Все данные которые будут сохранены, передаются всем кто подписан на событие чтобы пользователь мог добавить или 102 | изменить некоторые данные по своему усмотрению 103 | 104 | Обновление 1.5 105 | ------------------ 106 | 107 | * Класс `\lav45\activityLogger\ActiveRecordBehavior` был переименован в `\lav45\activityLogger\ActiveLogBehavior` 108 | Для поддержки обратной совместимости был добавлен пустой класс `\lav45\activityLogger\ActiveRecordBehavior` который 109 | будет удален с 1.6 версии 110 | * Немного доработано представление `src/modules/views/default/_item.php` 111 | * При записи в лог пустой строки она будет отображаться как `Yii::$app->formatter->nullDisplay` 112 | * Значение по умолчанию для `\lav45\activityLogger\ActiveRecordBehavior::$identicalAttributes` теперь `false` 113 | * `\lav45\activityLogger\ActiveRecordBehavior` не будет писать в лог пустые значения. За проверку наличия непустых 114 | данных отвечает метод `ActiveRecordBehavior::isEmpty()`, работу которого можно скорректировать с помощью свойства 115 | `ActiveRecordBehavior::$isEmpty` передав ему свою функцию. 116 | * Удалены методы `\lav45\activityLogger\LogMessage` 117 | * getEntityName() 118 | * setEntityName() 119 | * getEntityId() 120 | * setEntityId() 121 | * getCreatedAt() 122 | * setCreatedAt() 123 | * getUserId() 124 | * setUserId() 125 | * getUserName() 126 | * setUserName() 127 | * getAction() 128 | * setAction() 129 | * getEnv() 130 | * setEnv() 131 | 132 | В место этого будут использоваться публичные свойства. 133 | 134 | * Для того чтобы переопределить метод `\lav45\activityLogger\ActiveRecordBehavior::getEntityName()` используйте параметр 135 | `\lav45\activityLogger\ActiveRecordBehavior::$getEntityName`. Пользовательская функция должна возвращать строку. 136 | * Для того чтобы переопределить метод `\lav45\activityLogger\ActiveRecordBehavior::getEntityId()` используйте параметр 137 | `\lav45\activityLogger\ActiveRecordBehavior::$getEntityId`. Пользовательская функция должна возвращать строку или 138 | массив. 139 | ```php 140 | public function behaviors() 141 | { 142 | return [ 143 | [ 144 | '__class' => 'lav45\activityLogger\ActiveRecordBehavior', 145 | 146 | // Если необхадимо изменить стандартное значение `entityName` 147 | 'getEntityName' => function () { 148 | return 'global_news'; 149 | }, 150 | // Если необхадимо изменить стандартное значение `entityId` 151 | 'getEntityId' => function () { 152 | return $this->global_news_id; 153 | } 154 | ] 155 | ]; 156 | } 157 | ``` 158 | * `\lav45\activityLogger\DbStorage` 159 | * Удалён метод `clean($date)`, а также из интерфейса `\lav45\activityLogger\StorageInterface::clean($date)` 160 | * Метод `delete($entityName, $entityId)` теперь принимает `delete(\lav45\activityLogger\LogMessage $message)` 161 | * `\lav45\activityLogger\Manager` 162 | * Был удален метод `createMessage()` 163 | ```php 164 | Yii::$app->activityLogger 165 | ->createMessage($entityName, [ 166 | 'entityId' => $entityId, 167 | 'data' => [$messageText], 168 | 'action' => $action, 169 | ]) 170 | ->save(); 171 | ``` 172 | Вместо него был добавлен доработан более простой аналог `log()` 173 | ```php 174 | Yii::$app->activityLogger->log($entityName, $messageText, $action, $entityId); 175 | ``` 176 | 177 | Обновление 1.4 178 | ------------------ 179 | 180 | * Данные для `\lav45\activityLogger\modules\models\DataModel` теперь передаются через метод `setData(array $value)` 181 | * Был удален `\lav45\activityLogger\StorageTrait`, а его код перенесен в `\lav45\activityLogger\Manager` 182 | * Для переводов будет использоваться категория `lav45/logger` в место `app` 183 | ```php 184 | Yii::t('lav45/logger', $text); 185 | ``` 186 | * Для таблицы `activity_log` было добавлено поле `'id' => $this->bigPrimaryKey()` 187 | 188 | Обновление 1.3 189 | ------------------ 190 | 191 | * `\lav45\activityLogger\DbStorage` теперь должен быть зарегистрирован в списке компонентов под именем 192 | `activityLoggerStorage` и реализовывать интерфейс `\lav45\activityLogger\StorageInterface` 193 | 194 | Обновление 1.2 195 | ------------------ 196 | 197 | * `\lav45\activityLogger\modules\models\ActivityLogViewModel::getUserName()` генерирует ссылку для текущей страницы 198 | используя метод `Url::current()` 199 | * Изменилась фраза `The setting {attribute} has ben changed` на 200 | `{attribute} has ben changed` 201 | * В файле представления `src/modules/views/default/_item.php` добавлено отображение ссылки для фильтрации по 202 | конкретному пользователю `$model->getEntityName()` 203 | * Была переименована папка `migrates` в `migrations` 204 | * Метод `\lav45\activityLogger\modules\models\ActivityLog::getData()` теперь всегда будет возвращать массив 205 | * `\lav45\activityLogger\modules\Module::$createUserUrl` был удален. Вместо него будет использоваться ссылка выполняющая 206 | роль фильтрации данных по конкретному пользователю. 207 | * Параметр `\lav45\activityLogger\Manager::$user` может принимать только имя компонента зарегистрированного в приложении 208 | и соответствующего классу `\yii\web\User` 209 | 210 | Обновление 1.1 211 | ------------------ 212 | 213 | * Удалены интерфейсы `\lav45\activityLogger\contracts\ManagerInterface` и 214 | `\lav45\activityLogger\contracts\MessageInterface` 215 | * `\lav45\activityLogger\contracts\StorageInterface` => `lav45\activityLogger\StorageInterface` 216 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lav45/yii2-activity-logger", 3 | "description": "Tools to store user activity log for Yii2", 4 | "keywords": ["yii2", "extension", "log", "logger", "activity logger"], 5 | "homepage": "https://github.com/lav45/yii2-activity-logger", 6 | "type": "yii2-extension", 7 | "license": "BSD-3-Clause", 8 | "minimum-stability": "dev", 9 | "prefer-stable": true, 10 | "authors": [ 11 | { 12 | "name": "Aleksey Loban", 13 | "email": "lav451@gmail.com" 14 | } 15 | ], 16 | "repositories": [ 17 | { 18 | "type": "composer", 19 | "url": "https://asset-packagist.org" 20 | } 21 | ], 22 | "require": { 23 | "php": ">=7.4.0", 24 | "yiisoft/yii2": "2.0.*" 25 | }, 26 | "require-dev": { 27 | "roave/security-advisories": "dev-latest", 28 | "phpunit/phpunit": "9.*", 29 | "ext-sqlite3": "*", 30 | "ext-pdo": "*", 31 | "ext-pdo_sqlite": "*" 32 | }, 33 | "scripts": { 34 | "phpunit": "vendor/bin/phpunit --verbose", 35 | "coverage": "XDEBUG_MODE=coverage php -d zend_extension=xdebug.so vendor/bin/phpunit --verbose --coverage-html test/coverage" 36 | }, 37 | "autoload": { 38 | "psr-4": { 39 | "lav45\\activityLogger\\": "src" 40 | } 41 | }, 42 | "autoload-dev": { 43 | "psr-4": { 44 | "lav45\\activityLogger\\test\\": "test" 45 | } 46 | }, 47 | "extra": { 48 | "branch-alias": { 49 | "dev-master": "2.2.x-dev" 50 | } 51 | }, 52 | "config": { 53 | "allow-plugins": { 54 | "yiisoft/yii2-composer": true 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /doc/ActiveRecord.md: -------------------------------------------------------------------------------- 1 | # Подключение к ActiveRecord моделям 2 | 3 | ```php 4 | /** 5 | * @mixin \lav45\activityLogger\ActiveLogBehavior 6 | */ 7 | class News extends ActiveRecord 8 | { 9 | // Рекомендуется использовать 10 | public function transactions() 11 | { 12 | return [ 13 | ActiveRecord::SCENARIO_DEFAULT => ActiveRecord::OP_ALL, 14 | ]; 15 | } 16 | 17 | public function behaviors() 18 | { 19 | return [ 20 | [ 21 | '__class' => \lav45\activityLogger\ActiveLogBehavior::class, 22 | 23 | // Если необходимо изменить стандартное значение `entityName` 24 | 'getEntityName' => static::tableName(), 25 | // Если необходимо изменить стандартное значение `entityId` 26 | 'getEntityId' => function () { 27 | return $this->getPrimaryKey(); 28 | } 29 | /** 30 | * В случаях когда нужно для конкретного ActiveLogBehavior сделать подпись с понятным названием. 31 | * Если на странице выводится история изменений всех пользователей, 32 | * не всегда понятно, у кого именно изменился статус, день рождения или другие данные 33 | */ 34 | 'beforeSaveMessage' => static function ($data) { 35 | return ['attribute' => 'custom data'] + $data; 36 | } 37 | 38 | // Список полей, за изменением которых нужно следить 39 | 'attributes' => [ 40 | // Простые поля ( string|int|bool ) 41 | 'name', 42 | 43 | // Поля, значение которых можно найти в списке. 44 | // В данном примере `$model->getStatusList()[$model->status]` 45 | 'status' => [ 46 | 'list' => 'statusList', 47 | ], 48 | 49 | // Поле, значение которого является `id` связи с другой моделью 50 | 'template_id' => [ 51 | 'relation' => 'template', 52 | // Поле из связанной таблицы, которое будет использовано в качестве отображаемого значения 53 | 'attribute' => 'name', 54 | ], 55 | ] 56 | ], 57 | [ 58 | /** 59 | * В случаях когда нужно для всех логов делать подпись с понятным названием. 60 | * Если на странице выводятся история изменения всех пользователей, 61 | * не всегда понятно, у кого именно изменился статус, день рождения или другие данные 62 | */ 63 | '__class' => \lav45\activityLogger\LogInfoBehavior::class, 64 | 'template' => '{username} ({profile.email})', 65 | ], 66 | ]; 67 | } 68 | 69 | /** 70 | * Если необходимо форматировать отображаемое значение, 71 | * можно указать любой поддерживаемый формат компонентом `\yii\i18n\Formatter` 72 | * или использовать произвольную функцию 73 | * @return array 74 | */ 75 | public function attributeFormats() 76 | { 77 | return [ 78 | // Значение атрибута будет форматироваться с помощью Yii::$app->formatter->asDatetime($value); 79 | 'published_at' => 'datetime', 80 | 81 | // Можно использовать свою функцию обратного вызова 82 | 'is_published' => static function($value) { 83 | return Yii::$app->formatter->asBoolean($value); 84 | }, 85 | 86 | // Если нужно вывести имени картинки и ссылку на неё 87 | 'image' => static function($value) { 88 | if (empty($value)) { return null; } 89 | $url = "https://cdn.site.com/img/{$value}"; 90 | return Html::a($value, $url, ['target' => '_blank']); 91 | } 92 | ]; 93 | } 94 | 95 | /** 96 | * В процессе работы `\lav45\activityLogger\ActiveLogBehavior` вызывает событие 97 | * [[ActiveLogBehavior::EVENT_BEFORE_SAVE_MESSAGE]] - перед записью логов 98 | * [[ActiveLogBehavior::EVENT_AFTER_SAVE_MESSAGE]] - после записи логов 99 | */ 100 | public function init() 101 | { 102 | parent::init(); 103 | 104 | // Регистрируем обработчики событий 105 | $this->on(ActiveLogBehavior::EVENT_BEFORE_SAVE_MESSAGE, static function (\lav45\activityLogger\MessageEvent $event) { 106 | // Вы можете добавить в список логов свою информацию 107 | $event->logData[] = 'Reset password'; 108 | }); 109 | 110 | $this->on(ActiveLogBehavior::EVENT_AFTER_SAVE_MESSAGE, static function (\yii\base\Event $event) { 111 | // Какие-то действия после записи логов 112 | }); 113 | } 114 | 115 | /* 116 | * Вместо регистрации события вы можете создать одноименный метод, который будет вызываться вместо события 117 | */ 118 | 119 | /** 120 | * Будет вызываться вместо события [[ActiveLogBehavior::EVENT_BEFORE_SAVE_MESSAGE]] 121 | * @return array 122 | */ 123 | public function beforeSaveMessage($data) 124 | { 125 | // Вы можете добавить в список логов свою информацию 126 | $data[] = 'Reset password'; 127 | 128 | // или заменить отображаемое значение в логах для атрибута `password_hash` 129 | $data['password_hash'] = 'Reset password'; 130 | 131 | return $data; 132 | } 133 | 134 | /** 135 | * Будет вызываться вместо события [[ActiveLogBehavior::EVENT_AFTER_SAVE_MESSAGE]] 136 | */ 137 | public function afterSaveMessage() 138 | { 139 | // Какие-то действия после записи логов 140 | } 141 | } 142 | ``` 143 | -------------------------------------------------------------------------------- /doc/MessageData.md: -------------------------------------------------------------------------------- 1 | # Настройка MessageData 2 | 3 | Значения по умолчанию для всех лог записей можно задать через `Yii::$container` 4 | Для удобства этот код можно разместить в файле `bootstrap.php` 5 | 6 | ```php 7 | Yii::$container->set(\lav45\activityLogger\storage\MessageData::class, [ 8 | 'env' => 'console', // Окружение из которого производилось действие 9 | 'userId' => 'console', 10 | 'userName' => 'Droid R2-D2', 11 | ]); 12 | ``` 13 | -------------------------------------------------------------------------------- /doc/ViewModule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lav45/yii2-activity-logger/02dfefd0799fd2562c280fbdc170d470d46590d1/doc/ViewModule.png -------------------------------------------------------------------------------- /doc/addLogs.md: -------------------------------------------------------------------------------- 1 | # Добавление логов 2 | 3 | Пригодится в тех случаях, когда в процессе работы приложения не используются `ActiveRecord` модели. 4 | Например, при отправке отчетов, скачивании файлов, работе с внешним API, логирование процесса работы из консольного 5 | контроллера и т.д 6 | 7 | ```php 8 | use lav45\activityLogger\storage\MessageData; 9 | 10 | $message = Yii::createObject([ 11 | '__class' => MessageData::class, 12 | // Дата создания записи в логах 13 | 'createdAt' => time(), 14 | // имя сущности 15 | 'entityName' => 'user', 16 | // id сущности с которой производится действие 17 | 'entityId' => 10, 18 | // Действие которое сейчас выполняется 19 | 'action' => 'download', 20 | // текст с описанием действия 21 | 'data' => ['export data'], 22 | ]); 23 | 24 | Yii::$app->activityLogger->log($message); 25 | ``` 26 | 27 | Когда в логах нужно оставить одну запись со списком выполненных действий, можно воспользоваться `LogCollection` 28 | 29 | ```php 30 | use lav45\activityLogger\LogCollection; 31 | 32 | $collection = new LogCollection(Yii::$app->activityLogger, 'entityName'); 33 | 34 | /** 35 | * Добавляем все необходимые записи 36 | */ 37 | $collection->addMessage('Created: 100'); 38 | $collection->addMessage('Updated: 100500'); 39 | $collection->addMessage('Deleted: 5'); 40 | 41 | /** 42 | * Сохраняем все собранные сообщения как одну запись в логах 43 | * После записи список логов будет очищен 44 | */ 45 | $collection->push(); // => true 46 | ``` -------------------------------------------------------------------------------- /doc/clear.md: -------------------------------------------------------------------------------- 1 | # Очистки логов 2 | 3 | ## Добавим консольный контроллер 4 | 5 | Это необязательное расширение. Если вы не планируете удалять устаревшие логи, можете пропустить этот пункт. 6 | 7 | ```php 8 | return [ 9 | 'controllerMap' => [ 10 | 'logger' => [ 11 | '__class' => \lav45\activityLogger\console\DefaultController::class 12 | ] 13 | ], 14 | ]; 15 | ``` 16 | 17 | Если необходимо удалить старые логи, используйте консольный контроллер: 18 | 19 | ```bash 20 | yii logger/clean --old-than=1y 21 | # => Deleted 5 record(s) from the activity log. 22 | ``` 23 | 24 | ## Параметры командной строки 25 | 26 | * `--entity-id, -eid`: Идентификатор целевого объекта 27 | 28 | * `--entity-name, -e`: Псевдоним имени целевого объекта 29 | 30 | * `--user-id, -uid`: Идентификатор пользователя, который выполнил действие 31 | 32 | * `--log-action, -a`: Действие, которое было произведено над объектом 33 | 34 | * `--env`: Среда, из которой производилось действие 35 | 36 | * `--old-than, -o`: Удаление старых данных. 37 | 38 | Допустимые значения: 39 | - 1h - старше 1 часа 40 | - 2d - старше 2-х дней 41 | - 3m - старше 3-х месяцев 42 | - 4y - старше 4 лет -------------------------------------------------------------------------------- /doc/component.md: -------------------------------------------------------------------------------- 1 | # Компоненты 2 | 3 | Необходимо добавить в конфигурационный файл 4 | 5 | ```php 6 | Yii::$container->setDefinitions([ 7 | \lav45\activityLogger\ManagerInterface::class => static fn() => Yii::$app->get('activityLogger'), 8 | \lav45\activityLogger\storage\StorageInterface::class => static fn() => Yii::$app->get('activityLoggerStorage'), 9 | \lav45\activityLogger\middlewares\UserInterface::class => static fn() => Yii::$app->getUser()->getIdentity(), 10 | ]); 11 | 12 | // Добавьте в web/index.php и yii файлы для всех окружений 13 | define('LOG_ENV', 'api'); 14 | 15 | return [ 16 | 'components' => [ 17 | /** 18 | * Компонент принимает и управляет логами, реализует `\lav45\activityLogger\ManagerInterface` 19 | */ 20 | 'activityLogger' => [ 21 | '__class' => \lav45\activityLogger\Manager::class, 22 | 'middlewares' => [ 23 | [ 24 | 'class' => \lav45\activityLogger\middlewares\UserMiddleware::class, 25 | ], 26 | [ 27 | 'class' => \lav45\activityLogger\middlewares\EnvironmentMiddleware::class, 28 | '__construct()' => [ 'env' => LOG_ENV ], 29 | ], 30 | ], 31 | 32 | // В debug режиме, все Exception будут выбрасывать исключение, 33 | // иначе писать сообщение `Yii::error()` в логи. 34 | // 'debug' => YII_DEBUG 35 | ], 36 | 37 | /** 38 | * Хранилище для логов, реализует `\lav45\activityLogger\StorageInterface` 39 | */ 40 | 'activityLoggerStorage' => [ 41 | '__class' => \lav45\activityLogger\storage\DbStorage::class, 42 | 43 | // Если необходимо отключить логирование, можете использовать заглушку 44 | // '__class' => YII_ENV_PROD ? 45 | // \lav45\activityLogger\storage\DbStorage::class : 46 | // \lav45\activityLogger\storage\ArrayStorage::class, 47 | 48 | // Имя таблицы в которой будут храниться логи 49 | // 'tableName' => '{{%activity_log}}', 50 | 51 | // Идентификатор компонента `\yii\db\Connection` 52 | // 'db' => 'db', 53 | ], 54 | ] 55 | ]; 56 | ``` 57 | -------------------------------------------------------------------------------- /doc/migrate.md: -------------------------------------------------------------------------------- 1 | # Миграции 2 | 3 | ## Настраиваем 4 | 5 | Для начала нужно настроить `MigrateController` таким образом, чтобы он получал миграции из нескольких источников. 6 | В настройках консольного окружения необходимо добавить следующий код: 7 | 8 | ```php 9 | return [ 10 | 'controllerMap' => [ 11 | 'migrate' => [ 12 | 'class' => \yii\console\controllers\MigrateController::class, 13 | 'migrationPath' => [ 14 | '@app/migrations', 15 | '@vendor/lav45/yii2-activity-logger/migrations', 16 | ], 17 | ], 18 | ], 19 | ]; 20 | ``` 21 | 22 | ## Запускаем 23 | 24 | ```bash 25 | yii migrate 26 | ``` -------------------------------------------------------------------------------- /doc/viewModule.md: -------------------------------------------------------------------------------- 1 | # Отображение данных 2 | 3 | ## Необходимо добавить в конфигурационный файл `config/main.php` 4 | 5 | ```php 6 | return [ 7 | 'modules' => [ 8 | 'logger' => [ 9 | '__class' => \lav45\activityLogger\module\Module::class, 10 | // Список моделей которые логировались 11 | 'entityMap' => [ 12 | 'news' => 'common\models\News', 13 | ], 14 | ] 15 | ], 16 | ]; 17 | ``` 18 | 19 | ## Ссылки для просмотра логов 20 | 21 | ```php 22 | // На этой странице можно просмотреть все логи 23 | Url::toRoute(['/logger/default/index']); 24 | 25 | // На этой странице можно просмотреть журналы действий конкретного пользователя по его `$id` 26 | Url::toRoute(['/logger/default/index', 'userId' => 1]); 27 | 28 | // На этой странице можно просмотреть журналы действий для всех объектов "news" 29 | Url::toRoute(['/logger/default/index', 'entityName' => 'news']); 30 | 31 | // На этой странице можно просмотреть журналы действий для всех объектов "news" с "id" => 1 32 | Url::toRoute(['/logger/default/index', 'entityName' => 'news', 'entityId' => 1]); 33 | ``` 34 | 35 | ## Пример отображения 36 | 37 | ![ViewModule.png](ViewModule.png) 38 | -------------------------------------------------------------------------------- /migrations/m170427_214256_create_activity_log.php: -------------------------------------------------------------------------------- 1 | db->driverName === 'mysql') { 13 | /** @see https://stackoverflow.com/questions/766809 */ 14 | $this->tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB'; 15 | } 16 | } 17 | 18 | public function safeUp() 19 | { 20 | $this->createTable('{{%activity_log}}', [ 21 | 'id' => $this->bigPrimaryKey(), 22 | 'entity_name' => $this->string(32)->notNull(), 23 | 'entity_id' => $this->string(32), 24 | 'created_at' => $this->integer()->notNull(), 25 | 'user_id' => $this->string(32), 26 | 'user_name' => $this->string(255), 27 | 'action' => $this->string(32), 28 | 'env' => $this->string(32), 29 | 'data' => $this->text(), 30 | ], $this->tableOptions); 31 | 32 | $this->createIndex('activity_log-entity_name-entity_id-idx', '{{%activity_log}}', ['entity_name', 'entity_id']); 33 | $this->createIndex('activity_log-user_id', '{{%activity_log}}', 'user_id'); 34 | } 35 | 36 | public function safeDown() 37 | { 38 | $this->dropTable('{{%activity_log}}'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/ActiveLogBehavior.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/BSD-3-Clause 7 | */ 8 | 9 | namespace lav45\activityLogger; 10 | 11 | use lav45\activityLogger\storage\DeleteCommand; 12 | use yii\base\Behavior; 13 | use yii\base\InvalidConfigException; 14 | use yii\base\InvalidValueException; 15 | use yii\db\ActiveRecord; 16 | use yii\helpers\ArrayHelper; 17 | use yii\helpers\Inflector; 18 | use yii\helpers\StringHelper; 19 | 20 | /** 21 | * ======================= Example usage ====================== 22 | * 23 | * // Recommended 24 | * public function transactions() 25 | * { 26 | * return [ 27 | * ActiveRecord::SCENARIO_DEFAULT => ActiveRecord::OP_ALL, 28 | * ]; 29 | * } 30 | * 31 | * public function behaviors() 32 | * { 33 | * return [ 34 | * [ 35 | * '__class' => 'lav45\activityLogger\ActiveLogBehavior', 36 | * 'attributes' => [ 37 | * // simple attribute 38 | * 'title', 39 | * 40 | * // the value of the attribute is a item in the list 41 | * 'status' => [ 42 | * // => $this->getStatusList() 43 | * 'list' => 'statusList' 44 | * ], 45 | * 46 | * // the attribute value is the [id] of the relation model 47 | * 'owner_id' => [ 48 | * 'relation' => 'owner', 49 | * 'attribute' => 'username', 50 | * ], 51 | * ] 52 | * ] 53 | * ]; 54 | * } 55 | * ============================================================ 56 | * 57 | * @property string $entityName 58 | * @property string $entityId 59 | * @property ActiveRecord $owner 60 | */ 61 | class ActiveLogBehavior extends Behavior 62 | { 63 | /** 64 | * @event MessageEvent an event that is triggered before inserting a record. 65 | * You may add in to the [[MessageEvent::append]] your custom log message. 66 | * @since 1.5.3 67 | */ 68 | public const EVENT_BEFORE_SAVE_MESSAGE = 'beforeSaveMessage'; 69 | /** 70 | * @event Event an event that is triggered after inserting a record. 71 | * @since 1.5.3 72 | */ 73 | public const EVENT_AFTER_SAVE_MESSAGE = 'afterSaveMessage'; 74 | 75 | public bool $softDelete = false; 76 | /** @since 1.6.0 */ 77 | public ?\Closure $beforeSaveMessage = null; 78 | /** 79 | * [ 80 | * // simple attribute 81 | * 'title', 82 | * 83 | * // simple boolean attribute 84 | * 'is_publish', 85 | * 86 | * // the value of the attribute is a item in the list 87 | * // => $this->getStatusList() 88 | * 'status' => [ 89 | * 'list' => 'statusList' 90 | * ], 91 | * 92 | * // the attribute value is the [id] of the relation model 93 | * 'owner_id' => [ 94 | * 'relation' => 'user', 95 | * 'attribute' => 'username' 96 | * ] 97 | * ] 98 | */ 99 | public array $attributes = []; 100 | 101 | public bool $identicalAttributes = false; 102 | /** 103 | * A PHP callable that replaces the default implementation of [[isEmpty()]]. 104 | * @since 1.5.2 105 | */ 106 | public ?\Closure $isEmpty = null; 107 | /** 108 | * @var \Closure|array|string custom method to getEntityName 109 | * the callback function must return a string 110 | */ 111 | public $getEntityName; 112 | /** 113 | * @var \Closure|array|string custom method to getEntityId 114 | * the callback function can return a string or array 115 | */ 116 | public $getEntityId; 117 | /** 118 | * [ 119 | * 'title' => [ 120 | * 'new' => ['value' => 'New title'], 121 | * ], 122 | * 'is_publish' => [ 123 | * 'old' => ['value' => false], 124 | * 'new' => ['value' => true], 125 | * ], 126 | * 'status' => [ 127 | * 'old' => ['id' => 0, 'value' => 'Disabled'], 128 | * 'new' => ['id' => 1, 'value' => 'Active'], 129 | * ], 130 | * 'owner_id' => [ 131 | * 'old' => ['id' => 1, 'value' => 'admin'], 132 | * 'new' => ['id' => 2, 'value' => 'lucy'], 133 | * ] 134 | * ] 135 | */ 136 | private array $changedAttributes = []; 137 | 138 | private string $action; 139 | 140 | private ManagerInterface $logger; 141 | 142 | public function __construct( 143 | ManagerInterface $logger, 144 | array $config = [] 145 | ) 146 | { 147 | $this->logger = $logger; 148 | parent::__construct($config); 149 | } 150 | 151 | public function init(): void 152 | { 153 | $this->initAttributes(); 154 | } 155 | 156 | private function initAttributes(): void 157 | { 158 | foreach ($this->attributes as $key => $value) { 159 | if (is_int($key)) { 160 | unset($this->attributes[$key]); 161 | $this->attributes[$value] = []; 162 | } 163 | } 164 | } 165 | 166 | public function events(): array 167 | { 168 | return [ 169 | ActiveRecord::EVENT_BEFORE_INSERT => [$this, 'beforeSave'], 170 | ActiveRecord::EVENT_BEFORE_UPDATE => [$this, 'beforeSave'], 171 | ActiveRecord::EVENT_BEFORE_DELETE => [$this, 'beforeDelete'], 172 | ActiveRecord::EVENT_AFTER_INSERT => [$this, 'afterSave'], 173 | ActiveRecord::EVENT_AFTER_UPDATE => [$this, 'afterSave'], 174 | ]; 175 | } 176 | 177 | public function beforeSave(): void 178 | { 179 | $this->changedAttributes = $this->prepareChangedAttributes(); 180 | $this->action = $this->owner->getIsNewRecord() ? 'created' : 'updated'; 181 | } 182 | 183 | public function afterSave(): void 184 | { 185 | if (empty($this->changedAttributes)) { 186 | return; 187 | } 188 | $this->saveMessage($this->action, $this->changedAttributes); 189 | } 190 | 191 | public function beforeDelete(): void 192 | { 193 | if (false === $this->softDelete) { 194 | $this->deleteEntity(); 195 | } 196 | $this->saveMessage('deleted', $this->prepareChangedAttributes(true)); 197 | } 198 | 199 | private function prepareChangedAttributes(bool $unset = false): array 200 | { 201 | $result = []; 202 | foreach ($this->attributes as $attribute => $options) { 203 | $old = $this->owner->getOldAttribute($attribute); 204 | $new = false === $unset ? $this->owner->getAttribute($attribute) : null; 205 | 206 | if ($this->isEmpty($old) && $this->isEmpty($new)) { 207 | continue; 208 | } 209 | if (false === $unset && false === $this->owner->isAttributeChanged($attribute, $this->identicalAttributes)) { 210 | continue; 211 | } 212 | $result[$attribute] = $this->resolveStoreValues($old, $new, $options); 213 | } 214 | return $result; 215 | } 216 | 217 | /** 218 | * @param string|int|null $old_id 219 | * @param string|int|null $new_id 220 | */ 221 | protected function resolveStoreValues($old_id, $new_id, array $options): array 222 | { 223 | if (isset($options['list'])) { 224 | $value = $this->resolveListValues($old_id, $new_id, $options['list']); 225 | } elseif (isset($options['relation'], $options['attribute'])) { 226 | $value = $this->resolveRelationValues($old_id, $new_id, $options['relation'], $options['attribute']); 227 | } else { 228 | $value = $this->resolveSimpleValues($old_id, $new_id); 229 | } 230 | return $value; 231 | } 232 | 233 | /** 234 | * @param string|int|null $old_id 235 | * @param string|int|null $new_id 236 | */ 237 | private function resolveSimpleValues($old_id, $new_id): array 238 | { 239 | return [ 240 | 'old' => ['value' => $old_id], 241 | 'new' => ['value' => $new_id], 242 | ]; 243 | } 244 | 245 | /** 246 | * @param string|int|array|null $old_id 247 | * @param string|int|array|null $new_id 248 | * @param string|\Closure $listName 249 | */ 250 | private function resolveListValues($old_id, $new_id, $listName): array 251 | { 252 | $old = $new = []; 253 | $old['id'] = $old_id; 254 | $new['id'] = $new_id; 255 | $list = []; 256 | 257 | if (is_array($old_id) || is_array($new_id)) { 258 | $list = ArrayHelper::getValue($this->owner, $listName); 259 | } 260 | if (is_array($old_id)) { 261 | $old['value'] = array_intersect_key($list, array_flip($old_id)); 262 | } elseif ($old_id) { 263 | $old['value'] = ArrayHelper::getValue($this->owner, [$listName, $old_id]); 264 | } else { 265 | $old['value'] = null; 266 | } 267 | if (is_array($new_id)) { 268 | $new['value'] = array_intersect_key($list, array_flip($new_id)); 269 | } elseif ($new_id) { 270 | $new['value'] = ArrayHelper::getValue($this->owner, [$listName, $new_id]); 271 | } else { 272 | $new['value'] = null; 273 | } 274 | return [ 275 | 'old' => $old, 276 | 'new' => $new 277 | ]; 278 | } 279 | 280 | /** 281 | * @param string|int|null $old_id 282 | * @param string|int|null $new_id 283 | */ 284 | private function resolveRelationValues($old_id, $new_id, string $relation, string $attribute): array 285 | { 286 | $old = $new = []; 287 | $old['id'] = $old_id; 288 | $new['id'] = $new_id; 289 | 290 | $relationQuery = clone $this->owner->getRelation($relation); 291 | if (count($relationQuery->link) > 1) { 292 | throw new InvalidConfigException('Relation model can only be linked through one primary key.'); 293 | } 294 | $relationQuery->primaryModel = null; 295 | $idAttribute = array_keys($relationQuery->link)[0]; 296 | $targetId = array_filter([$old_id, $new_id]); 297 | 298 | $relationModels = $relationQuery 299 | ->where([$idAttribute => $targetId]) 300 | ->indexBy($idAttribute) 301 | ->limit(count($targetId)) 302 | ->all(); 303 | 304 | if ($old_id) { 305 | $old['value'] = ArrayHelper::getValue($relationModels, [$old_id, $attribute]); 306 | } else { 307 | $old['value'] = null; 308 | } 309 | if ($new_id) { 310 | $new['value'] = ArrayHelper::getValue($relationModels, [$new_id, $attribute]); 311 | } else { 312 | $new['value'] = null; 313 | } 314 | return [ 315 | 'old' => $old, 316 | 'new' => $new 317 | ]; 318 | } 319 | 320 | protected function deleteEntity(): void 321 | { 322 | $this->logger->delete(new DeleteCommand([ 323 | 'entityName' => $this->getEntityName(), 324 | 'entityId' => $this->getEntityId(), 325 | ])); 326 | } 327 | 328 | protected function saveMessage(string $action, array $data): void 329 | { 330 | $data = $this->beforeSaveMessage($data); 331 | $this->addLog($data, $action); 332 | $this->afterSaveMessage(); 333 | } 334 | 335 | /** 336 | * @param string|array $data 337 | * @since 1.7.0 338 | */ 339 | public function addLog($data, string $action = null): bool 340 | { 341 | $message = $this->logger->createMessageBuilder($this->getEntityName()) 342 | ->withEntityId($this->getEntityId()) 343 | ->withAction($action) 344 | ->withData($data) 345 | ->build(time()); 346 | 347 | return $this->logger->log($message); 348 | } 349 | 350 | /** 351 | * @since 1.5.3 352 | */ 353 | public function beforeSaveMessage(array $data): array 354 | { 355 | if (null !== $this->beforeSaveMessage) { 356 | return call_user_func($this->beforeSaveMessage, $data); 357 | } 358 | $name = self::EVENT_BEFORE_SAVE_MESSAGE; 359 | if (method_exists($this->owner, $name)) { 360 | return $this->owner->$name($data); 361 | } 362 | $event = new MessageEvent(); 363 | $event->logData = $data; 364 | $this->owner->trigger($name, $event); 365 | return $event->logData; 366 | } 367 | 368 | /** 369 | * @since 1.5.3 370 | */ 371 | public function afterSaveMessage(): void 372 | { 373 | $name = self::EVENT_AFTER_SAVE_MESSAGE; 374 | if (method_exists($this->owner, $name)) { 375 | $this->owner->$name(); 376 | } else { 377 | $this->owner->trigger($name); 378 | } 379 | } 380 | 381 | public function getEntityName(): string 382 | { 383 | if (is_string($this->getEntityName)) { 384 | return $this->getEntityName; 385 | } 386 | if (is_callable($this->getEntityName)) { 387 | return call_user_func($this->getEntityName); 388 | } 389 | $class = get_class($this->owner); 390 | $class = StringHelper::basename($class); 391 | $this->getEntityName = Inflector::camel2id($class, '_'); 392 | return $this->getEntityName; 393 | } 394 | 395 | public function getEntityId(): string 396 | { 397 | if (null === $this->getEntityId) { 398 | $result = $this->owner->getPrimaryKey(); 399 | } elseif (is_callable($this->getEntityId)) { 400 | $result = call_user_func($this->getEntityId); 401 | } else { 402 | $result = $this->getEntityId; 403 | } 404 | if ($this->isEmpty($result)) { 405 | throw new InvalidValueException('the property "entityId" can not be empty'); 406 | } 407 | if (is_array($result)) { 408 | ksort($result); 409 | $result = json_encode($result, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); 410 | } 411 | return $result; 412 | } 413 | 414 | /** 415 | * Checks if the given value is empty. 416 | * A value is considered empty if it is null, an empty array, or an empty string. 417 | * Note that this method is different from PHP empty(). It will return false when the value is 0. 418 | * @param mixed $value the value to be checked 419 | * @return bool whether the value is empty 420 | * @since 1.5.2 421 | */ 422 | public function isEmpty($value): bool 423 | { 424 | if (null !== $this->isEmpty) { 425 | return call_user_func($this->isEmpty, $value); 426 | } 427 | return null === $value || '' === $value || [] === $value; 428 | } 429 | } -------------------------------------------------------------------------------- /src/LogCollection.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/BSD-3-Clause 7 | */ 8 | 9 | namespace lav45\activityLogger; 10 | 11 | class LogCollection 12 | { 13 | private ManagerInterface $logger; 14 | 15 | private MessageBuilderInterface $builder; 16 | /** @var string[] */ 17 | private array $data = []; 18 | 19 | public function __construct(ManagerInterface $logger, string $entityName) 20 | { 21 | $this->logger = $logger; 22 | $this->builder = $logger->createMessageBuilder($entityName); 23 | } 24 | 25 | /** 26 | * @param string|int $value 27 | */ 28 | public function setEntityId($value): self 29 | { 30 | $this->builder = $this->builder->withEntityId($value); 31 | return $this; 32 | } 33 | 34 | public function setAction(string $value): self 35 | { 36 | $this->builder = $this->builder->withAction($value); 37 | return $this; 38 | } 39 | 40 | public function addMessage(string $value): void 41 | { 42 | $this->data[] = $value; 43 | } 44 | 45 | /** 46 | * @return string[] 47 | */ 48 | private function flushData(): array 49 | { 50 | $data = $this->data; 51 | $this->data = []; 52 | return $data; 53 | } 54 | 55 | public function push(): bool 56 | { 57 | $data = $this->flushData(); 58 | if (empty($data)) { 59 | return false; 60 | } 61 | 62 | $message = $this->builder 63 | ->withData($data) 64 | ->build(time()); 65 | 66 | return $this->logger->log($message); 67 | } 68 | } -------------------------------------------------------------------------------- /src/LogInfoBehavior.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/BSD-3-Clause 7 | */ 8 | 9 | namespace lav45\activityLogger; 10 | 11 | use yii\base\Behavior; 12 | use yii\helpers\ArrayHelper; 13 | 14 | /** 15 | * Class LogInfoBehavior 16 | * @package lav45\activityLogger 17 | * 18 | * ======================= Example usage ====================== 19 | * public function behaviors() 20 | * { 21 | * return [ 22 | * [ 23 | * '__class' => 'lav45\activityLogger\LogInfoBehavior', 24 | * 'template' => '{username} ({profile.email})', 25 | * // OR 26 | * //'template' => function() { 27 | * // return "{$this->username} ({$this->profile->email})"; 28 | * //}, 29 | * ] 30 | * ]; 31 | * } 32 | * ============================================================ 33 | * 34 | * @since 1.6.0 35 | */ 36 | class LogInfoBehavior extends Behavior 37 | { 38 | /** 39 | * @var string|\Closure information field that will be displayed at the beginning of the list of logs for more information. 40 | * 41 | * example: '{username} ({profile.email})' 42 | * result: 'Maxim (max@gmail.com)' 43 | * {username} is an attribute of the `owner` model 44 | * {profile.email} is the relations attribute of the `profile` model 45 | */ 46 | public $template; 47 | /** 48 | * add log data to start 49 | */ 50 | public bool $prepend = true; 51 | 52 | public function events(): array 53 | { 54 | return [ 55 | ActiveLogBehavior::EVENT_BEFORE_SAVE_MESSAGE => 'beforeSave', 56 | ]; 57 | } 58 | 59 | public function beforeSave(MessageEvent $event): void 60 | { 61 | if ($data = $this->getInfoData()) { 62 | if (true === $this->prepend) { 63 | array_unshift($event->logData, $data); 64 | } else { 65 | $event->logData[] = $data; 66 | } 67 | } 68 | } 69 | 70 | protected function getInfoData(): ?string 71 | { 72 | if (null === $this->template) { 73 | return null; 74 | } 75 | if (is_callable($this->template)) { 76 | return call_user_func($this->template); 77 | } 78 | $callback = function ($matches) { 79 | return ArrayHelper::getValue($this->owner, $matches[1]); 80 | }; 81 | return preg_replace_callback('/\\{([\w\._]+)\\}/', $callback, $this->template); 82 | } 83 | } -------------------------------------------------------------------------------- /src/Manager.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/BSD-3-Clause 7 | */ 8 | 9 | namespace lav45\activityLogger; 10 | 11 | use lav45\activityLogger\middlewares\Middleware; 12 | use lav45\activityLogger\middlewares\MiddlewarePipeline; 13 | use lav45\activityLogger\storage\DeleteCommand; 14 | use lav45\activityLogger\storage\MessageData; 15 | use lav45\activityLogger\storage\StorageInterface; 16 | use Throwable; 17 | use Yii; 18 | use yii\base\BaseObject; 19 | use yii\di\Instance; 20 | 21 | class Manager extends BaseObject implements ManagerInterface 22 | { 23 | public bool $debug = YII_DEBUG; 24 | 25 | /** @var array{string, array, Middleware} */ 26 | public array $middlewares = []; 27 | 28 | private StorageInterface $storage; 29 | 30 | public function __construct( 31 | StorageInterface $storage, 32 | array $config = [] 33 | ) 34 | { 35 | parent::__construct($config); 36 | $this->storage = $storage; 37 | } 38 | 39 | /** 40 | * @return Middleware[] 41 | */ 42 | private function createMiddlewares(): array 43 | { 44 | $result = []; 45 | foreach ($this->middlewares as $middleware) { 46 | $result[] = Instance::ensure($middleware, Middleware::class); 47 | } 48 | return $result; 49 | } 50 | 51 | public function createMessageBuilder(string $entityName): MessageBuilderInterface 52 | { 53 | $middlewares = $this->createMiddlewares(); 54 | $pipeline = new MiddlewarePipeline(...$middlewares); 55 | $builder = new MessageBuilder($entityName); 56 | return $pipeline->handle($builder); 57 | } 58 | 59 | public function log(MessageData $message): bool 60 | { 61 | try { 62 | $this->storage->save($message); 63 | return true; 64 | } catch (Throwable $e) { 65 | $this->throwException($e); 66 | return false; 67 | } 68 | } 69 | 70 | public function delete(DeleteCommand $command): bool 71 | { 72 | try { 73 | $this->storage->delete($command); 74 | return true; 75 | } catch (Throwable $e) { 76 | $this->throwException($e); 77 | return false; 78 | } 79 | } 80 | 81 | private function throwException(Throwable $e): void 82 | { 83 | if ($this->debug) { 84 | throw $e; 85 | } 86 | Yii::error($e->getMessage(), static::class); 87 | } 88 | } -------------------------------------------------------------------------------- /src/ManagerInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/BSD-3-Clause 7 | */ 8 | 9 | namespace lav45\activityLogger; 10 | 11 | use lav45\activityLogger\storage\DeleteCommand; 12 | use lav45\activityLogger\storage\MessageData; 13 | 14 | interface ManagerInterface 15 | { 16 | public function createMessageBuilder(string $entityName): MessageBuilderInterface; 17 | 18 | public function log(MessageData $message): bool; 19 | 20 | public function delete(DeleteCommand $command): bool; 21 | } -------------------------------------------------------------------------------- /src/MessageBuilder.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/BSD-3-Clause 7 | */ 8 | 9 | namespace lav45\activityLogger; 10 | 11 | use lav45\activityLogger\storage\MessageData; 12 | 13 | final class MessageBuilder implements MessageBuilderInterface 14 | { 15 | private MessageData $message; 16 | 17 | public function __construct(string $entityName) 18 | { 19 | $this->message = new MessageData(); 20 | $this->message->entityName = $entityName; 21 | } 22 | 23 | /** 24 | * @param string|int $id 25 | */ 26 | public function withEntityId($id): self 27 | { 28 | $new = clone $this; 29 | $new->message->entityId = (string)$id; 30 | return $new; 31 | } 32 | 33 | public function withUserId(string $id): self 34 | { 35 | $new = clone $this; 36 | $new->message->userId = $id; 37 | return $new; 38 | } 39 | 40 | public function withUserName(string $name): self 41 | { 42 | $new = clone $this; 43 | $new->message->userName = $name; 44 | return $new; 45 | } 46 | 47 | /** 48 | * @param string|null $action 49 | */ 50 | public function withAction($action): self 51 | { 52 | $new = clone $this; 53 | $new->message->action = $action; 54 | return $new; 55 | } 56 | 57 | public function withEnv(string $env): self 58 | { 59 | $new = clone $this; 60 | $new->message->env = $env; 61 | return $new; 62 | } 63 | 64 | /** 65 | * @param array|string $data 66 | */ 67 | public function withData($data): self 68 | { 69 | $new = clone $this; 70 | $new->message->data = $data; 71 | return $new; 72 | } 73 | 74 | public function build(int $now): MessageData 75 | { 76 | $this->message->createdAt = $now; 77 | return $this->message; 78 | } 79 | 80 | public function __clone() 81 | { 82 | $this->message = clone $this->message; 83 | } 84 | } -------------------------------------------------------------------------------- /src/MessageBuilderInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/BSD-3-Clause 7 | */ 8 | 9 | namespace lav45\activityLogger; 10 | 11 | use lav45\activityLogger\storage\MessageData; 12 | 13 | interface MessageBuilderInterface 14 | { 15 | /** 16 | * @param string|int $id 17 | */ 18 | public function withEntityId($id): self; 19 | 20 | public function withUserId(string $id): self; 21 | 22 | public function withUserName(string $name): self; 23 | 24 | public function withAction($action): self; 25 | 26 | public function withEnv(string $env): self; 27 | 28 | /** 29 | * @param array|string $data 30 | */ 31 | public function withData($data): self; 32 | 33 | public function build(int $now): MessageData; 34 | } -------------------------------------------------------------------------------- /src/MessageEvent.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/BSD-3-Clause 7 | */ 8 | 9 | namespace lav45\activityLogger; 10 | 11 | use yii\base\Event; 12 | 13 | /** 14 | * Class MessageEvent 15 | * @package lav45\activityLogger 16 | * @since 1.5.3 17 | */ 18 | class MessageEvent extends Event 19 | { 20 | /** 21 | * @var array property to store data that will be recorded in the history of logs 22 | */ 23 | public $logData = []; 24 | } -------------------------------------------------------------------------------- /src/console/DefaultController.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/BSD-3-Clause 7 | */ 8 | 9 | namespace lav45\activityLogger\console; 10 | 11 | use lav45\activityLogger\ManagerInterface; 12 | use lav45\activityLogger\storage\DeleteCommand; 13 | use yii\base\Module; 14 | use yii\console\Controller; 15 | 16 | class DefaultController extends Controller 17 | { 18 | /** Target entity name */ 19 | public ?string $entityName = null; 20 | /** Entity target id */ 21 | public ?string $entityId = null; 22 | /** User id who performed the action */ 23 | public ?string $userId = null; 24 | /** Action performed on the object */ 25 | public ?string $logAction = null; 26 | /** Environment, which produced the effect */ 27 | public ?string $env = null; 28 | /** 29 | * Delete older than 30 | * Valid values: 31 | * 1h - 1 hour 32 | * 2d - 2 days 33 | * 3m - 3 month 34 | * 1y - 1 year 35 | */ 36 | public ?string $oldThan = null; 37 | 38 | private ManagerInterface $logger; 39 | 40 | public function __construct( 41 | string $id, 42 | Module $module, 43 | ManagerInterface $logger, 44 | array $config = [] 45 | ) 46 | { 47 | parent::__construct($id, $module, $config); 48 | $this->logger = $logger; 49 | } 50 | 51 | public function options($actionID): array 52 | { 53 | return array_merge(parent::options($actionID), [ 54 | 'entityName', 55 | 'entityId', 56 | 'userId', 57 | 'logAction', 58 | 'env', 59 | 'oldThan', 60 | ]); 61 | } 62 | 63 | public function optionAliases(): array 64 | { 65 | return array_merge(parent::optionAliases(), [ 66 | 'o' => 'old-than', 67 | 'a' => 'log-action', 68 | 'eid' => 'entity-id', 69 | 'e' => 'entity-name', 70 | 'uid' => 'user-id', 71 | ]); 72 | } 73 | 74 | /** 75 | * Clean storage activity log 76 | */ 77 | public function actionClean(): void 78 | { 79 | $oldThan = $this->parseDate($this->oldThan); 80 | 81 | $command = new DeleteCommand([ 82 | 'entityName' => $this->entityName, 83 | 'entityId' => $this->entityId, 84 | 'userId' => $this->userId, 85 | 'action' => $this->logAction, 86 | 'env' => $this->env, 87 | 'oldThan' => $oldThan, 88 | ]); 89 | 90 | if ($this->logger->delete($command)) { 91 | $this->stdout("Successful clearing the logs.\n"); 92 | } else { 93 | $this->stdout("Error while cleaning the logs.\n"); 94 | } 95 | } 96 | 97 | private function parseDate(?string $str): ?int 98 | { 99 | if (empty($str)) { 100 | return null; 101 | } 102 | if (preg_match("/^(\d+)([hdmy]+)$/", $str, $matches)) { 103 | [$_, $count, $alias] = $matches; 104 | $aliases = [ 105 | 'h' => 'hour', 106 | 'd' => 'day', 107 | 'm' => 'month', 108 | 'y' => 'year', 109 | ]; 110 | return strtotime("-{$count} {$aliases[$alias]} 0:00:00 UTC"); 111 | } 112 | throw new \InvalidArgumentException("Invalid old-than value: '{$str}'. You can use one of the 1h, 2d, 3m or 4y"); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/middlewares/EnvironmentMiddleware.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/BSD-3-Clause 7 | */ 8 | 9 | namespace lav45\activityLogger\middlewares; 10 | 11 | use Closure; 12 | use lav45\activityLogger\MessageBuilderInterface; 13 | 14 | final class EnvironmentMiddleware implements Middleware 15 | { 16 | private string $env; 17 | 18 | public function __construct(string $env) 19 | { 20 | $this->env = $env; 21 | } 22 | 23 | public function handle(MessageBuilderInterface $builder, Closure $next): MessageBuilderInterface 24 | { 25 | $builder = $builder->withEnv($this->env); 26 | return $next($builder); 27 | } 28 | } -------------------------------------------------------------------------------- /src/middlewares/Middleware.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/BSD-3-Clause 7 | */ 8 | 9 | namespace lav45\activityLogger\middlewares; 10 | 11 | use Closure; 12 | use lav45\activityLogger\MessageBuilderInterface; 13 | 14 | interface Middleware 15 | { 16 | public function handle(MessageBuilderInterface $builder, Closure $next): MessageBuilderInterface; 17 | } -------------------------------------------------------------------------------- /src/middlewares/MiddlewarePipeline.php: -------------------------------------------------------------------------------- 1 | middlewares = $middleware; 15 | } 16 | 17 | public function handle(MessageBuilderInterface $builder): MessageBuilderInterface 18 | { 19 | $middlewareChain = array_reduce( 20 | array_reverse($this->middlewares), 21 | static function (Closure $next, Middleware $middleware) { 22 | return static fn (MessageBuilderInterface $builder): MessageBuilderInterface => $middleware->handle($builder, $next); 23 | }, 24 | static function(MessageBuilderInterface $builder): MessageBuilderInterface { 25 | return $builder; 26 | } 27 | ); 28 | return $middlewareChain($builder); 29 | } 30 | } -------------------------------------------------------------------------------- /src/middlewares/UserInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/BSD-3-Clause 7 | */ 8 | 9 | namespace lav45\activityLogger\middlewares; 10 | 11 | interface UserInterface 12 | { 13 | public function getId(): string; 14 | 15 | public function getName(): string; 16 | } -------------------------------------------------------------------------------- /src/middlewares/UserMiddleware.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/BSD-3-Clause 7 | */ 8 | 9 | namespace lav45\activityLogger\middlewares; 10 | 11 | use Closure; 12 | use lav45\activityLogger\MessageBuilderInterface; 13 | 14 | final class UserMiddleware implements Middleware 15 | { 16 | private ?UserInterface $user; 17 | 18 | public function __construct(?UserInterface $user = null) 19 | { 20 | $this->user = $user; 21 | } 22 | 23 | public function handle(MessageBuilderInterface $builder, Closure $next): MessageBuilderInterface 24 | { 25 | if ($this->user === null) { 26 | return $next($builder); 27 | } 28 | 29 | $builder = $builder 30 | ->withUserId($this->user->getId()) 31 | ->withUserName($this->user->getName()); 32 | 33 | return $next($builder); 34 | } 35 | } -------------------------------------------------------------------------------- /src/module/Module.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/BSD-3-Clause 7 | */ 8 | 9 | namespace lav45\activityLogger\module; 10 | 11 | use Yii; 12 | use yii\i18n\PhpMessageSource; 13 | 14 | class Module extends \yii\base\Module 15 | { 16 | /** 17 | * @var array Список моделей которые логировались 18 | * [ entityName => \namespace\to\Model\EntityClass ] 19 | * Эта информация используется для корректного отображения имен полей, записанных данных 20 | * Если `entityName` не будет найдена в списке то имена полей будут выводиться без преобразования 21 | * @see Model::getAttributeLabel() 22 | */ 23 | public array $entityMap = []; 24 | 25 | /** 26 | * Initializes the module. 27 | */ 28 | public function init() 29 | { 30 | parent::init(); 31 | $this->initTranslations(); 32 | } 33 | 34 | private function initTranslations(): void 35 | { 36 | Yii::$app->getI18n()->translations['lav45/logger'] = [ 37 | '__class' => PhpMessageSource::class, 38 | 'basePath' => __DIR__ . '/messages', 39 | ]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/module/controllers/DefaultController.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/BSD-3-Clause 7 | */ 8 | 9 | namespace lav45\activityLogger\module\controllers; 10 | 11 | use Yii; 12 | use yii\web\Controller; 13 | use lav45\activityLogger\module\models\ActivityLogSearch; 14 | use lav45\activityLogger\module\models\ActivityLogViewModel; 15 | 16 | /** 17 | * @property \lav45\activityLogger\module\Module $module 18 | */ 19 | class DefaultController extends Controller 20 | { 21 | public function actionIndex() 22 | { 23 | Yii::$container->set(ActivityLogViewModel::class, [ 24 | 'entityMap' => $this->module->entityMap 25 | ]); 26 | 27 | $searchModel = new ActivityLogSearch(); 28 | $dataProvider = $searchModel->search(Yii::$app->getRequest()->getQueryParams()); 29 | 30 | return $this->render('index', [ 31 | 'dataProvider' => $dataProvider, 32 | ]); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/module/messages/config.php: -------------------------------------------------------------------------------- 1 | __DIR__ . DIRECTORY_SEPARATOR . '../..', 6 | // array, required, list of language codes that the extracted messages 7 | // should be translated to. For example, ['zh-CN', 'de']. 8 | 'languages' => ['ru'], 9 | // string, the name of the function for translating messages. 10 | // Defaults to 'Yii::t'. This is used as a mark to find the messages to be 11 | // translated. You may use a string for single function name or an array for 12 | // multiple function names. 13 | 'translator' => 'Yii::t', 14 | // boolean, whether to sort messages by keys when merging new messages 15 | // with the existing ones. Defaults to false, which means the new (untranslated) 16 | // messages will be separated from the old (translated) ones. 17 | 'sort' => true, 18 | // boolean, whether to remove messages that no longer appear in the source code. 19 | // Defaults to false, which means these messages will NOT be removed. 20 | 'removeUnused' => false, 21 | // boolean, whether to mark messages that no longer appear in the source code. 22 | // Defaults to true, which means each of these messages will be enclosed with a pair of '@@' marks. 23 | 'markUnused' => false, 24 | // array, list of patterns that specify which files (not directories) should be processed. 25 | // If empty or not set, all files will be processed. 26 | // See helpers/FileHelper::findFiles() for pattern matching rules. 27 | // If a file/directory matches both a pattern in "only" and "except", it will NOT be processed. 28 | 'only' => ['*.php'], 29 | // array, list of patterns that specify which files/directories should NOT be processed. 30 | // If empty or not set, all files/directories will be processed. 31 | // See helpers/FileHelper::findFiles() for pattern matching rules. 32 | // If a file/directory matches both a pattern in "only" and "except", it will NOT be processed. 33 | 'except' => [], 34 | 35 | // 'php' output format is for saving messages to php files. 36 | 'format' => 'php', 37 | // Root directory containing message translations. 38 | 'messagePath' => __DIR__ . DIRECTORY_SEPARATOR . '../messages', 39 | // boolean, whether the message file should be overwritten with the merged messages 40 | 'overwrite' => true, 41 | ]; 42 | -------------------------------------------------------------------------------- /src/module/messages/ru/lav45/logger.php: -------------------------------------------------------------------------------- 1 | {attribute} has been changed' => '{attribute} был изменен', 21 | 'Action' => 'Действие', 22 | 'Activity log' => 'Журнал активности', 23 | 'Created' => 'Создано', 24 | 'Data' => 'Данные', 25 | 'Entity' => 'Сущность', 26 | 'Entity name' => 'Имя объекта', 27 | 'Environment' => 'Окружение', 28 | 'ID' => 'ID', 29 | 'Reset' => 'Сбросить', 30 | 'User' => 'Пользователь', 31 | 'User name' => 'Имя пользователя', 32 | 'created' => 'создал', 33 | 'from' => 'с', 34 | 'removed' => 'удалил', 35 | 'to' => 'на', 36 | 'updated' => 'обновил', 37 | ]; 38 | -------------------------------------------------------------------------------- /src/module/models/ActivityLog.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/BSD-3-Clause 7 | */ 8 | 9 | namespace lav45\activityLogger\module\models; 10 | 11 | use Yii; 12 | use yii\base\InvalidConfigException; 13 | use yii\db\ActiveRecord; 14 | use yii\db\Connection; 15 | 16 | /** 17 | * @property int $id 18 | * @property string $entity_name 19 | * @property string $entity_id 20 | * @property string $user_id 21 | * @property string $user_name 22 | * @property integer $created_at 23 | * @property string $action 24 | * @property string $env 25 | * @property string $data 26 | */ 27 | class ActivityLog extends ActiveRecord 28 | { 29 | /** 30 | * @since 1.7.0 31 | */ 32 | public static ?string $db = null; 33 | /** 34 | * @since 1.7.0 35 | */ 36 | public static string $tableName = '{{%activity_log}}'; 37 | 38 | public static function tableName(): string 39 | { 40 | return static::$tableName ?: parent::tableName(); 41 | } 42 | 43 | public static function getDb(): Connection 44 | { 45 | if (static::$db) { 46 | $db = Yii::$app->get(static::$db); 47 | if ($db instanceof Connection) { 48 | return $db; 49 | } 50 | throw new InvalidConfigException('Invalid db connection'); 51 | } 52 | return parent::getDb(); 53 | } 54 | 55 | public function attributeLabels(): array 56 | { 57 | return [ 58 | 'id' => Yii::t('lav45/logger', 'ID'), 59 | 'entity_name' => Yii::t('lav45/logger', 'Entity name'), 60 | 'entity_id' => Yii::t('lav45/logger', 'Entity'), 61 | 'user_id' => Yii::t('lav45/logger', 'User'), 62 | 'user_name' => Yii::t('lav45/logger', 'User name'), 63 | 'created_at' => Yii::t('lav45/logger', 'Created'), 64 | 'action' => Yii::t('lav45/logger', 'Action'), 65 | 'env' => Yii::t('lav45/logger', 'Environment'), 66 | 'data' => Yii::t('lav45/logger', 'Data'), 67 | ]; 68 | } 69 | 70 | public function getData(): iterable 71 | { 72 | if ($this->data) { 73 | return (array)json_decode($this->data, true, 512, JSON_THROW_ON_ERROR); 74 | } 75 | return []; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/module/models/ActivityLogSearch.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/BSD-3-Clause 7 | */ 8 | 9 | namespace lav45\activityLogger\module\models; 10 | 11 | use Yii; 12 | use yii\base\Model; 13 | use yii\data\ActiveDataProvider; 14 | 15 | class ActivityLogSearch extends Model 16 | { 17 | /** 18 | * @var string 19 | */ 20 | public $entityName; 21 | /** 22 | * @var int|string 23 | */ 24 | public $entityId; 25 | /** 26 | * @var int|string 27 | */ 28 | public $userId; 29 | /** 30 | * @var string 31 | */ 32 | public $env; 33 | /** 34 | * @var string 35 | */ 36 | public $date; 37 | 38 | /** 39 | * @inheritdoc 40 | */ 41 | public function rules() 42 | { 43 | return [ 44 | [['entityName', 'entityId', 'userId', 'env'], 'string', 'max' => 32], 45 | [['date'], 'date', 'format' => 'dd.MM.yyyy'], 46 | ]; 47 | } 48 | 49 | /** 50 | * For beautiful links in the browser bar when filtering and searching 51 | * @return string 52 | */ 53 | public function formName() 54 | { 55 | return ''; 56 | } 57 | 58 | /** 59 | * Creates data provider instance with search query applied 60 | * @param array $params 61 | * @return ActiveDataProvider 62 | */ 63 | public function search($params) 64 | { 65 | $query = ActivityLogViewModel::find() 66 | ->orderBy(['id' => SORT_DESC]); 67 | 68 | $dataProvider = new ActiveDataProvider([ 69 | 'query' => $query, 70 | 'sort' => false, 71 | ]); 72 | 73 | if (!($this->load($params) && $this->validate())) { 74 | return $dataProvider; 75 | } 76 | 77 | if (!empty($this->date)) { 78 | $time_zone = Yii::$app->getTimeZone(); 79 | $date_from = strtotime("{$this->date} 00:00:00 {$time_zone}"); 80 | $date_to = $date_from + 86399; // + 23:59:59 81 | $query->andWhere(['between', 'created_at', $date_from, $date_to]); 82 | } 83 | 84 | $query->andFilterWhere([ 85 | 'entity_name' => $this->entityName, 86 | 'entity_id' => $this->entityId, 87 | 'user_id' => $this->userId, 88 | 'env' => $this->env, 89 | ]); 90 | 91 | return $dataProvider; 92 | } 93 | } -------------------------------------------------------------------------------- /src/module/models/ActivityLogViewModel.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/BSD-3-Clause 7 | */ 8 | 9 | namespace lav45\activityLogger\module\models; 10 | 11 | use Yii; 12 | 13 | class ActivityLogViewModel extends ActivityLog 14 | { 15 | /** 16 | * @var DataModel|string|array 17 | */ 18 | public $dataModel = DataModel::class; 19 | /** 20 | * [ entity_name => Entity::class ] 21 | */ 22 | public array $entityMap = []; 23 | 24 | private array $entityModel = []; 25 | 26 | /** 27 | * @param array $row 28 | * @return ActivityLog|object|static 29 | */ 30 | public static function instantiate($row) 31 | { 32 | return Yii::createObject(static::class); 33 | } 34 | 35 | /** 36 | * @return \yii\base\Model|null 37 | */ 38 | protected function getEntityModel() 39 | { 40 | if (isset($this->entityModel[$this->entity_name]) === false) { 41 | $this->entityModel[$this->entity_name] = $this->getEntityObject($this->entity_name); 42 | } 43 | return $this->entityModel[$this->entity_name] ?: null; 44 | } 45 | 46 | /** 47 | * @return false|\yii\base\Model 48 | */ 49 | private function getEntityObject(string $id) 50 | { 51 | if (isset($this->entityMap[$id]) === false) { 52 | return false; 53 | } 54 | /** @var \yii\base\Model $class */ 55 | $class = $this->entityMap[$id]; 56 | return $class::instance(); 57 | } 58 | 59 | /** 60 | * @return \Generator|DataModel[] 61 | */ 62 | public function getData(): iterable 63 | { 64 | foreach (parent::getData() as $attribute => $values) { 65 | if (is_string($values)) { 66 | $label = is_string($attribute) ? $this->getEntityAttributeLabel($attribute) : $attribute; 67 | yield $label => $values; 68 | } else { 69 | $dataModel = $this->getDataModel() 70 | ->setFormat($this->getAttributeFormat($attribute)) 71 | ->setData($values); 72 | 73 | yield $this->getEntityAttributeLabel($attribute) => $dataModel; 74 | } 75 | } 76 | } 77 | 78 | protected function getDataModel(): DataModel 79 | { 80 | if (!is_object($this->dataModel)) { 81 | $this->dataModel = Yii::createObject($this->dataModel); 82 | } 83 | return $this->dataModel; 84 | } 85 | 86 | protected function getEntityAttributeLabel(string $attribute): string 87 | { 88 | if ($entityModel = $this->getEntityModel()) { 89 | return $entityModel->getAttributeLabel($attribute); 90 | } 91 | return $this->generateAttributeLabel($attribute); 92 | } 93 | 94 | protected function getEntityAttributeFormats(): array 95 | { 96 | $entityModel = $this->getEntityModel(); 97 | if (null !== $entityModel && method_exists($entityModel, 'attributeFormats')) { 98 | return $entityModel->attributeFormats(); 99 | } 100 | return []; 101 | } 102 | 103 | /** 104 | * @return string|null|\Closure 105 | */ 106 | protected function getAttributeFormat(string $attribute) 107 | { 108 | $formats = $this->getEntityAttributeFormats(); 109 | return $formats[$attribute] ?? null; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/module/models/DataModel.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/BSD-3-Clause 7 | */ 8 | 9 | namespace lav45\activityLogger\module\models; 10 | 11 | use yii\base\BaseObject; 12 | use yii\di\Instance; 13 | use yii\helpers\Html; 14 | use yii\helpers\ArrayHelper; 15 | use yii\i18n\Formatter; 16 | 17 | class DataModel extends BaseObject 18 | { 19 | /** 20 | * @var array 21 | */ 22 | private $data; 23 | /** 24 | * @var string|\Closure 25 | */ 26 | private $format; 27 | /** 28 | * @var string|array|Formatter 29 | */ 30 | public $formatter = 'formatter'; 31 | 32 | public function init(): void 33 | { 34 | $this->formatter = Instance::ensure($this->formatter, Formatter::class); 35 | } 36 | 37 | /** 38 | * @param array $value 39 | * @return $this 40 | */ 41 | public function setData(array $value) 42 | { 43 | $this->data = $value; 44 | return $this; 45 | } 46 | 47 | /** 48 | * @return array 49 | */ 50 | public function getData() 51 | { 52 | return $this->data; 53 | } 54 | 55 | /** 56 | * @param string|\Closure $value 57 | * @return $this 58 | */ 59 | public function setFormat($value) 60 | { 61 | $this->format = $value; 62 | return $this; 63 | } 64 | 65 | /** 66 | * @return null|string 67 | */ 68 | public function getOldValue() 69 | { 70 | $values = $this->getValue('old'); 71 | return $this->formattedValue($values); 72 | } 73 | 74 | /** 75 | * @return null|string 76 | */ 77 | public function getNewValue() 78 | { 79 | $values = $this->getValue('new'); 80 | return $this->formattedValue($values); 81 | } 82 | 83 | /** 84 | * @param mixed $value 85 | * @return string 86 | */ 87 | protected function formattedValue($value) 88 | { 89 | if ($this->format && is_string($this->format)) { 90 | return $this->formatter->format($value, $this->format); 91 | } 92 | if ($this->format && is_callable($this->format)) { 93 | $value = call_user_func($this->format, $value); 94 | return $value ?? $this->formatter->nullDisplay; 95 | } 96 | if (null === $value) { 97 | return $this->formatter->nullDisplay; 98 | } 99 | if (is_numeric($value)) { 100 | return $value; 101 | } 102 | if (filter_var($value, FILTER_VALIDATE_URL)) { 103 | return Html::a(Html::encode($value), $value, ['target' => '_blank']); 104 | } 105 | if (is_string($value)) { 106 | if (empty($value)) { 107 | return $this->formatter->nullDisplay; 108 | } 109 | return $this->formatter->asNtext($value); 110 | } 111 | if (is_bool($value)) { 112 | return $this->formatter->asBoolean($value); 113 | } 114 | if (is_array($value)) { 115 | $value = json_encode($value, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT); 116 | return Html::tag('pre', $value); 117 | } 118 | return $value; 119 | } 120 | 121 | /** 122 | * @return mixed 123 | */ 124 | protected function getValue(string $tag) 125 | { 126 | return ArrayHelper::getValue($this->data, [$tag, 'value']); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/module/views/default/_item.php: -------------------------------------------------------------------------------- 1 | 14 |

15 | [ 16 | entity_name), Url::current([ 17 | 'entityName' => $model->entity_name, 18 | 'entityId' => null, 19 | 'page' => null 20 | ])) ?> 21 | entity_id): ?> 22 | entity_id), Url::current([ 23 | 'entityName' => $model->entity_name, 24 | 'entityId' => $model->entity_id, 25 | 'page' => null 26 | ])) ?> 27 | 28 | ] 29 | 30 | $model->user_id, 'page' => null]); 32 | $action = Yii::t('lav45/logger', $model->action); 33 | ?> 34 | user_name), $url) . ' ' . Html::encode($action) ?> 35 | 36 | getFormatter()->asDatetime($model->created_at) ?> 37 | 38 | env): ?> 39 | 40 | $model->env, 'page' => null]); ?> 41 | env), $url) ?> 42 | 43 | 44 |

45 | 67 | -------------------------------------------------------------------------------- /src/module/views/default/index.php: -------------------------------------------------------------------------------- 1 | title = Yii::t('lav45/logger', 'Activity log'); 12 | $this->params['breadcrumbs'][] = $this->title; 13 | 14 | $this->registerCss(<< 30 |
31 | 32 |

title) ?>

33 | 34 | 35 | 36 |
37 | 38 |
39 | 40 | $dataProvider, 42 | 'itemView' => '_item', 43 | 'layout' => "{items}\n{pager}", 44 | ]) ?> 45 | 46 | 47 | 48 |
49 | -------------------------------------------------------------------------------- /src/storage/ArrayStorage.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/BSD-3-Clause 7 | */ 8 | 9 | namespace lav45\activityLogger\storage; 10 | 11 | final class ArrayStorage implements StorageInterface 12 | { 13 | /** @var MessageData[] */ 14 | public array $messages = []; 15 | /** @var DeleteCommand[] */ 16 | public array $commands = []; 17 | 18 | public function save(MessageData $message): void 19 | { 20 | $this->messages[] = $message; 21 | } 22 | 23 | public function delete(DeleteCommand $command): void 24 | { 25 | $this->commands[] = $command; 26 | } 27 | 28 | public function __get(string $name) 29 | { 30 | return null; 31 | } 32 | 33 | public function __set(string $name, $value): void 34 | { 35 | } 36 | 37 | public function __isset(string $name) 38 | { 39 | return false; 40 | } 41 | } -------------------------------------------------------------------------------- /src/storage/DbStorage.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/BSD-3-Clause 7 | */ 8 | 9 | namespace lav45\activityLogger\storage; 10 | 11 | use yii\db\Query; 12 | use yii\db\Connection; 13 | use yii\di\Instance; 14 | use yii\base\BaseObject; 15 | 16 | class DbStorage extends BaseObject implements StorageInterface 17 | { 18 | /** @var Connection|string|array */ 19 | public $db = 'db'; 20 | 21 | public string $tableName = '{{%activity_log}}'; 22 | 23 | public function init(): void 24 | { 25 | $this->db = Instance::ensure($this->db, Connection::class); 26 | } 27 | 28 | public function save(MessageData $message): void 29 | { 30 | (new Query) 31 | ->createCommand($this->db) 32 | ->insert($this->tableName, [ 33 | 'entity_name' => $message->entityName, 34 | 'entity_id' => $message->entityId, 35 | 'created_at' => $message->createdAt, 36 | 'user_id' => $message->userId, 37 | 'user_name' => $message->userName, 38 | 'action' => $message->action, 39 | 'env' => $message->env, 40 | 'data' => $this->encode($message->data), 41 | ]) 42 | ->execute(); 43 | } 44 | 45 | /** 46 | * @param array|string $data 47 | */ 48 | private function encode($data): string 49 | { 50 | return json_encode($data, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); 51 | } 52 | 53 | public function delete(DeleteCommand $command): void 54 | { 55 | $condition = array_filter([ 56 | 'entity_name' => $command->entityName, 57 | 'entity_id' => $command->entityId, 58 | 'user_id' => $command->userId, 59 | 'action' => $command->action, 60 | 'env' => $command->env, 61 | ]); 62 | 63 | if ($command->oldThan) { 64 | if (empty($condition)) { 65 | throw new \InvalidArgumentException("Condition can't be empty"); 66 | } 67 | $condition = ['and', $condition, ['<=', 'created_at', $command->oldThan]]; 68 | } 69 | 70 | (new Query) 71 | ->createCommand($this->db) 72 | ->delete($this->tableName, $condition) 73 | ->execute(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/storage/DeleteCommand.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/BSD-3-Clause 7 | */ 8 | 9 | namespace lav45\activityLogger\storage; 10 | 11 | use yii\base\BaseObject; 12 | 13 | final class DeleteCommand extends BaseObject 14 | { 15 | public ?string $entityName = null; 16 | 17 | public ?string $entityId = null; 18 | 19 | public ?string $userId = null; 20 | 21 | public ?string $action = null; 22 | 23 | public ?string $env = null; 24 | 25 | public ?string $oldThan = null; 26 | } -------------------------------------------------------------------------------- /src/storage/MessageData.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/BSD-3-Clause 7 | */ 8 | 9 | namespace lav45\activityLogger\storage; 10 | 11 | final class MessageData 12 | { 13 | /** Alias a name target object */ 14 | public string $entityName; 15 | /** ID target object */ 16 | public ?string $entityId = null; 17 | /** Creation date of the action */ 18 | public int $createdAt; 19 | /** ID user who performed the action */ 20 | public ?string $userId = null; 21 | /** UserName, who performed the action */ 22 | public ?string $userName = null; 23 | /** Action performed on the object */ 24 | public ?string $action = null; 25 | /** Environment, which produced the effect */ 26 | public ?string $env = null; 27 | /** @var array|string|null */ 28 | public $data; 29 | } 30 | -------------------------------------------------------------------------------- /src/storage/StorageInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/BSD-3-Clause 7 | */ 8 | 9 | namespace lav45\activityLogger\storage; 10 | 11 | interface StorageInterface 12 | { 13 | public function save(MessageData $message): void; 14 | 15 | public function delete(DeleteCommand $command): void; 16 | } 17 | --------------------------------------------------------------------------------