docs: rewrite JsonRpcBundle docs, add SDK docs, move them under docs/api
uk/docs/api/json-rpc-bundle/developers/lifecycle.md
новий файл +95
−0
1
+
# Життєвий цикл запиту
2
+
3
+
Як JsonRpcBundle обробляє запит від моменту отримання до відповіді — для кожної точки входу. Знадобиться, щоб писати лістенери [подій](/docs/api/json-rpc-bundle/functionality/events) і [розширення](/docs/api/json-rpc-bundle/developers).
4
+
5
+
## HTTP POST
6
+
7
+
Маршрути бандла оголошені як `GET` (для документації); POST-запити на ту саму адресу перехоплюються на `kernel.request` ще до роутингу й контролера.
8
+
9
+
1. **`kernel.request`, пріоритет 240 — `SymfonyFlowListener::initHttpRequest`**
10
+
* визначає [групу API](/docs/api/json-rpc-bundle/functionality/groups) з маршруту й записує в `ApiGroupContext`;
11
+
* `OPTIONS` (CORS preflight) отримує порожню відповідь;
12
+
* для POST створює carrier `RpcFromHttp` (розбір JSON; тіло, що починається з `[`, — batch), токен-холдер `HttpTokenHolder` і перевіряє токен (якщо `protected_api: true`) — валідатором групи або базовим.
13
+
2. **`kernel.request`, пріоритет 230 — `RpcRequestHandler::handle()`**
14
+
* **batch** — перевірка `batch.enabled` і порожнього batch, вибір стратегії (`IBatchStrategy`) і виконання елементів;
15
+
* **одиночний запит** — перевірка `jsonrpc: "2.0"`, подія **`rpc.request`**, далі одна з гілок:
16
+
* запит з `$rpc.callback` — миттєва відповідь-заглушка й **`rpc.pre_response`**, виконання відкладається;
17
+
* notification — заглушка й **`rpc.pre_response`**, виконання відкладається;
18
+
* звичайний запит — `RpcServer::handle()`.
19
+
3. **`RpcServer::handle()`**
20
+
* пошук методу в карті сервісів (з урахуванням групи й версії); невідомий метод — помилка `-32601`;
21
+
* **кеш**: для методів з `#[RPC\Cache]` (і без `$rpc.ignoreCache`) готова відповідь повертається одразу, без подій виконання;
22
+
* подія **`rpc.pre_execute`**, на якій послідовно: формування аргументів (1002), валідація (1001), конвертація (1000), `#[RPC\Lock]` (-100), **виклик методу** (-1000);
23
+
* успіх — **`rpc.pre_response`**, потім **`rpc.post_execute`**; помилка — **`rpc.error`**, потім **`rpc.pre_response`**;
24
+
* збереження відповіді в кеш (для `#[RPC\Cache]`).
25
+
4. **Відповідь** — JSON; `204` без тіла, якщо відповідати нічого (notification, batch лише з notifications). Роутер і контролер Symfony для POST не запускаються.
26
+
5. **`kernel.terminate`** — після `fastcgi_finish_request()` (PHP-FPM) для кожного успішно виконаного запиту генерується **`rpc.post_response`**: інвалідація `#[RPC\CacheRelation]` і виконання notifications / callback. Потім `ApiGroupContext` скидається.
27
+
28
+
Помилки, що виникли до виконання методу (недійсний JSON, токен, невідомий метод тощо), перетворює на відповідь `SymfonyErrorListener` на `kernel.exception`. HTTP-статус: 401 — токен не передано, 403 — недійсний, 500 — недійсний JSON, інакше 200.
29
+
30
+
## HTTP GET (документація)
31
+
32
+
1. Пріоритет 240 — визначення групи; якщо `protected_doc: true` — перевірка токена.
33
+
2. Пріоритет 8 — вибір мови з `Accept-Language` (`RpcLocaleNegotiator`).
34
+
3. Пріоритет 4 — поза debug-режимом відповідь з кешу: `304` для `If-None-Match` або збережений документ.
35
+
4. `ApiController::openrpc()` / `postman()` будує документ для версії та групи.
36
+
5. `kernel.response` — `Content-Language`, `Vary: Accept-Language`, `ETag`, збереження в кеш (лише для `200`).
37
+
38
+
RPC-події на цьому шляху не генеруються.
39
+
40
+
## Консоль: `ufo:rpc:process`
41
+
42
+
Команда не має власної логіки — запит обробляє лістенер `console.command` (пріоритет 999999):
43
+
44
+
1. читання JSON (аргумент, stdin `-` або змінна `UFO_RPC_REQUEST`);
45
+
2. група з `--api-group`, версія з `--ver`;
46
+
3. carrier `RpcFromCli`, токен з опції `--<token_name>` або змінної `UFO_RPC_TOKEN`;
47
+
4. `RpcRequestHandler::handle()` — далі як у HTTP;
48
+
5. вивід JSON у stdout;
49
+
6. `console.terminate` — `rpc.post_response`.
50
+
51
+
Помилки обробляються на `console.error` і проходять через `rpc.error`.
52
+
53
+
## Symfony Messenger
54
+
55
+
`RpcAsyncProcessor` — обробник повідомлень `RpcAsyncRequest`:
56
+
57
+
1. група з `$message->apiGroup`, токен з `$message->token`, перевірка токена;
58
+
2. виклик `RpcServer::handle()` напряму.
59
+
60
+
Тож для запитів з черги **немає** `rpc.request` і `rpc.post_response`; `rpc.pre_execute`, `rpc.pre_response`, `rpc.post_execute` і `rpc.error` генеруються. Помилки токена й невідомого методу стають помилками обробки повідомлення Messenger (повторні спроби, failure transport).
61
+
62
+
## Batch
63
+
64
+
* **Процесний режим** (`batch.process: true`) — кожен готовий елемент запускається в дочірньому процесі `ufo:rpc:process -` (запит через stdin, токен через змінну оточення, група й версія — опціями), не більше `batch.process_count` одночасно. Кожен дочірній процес — повний консольний цикл зі своїми подіями. Батьківський процес на виводі кожного генерує **`rpc.async_output`**; коли розв'язується залежність `@FROM`, запускає наступні елементи.
65
+
* **Inline-режим** (`batch.process: false`) — елементи по черзі в поточному процесі через `RpcExecutor`, кожен з власним пулом подій; `rpc.post_response` — одразу після елемента.
66
+
* Елементи, залежність яких не виконалась, завершуються помилкою в батьківському процесі.
67
+
68
+
## Notifications і `$rpc.callback`
69
+
70
+
1. При прийомі — `rpc.request` і `rpc.pre_response` із заглушкою; клієнт отримує `204` (notification) або `{"async": true, "callback": …}`.
71
+
2. На `rpc.post_response` (пріоритет 10) `AsyncServerEventListener` виконує метод — повний цикл `rpc.pre_execute` → `rpc.pre_response` → `rpc.post_execute` / `rpc.error`.
72
+
3. Для callback результат надсилається POST-запитом на вказаний URL.
73
+
74
+
З `notification.process: true` одиночний HTTP-запит передається від'єднаному процесу `ufo:rpc:process @env`: воркер чекає підтвердження до 3 секунд, інакше виконує запит сам. Кількість від'єднаних процесів обмежує `notification.process_count`.
75
+
76
+
## `RpcExecutor` (адаптери)
77
+
78
+
`Ufo\JsonRpcBundle\Server\RpcExecutor::execute(IRpcRequestCarrier $carrier): RpcResponse` — точка входу для розширень, що приймають запити не через маршрут бандла (REST, MCP):
79
+
80
+
* встановлює carrier, генерує `rpc.request` і далі як в одиночному запиті;
81
+
* будь-який виняток проходить через `rpc.error` і повертається як відповідь з помилкою;
82
+
* `rpc.post_response` — на `kernel.terminate` основного HTTP-запиту;
83
+
* **токен не перевіряє** — це робить адаптер.
84
+
85
+
## Де що зберігається під час запиту
86
+
87
+
| Сервіс | Що містить |
88
+
| --- | --- |
89
+
| `RequestCarrier` | Поточний carrier (`RpcFromHttp`, `RpcFromCli`, carrier адаптера) і об'єкти запиту |
90
+
| `ApiGroupContext` | Група API поточного запиту |
91
+
| `IRpcSecurity` | Токен-холдер поточного запиту |
92
+
| `RpcEventFactory` | Усі події, згенеровані для запиту (`getEvent()`, `getEvents()`) |
93
+
| `PostResponseQueue` | Запити, для яких буде згенеровано `rpc.post_response` |
94
+
95
+
У довгоживучих воркерах ці сервіси скидаються між запитами (`ResetInterface`).