Skip to content

Commit e8bcb5b

Browse files
Added batch events creation
1 parent df25609 commit e8bcb5b

File tree

12 files changed

+322
-41
lines changed

12 files changed

+322
-41
lines changed

docs/api.md

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
### Create Event
66

7-
**POST** `/events`
7+
**POST** `/event`
88

99
Create a new user event.
1010

@@ -37,6 +37,51 @@ Create a new user event.
3737
}
3838
```
3939

40+
### Batch Create Events
41+
42+
**POST** `/events`
43+
44+
Create multiple events.
45+
46+
**Request Body:**
47+
48+
```json
49+
[
50+
{
51+
"user_id": 8,
52+
"event_type": "page_view",
53+
"timestamp": "2025-06-08T10:30:00Z",
54+
"metadata": {
55+
"page": "/dashboard",
56+
"referrer": "https://google.com"
57+
}
58+
},
59+
{
60+
"user_id": 9,
61+
"event_type": "page_view",
62+
"timestamp": "2025-06-08T10:30:00Z",
63+
"metadata": {
64+
"page": "/main",
65+
"referrer": "https://ya.ru"
66+
}
67+
}
68+
]
69+
```
70+
71+
**Response (201):**
72+
73+
```json
74+
{
75+
"data": "queued"
76+
}
77+
```
78+
79+
```json
80+
{
81+
"data": "failed to queue"
82+
}
83+
```
84+
4085
### List Events
4186

4287
**GET** `/events?page=1&limit=100`
@@ -71,6 +116,35 @@ Get paginated list of events sorted by timestamp.
71116
}
72117
```
73118

119+
### Total Events Count
120+
121+
**GET** `/events/total`
122+
123+
Get total count of events.
124+
125+
**Response (200):**
126+
127+
```json
128+
{
129+
"data": [
130+
{
131+
"id": 1,
132+
"user_id": "123",
133+
"type": "click",
134+
"timestamp": "2025-05-28T12:34:56+00:00",
135+
"metadata": {
136+
"page": "/home"
137+
}
138+
}
139+
],
140+
"query": {
141+
"page": 1,
142+
"limit": 100,
143+
"total": 1000
144+
}
145+
}
146+
```
147+
74148
### Delete Events
75149

