@Eugene Sukhodolskiy Eugene Sukhodolskiy authored 23 hours ago
examples Initial client library skeleton (v0.1.0) 23 hours ago
src Initial client library skeleton (v0.1.0) 23 hours ago
tests Initial client library skeleton (v0.1.0) 23 hours ago
.gitignore Initial client library skeleton (v0.1.0) 23 hours ago
README.md Initial client library skeleton (v0.1.0) 23 hours ago
composer.json Initial client library skeleton (v0.1.0) 23 hours ago
phpunit.xml.dist Initial client library skeleton (v0.1.0) 23 hours 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).