Skip to content

Commit d1b732e

Browse files
authored
Merge pull request #16655 from craftcms/feature/cms-1362-track-element-events-with-bulkopevent
Deferred event handling for bulk ops
2 parents a9fc4a4 + 897cc7a commit d1b732e

File tree

8 files changed

+169
-6
lines changed

8 files changed

+169
-6
lines changed

CHANGELOG-WIP.md

+3
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,14 @@
1717
- Editable tables now support `icon` columns.
1818
- Added `craft\base\ElementInterface::getSerializedFieldValuesForDb()`.
1919
- Added `craft\base\FieldInterface::serializeValueForDb()`.
20+
- Added `craft\db\Table::BULKOPEVENTS`.
2021
- Added `craft\db\Table::SEARCHINDEXQUEUE_FIELDS`.
2122
- Added `craft\db\Table::SEARCHINDEXQUEUE`.
23+
- Added `craft\events\BulkOpEvent::defer()`. ([#16655](https://github.com/craftcms/cms/pull/16655))
2224
- Added `craft\fields\BaseOptionsField::$optionColors`, which can be set to `true` by subclasses to enable the “Color” setting for field options. ([#16645](https://github.com/craftcms/cms/pull/16645))
2325
- Added `craft\fields\BaseOptionsField::$optionIcons`, which can be set to `true` by subclasses to enable the “Icon” setting for field options. ([#16645](https://github.com/craftcms/cms/pull/16645))
2426
- Added `craft\fields\data\ColorData::$label`. ([#16492](https://github.com/craftcms/cms/pull/16492))
27+
- Added `craft\services\Elements::getBulkOpKeys()`.
2528
- Added `craft\services\Search::indexElementIfQueued()`.
2629
- Added `craft\services\Search::queueIndexElement()`.
2730
- Added `Craft.ui.createIconPicker()`.

src/config/app.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
'id' => 'CraftCMS',
55
'name' => 'Craft CMS',
66
'version' => '5.6.5.1',
7-
'schemaVersion' => '5.7.0.0',
7+
'schemaVersion' => '5.7.0.1',
88
'minVersionRequired' => '4.5.0',
99
'basePath' => dirname(__DIR__), // Defines the @app alias
1010
'runtimePath' => '@storage/runtime', // Defines the @runtime alias

src/db/Table.php

+2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ abstract class Table
2626
public const ASSETS_SITES = '{{%assets_sites}}';
2727
/** @since 5.0 */
2828
public const AUTHENTICATOR = '{{%authenticator}}';
29+
/** @since 5.7.0 */
30+
public const BULKOPEVENTS = '{{%bulkopevents}}';
2931
/** @since 3.4.14 */
3032
public const CACHE = '{{%cache}}';
3133
public const CATEGORIES = '{{%categories}}';

src/events/BulkOpEvent.php

+98
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@
77

88
namespace craft\events;
99

10+
use Craft;
11+
use craft\db\Query;
12+
use craft\db\Table;
13+
use craft\helpers\ArrayHelper;
14+
use craft\helpers\DateTimeHelper;
15+
use craft\helpers\Db;
16+
use craft\services\Elements;
17+
use yii\base\Application;
18+
use yii\base\Event;
19+
1020
/**
1121
* Bulk operation event class.
1222
*
@@ -15,6 +25,94 @@
1525
*/
1626
class BulkOpEvent extends ElementQueryEvent
1727
{
28+
private static array $handlers;
29+
private static array $triggers;
30+
31+
/**
32+
* Listens to a class-level event, but defers calling the handler until after a bulk operation
33+
* is completed, and only if the event was triggered during the bulk operation.
34+
*
35+
* ```php
36+
* BulkOpEvent::deferredOn(ActiveRecord::class, ActiveRecord::EVENT_AFTER_INSERT, function ($event) {
37+
* Yii::trace(get_class($event->sender) . ' is inserted.');
38+
* });
39+
* ```
40+
*
41+
* @param string $class the fully qualified class name to which the event handler needs to attach.
42+
* @param string $name the event name.
43+
* @param callable $handler the event handler.
44+
* @param mixed $data the data to be passed to the event handler when the event is triggered.
45+
* When the event handler is invoked, this data can be accessed via [[\yii\base\Event::data]].
46+
* @since 5.7.0
47+
*/
48+
public static function defer(
49+
string $class,
50+
string $name,
51+
callable $handler,
52+
mixed $data = null,
53+
): void {
54+
if (!isset(self::$handlers)) {
55+
self::$handlers = [];
56+
self::$triggers = [];
57+
58+
Event::on(Elements::class, Elements::EVENT_AFTER_BULK_OP, function(self $event) {
59+
$triggers = ArrayHelper::remove(self::$triggers, $event->key, []);
60+
$db = Craft::$app->getElements()->bulkOpDb;
61+
62+
// see if any events were fired for the same bulk op key from previous requests
63+
$storedTriggers = (new Query())
64+
->select(['senderClass', 'eventName'])
65+
->from(Table::BULKOPEVENTS)
66+
->where(['key' => $event->key])
67+
->all($db);
68+
if (!empty($storedTriggers)) {
69+
Db::delete(Table::BULKOPEVENTS, ['key' => $event->key], db: $db);
70+
foreach ($storedTriggers as $trigger) {
71+
$triggers[$trigger['senderClass']][$trigger['eventName']] = true;
72+
}
73+
}
74+
75+
foreach ($triggers as $class => $eventNames) {
76+
foreach (array_keys($eventNames) as $eventName) {
77+
$handlers = self::$handlers[$class][$eventName] ?? [];
78+
foreach ($handlers as [$handler, $data]) {
79+
$event->data = $data;
80+
call_user_func($handler, $event);
81+
}
82+
}
83+
}
84+
}, append: false);
85+
86+
Craft::$app->on(Application::EVENT_AFTER_REQUEST, function() {
87+
// keep track of any event triggers that haven’t been handled yet
88+
if (!empty(self::$triggers)) {
89+
$timestamp = Db::prepareDateForDb(DateTimeHelper::now());
90+
$db = Craft::$app->getElements()->bulkOpDb;
91+
foreach (self::$triggers as $key => $triggers) {
92+
foreach ($triggers as $class => $eventNames) {
93+
foreach (array_keys($eventNames) as $eventName) {
94+
Db::upsert(Table::BULKOPEVENTS, [
95+
'key' => $key,
96+
'senderClass' => $class,
97+
'eventName' => $eventName,
98+
'timestamp' => $timestamp,
99+
], db: $db);
100+
}
101+
}
102+
}
103+
}
104+
}, append: false);
105+
}
106+
107+
self::$handlers[$class][$name][] = [$handler, $data];
108+
109+
static::on($class, $name, function() use ($class, $name) {
110+
foreach (Craft::$app->getElements()->getBulkOpKeys() as $key) {
111+
self::$triggers[$key][$class][$name] = true;
112+
}
113+
}, append: false);
114+
}
115+
18116
/**
19117
* @var string The bulk operation key.
20118
*/

src/migrations/Install.php

+8
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,13 @@ public function createTables(): void
237237
'dateCreated' => $this->dateTime()->notNull(),
238238
'dateUpdated' => $this->dateTime()->notNull(),
239239
]);
240+
$this->createTable(Table::BULKOPEVENTS, [
241+
'key' => $this->char(10)->notNull(),
242+
'senderClass' => $this->string()->notNull(),
243+
'eventName' => $this->string()->notNull(),
244+
'timestamp' => $this->dateTime()->notNull(),
245+
'PRIMARY KEY([[key]], [[senderClass]], [[eventName]])',
246+
]);
240247
$this->createTable(Table::CATEGORIES, [
241248
'id' => $this->integer()->notNull(),
242249
'groupId' => $this->integer()->notNull(),
@@ -853,6 +860,7 @@ public function createIndexes(): void
853860
$this->createIndex(null, Table::ASSETS, ['filename', 'folderId'], false);
854861
$this->createIndex(null, Table::ASSETS, ['folderId'], false);
855862
$this->createIndex(null, Table::ASSETS, ['volumeId'], false);
863+
$this->createIndex(null, Table::BULKOPEVENTS, ['timestamp'], false);
856864
$this->createIndex(null, Table::CATEGORIES, ['groupId'], false);
857865
$this->createIndex(null, Table::CATEGORYGROUPS, ['name'], false);
858866
$this->createIndex(null, Table::CATEGORYGROUPS, ['handle'], false);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
namespace craft\migrations;
4+
5+
use craft\db\Migration;
6+
use craft\db\Table;
7+
8+
/**
9+
* m250207_172349_bulkop_events migration.
10+
*/
11+
class m250207_172349_bulkop_events extends Migration
12+
{
13+
/**
14+
* @inheritdoc
15+
*/
16+
public function safeUp(): bool
17+
{
18+
$this->safeDown();
19+
$this->createTable(Table::BULKOPEVENTS, [
20+
'key' => $this->char(10)->notNull(),
21+
'senderClass' => $this->string()->notNull(),
22+
'eventName' => $this->string()->notNull(),
23+
'timestamp' => $this->dateTime()->notNull(),
24+
'PRIMARY KEY([[key]], [[senderClass]], [[eventName]])',
25+
]);
26+
$this->createIndex(null, Table::BULKOPEVENTS, ['timestamp'], false);
27+
return true;
28+
}
29+
30+
/**
31+
* @inheritdoc
32+
*/
33+
public function safeDown(): bool
34+
{
35+
$this->dropTableIfExists(Table::BULKOPEVENTS);
36+
return true;
37+
}
38+
}

src/services/Elements.php

+11
Original file line numberDiff line numberDiff line change
@@ -1110,6 +1110,17 @@ public function getEnabledSiteIdsForElement(int $elementId): array
11101110

11111111
private array $bulkKeys = [];
11121112

1113+
/**
1114+
* Returns the active bulk op keys.
1115+
*
1116+
* @return string[]
1117+
* @since 5.7.0
1118+
*/
1119+
public function getBulkOpKeys(): array
1120+
{
1121+
return array_keys($this->bulkKeys);
1122+
}
1123+
11131124
/**
11141125
* Begins tracking element saves and deletes as part of a bulk operation, identified by a unique key.
11151126
*

src/services/Gc.php

+8-5
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ public function run(bool $force = false): void
111111
$this->_deleteStaleSessions();
112112
$this->_deleteStaleAnnouncements();
113113
$this->_deleteStaleElementActivity();
114-
$this->_deleteStaleBulkElementOps();
114+
$this->_deleteStaleBulkOpData();
115115

116116
// elements should always go first
117117
$this->hardDeleteElements();
@@ -434,12 +434,15 @@ private function _deleteStaleElementActivity(): void
434434
}
435435

436436
/**
437-
* Deletes any stale bulk element operation records.
437+
* Deletes any stale bulk operation data.
438438
*/
439-
private function _deleteStaleBulkElementOps(): void
439+
private function _deleteStaleBulkOpData(): void
440440
{
441-
$this->_stdout(' > deleting stale bulk element operation records ... ');
442-
Db::delete(Table::ELEMENTS_BULKOPS, ['<', 'timestamp', Db::prepareDateForDb(new DateTime('2 weeks ago'))]);
441+
$this->_stdout(' > deleting stale bulk operation data ... ');
442+
$condition = ['<', 'timestamp', Db::prepareDateForDb(new DateTime('2 weeks ago'))];
443+
foreach ([Table::BULKOPEVENTS, Table::ELEMENTS_BULKOPS] as $table) {
444+
Db::delete($table, $condition);
445+
}
443446
$this->_stdout("done\n", Console::FG_GREEN);
444447
}
445448

0 commit comments

Comments
 (0)