76150
**DELETE** `/events?before=2025-01-01T00:00:00Z`
@@ -186,6 +260,20 @@ Get aggregated statistics for events.
186260
}
187261
```
188262

263+
### Clear cache
264+
265+
**GET** `/clear_cache`
266+
267+
Clears caches
268+
269+
**Response (200):**
270+
271+
```json
272+
{
273+
"data": "Ok"
274+
}
275+
```
276+
189277
## Error Responses
190278

191279
All endpoints may return error responses in the following format:

src/Application/Bootloader/Domain/UserAnalyticsBootloader.php

Lines changed: 0 additions & 25 deletions
This file was deleted.

src/Application/Bootloader/TasksBootloader.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44

55
namespace App\Application\Bootloader;
66

7+
use App\Domain\UserAnalytics\Task\BatchCreateEventsTask;
78
use App\Domain\UserAnalytics\Task\DeleteOldEventsTask;
89
use Spiral\Boot\Bootloader\Bootloader;
910

1011
final class TasksBootloader extends Bootloader
1112
{
1213
protected const array SINGLETONS = [
1314
DeleteOldEventsTask::class => DeleteOldEventsTask::class,
15+
BatchCreateEventsTask::class => BatchCreateEventsTask::class,
1416
];
1517
}

src/Domain/UserAnalytics/Repository/EventRepository.php

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44

55
namespace App\Domain\UserAnalytics\Repository;
66

7+
use App\Domain\UserAnalytics\ValueObject\CreateEventsRequest;
78
use Cycle\Database\DatabaseInterface;
89
use Cycle\Database\StatementInterface;
910
use Cycle\ORM\RepositoryInterface;
1011
use DateTimeInterface;
12+
use DomainException;
13+
use PDO;
1114

1215
readonly class EventRepository implements RepositoryInterface
1316
{
@@ -133,7 +136,7 @@ public function getStats(
133136
public function countAll(): int
134137
{
135138
$result = $this->database
136-
->query('SELECT COUNT(*) as total FROM events')
139+
->query('SELECT COUNT(id) as total FROM events')
137140
->fetch();
138141

139142
return (int)($result['total'] ?? 0);
@@ -225,4 +228,44 @@ public function findAll(array $scope = []): iterable
225228

226229
return $this->database->query($sql, $parameters)->fetchAll();
227230
}
231+
232+
public function insertBatch(CreateEventsRequest $events): int
233+
{
234+
$eventTypeIdMap = $this->database
235+
->select(['name', 'id'])
236+
->from('event_types')
237+
->fetchAll(PDO::FETCH_KEY_PAIR);
238+
239+
$userIds = $this->database
240+
->select(['id'])
241+
->from('users')
242+
->fetchAll(PDO::FETCH_COLUMN);
243+
244+
$placeholders = [];
245+
$values = [];
246+
247+
foreach ($events->events as $event) {
248+
if (!isset($userIds[$event->userId])) {
249+
throw new DomainException("Unknown user_id: {$event->userId}");
250+
}
251+
if (!isset($eventTypeIdMap[$event->eventType])) {
252+
throw new DomainException("Unknown event_type: {$event->eventType}");
253+
}
254+
255+
$placeholders[] = "(?, ?, ?, ?)";
256+
array_push(
257+
$values,
258+
$event->userId,
259+
$eventTypeIdMap[$event->eventType],
260+
$event->timestamp->format('Y-m-d H:i:s'),
261+
json_encode($event->metadata, JSON_THROW_ON_ERROR)
262+
);
263+
}
264+
265+
$sql = 'INSERT INTO events (user_id, type_id, timestamp, metadata) VALUES ' . implode(',', $placeholders);
266+
$this->database->getDriver()->query($sql, $values);
267+
unset($sql, $values, $placeholders, $eventTypeIdMap);
268+
269+
return count($events->events);
270+
}
228271
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Domain\UserAnalytics\Task;
6+
7+
use App\Domain\UserAnalytics\Repository\CachedEventRepository;
8+
use App\Domain\UserAnalytics\Repository\EventRepository;
9+
use App\Domain\UserAnalytics\ValueObject\CreateEventsRequest;
10+
11+
readonly class BatchCreateEventsTask implements TaskInterface
12+
{
13+
public function __construct(
14+
private CachedEventRepository $cachedEventRepository,
15+
private EventRepository $eventRepository
16+
) {
17+
}
18+
19+
public function run(array $data = []): void
20+
{
21+
$events = $data['events'] ?? null;
22+
if (!$events instanceof CreateEventsRequest) {
23+
return;
24+
}
25+
26+
$created = $this->eventRepository->insertBatch($events);
27+
28+
$this->cachedEventRepository->invalidateCache();
29+
30+
echo "[Task] Create $created events\n";
31+
}
32+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Domain\UserAnalytics\UseCase\Event;
6+
7+
use App\Domain\UserAnalytics\Task\BatchCreateEventsTask;
8+
use App\Domain\UserAnalytics\ValueObject\CreateEventsRequest;
9+
use App\Infra\Swoole\Server;
10+
11+
final readonly class CreateEventsUseCase
12+
{
13+
public function __construct(
14+
private Server $server,
15+
) {
16+
}
17+
18+
public function execute(CreateEventsRequest $request): bool
19+
{
20+
$this->server->dispatchTask([
21+
'taskClass' => BatchCreateEventsTask::class,
22+
'events' => $request,
23+
]);
24+
25+
return true;
26+
}
27+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Domain\UserAnalytics\UseCase\Event;
6+
7+
use App\Domain\UserAnalytics\Repository\CachedEventRepository;
8+
use App\Domain\UserAnalytics\ValueObject\GetEventsResponse;
9+
10+
final readonly class EventsTotalCountUseCase
11+
{
12+
public function __construct(
13+
private CachedEventRepository $eventRepository
14+
) {
15+
}
16+
17+
public function execute(): GetEventsResponse
18+
{
19+
$total = $this->eventRepository->countAll();
20+
21+
return new GetEventsResponse(
22+
events: [],
23+
page: 1,
24+
limit: 1,
25+
total: $total
26+
);
27+
}
28+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Domain\UserAnalytics\ValueObject;
6+
7+
use DateTimeImmutable;
8+
9+
final readonly class CreateEventsRequest
10+
{
11+
/**
12+
* @var CreateEventRequest[]
13+
*/
14+
public array $events;
15+
16+
public function __construct(array $events)
17+
{
18+
$_events = [];
19+
foreach ($events as $event) {
20+
$_events[] = new CreateEventRequest(
21+
userId: $event['user_id'],
22+
eventType: $event['event_type'],
23+
timestamp: new DateTimeImmutable($event['timestamp']),
24+
metadata: $event['metadata'],
25+
);
26+
}
27+
$this->events = $_events;
28+
}
29+
}

0 commit comments

Comments
 (0)