Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 48 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -329,14 +329,31 @@ foreach ($client->iterateProducts(lang: [Language::ALL]) as $product) {

Pobo can notify your application when content changes in the administration. Register a webhook URL in the Pobo e-shop settings; Pobo POSTs to it with HMAC-SHA256 signed requests.

| Event | Constant | Fired when |
|----------------------|-----------------------------------|----------------------------------------|
| `Products.update` | `WebhookEvent::PRODUCTS_UPDATE` | a product was edited and saved in Pobo |
| `Categories.update` | `WebhookEvent::CATEGORIES_UPDATE` | a category was edited |
| `Blogs.update` | `WebhookEvent::BLOGS_UPDATE` | a blog was edited |
### Supported events

| Event | Constant | Fired when |
|------------------------|-------------------------------------|-------------------------------------|
| `Products.create` | `WebhookEvent::PRODUCTS_CREATE` | a product was created |
| `Products.update` | `WebhookEvent::PRODUCTS_UPDATE` | a product was edited |
| `Products.delete` | `WebhookEvent::PRODUCTS_DELETE` | a product was soft-deleted |
| `Categories.create` | `WebhookEvent::CATEGORIES_CREATE` | a category was created |
| `Categories.update` | `WebhookEvent::CATEGORIES_UPDATE` | a category was edited |
| `Categories.delete` | `WebhookEvent::CATEGORIES_DELETE` | a category was soft-deleted |
| `Brands.create` | `WebhookEvent::BRANDS_CREATE` | a brand was created |
| `Brands.update` | `WebhookEvent::BRANDS_UPDATE` | a brand was edited |
| `Brands.delete` | `WebhookEvent::BRANDS_DELETE` | a brand was soft-deleted |
| `Blogs.create` | `WebhookEvent::BLOGS_CREATE` | a blog was created |
| `Blogs.update` | `WebhookEvent::BLOGS_UPDATE` | a blog was edited |
| `Blogs.delete` | `WebhookEvent::BLOGS_DELETE` | a blog was soft-deleted |

The enum exposes `isCreate()` / `isUpdate()` / `isDelete()` helpers if you only need to branch on the lifecycle action regardless of entity type.

> **Note:** the API also defines `Languages.create/update/delete` events. The SDK does not expose them yet — payloads carrying these events will throw `WebhookException::unknownEvent()`.

The webhook is a notification only — it does not carry the changed entities. Use it as a trigger to re-sync via the export endpoints (typically combined with `lastUpdateFrom`).

### Basic usage

```php
use Pobo\Sdk\WebhookHandler;
use Pobo\Sdk\Enum\WebhookEvent;
Expand All @@ -348,9 +365,18 @@ try {
$payload = $handler->handleFromGlobals();

match ($payload->event) {
WebhookEvent::PRODUCTS_UPDATE => syncProducts($client),
WebhookEvent::CATEGORIES_UPDATE => syncCategories($client),
WebhookEvent::BLOGS_UPDATE => syncBlogs($client),
WebhookEvent::PRODUCTS_CREATE,
WebhookEvent::PRODUCTS_UPDATE,
WebhookEvent::PRODUCTS_DELETE => syncProducts($client),
WebhookEvent::CATEGORIES_CREATE,
WebhookEvent::CATEGORIES_UPDATE,
WebhookEvent::CATEGORIES_DELETE => syncCategories($client),
WebhookEvent::BRANDS_CREATE,
WebhookEvent::BRANDS_UPDATE,
WebhookEvent::BRANDS_DELETE => syncBrands($client),
WebhookEvent::BLOGS_CREATE,
WebhookEvent::BLOGS_UPDATE,
WebhookEvent::BLOGS_DELETE => syncBlogs($client),
};

http_response_code(200);
Expand All @@ -362,6 +388,20 @@ try {

`$payload` exposes `event` (`WebhookEvent` enum), `timestamp` (`DateTimeInterface`), and `eshopId` (int).

### Branching only on lifecycle action

```php
$payload = $handler->handleFromGlobals();

if ($payload->event->isDelete()) {
invalidateCache($payload->event, $payload->eshopId);
}

if ($payload->event->isCreate() || $payload->event->isUpdate()) {
triggerResync($payload->event, $payload->eshopId);
}
```

## Error Handling

```php
Expand Down
34 changes: 28 additions & 6 deletions src/Enum/WebhookEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,21 @@

enum WebhookEvent: string
{
case PRODUCTS_CREATE = 'Products.create';
case PRODUCTS_UPDATE = 'Products.update';
case PRODUCTS_DELETE = 'Products.delete';

case CATEGORIES_CREATE = 'Categories.create';
case CATEGORIES_UPDATE = 'Categories.update';
case CATEGORIES_DELETE = 'Categories.delete';

case BRANDS_CREATE = 'Brands.create';
case BRANDS_UPDATE = 'Brands.update';
case BRANDS_DELETE = 'Brands.delete';

case BLOGS_CREATE = 'Blogs.create';
case BLOGS_UPDATE = 'Blogs.update';
case BLOGS_DELETE = 'Blogs.delete';

/**
* @return array<string>
Expand All @@ -20,11 +32,21 @@ public static function values(): array

public static function fromString(string $value): ?self
{
return match ($value) {
'Products.update' => self::PRODUCTS_UPDATE,
'Categories.update' => self::CATEGORIES_UPDATE,
'Blogs.update' => self::BLOGS_UPDATE,
default => null,
};
return self::tryFrom($value);
}

public function isCreate(): bool
{
return str_ends_with($this->value, '.create');
}

public function isUpdate(): bool
{
return str_ends_with($this->value, '.update');
}

public function isDelete(): bool
{
return str_ends_with($this->value, '.delete');
}
}
81 changes: 79 additions & 2 deletions tests/Enum/WebhookEventTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,70 @@ final class WebhookEventTest extends TestCase
{
public function testAllCasesExist(): void
{
$this->assertSame('Products.create', WebhookEvent::PRODUCTS_CREATE->value);
$this->assertSame('Products.update', WebhookEvent::PRODUCTS_UPDATE->value);
$this->assertSame('Products.delete', WebhookEvent::PRODUCTS_DELETE->value);

$this->assertSame('Categories.create', WebhookEvent::CATEGORIES_CREATE->value);
$this->assertSame('Categories.update', WebhookEvent::CATEGORIES_UPDATE->value);
$this->assertSame('Categories.delete', WebhookEvent::CATEGORIES_DELETE->value);

$this->assertSame('Brands.create', WebhookEvent::BRANDS_CREATE->value);
$this->assertSame('Brands.update', WebhookEvent::BRANDS_UPDATE->value);
$this->assertSame('Brands.delete', WebhookEvent::BRANDS_DELETE->value);

$this->assertSame('Blogs.create', WebhookEvent::BLOGS_CREATE->value);
$this->assertSame('Blogs.update', WebhookEvent::BLOGS_UPDATE->value);
$this->assertSame('Blogs.delete', WebhookEvent::BLOGS_DELETE->value);
}

public function testValues(): void
{
$values = WebhookEvent::values();

$this->assertCount(12, $values);
$this->assertContains('Products.create', $values);
$this->assertContains('Products.update', $values);
$this->assertContains('Products.delete', $values);
$this->assertContains('Categories.create', $values);
$this->assertContains('Categories.update', $values);
$this->assertContains('Categories.delete', $values);
$this->assertContains('Brands.create', $values);
$this->assertContains('Brands.update', $values);
$this->assertContains('Brands.delete', $values);
$this->assertContains('Blogs.create', $values);
$this->assertContains('Blogs.update', $values);
$this->assertCount(3, $values);
$this->assertContains('Blogs.delete', $values);
}

public function testFromStringReturnsCorrectEnum(): void
public function testFromStringReturnsCorrectEnumForExistingEvents(): void
{
$this->assertSame(WebhookEvent::PRODUCTS_UPDATE, WebhookEvent::fromString('Products.update'));
$this->assertSame(WebhookEvent::CATEGORIES_UPDATE, WebhookEvent::fromString('Categories.update'));
$this->assertSame(WebhookEvent::BLOGS_UPDATE, WebhookEvent::fromString('Blogs.update'));
}

public function testFromStringReturnsCorrectEnumForCreateEvents(): void
{
$this->assertSame(WebhookEvent::PRODUCTS_CREATE, WebhookEvent::fromString('Products.create'));
$this->assertSame(WebhookEvent::CATEGORIES_CREATE, WebhookEvent::fromString('Categories.create'));
$this->assertSame(WebhookEvent::BRANDS_CREATE, WebhookEvent::fromString('Brands.create'));
$this->assertSame(WebhookEvent::BLOGS_CREATE, WebhookEvent::fromString('Blogs.create'));
}

public function testFromStringReturnsCorrectEnumForDeleteEvents(): void
{
$this->assertSame(WebhookEvent::PRODUCTS_DELETE, WebhookEvent::fromString('Products.delete'));
$this->assertSame(WebhookEvent::CATEGORIES_DELETE, WebhookEvent::fromString('Categories.delete'));
$this->assertSame(WebhookEvent::BRANDS_DELETE, WebhookEvent::fromString('Brands.delete'));
$this->assertSame(WebhookEvent::BLOGS_DELETE, WebhookEvent::fromString('Blogs.delete'));
}

public function testFromStringReturnsCorrectEnumForBrandUpdate(): void
{
$this->assertSame(WebhookEvent::BRANDS_UPDATE, WebhookEvent::fromString('Brands.update'));
}

public function testFromStringReturnsNullForUnknown(): void
{
$this->assertNull(WebhookEvent::fromString('Unknown.event'));
Expand All @@ -41,5 +83,40 @@ public function testFromStringReturnsNullForUnknown(): void
$this->assertNull(WebhookEvent::fromString('PRODUCTS.UPDATE'));
$this->assertNull(WebhookEvent::fromString('blogs.update'));
$this->assertNull(WebhookEvent::fromString('BLOGS.UPDATE'));
$this->assertNull(WebhookEvent::fromString('brands.create'));
$this->assertNull(WebhookEvent::fromString('Languages.create')); // not in SDK yet
}

public function testIsCreateHelper(): void
{
$this->assertTrue(WebhookEvent::PRODUCTS_CREATE->isCreate());
$this->assertTrue(WebhookEvent::CATEGORIES_CREATE->isCreate());
$this->assertTrue(WebhookEvent::BRANDS_CREATE->isCreate());
$this->assertTrue(WebhookEvent::BLOGS_CREATE->isCreate());

$this->assertFalse(WebhookEvent::PRODUCTS_UPDATE->isCreate());
$this->assertFalse(WebhookEvent::BRANDS_DELETE->isCreate());
}

public function testIsUpdateHelper(): void
{
$this->assertTrue(WebhookEvent::PRODUCTS_UPDATE->isUpdate());
$this->assertTrue(WebhookEvent::CATEGORIES_UPDATE->isUpdate());
$this->assertTrue(WebhookEvent::BRANDS_UPDATE->isUpdate());
$this->assertTrue(WebhookEvent::BLOGS_UPDATE->isUpdate());

$this->assertFalse(WebhookEvent::PRODUCTS_CREATE->isUpdate());
$this->assertFalse(WebhookEvent::BRANDS_DELETE->isUpdate());
}

public function testIsDeleteHelper(): void
{
$this->assertTrue(WebhookEvent::PRODUCTS_DELETE->isDelete());
$this->assertTrue(WebhookEvent::CATEGORIES_DELETE->isDelete());
$this->assertTrue(WebhookEvent::BRANDS_DELETE->isDelete());
$this->assertTrue(WebhookEvent::BLOGS_DELETE->isDelete());

$this->assertFalse(WebhookEvent::PRODUCTS_CREATE->isDelete());
$this->assertFalse(WebhookEvent::BRANDS_UPDATE->isDelete());
}
}
86 changes: 86 additions & 0 deletions tests/WebhookHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -158,4 +158,90 @@ public function testHandleWithDifferentTimestampFormats(): void

$this->assertInstanceOf(\DateTimeInterface::class, $result->timestamp);
}

/**
* @return array<string, array{0: string, 1: WebhookEvent}>
*/
public static function brandEventProvider(): array
{
return [
'Brands.create' => ['Brands.create', WebhookEvent::BRANDS_CREATE],
'Brands.update' => ['Brands.update', WebhookEvent::BRANDS_UPDATE],
'Brands.delete' => ['Brands.delete', WebhookEvent::BRANDS_DELETE],
];
}

/**
* @dataProvider brandEventProvider
*/
public function testHandleValidBrandEvent(string $eventValue, WebhookEvent $expected): void
{
$payload = json_encode([
'event' => $eventValue,
'timestamp' => '2026-05-05T14:30:00+02:00',
'eshop_id' => 789,
]);

$signature = hash_hmac('sha256', $payload, self::WEBHOOK_SECRET);

$result = $this->handler->handle($payload, $signature);

$this->assertSame($expected, $result->event);
$this->assertSame(789, $result->eshopId);
}

/**
* @return array<string, array{0: string, 1: WebhookEvent}>
*/
public static function createDeleteEventProvider(): array
{
return [
'Products.create' => ['Products.create', WebhookEvent::PRODUCTS_CREATE],
'Products.delete' => ['Products.delete', WebhookEvent::PRODUCTS_DELETE],
'Categories.create' => ['Categories.create', WebhookEvent::CATEGORIES_CREATE],
'Categories.delete' => ['Categories.delete', WebhookEvent::CATEGORIES_DELETE],
'Blogs.create' => ['Blogs.create', WebhookEvent::BLOGS_CREATE],
'Blogs.delete' => ['Blogs.delete', WebhookEvent::BLOGS_DELETE],
];
}

/**
* @dataProvider createDeleteEventProvider
*/
public function testHandleValidCreateOrDeleteEvent(string $eventValue, WebhookEvent $expected): void
{
$payload = json_encode([
'event' => $eventValue,
'timestamp' => '2026-05-05T14:30:00+02:00',
'eshop_id' => 555,
]);

$signature = hash_hmac('sha256', $payload, self::WEBHOOK_SECRET);

$result = $this->handler->handle($payload, $signature);

$this->assertSame($expected, $result->event);
}

public function testHandleBrandsCreateMatchableInUserCode(): void
{
// Verifies the typical match() pattern works for brand events.
$payload = json_encode([
'event' => 'Brands.create',
'timestamp' => '2026-05-05T14:30:00+02:00',
'eshop_id' => 1,
]);

$signature = hash_hmac('sha256', $payload, self::WEBHOOK_SECRET);
$result = $this->handler->handle($payload, $signature);

$branch = match ($result->event) {
WebhookEvent::BRANDS_CREATE => 'created',
WebhookEvent::BRANDS_UPDATE => 'updated',
WebhookEvent::BRANDS_DELETE => 'deleted',
default => 'other',
};

$this->assertSame('created', $branch);
}
}
Loading