Skip to content
Open
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
26 changes: 26 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,32 @@ jobs:
- name: Run tests
run: vendor/bin/phpunit --colors=always

phpstan:
runs-on: ubuntu-latest
name: PHPStan

strategy:
fail-fast: false
matrix:
php: ['8.1', '8.2', '8.3', '8.4', '8.5']

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: curl, json
coverage: none

- name: Install dependencies
run: composer install --prefer-dist --no-progress

- name: Run PHPStan
run: vendor/bin/phpstan analyse --memory-limit=512M

code-style:
runs-on: ubuntu-latest
name: Code Style
Expand Down
117 changes: 97 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,14 +293,42 @@ foreach ($client->iterateBlogs() as $blog) {
}
```

## Language Filtering

By default, only the `default` language is returned. Use the `lang` parameter to request specific languages:

```php
use Pobo\Sdk\Enum\Language;

// Get all languages
$response = $client->getProducts(lang: [Language::ALL]);

// Get specific languages
$response = $client->getProducts(lang: [Language::DEFAULT, Language::CS, Language::SK]);

// Iterate with language filter
foreach ($client->iterateProducts(lang: [Language::ALL]) as $product) {
echo $product->name->get(Language::CS);
echo $product->content?->getHtml(Language::CS);
}

// Same for categories and blogs
$response = $client->getCategories(lang: [Language::DEFAULT, Language::CS]);
$response = $client->getBlogs(lang: [Language::ALL]);
```

> **Note:** Without the `lang` parameter, only `default` is returned. Invalid language values are silently ignored.

## Content (HTML/Marketplace/Nested)

By default, only `content.html` is returned. Use the `include` parameter to request additional content:

| Value | Description |
|---------------|----------------------------------------------|
| `marketplace` | HTML content for marketplace (no custom CSS) |
| `nested` | Raw widget JSON from widget tables |
| Value | Description | Available for |
|----------------|---------------------------------------------------|--------------------------|
| `marketplace` | HTML content for marketplace (no custom CSS) | product, category, blog |
| `nested` | Raw widget JSON from widget tables | product, category, blog |
| `site_link` | Anchor navigation on H2 headings | product, blog |
| `rich_snippet` | JSON-LD structured data (FAQPage) | product, category, blog |

```php
use Pobo\Sdk\Enum\IncludeContent;
Expand Down Expand Up @@ -344,6 +372,55 @@ foreach ($client->iterateBlogs(include: [IncludeContent::MARKETPLACE]) as $blog)
}
```

## Site Links

Anchor navigation generated from H2 headings in content widgets. Available for products and blogs.

```php
use Pobo\Sdk\Enum\IncludeContent;
use Pobo\Sdk\Enum\Language;

foreach ($client->iterateProducts(include: [IncludeContent::SITE_LINK], lang: [Language::ALL]) as $product) {
if ($product->siteLink !== null) {
// Get rendered navigation HTML
$navHtml = $product->siteLink->getHtml(Language::DEFAULT);

// Get structured list of headings
$items = $product->siteLink->getList(Language::DEFAULT);
foreach ($items as $item) {
echo sprintf('<a href="#%s">%s</a>', $item->slug, $item->heading);
}
}
}
```

## Rich Snippets

JSON-LD structured data (FAQPage schema) generated from FAQ widgets. Available for products, categories, and blogs.

```php
use Pobo\Sdk\Enum\IncludeContent;
use Pobo\Sdk\Enum\Language;

foreach ($client->iterateProducts(include: [IncludeContent::RICH_SNIPPET], lang: [Language::ALL]) as $product) {
if ($product->richSnippet !== null) {
// Get rendered JSON-LD script tag
$scriptHtml = $product->richSnippet->getHtml(Language::DEFAULT);

// Get parsed JSON-LD object
$jsonLd = $product->richSnippet->getJson(Language::DEFAULT);
echo $jsonLd['@type']; // 'FAQPage'
}
}

