PHPackages                             evgenmil/webhook-storage - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. evgenmil/webhook-storage

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

evgenmil/webhook-storage
========================

Framework-agnostic webhook storage layer for PHP. Per-source MySQL tables, single schema, status lifecycle.

v0.2.0(1mo ago)022MITPHPPHP ^8.1

Since May 28Pushed 1mo agoCompare

[ Source](https://github.com/evgenmil/webhook-storage)[ Packagist](https://packagist.org/packages/evgenmil/webhook-storage)[ RSS](/packages/evgenmil-webhook-storage/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (1)Versions (3)Used By (0)

evgenmil/webhook-storage
========================

[](#evgenmilwebhook-storage)

Framework-agnostic слой хранения вебхуков для PHP.

- Одна таблица MySQL на каждый источник вебхуков.
- Единая схема, отличается только имя таблицы.
- Жизненный цикл записи: `pending → processing → done | failed`.
- Идемпотентность по `external_event_id`.
- Подключается в любой фреймворк через DI. Никаких зависимостей от Yii/Laravel/Symfony.

Что модуль **делает**: сохраняет вебхук, читает запись по id, обновляет её статус. Что модуль **не делает**: парсинг payload, проверка подписи, очереди, бизнес-логика, HTTP-роутинг.

Установка
---------

[](#установка)

```
composer require evgenmil/webhook-storage
```

Требования: PHP ^8.1, `ext-pdo`, `ext-json`.

Концепция
---------

[](#концепция)

- **Источник** — строковый slug (`amocrm`, `bitrix24`, ...). Задаёт приложение.
- **Таблица** — своя на каждый источник, имя задаёт приложение через `SourceTableMap`.
- **Схема таблицы** — берётся из `WebhookSchema`, единая для всех источников.

Приложение отвечает за:

1. создание таблицы (миграция своего фреймворка, SQL берётся из `WebhookSchema`);
2. вычисление `external_event_id` (id события от вендора или хэш тела);
3. перевод статуса (`markProcessing` / `markDone` / `markFailed`).

Миграции
--------

[](#миграции)

SQL живёт **в пакете**, чтобы не было копипасты. Миграция приложения — это одна строка.

### Yii2

[](#yii2)

```
use evgenmil\WebhookStorage\Schema\WebhookSchema;

class m260528_120000_create_webhooks_amocrm extends \yii\db\Migration
{
    public function safeUp(): void
    {
        $this->execute(WebhookSchema::createTableSql('webhooks_amocrm'));
    }

    public function safeDown(): void
    {
        $this->execute(WebhookSchema::dropTableSql('webhooks_amocrm'));
    }
}
```

Новый источник = новая миграция, в которой меняется **только имя таблицы**. То же самое в Phinx / Doctrine Migrations / Laravel — везде сводится к `execute(WebhookSchema::createTableSql($table))`.

Сборка через DI
---------------

[](#сборка-через-di)

```
use evgenmil\WebhookStorage\WebhookStore;
use evgenmil\WebhookStorage\SourceTableMap;
use evgenmil\WebhookStorage\Repository\PdoMysqlWebhookRepository;

$pdo = new \PDO(
    'mysql:host=127.0.0.1;dbname=app;charset=utf8mb4',
    $user,
    $password,
    [
        \PDO::ATTR_ERRMODE            => \PDO::ERRMODE_EXCEPTION,
        \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
        \PDO::ATTR_EMULATE_PREPARES   => false,
    ]
);

$store = new WebhookStore(
    new PdoMysqlWebhookRepository($pdo),
    new SourceTableMap([
        'amocrm'   => 'webhooks_amocrm',
        'bitrix24' => 'webhooks_bitrix24',
    ]),
);
```

В контейнере (любой PSR-11 DI) регистрируется `WebhookStore` синглтоном — всё.

Использование
-------------

[](#использование)

```
// 1. На приёме вебхука:
$result = $store->save(
    source:          'amocrm',
    externalEventId: $eventId,  // приложение само его извлекает или хэширует
    payload:         $payload,
);
// $result->id          — id записи в таблице webhooks_amocrm
// $result->isDuplicate — true, если такой event_id уже был

// 2. Отдаёте $result->id в очередь / воркер.

// 3. В воркере:
$webhook = $store->get('amocrm', $id);
if ($webhook === null) {
    // запись удалена или неверный id — решение за приложением
    return;
}

$store->markProcessing('amocrm', $id);

try {
    // ... ваша бизнес-логика на $webhook->payload ...
    $store->markDone('amocrm', $id);
} catch (\Throwable $e) {
    $store->markFailed('amocrm', $id, $e->getMessage());
}
```

API
---

[](#api)

```
WebhookStore::save(string $source, string $externalEventId, array $payload): SaveResult
WebhookStore::get(string $source, int $id): ?WebhookRecord
WebhookStore::markProcessing(string $source, int $id): void   // attempts++
WebhookStore::markDone(string $source, int $id): void          // last_error = NULL
WebhookStore::markFailed(string $source, int $id, string $error): void

SaveResult { public int $id; public bool $isDuplicate; }

WebhookRecord {
    public int $id;
    public string $externalEventId;
    public array $payload;              // уже декодирован из JSON
    public Status $status;
    public int $attempts;
    public ?string $lastError;
    public \DateTimeImmutable $receivedAt;
    public \DateTimeImmutable $updatedAt;
}

enum Status: string { Pending, Processing, Done, Failed }
```

Исключения
----------

[](#исключения)

- `evgenmil\WebhookStorage\Exception\UnknownSourceException` — `source` не зарегистрирован в `SourceTableMap`.
- `evgenmil\WebhookStorage\Exception\WebhookStorageException` — базовый класс пакетных ошибок.
- Ошибки `\PDOException` пробрасываются как есть (кроме `SQLSTATE 23000` в `save()`, который трактуется как дубликат).

Тесты
-----

[](#тесты)

```
composer install
composer test:unit          # юнит-тесты, без БД
composer test:integration   # интеграционные, требуют MySQL
composer test               # всё вместе
```

### Юнит-тесты

[](#юнит-тесты)

Покрывают framework-agnostic ядро: `WebhookStore`, `SourceTableMap`, `WebhookSchema`, `TableNameGuard`, `SaveResult`, `Status`, исключения. Репозиторий заменяется моком `WebhookRepositoryInterface`. БД не нужна.

### Интеграционные тесты `PdoMysqlWebhookRepository`

[](#интеграционные-тесты-pdomysqlwebhookrepository)

Поднимают реальные `CREATE TABLE` / `INSERT` / `UPDATE` на MySQL. Каждый тест создаёт уникальную таблицу `__wh_test_` и дропает её в `tearDown()`. Если БД недоступна, эти тесты автоматически пропускаются — `composer test:unit` зелёный без MySQL.

Подготовка БД (один раз):

```
composer db:test:setup
```

Скрипт читает `WH_TEST_DSN`, подключается к серверу без указания БД и делает `CREATE DATABASE IF NOT EXISTS` с `utf8mb4 / utf8mb4_unicode_ci`. Безопасно запускать повторно.

Альтернатива — создать БД руками:

```
CREATE DATABASE webhook_storage_test
  DEFAULT CHARACTER SET utf8mb4
  DEFAULT COLLATE utf8mb4_unicode_ci;
```

Параметры подключения берутся из переменных окружения. Приоритет такой: реальный `export` в шелле → файл `.env` в корне пакета (читает `tests/bootstrap.php`) → ничего.

ПеременнаяНазначение`WH_TEST_DSN`PDO DSN, например `mysql:host=127.0.0.1;port=3306;dbname=webhook_storage_test;charset=utf8mb4``WH_TEST_USER`пользователь MySQL`WH_TEST_PASS`парольПример `.env` в корне пакета (файл в `.gitignore`):

```
WH_TEST_DSN="mysql:host=127.0.0.1;port=3306;dbname=webhook_storage_test;charset=utf8mb4"
WH_TEST_USER="root"
WH_TEST_PASS=""
```

Полный цикл первой настройки:

```
composer install
composer db:test:setup
composer test
```

Изолированный прогон, который не оставляет следов на сервере MySQL:

```
composer test:ci
# = db:test:setup → test → db:test:drop
```

Если тест упал — `db:test:drop` намеренно **не** выполняется, чтобы можно было залезть в БД и посмотреть, что не так. Удалить вручную:

```
composer db:test:drop
```

Скрипт удаления отказывается дропать БД, имя которой не содержит `test`и не начинается с `__` — защита от случайной подмены DSN.

Структура
---------

[](#структура)

```
src/
  WebhookStore.php                   фасад (публичный API)
  SaveResult.php                     DTO результата save
  WebhookRecord.php                  DTO записи из БД
  Status.php                         enum статусов
  SourceTableMap.php                 source -> table
  WebhookRepositoryInterface.php     контракт хранилища
  Repository/
    PdoMysqlWebhookRepository.php    реализация на \PDO MySQL
  Schema/
    WebhookSchema.php                генератор CREATE/DROP TABLE SQL
  Exception/
    WebhookStorageException.php
    UnknownSourceException.php
  Internal/
    TableNameGuard.php               валидация имени таблицы
tests/
  Unit/                              юнит-тесты (без БД)
  Integration/                       интеграционные тесты (нужен MySQL)
phpunit.xml.dist

```

###  Health Score

36

—

LowBetter than 79% of packages

Maintenance89

Actively maintained with recent releases

Popularity9

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity33

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

2

Last Release

57d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/6258440?v=4)[Eugene M](/maintainers/evgenmil)[@evgenmil](https://github.com/evgenmil)

---

Top Contributors

[![evgenmil](https://avatars.githubusercontent.com/u/6258440?v=4)](https://github.com/evgenmil "evgenmil (6 commits)")

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/evgenmil-webhook-storage/health.svg)

```
[![Health](https://phpackages.com/badges/evgenmil-webhook-storage/health.svg)](https://phpackages.com/packages/evgenmil-webhook-storage)
```

###  Alternatives

[sonnenglas/amazon-fba-fulfillment-center-list

List of Amazon fulfillment centers around the world available as JSON and PHP array

1910.4k](/packages/sonnenglas-amazon-fba-fulfillment-center-list)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
