@Eugene Sukhodolskiy Eugene Sukhodolskiy authored 1 day ago
examples Initial client library skeleton (v0.1.0) 1 day ago
src Initial client library skeleton (v0.1.0) 1 day ago
tests Initial client library skeleton (v0.1.0) 1 day ago
.gitignore Initial client library skeleton (v0.1.0) 1 day ago
README.md Initial client library skeleton (v0.1.0) 1 day ago
composer.json Initial client library skeleton (v0.1.0) 1 day ago
phpunit.xml.dist Initial client library skeleton (v0.1.0) 1 day ago
README.md

gnexus-synapse-client-php

Тонкий PHP-клиент Synapse (централизованный хаб уведомлений Gnexus) для сервисов экосистемы: gnexus-auth (Laravel), Navi-инстансы и другие PHP-сервисы.

Клиент берёт на себя весь повторяемый шаблон: env-конфиг, сборку и локальную валидацию конверта, типизированные исключения по HTTP-статусам, fire-and-forget emit, batch, статус события. Ретраев и локальных очередей нет — Synapse принимает событие с ответом 202 мгновенно, всё остальное делает его воркер.

Транспорт инжектный (PSR-18 + PSR-17, например Guzzle — в require-dev для тестов и примеров, жёсткой зависимости нет).

Установка

В composer.json сервиса:

{
    "repositories": [
        {
            "type": "vcs",
            "url": "https://git.gnexus.space/git/root/gn-synapse-client-php.git"
        }
    ]
}
composer require gnexus/synapse-client:^0.1.0

Quickstart

use GNexus\Synapse\Config\SynapseConfig;
use GNexus\Synapse\SynapseClient;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\HttpFactory;

$client = new SynapseClient(
    SynapseConfig::fromGlobals(),               // SYNAPSE_URL, SYNAPSE_API_KEY, ...
    new Client(['timeout' => 10.0]),            // PSR-18 (таймаут задаёт потребитель)
    new HttpFactory(),                          // PSR-17 RequestFactory
    new HttpFactory(),                          // PSR-17 StreamFactory
);

// Обычная отправка: событие обязательно дойти — ошибка бросает исключение
$event = $client->send('bugtrail', 'test', 'failed', 'high',
    ['user_id' => 'u-42'], dedupKey: "failed-42");
$event->id;        // uuid
$event->status;    // 'queued' — 202, воркер заберёт позже

// Fire-and-forget: упавший Synapse не должен ломать основной путь сервиса
$client->emit(null, 'user', 'password_changed', payload: ['user_id' => $sub]);
// → SentEvent|null; SynapseException и транспортные сбои ловятся,
//   пишутся в $logger->warning('synapse emit failed: ...', ...) и глотаются.

// Статус и доставки
$status = $client->status($event->id);
foreach ($status->deliveries as $d) { /* channel, target, status, attempts, error */ }

Конфигурация (env)

Переменная Обязательна По умолчанию Что делает
SYNAPSE_URL да — База Synapse (http://localhost:8013)
SYNAPSE_API_KEY да (для send/emit) — Ключ источника (syn_...)
SYNAPSE_TIMEOUT нет 10.0 Секунды (используется как fromGlobals()->timeoutSeconds)
SYNAPSE_DEFAULT_SOURCE нет — source по умолчанию: $client->send(null, ...)

SynapseConfig валидирует всё на конструировании (fail fast): пустой URL, схема не http(s), таймаут ≤ 0, ключ-пустая-строка → ConfigurationException. Send/sendBatch при отсутствии ключа тоже бросает ConfigurationException до HTTP.

default_source — смягчение антивспуфинга сервера: 403 получают вызовы, где source конверта не совпадает с источником ключа; с SYNAPSE_DEFAULT_SOURCE это ошибка конфигурации, а не кода.

Методы

  • send(...): SentEvent — конверт валидируется локально (без HTTP): имена ^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$ 1..64, priority ∈ low|normal|high|critical, ttlSeconds 1..604800, dedupKey ≤255 ('' → null), payload — assoc-массив. normal priority в конверт не попадает, null-поля не шлются (scheduledAt → UTC DATE_ATOM). Ошибки валидации — локальный ValidationException (statusCode = null: это ошибка кода, не Synapse).
  • emit(..., bool $throw = false): ?SentEvent — то же, но любые SynapseException/транспортные ошибки ловятся → $logger->warning(...) → null (throw: true → перебрасывает).
  • sendBatch(array $envelopes): BatchResult — каждый элемент прогоняется через EnvelopeBuilder::build(); локально битые — BatchRejection(index, detail) без HTTP; ответ сервера 202 и 422 («ни одного не принято») парсится одинаково — index серверного отклонения ремапится на исходную позицию; 5xx/транспорт — исключение целиком.
  • status(string $eventId): EventStatus — uuid-проверка, 404 → NotFoundException.
  • health(): array, ready(): array — /api/healthz и /api/readyz, без ключа.

Исключения (GNexus\Synapse\Exception\)

Класс Код Когда
SynapseException — База: .detail, .statusCode
ConfigurationException — Битый конфиг / нет ключа (до HTTP)
TransportException — Сеть упала (.transportError)
AuthException 401 Ключ нет/отозван/источник архивен
ForbiddenException 403 source ≠ источник ключа (антивспуфинг)
NotFoundException 404 Статус чужого/несуществующего события
ValidationException 422 / null Незарегистрированный тип или битый конверт (null = локально)
ServerException 5xx Synapse упал

Ловить — одну SynapseException.

Оговорки

  • Дедуп best-effort в окне 24ч: повтор возвращает первое событие с deduplicated: true. Не строить бизнес-логику на отсутствии дублей.
  • Инжектный PSR-18 без таймаута висит вечно (какой бы ни был timeoutSeconds) — в Guzzle и аналогах явно задавайте таймаут (Laravel-binding с таймаутом — examples/laravel/README.md).
  • scheduled_at — резерв MVP: принимается, не отправляется.
  • payload.user_id — непустая строка (uuid gnexus-auth); остальное — предупреждение в лог (сервер запишет address-доставки со статусом skipped).

Разработка

composer install
vendor/bin/phpunit

Интеграционный смок против живого стека — examples/plain-php/smoke.php.

Лицензия: проприетарная (внутри экосистемы Gnexus).