// Categories have rich snippets but no site links
foreach ($client->iterateCategories(include: [IncludeContent::RICH_SNIPPET]) as $category) {
if ($category->richSnippet !== null) {
echo $category->richSnippet->getHtml(Language::DEFAULT);
}
}
```

## Webhook Handler

### Basic Usage
Expand Down Expand Up @@ -443,21 +520,21 @@ $name->toArray(); // ['default' => '...', 'cs' => '...', ...]

## API Methods

| Method | Description |
|---------------------------------------------------------------------------------------------------------|----------------------------------|
| `importProducts(array $products)` | Bulk import products (max 100) |
| `importCategories(array $categories)` | Bulk import categories (max 100) |
| `importParameters(array $parameters)` | Bulk import parameters (max 100) |
| `importBlogs(array $blogs)` | Bulk import blogs (max 100) |
| `deleteProducts(array $ids)` | Bulk delete products (max 100) |
| `deleteCategories(array $ids)` | Bulk delete categories (max 100) |
| `deleteBlogs(array $ids)` | Bulk delete blogs (max 100) |
| `getProducts(?int $page, ?int $perPage, ?DateTime $lastUpdateFrom, ?bool $isEdited, ?array $include)` | Get products page |
| `getCategories(?int $page, ?int $perPage, ?DateTime $lastUpdateFrom, ?bool $isEdited, ?array $include)` | Get categories page |
| `getBlogs(?int $page, ?int $perPage, ?DateTime $lastUpdateFrom, ?bool $isEdited, ?array $include)` | Get blogs page |
| `iterateProducts(?DateTime $lastUpdateFrom, ?bool $isEdited, ?array $include)` | Iterate all products |
| `iterateCategories(?DateTime $lastUpdateFrom, ?bool $isEdited, ?array $include)` | Iterate all categories |
| `iterateBlogs(?DateTime $lastUpdateFrom, ?bool $isEdited, ?array $include)` | Iterate all blogs |
| Method | Description |
|-----------------------------------------------------------------------------------------------------------------------|----------------------------------|
| `importProducts(array $products)` | Bulk import products (max 100) |
| `importCategories(array $categories)` | Bulk import categories (max 100) |
| `importParameters(array $parameters)` | Bulk import parameters (max 100) |
| `importBlogs(array $blogs)` | Bulk import blogs (max 100) |
| `deleteProducts(array $ids)` | Bulk delete products (max 100) |
| `deleteCategories(array $ids)` | Bulk delete categories (max 100) |
| `deleteBlogs(array $ids)` | Bulk delete blogs (max 100) |
| `getProducts(?int $page, ?int $perPage, ?DateTime $lastUpdateFrom, ?bool $isEdited, ?array $include, ?array $lang)` | Get products page |
| `getCategories(?int $page, ?int $perPage, ?DateTime $lastUpdateFrom, ?bool $isEdited, ?array $include, ?array $lang)` | Get categories page |
| `getBlogs(?int $page, ?int $perPage, ?DateTime $lastUpdateFrom, ?bool $isEdited, ?array $include, ?array $lang)` | Get blogs page |
| `iterateProducts(?DateTime $lastUpdateFrom, ?bool $isEdited, ?array $include, ?array $lang)` | Iterate all products |
| `iterateCategories(?DateTime $lastUpdateFrom, ?bool $isEdited, ?array $include, ?array $lang)` | Iterate all categories |
| `iterateBlogs(?DateTime $lastUpdateFrom, ?bool $isEdited, ?array $include, ?array $lang)` | Iterate all blogs |

## Limits

Expand All @@ -470,7 +547,7 @@ $name->toArray(); // ['default' => '...', 'cs' => '...', ...]
| Name length | 250 chars |
| URL length | 255 chars |
| Image URL length | 650 chars |
| Description length | 65,000 chars |
| Description length | 500,000 chars |
| SEO description length | 500 chars |

## License
Expand Down
6 changes: 4 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
"ext-json": "*"
},
"require-dev": {
"phpunit/phpunit": "^10.0"
"phpunit/phpunit": "^10.0",
"phpstan/phpstan": "^2.0"
},
"autoload": {
"psr-4": {
Expand All @@ -41,7 +42,8 @@
}
},
"scripts": {
"test": "phpunit"
"test": "phpunit",
"phpstan": "phpstan analyse"
},
"minimum-stability": "stable",
"prefer-stable": true
Expand Down
5 changes: 5 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
parameters:
level: 9
paths:
- src
phpVersion: 80100
24 changes: 24 additions & 0 deletions src/DTO/Blog.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,25 @@

namespace Pobo\Sdk\DTO;

/**
* @phpstan-type BlogData array{
* id: string,
* is_visible: bool,
* name: array<string, string|null>,
* url: array<string, string|null>,
* category?: string|null,
* description?: array<string, string|null>,
* seo_title?: array<string, string|null>,
* seo_description?: array<string, string|null>,
* content?: array<string, mixed>,
* site_link?: array<string, mixed>,
* rich_snippet?: array<string, mixed>,
* images?: array<string>,
* is_loaded?: bool,
* created_at?: string,
* updated_at?: string,
* }
*/
final class Blog
{
/**
Expand All @@ -19,6 +38,8 @@ public function __construct(
public readonly ?LocalizedString $seoTitle = null,
public readonly ?LocalizedString $seoDescription = null,
public readonly ?Content $content = null,
public readonly ?SiteLink $siteLink = null,
public readonly ?RichSnippet $richSnippet = null,
public readonly array $images = [],
public readonly ?bool $isLoaded = null,
public readonly ?\DateTimeInterface $createdAt = null,
Expand Down Expand Up @@ -66,6 +87,7 @@ public function toArray(): array
*/
public static function fromArray(array $data): self
{
/** @var BlogData $data */
return new self(
id: $data['id'],
isVisible: $data['is_visible'],
Expand All @@ -76,6 +98,8 @@ public static function fromArray(array $data): self
seoTitle: isset($data['seo_title']) ? LocalizedString::fromArray($data['seo_title']) : null,
seoDescription: isset($data['seo_description']) ? LocalizedString::fromArray($data['seo_description']) : null,
content: isset($data['content']) ? Content::fromArray($data['content']) : null,
siteLink: isset($data['site_link']) ? SiteLink::fromArray($data['site_link']) : null,
richSnippet: isset($data['rich_snippet']) ? RichSnippet::fromArray($data['rich_snippet']) : null,
images: $data['images'] ?? [],
isLoaded: $data['is_loaded'] ?? null,
createdAt: isset($data['created_at']) ? new \DateTimeImmutable($data['created_at']) : null,
Expand Down
21 changes: 21 additions & 0 deletions src/DTO/Category.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,24 @@

namespace Pobo\Sdk\DTO;

/**
* @phpstan-type CategoryData array{
* id: string,
* is_visible: bool,
* name: array<string, string|null>,
* url: array<string, string|null>,
* description?: array<string, string|null>,
* seo_title?: array<string, string|null>,
* seo_description?: array<string, string|null>,
* content?: array<string, mixed>,
* rich_snippet?: array<string, mixed>,
* images?: array<string>,
* guid?: string|null,
* is_loaded?: bool,
* created_at?: string,
* updated_at?: string,
* }
*/
final class Category
{
/**
Expand All @@ -18,6 +36,7 @@ public function __construct(
public readonly ?LocalizedString $seoTitle = null,
public readonly ?LocalizedString $seoDescription = null,
public readonly ?Content $content = null,
public readonly ?RichSnippet $richSnippet = null,
public readonly array $images = [],
public readonly ?string $guid = null,
public readonly ?bool $isLoaded = null,
Expand Down Expand Up @@ -62,6 +81,7 @@ public function toArray(): array
*/
public static function fromArray(array $data): self
{
/** @var CategoryData $data */
return new self(
id: $data['id'],
isVisible: $data['is_visible'],
Expand All @@ -71,6 +91,7 @@ public static function fromArray(array $data): self
seoTitle: isset($data['seo_title']) ? LocalizedString::fromArray($data['seo_title']) : null,
seoDescription: isset($data['seo_description']) ? LocalizedString::fromArray($data['seo_description']) : null,
content: isset($data['content']) ? Content::fromArray($data['content']) : null,
richSnippet: isset($data['rich_snippet']) ? RichSnippet::fromArray($data['rich_snippet']) : null,
images: $data['images'] ?? [],
guid: $data['guid'] ?? null,
isLoaded: $data['is_loaded'] ?? null,
Expand Down
1 change: 1 addition & 0 deletions src/DTO/Content.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ public function toArray(): array
*/
public static function fromArray(array $data): self
{
/** @var array{html?: array<string, string>, marketplace?: array<string, string>, nested?: array<int, array<mixed>>} $data */
return new self(
html: $data['html'] ?? [],
marketplace: $data['marketplace'] ?? [],
Expand Down
9 changes: 9 additions & 0 deletions src/DTO/DeleteResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@

namespace Pobo\Sdk\DTO;

/**
* @phpstan-type DeleteResultData array{
* success?: bool,
* deleted?: int,
* skipped?: int,
* errors?: array<array{index: int, id: string, errors: array<string>}>,
* }
*/
final class DeleteResult
{
/**
Expand All @@ -27,6 +35,7 @@ public function hasErrors(): bool
*/
public static function fromArray(array $data): self
{
/** @var DeleteResultData $data */
return new self(
success: $data['success'] ?? false,
deleted: $data['deleted'] ?? 0,
Expand Down
12 changes: 12 additions & 0 deletions src/DTO/ImportResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@

namespace Pobo\Sdk\DTO;

/**
* @phpstan-type ImportResultData array{
* success?: bool,
* imported?: int,
* updated?: int,
* skipped?: int,
* errors?: array<array{index: int, id: string, errors: array<string>}>,
* values_imported?: int,
* values_updated?: int,
* }
*/
final class ImportResult
{
/**
Expand All @@ -30,6 +41,7 @@ public function hasErrors(): bool
*/
public static function fromArray(array $data): self
{
/** @var ImportResultData $data */
return new self(
success: $data['success'] ?? false,
imported: $data['imported'] ?? 0,
Expand Down
3 changes: 2 additions & 1 deletion src/DTO/LocalizedString.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,11 @@ public function toArray(): array
}

/**
* @param array<string, string|null> $data
* @param array<string, mixed> $data
*/
public static function fromArray(array $data): self
{
/** @var array<string, string|null> $data */
return new self($data);
}
}
Loading
Loading