Глава 39 · После массивов и функций
От функции listMethod к клиенту B24: что даёт ООП на одной интеграции
Получаем сделки одной строкой, подключаем второй портал и сравниваем честную процедурную версию с объектной. Затем — изменение отчёта без копирования и разнесение файлов.
Как пройти сравнение: сначала одна строка, затем два клиента
Нужно показать менеджеру сделку вместе с названием портала. Прежде чем сравнивать полноценные клиенты, проверьте на маленьком результате одну разницу: функция получает настройку при вызове, объект может получить её при создании и помнить. Сеть, JSON, токены и обработку ошибок подключим после этого наблюдения.
Для этого маршрута нужны чтение поля массива, параметры и return и явное свойство, new и метод. Если стрелка $report->line ещё незнакома, выполните два маленьких примера класса по последней ссылке и вернитесь. Здесь мы применяем эти конструкции к одной задаче, а не вводим их одновременно с HTTP-клиентом.
- Данные сделки: TITLE = Учебная поставка
- Путь функции: north + массив → portalLine → строка
- Путь объекта: north → конструктор; затем массив → line → та же portalLine
- В обоих случаях вывод: [north] Учебная поставка
Сохраните следующий пример целиком в first-portal-report.php и из его каталога запустите php first-portal-report.php. Composer, архив интеграции и доступ к Bitrix24 для него не нужны. Не объединяйте этот код со всеми программами главы в один файл: самостоятельные примеры могут повторно объявлять одни и те же имена.
| Встретившаяся запись | Значение именно в этом опыте |
|---|---|
| portalLine('north', $deal) | Два аргумента — настройка портала и массив сделки — попадут в параметры функции. |
| array $deal и : string | Функция принимает массив и возвращает строку; формат его поля TITLE здесь задан самой программой. |
| new PortalReport('north') | Конструктор один раз сохранит настройку в private-свойстве; отчёт пока не напечатан. |
| $report->line($deal) | Метод берёт сохранённый портал и передаёт его той же функции вместе с текущей сделкой. |
<?php
declare(strict_types=1);
function portalLine(string $portal, array $deal): string
{
return '[' . $portal . '] ' . $deal['TITLE'];
}
class PortalReport
{
private string $portal;
public function __construct(string $portal)
{
$this->portal = $portal;
}
public function line(array $deal): string
{
return portalLine($this->portal, $deal);
}
}
$deal = ['TITLE' => 'Учебная поставка'];
echo portalLine('north', $deal), "\n";
$report = new PortalReport('north');
echo $report->line($deal), "\n";
Ожидаемый результат
[north] Учебная поставка
[north] Учебная поставка
Две строки совпали: способ организации не изменил отчёт. Вызов функции передал north прямо сейчас; конструктор объекта запомнил north для следующих line. Сам алгоритм строки остаётся одним — portalLine. Здесь доверенный массив с одним известным полем, а не проверка произвольного ответа CRM; её добавят следующие самостоятельные примеры.
Теперь измените только первый аргумент функции на south и снова запустите программу: первая строка станет [south], вторая останется [north], потому что настройка уже передана объекту отдельно. Затем измените аргумент new — и строки снова совпадут. Так видно, где именно выбирается портал; это не тест скорости и не доказательство превосходства одного подхода.
Дальше идите по главе в четыре прохода. Сначала прочитайте два коротких вызова listMethod. Затем сохраните полную процедурную программу отдельно и найдите внизу выбор north/south; помощники вверху пока обеспечивают одинаковую проверку данных. В объектной программе найдите new B24 и вызовы на каждом экземпляре. Только после совпадения результатов переходите к разным форматам отчёта, namespace и Composer.
Три больших самостоятельных опыта — это исходники прямо в разделах, а дерево файлов позже показывает возможное разнесение. Готовый архив, который действительно можно распаковать и запустить, находится в пошаговом старте лаборатории интеграции. Полноценный HTTP-транспорт, правила JSON и пагинация появятся там; выбор функций или объектов не отменяет ни одну из этих проверок.
Сначала результат: получить сделки одной строкой
Два портала: настройки в аргументах или в объектах
Оба подхода должны получить N-101 с северного портала и S-201 с южного, не копируя алгоритм запроса.
- Процедурно: зависимости в вызове
Одна listMethod обслуживает оба портала.
- Настройки north / south снаружи
Вызывающий код передаёт нужный набор вместе с параметрами, методом и транспортом.
- Общий requestList и decodeDeals
Алгоритм не копируется; транспорт также передаётся явно и заменяется в тесте.
- Два проверенных результата
north → N-101, south → S-201. Нужно не перепутать настройки в месте вызова.
- ООП: зависимости внутри клиента
Создаются два B24 с разными проверенными настройками.
- $north и $south — разные объекты
Каждый запоминает свой portal, timeout и переданный транспорт.
- Те же requestList и decodeDeals
listMethod получает параметры и имя метода; настройки берёт из выбранного экземпляра.
- Те же N-101 и S-201
Удобство — в настроенном получателе вызова. Сам алгоритм от появления class не улучшился.
Транспорт сравнения возвращает одну локальную страницу; полный обход показан в проекте интеграции. Composer поддерживает и файлы функций, и загрузку классов: autoload не создаёт клиенты и не загружает заранее все классы.
$deals = listMethod($portalSettings, [], "crm.deal.list", $transport);
$deals = $b24->listMethod([], "crm.deal.list");
Это короткие иллюстрации вызова; полные самостоятельные программы идут ниже. Здесь listMethod возвращает одну локальную страницу сделок. В лаборатории интеграции этот же интерфейс развивается до сбора всех страниц с проверками и ограничением бюджета. Оба вызова нужны ради одного результата: получить массив сделок и дальше показать, проверить или обработать его. Например, у северного учебного портала будет N-101 — Учебная поставка, у южного — S-201 — Учебная подписка. Сначала посмотрим этот результат, затем разберём, где живут настройки и общий код.
В первом вызове параметры подключения передаются функции. Во втором они уже сохранены в настроенном клиенте. Короткий метод не отправляет запрос магически: внутри остаются получение ответа, разбор JSON и проверка договора. Функциональная версия тоже может иметь короткий вызов, если вы заранее выбрали удобную оболочку.
Три программы ниже запускаются по отдельности в CLI и работают с локальными JSON-фикстурами. Имена B24 и listMethod помогают узнать задачу, но это наша маленькая учебная реализация, не готовый официальный SDK. Запросов, токенов и реальных CRM-данных в ней нет. Для опыта с настоящим локальным HTTP, ошибками и паузами перейдите в лабораторию интеграции.
Один портал стал двумя: ошибка копирования и нормальное процедурное решение
Первый скрипт читал сделки north. Для south скопировали обработчик и забыли заменить зашитое имя портала: отчёт выводится успешно, но из чужого набора данных. Это правдоподобная ошибка роста, однако она не доказывает, что процедуры плохи. Исправление — вынести общую работу в функцию, а разные настройки сделать явными аргументами.
В начале программы находятся короткие помощники проверки и разбора. Главный опыт — внизу: неверная копия возвращает N-101, после исправления два вызова одной функции возвращают N-101 и S-201. transport означает «как получить ответ»; здесь это обычная функция, возвращающая заранее заданный JSON.
<?php
declare(strict_types=1);
function validatePortalSettings(array $settings): array
{
$portal = $settings['portal'] ?? null;
$timeout = $settings['timeoutSeconds'] ?? null;
if (!is_string($portal) || !in_array($portal, ['north', 'south'], true)) {
throw new InvalidArgumentException('Неизвестный учебный портал');
}
if (!is_int($timeout) || $timeout < 1 || $timeout > 10) {
throw new InvalidArgumentException('Таймаут от 1 до 10 секунд');
}
return ['portal' => $portal, 'timeoutSeconds' => $timeout];
}
function decodeDeals(string $json): array
{
if (strlen($json) > 10000) {
throw new LengthException('Учебный ответ слишком большой');
}
$response = json_decode($json, true, 16, JSON_THROW_ON_ERROR);
$rows = is_array($response) ? ($response['result'] ?? null) : null;
if (!is_array($rows) || !array_is_list($rows)) {
throw new UnexpectedValueException('Ожидался список result');
}
foreach ($rows as $row) {
if (!is_array($row) || !is_string($row['ID'] ?? null) || !is_string($row['TITLE'] ?? null)) {
throw new UnexpectedValueException('Некорректная учебная сделка');
}
}
return $rows;
}
// Внутренний общий алгоритм получает уже проверенные настройки.
function requestList(array $settings, string $method, array $parameters, callable $transport): array
{
if ($method !== 'crm.deal.list') {
throw new InvalidArgumentException('Метод не разрешён в этом учебном клиенте');
}
$json = $transport($settings['portal'], $method, $parameters, $settings['timeoutSeconds']);
return decodeDeals($json);
}
// Небольшие фикстуры вместо сети. Контракт ответов намеренно ограничен.
$fixtures = [
'north' => '{"result":[{"ID":"N-101","TITLE":"Учебная поставка"}]}',
'south' => '{"result":[{"ID":"S-201","TITLE":"Учебная подписка"}]}',
];
$transport = static function (string $portal, string $method, array $parameters, int $timeoutSeconds) use ($fixtures): string {
if (!isset($fixtures[$portal])) {
throw new RuntimeException('Для портала нет фикстуры');
}
return $fixtures[$portal];
};
// В копии обработчика для south забыли изменить имя портала.
function copiedSouthList(callable $transport): array
{
return decodeDeals($transport('north', 'crm.deal.list', [], 2));
}
echo 'Копия для south вернула: ', copiedSouthList($transport)[0]['ID'], "\n";
// Исправление не требует класса: настройки стали явным аргументом.
function listMethod(array $settings, array $parameters, string $method, callable $transport): array
{
return requestList(validatePortalSettings($settings), $method, $parameters, $transport);
}
$north = ['portal' => 'north', 'timeoutSeconds' => 2];
$south = ['portal' => 'south', 'timeoutSeconds' => 5];
$northDeals = listMethod($north, [], 'crm.deal.list', $transport);
$southDeals = listMethod($south, [], 'crm.deal.list', $transport);
echo 'Функция north: ', $northDeals[0]['ID'], ' — ', $northDeals[0]['TITLE'], "\n";
echo 'Функция south: ', $southDeals[0]['ID'], ' — ', $southDeals[0]['TITLE'], "\n";
try {
listMethod(['portal' => 'north', 'timeoutSeconds' => 0], [], 'crm.deal.list', $transport);
} catch (InvalidArgumentException $error) {
echo "Неверная настройка отклонена\n";
}
Ожидаемый результат
Копия для south вернула: N-101
Функция north: N-101 — Учебная поставка
Функция south: S-201 — Учебная подписка
Неверная настройка отклонена
Получены обычные PHP-массивы со сделками. Ошибка была в зашитой настройке скопированного обработчика; общая функция её устраняет без ООП. В примере нет реального B24-запроса: transport возвращает маленький локальный JSON и не реализует фильтры, пагинацию или таймаут. Его сигнатура показывает, что понадобится настоящему адаптеру. Это учебная оболочка, а не официальный SDK.
Общая обработка больше не копируется. Если изменился формат ответа, исправляется decodeDeals; если появился новый HTTP-клиент, заменяется transport. Процедурное приложение не обязано хранить тысячи строк в одном файле: функции можно разнести по смыслу, подключать и тестировать отдельно.
Не преувеличивайте эффект ООП: неверное значение portal в настройках может попасть и в конструктор класса. Типизация и инкапсуляция не знают, какой бизнес-портал вы имели в виду. Нужны понятные имена настроек и проверка маршрута данных.
Тот же listMethod в объекте: настройки закреплены за конкретным клиентом
Теперь приложение много раз обращается к обоим порталам: в обработчике, импорте и отчёте. Можно каждый раз передавать настройки функции — это нормально. Если важно передавать дальше уже настроенного клиента, удобно один раз создать north и south. Конструктор проверяет настройки, объект сохраняет их, метод принимает только параметры отдельного запроса.
Ни JSON, ни транспортный алгоритм не переписываются ради class. В программе ниже B24 вызывает ту же requestList. Сравнивайте ответственность, а не количество строк: где собирают клиента, кто может изменить его настройки и какие данные нужны конкретному вызову.
<?php
declare(strict_types=1);
function validatePortalSettings(array $settings): array
{
$portal = $settings['portal'] ?? null;
$timeout = $settings['timeoutSeconds'] ?? null;
if (!is_string($portal) || !in_array($portal, ['north', 'south'], true)) {
throw new InvalidArgumentException('Неизвестный учебный портал');
}
if (!is_int($timeout) || $timeout < 1 || $timeout > 10) {
throw new InvalidArgumentException('Таймаут от 1 до 10 секунд');
}
return ['portal' => $portal, 'timeoutSeconds' => $timeout];
}
function decodeDeals(string $json): array
{
if (strlen($json) > 10000) {
throw new LengthException('Учебный ответ слишком большой');
}
$response = json_decode($json, true, 16, JSON_THROW_ON_ERROR);
$rows = is_array($response) ? ($response['result'] ?? null) : null;
if (!is_array($rows) || !array_is_list($rows)) {
throw new UnexpectedValueException('Ожидался список result');
}
foreach ($rows as $row) {
if (!is_array($row) || !is_string($row['ID'] ?? null) || !is_string($row['TITLE'] ?? null)) {
throw new UnexpectedValueException('Некорректная учебная сделка');
}
}
return $rows;
}
// Внутренний общий алгоритм получает уже проверенные настройки.
function requestList(array $settings, string $method, array $parameters, callable $transport): array
{
if ($method !== 'crm.deal.list') {
throw new InvalidArgumentException('Метод не разрешён в этом учебном клиенте');
}
$json = $transport($settings['portal'], $method, $parameters, $settings['timeoutSeconds']);
return decodeDeals($json);
}
// Небольшие фикстуры вместо сети. Контракт ответов намеренно ограничен.
$fixtures = [
'north' => '{"result":[{"ID":"N-101","TITLE":"Учебная поставка"}]}',
'south' => '{"result":[{"ID":"S-201","TITLE":"Учебная подписка"}]}',
];
$transport = static function (string $portal, string $method, array $parameters, int $timeoutSeconds) use ($fixtures): string {
if (!isset($fixtures[$portal])) {
throw new RuntimeException('Для портала нет фикстуры');
}
return $fixtures[$portal];
};
class B24
{
private array $settings;
private Closure $transport;
public function __construct(array $settings, callable $transport)
{
$this->settings = validatePortalSettings($settings);
$this->transport = Closure::fromCallable($transport);
}
public function listMethod(array $parameters, string $method): array
{
return requestList($this->settings, $method, $parameters, $this->transport);
}
}
$northSettings = ['portal' => 'north', 'timeoutSeconds' => 2];
$north = new B24($northSettings, $transport);
$south = new B24(['portal' => 'south', 'timeoutSeconds' => 5], $transport);
$northDeals = $north->listMethod([], 'crm.deal.list');
$southDeals = $south->listMethod([], 'crm.deal.list');
echo 'Клиент north: ', $northDeals[0]['ID'], ' — ', $northDeals[0]['TITLE'], "\n";
echo 'Клиент south: ', $southDeals[0]['ID'], ' — ', $southDeals[0]['TITLE'], "\n";
$northSettings['portal'] = 'south';
echo 'После изменения внешнего массива: ', $north->listMethod([], 'crm.deal.list')[0]['ID'], "\n";
try {
new B24(['portal' => 'north', 'timeoutSeconds' => 0], $transport);
} catch (InvalidArgumentException $error) {
echo "Неверный клиент не создан\n";
}
Ожидаемый результат
Клиент north: N-101 — Учебная поставка
Клиент south: S-201 — Учебная подписка
После изменения внешнего массива: N-101
Неверный клиент не создан
Оба объекта вызывают тот же requestList, что процедурная версия. Отличается место хранения проверенных настроек: каждый клиент сохраняет свой небольшой массив и транспорт. Изменение внешнего массива не перенастраивает уже созданный объект. Время ожидания лишь передаётся фиктивному транспорту; реальное соблюдение таймаута нужно отдельно реализовать и проверить в HTTP-адаптере.
| Что меняется | Процедурный вариант | Объектный вариант |
|---|---|---|
| Настройки двух порталов | Отдельные массивы передаются в вызов | Отдельные объекты сохраняют проверенные настройки |
| Формат JSON | Одна decodeDeals | Та же decodeDeals |
| Способ получения ответа | Переданный transport | Сохранённый transport |
| Ошибочная конфигурация | Проверяется на границе вызова | Проверяется на границе создания клиента |
| Время выполнения сети | Зависит от реального транспорта | Также зависит от реального транспорта |
Польза состояния объекта здесь конкретная: получатель north не должен каждый раз подбирать пару portal/timeout, а внешний массив уже не перенастроит этот экземпляр. Но объект не имеет вечной жизни и не хранится между независимыми HTTP-запросами сам по себе. Секреты и контекст разных клиентов нельзя переносить в общий static.
Для одноразовой выгрузки хорошей функции достаточно. Для клиента с несколькими связанными операциями, настройками и проверками объект часто удобнее. При необходимости одинакового log у B24 и Telegram прочитайте рабочий пример трейта — он отвечает уже на другую задачу: общее поведение у независимых классов.
Новый вид отчёта: меняем маленький шаг, общую часть не копируем
Менеджеру нужны названия сделок, техническому отчёту — только ID. В обоих случаях нужно проверить строки и пройти список. Копирование полного экспортёра ради одной строки вывода создаст второе место исправления. Есть как минимум два ясных решения: передать форматирующий callback или оставить общий алгоритм в базовом типе с узким переопределяемым шагом.
Следующий пример намеренно использует одну общую функцию обработки в обоих вариантах. Наследники меняют только возвращаемое поле в formatRow. Ни наследование, ни процедура не требуют копировать весь алгоритм ради этих нескольких строк.
<?php
declare(strict_types=1);
function buildReport(array $rows, callable $formatRow): string
{
if (count($rows) > 100) {
throw new LengthException('Учебный отчёт ограничен ста строками');
}
$lines = [];
foreach ($rows as $row) {
if (!is_array($row) || !is_string($row['ID'] ?? null) || !is_string($row['TITLE'] ?? null)) {
throw new UnexpectedValueException('Нужны ID и TITLE');
}
$line = $formatRow($row);
if (!is_string($line)) {
throw new UnexpectedValueException('Форматтер должен вернуть строку');
}
$lines[] = $line;
}
return implode(' | ', $lines);
}
abstract class AbstractDealReport
{
final public function build(array $rows): string
{
return buildReport($rows, fn (array $row): string => $this->formatRow($row));
}
abstract protected function formatRow(array $row): string;
}
class DealIdReport extends AbstractDealReport
{
protected function formatRow(array $row): string
{
return $row['ID'];
}
}
class DealTitleReport extends AbstractDealReport
{
protected function formatRow(array $row): string
{
return $row['TITLE'];
}
}
$rows = [
['ID' => 'N-101', 'TITLE' => 'Поставка'],
['ID' => 'N-102', 'TITLE' => 'Подписка'],
];
echo 'Callback: ', buildReport($rows, static fn (array $row): string => $row['TITLE']), "\n";
echo 'Отчёт ID: ', (new DealIdReport())->build($rows), "\n";
echo 'Отчёт TITLE: ', (new DealTitleReport())->build($rows), "\n";
try {
(new DealTitleReport())->build([['ID' => 'N-103']]);
} catch (UnexpectedValueException $error) {
echo "Общая проверка обнаружила отсутствующий TITLE\n";
}
Ожидаемый результат
Callback: Поставка | Подписка
Отчёт ID: N-101 | N-102
Отчёт TITLE: Поставка | Подписка
Общая проверка обнаружила отсутствующий TITLE
Обход и проверка строк находятся только в buildReport и используются обоими подходами. Наследники меняют одну конкретную операцию formatRow; общую обработку не копируют. Процедурный callback даёт такой же результат и часто проще для разового отчёта. Здесь создаётся обычный текст; перед помещением результата в HTML его экранируют для выбранного контекста.
Для разового преобразования callback короче и вполне профессионален. Именованный класс полезен, если формат отчёта повторно выбирают в разных местах, он имеет свои правила или настройки и его удобно проверять отдельно. Абстрактный класс выражает стабильный договор build → formatRow; final у build сохраняет общий путь проверки.
Если независимо меняются источник, формат и доставка, не стройте наследников на каждую комбинацию. Передавайте эти части отдельно. Детальный выбор разобран в абстрактных классах и композиции.
Где лежат классы и функции: файлы, namespace и Composer
Самостоятельные примеры собраны в одном файле только ради запуска. В проекте их можно разнести так; public — корень сайта, остальные каталоги не должны раздаваться как публичные файлы.
project/
├── composer.json
├── public/
│ └── index.php
├── src/
│ ├── Integration/
│ │ ├── B24.php
│ │ └── functions.php
│ └── Report/
│ ├── AbstractDealReport.php
│ ├── DealIdReport.php
│ └── DealTitleReport.php
├── tests/fixtures/
│ ├── deals-north.json
│ └── deals-south.json
├── storage/logs/
└── vendor/ ← создаёт Composer
{
"autoload": {
"psr-4": {"Demo\\": "src/"},
"files": ["src/Integration/functions.php"]
}
}
Это фрагмент конфигурации отдельного проекта, а не PHP-код. После подготовки файлов выполните composer dump-autoload из корня проекта. Сеть для собственных правил автозагрузки не нужна; установка Composer и зависимости проекта рассматриваются в отдельной главе.
Правило Demo\ → src/ связывает имя Demo\Integration\B24 с src/Integration/B24.php. В файле объявляют namespace Demo\Integration и класс B24. Регистр каталогов и имени файла должен совпадать: ошибка, незаметная на одной файловой системе, может проявиться на сервере. Правила PSR-4.
Функции также можно хранить отдельно. Для src/Integration/functions.php здесь выбрано autoload.files; при разнесении добавьте ему соответствующий namespace. Такой файл выполняется при подключении загрузчика, поэтому пусть он объявляет функции, а не делает запросы и не создаёт клиентов. Вызов функции можно импортировать через use function Demo\Integration\listMethod. Composer: files.
<?php
declare(strict_types=1);
use Demo\Integration\B24;
require dirname(__DIR__) . '/vendor/autoload.php';
// $portalSettings и $transport подготовлены доверенной сборкой приложения.
$b24 = new B24($portalSettings, $transport);
$deals = $b24->listMethod([], 'crm.deal.list');
Это схема точки входа, а не самостоятельный файл: нужны разнесённые по namespace классы/функции, сгенерированный vendor/autoload.php и явно подготовленные настройки с транспортом. Без них фрагмент не следует запускать как готовую интеграцию.
require подключает vendor/autoload.php и регистрирует загрузчик. Он знает, где искать классы, но не создаёт ваши B24-объекты и не подключает сразу все файлы классов. Когда потребуется ещё неизвестный класс, загрузчик найдёт его файл. Отдельное исключение — явно перечисленные autoload.files: они подключаются при загрузчике. Инструкция use задаёт короткое имя для обращения к классу; это не require. new — уже создание конкретного экземпляра. Composer: автозагрузка.
После изменения правил выполните composer dump-autoload. Для структуры с пакетами и lock-файлом продолжите главу о Composer. Дерево выше — схема разнесения примеров, не ещё один выданный готовый архив магазина.
Что выбрать для своего сервиса и что проверить после изменения
| Ваш случай | Разумная отправная точка | Проверяемый результат |
|---|---|---|
| Один раз выгрузить сделки | Функция с явными настройками и транспортом | Получены ожидаемые записи; сбой не превращён в пустой успех |
| Несколько операций одного настроенного клиента | Объект с проверенной конфигурацией | north и south не смешивают параметры и ответы |
| Один короткий вариант отображения | Callback к общей обработке | Изменилось отображение, общая проверка сохранилась |
| Стабильная семейная группа отчётов | Общий договор и узкий шаг наследника либо отдельная стратегия | Одинаковый вход соблюдает обещанный договор каждого варианта |
| Одинаковый log у независимых клиентов | Небольшой трейт с явным договором каталога | Один вызов создаёт запись, общая правка работает в обоих клиентах |
Начните с действия, которое должен выполнить человек или программа. Получите наблюдаемый результат, добавьте новый случай и посмотрите, что именно стало неудобно. Если проблему решает аргумент функции, не требуется класс. Если несколько операций должны сохранять согласованную настройку или состояние, объект может дать ясную границу. Счётчик строк файла не выбирает архитектуру.
Упражнение: добавьте третий учебный портал. В обеих версиях измените фикстуры и допустимые настройки. Затем испортите JSON, уберите TITLE и передайте неизвестный метод. Оба подхода должны сообщить о нарушении договора. Проверьте, что обработку ответа вы исправляете в одном месте, а не в трёх копиях.
Фикстуры проверяют выбранные решения, но не доказывают настоящий TLS, таймаут, ограничения провайдера, пагинацию и авторизацию. Следующий шаг — запускаемый учебный сервис интеграции, затем HTTP-клиент, лимиты и повторы и проверка вложенных данных.