├── 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 |
6 |
7 | |
8 |
9 | Это расширение поможет вам отслеживать пользовательскую активность на сайте.
10 | Когда в админке над контентом работает больше двух человек, не всегда понятно кто, когда и зачем сделал изменения в описание статьи, убрал статью из публикации, добавил непонятного пользователя, удалил организацию.
11 | Для того чтобы была возможность поблагодарить автора за усердную работу и был разработан этот модуль.
12 | |
13 |
14 |
15 |
16 |
17 | [](https://packagist.org/packages/lav45/yii2-activity-logger)
18 | [](https://github.com/lav45/yii2-activity-logger/actions)
19 | [](https://scrutinizer-ci.com/g/lav45/yii2-activity-logger/)
20 | [](https://scrutinizer-ci.com/g/lav45/yii2-activity-logger/)
21 | [](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 | 
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 | = Html::a(Html::encode($model->entity_name), Url::current([
17 | 'entityName' => $model->entity_name,
18 | 'entityId' => null,
19 | 'page' => null
20 | ])) ?>
21 | entity_id): ?>
22 | = ':' . Html::a(Html::encode($model->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 | = Html::a(Html::encode($model->user_name), $url) . ' ' . Html::encode($action) ?>
35 |
36 | = Yii::$app->getFormatter()->asDatetime($model->created_at) ?>
37 |
38 | env): ?>
39 |
40 | $model->env, 'page' => null]); ?>
41 | = Html::a(Html::encode($model->env), $url) ?>
42 |
43 |
44 |
45 |
46 | getData() as $attribute => $values): ?>
47 |
48 | -
49 |
50 | = Html::encode($attribute) ?>
51 |
52 | = Html::encode(Yii::t('lav45/logger', $values)) ?>
53 |
54 |
55 | -
56 | = Yii::t('lav45/logger', '{attribute} has been changed', ['attribute' => Html::encode($attribute)]) ?>
57 |
58 | = Html::encode(Yii::t('lav45/logger', 'from')) ?>
59 | = $values->getOldValue() ?>
60 |
61 | = Html::encode(Yii::t('lav45/logger', 'to')) ?>
62 | = $values->getNewValue() ?>
63 |
64 |
65 |
66 |
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 |
= Html::encode($this->title) ?>
33 |
34 |
35 |
36 |
37 | = Html::a(Yii::t('lav45/logger', 'Reset'), ['index']) ?>
38 |
39 |
40 | = ListView::widget([
41 | 'dataProvider' => $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 |
--------------------------------------------------------------------------------