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
655 changes: 231 additions & 424 deletions README.md

Large diffs are not rendered by default.

109 changes: 109 additions & 0 deletions src/DTO/Brand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php

declare(strict_types=1);

namespace Pobo\Sdk\DTO;

/**
* @phpstan-type BrandData array{
* id: string,
* is_visible: bool,
* name: array<string, string|null>,
* url: array<string, string|null>,
* image_preview?: 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>,
* guid?: string|null,
* is_loaded?: bool,
* created_at?: string,
* updated_at?: string,
* }
*/
final class Brand
{
/**
* Sentinel meaning "do not send image_preview in the import payload" — the
* server will leave brand.image_preview unchanged. Distinct from passing
* null, which explicitly clears the logo in the DB.
*/
public const IMAGE_PREVIEW_UNSET = "\x00POBO_IMAGE_PREVIEW_UNSET\x00";

public function __construct(
public readonly string $id,
public readonly bool $isVisible,
public readonly LocalizedString $name,
public readonly LocalizedString $url,
public readonly ?string $imagePreview = self::IMAGE_PREVIEW_UNSET,
public readonly ?LocalizedString $description = null,
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 ?string $guid = null,
public readonly ?bool $isLoaded = null,
public readonly ?\DateTimeInterface $createdAt = null,
public readonly ?\DateTimeInterface $updatedAt = null,
) {
}

/**
* @return array<string, mixed>
*/
public function toArray(): array
{
$data = [
'id' => $this->id,
'is_visible' => $this->isVisible,
'name' => $this->name->toArray(),
'url' => $this->url->toArray(),
];

if ($this->imagePreview !== self::IMAGE_PREVIEW_UNSET) {
$data['image_preview'] = $this->imagePreview;
}

if ($this->description !== null) {
$data['description'] = $this->description->toArray();
}

if ($this->seoTitle !== null) {
$data['seo_title'] = $this->seoTitle->toArray();
}

if ($this->seoDescription !== null) {
$data['seo_description'] = $this->seoDescription->toArray();
}

return $data;
}

/**
* @param array<string, mixed> $data
*/
public static function fromArray(array $data): self
{
/** @var BrandData $data */
return new self(
id: $data['id'],
isVisible: $data['is_visible'],
name: LocalizedString::fromArray($data['name']),
url: LocalizedString::fromArray($data['url']),
imagePreview: array_key_exists('image_preview', $data) ? $data['image_preview'] : self::IMAGE_PREVIEW_UNSET,
description: isset($data['description']) ? LocalizedString::fromArray($data['description']) : null,
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,
guid: $data['guid'] ?? null,
isLoaded: $data['is_loaded'] ?? null,
createdAt: isset($data['created_at']) ? new \DateTimeImmutable($data['created_at']) : null,
updatedAt: isset($data['updated_at']) ? new \DateTimeImmutable($data['updated_at']) : null,
);
}
}
4 changes: 2 additions & 2 deletions src/DTO/PaginatedResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
namespace Pobo\Sdk\DTO;

/**
* @template T of Product|Category|Blog
* @template T of Product|Category|Blog|Brand
*/
final class PaginatedResponse
{
Expand All @@ -31,7 +31,7 @@ public function getTotalPages(): int
}

/**
* @template TEntity of Product|Category|Blog
* @template TEntity of Product|Category|Blog|Brand
* @param array<string, mixed> $response
* @param class-string<TEntity> $entityClass
* @return self<TEntity>
Expand Down
42 changes: 38 additions & 4 deletions src/DTO/Product.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
* images?: array<string>,
* categories_ids?: array<string>,
* parameters_ids?: array<int>,
* brand_id?: string|null,
* guid?: string|null,
* is_loaded?: bool,
* categories?: array<array{id: string, name: array<string, string>}>,
Expand All @@ -29,10 +30,23 @@
*/
final class Product
{
/**
* Sentinel meaning "do not send brand_id in the import payload" — the server
* will leave product.brand_id unchanged. Distinct from passing null, which
* explicitly clears the brand assignment in the DB.
*/
public const BRAND_ID_UNSET = "\x00POBO_BRAND_ID_UNSET\x00";

/** @var array<string> */
public readonly array $categoriesIds;

/** @var array<int> */
public readonly array $parametersIds;

/**
* @param array<string> $images
* @param array<string> $categoriesIds
* @param array<int> $parametersIds
* @param array<string|int|null> $categoriesIds Empty strings and nulls are filtered defensively
* @param array<int|string|null> $parametersIds Empty strings and nulls are filtered defensively
* @param array<array{id: string, name: array<string, string>}> $categories
*/
public function __construct(
Expand All @@ -48,14 +62,29 @@ public function __construct(
public readonly ?SiteLink $siteLink = null,
public readonly ?RichSnippet $richSnippet = null,
public readonly array $images = [],
public readonly array $categoriesIds = [],
public readonly array $parametersIds = [],
array $categoriesIds = [],
array $parametersIds = [],
public readonly ?string $brandId = self::BRAND_ID_UNSET,
public readonly ?string $guid = null,
public readonly ?bool $isLoaded = null,
public readonly array $categories = [],
public readonly ?\DateTimeInterface $createdAt = null,
public readonly ?\DateTimeInterface $updatedAt = null,
) {
$this->categoriesIds = array_values(array_map(
static fn(mixed $value): string => (string) $value,
array_filter(
$categoriesIds,
static fn(mixed $value): bool => $value !== '' && $value !== null,
),
));
$this->parametersIds = array_values(array_map(
static fn(mixed $value): int => (int) $value,
array_filter(
$parametersIds,
static fn(mixed $value): bool => $value !== '' && $value !== null,
),
));
}

/**
Expand Down Expand Up @@ -98,6 +127,10 @@ public function toArray(): array
$data['parameters_ids'] = $this->parametersIds;
}

if ($this->brandId !== self::BRAND_ID_UNSET) {
$data['brand_id'] = $this->brandId;
}

return $data;
}

Expand All @@ -122,6 +155,7 @@ public static function fromArray(array $data): self
images: $data['images'] ?? [],
categoriesIds: $data['categories_ids'] ?? [],
parametersIds: $data['parameters_ids'] ?? [],
brandId: array_key_exists('brand_id', $data) ? $data['brand_id'] : self::BRAND_ID_UNSET,
guid: $data['guid'] ?? null,
isLoaded: $data['is_loaded'] ?? null,
categories: $data['categories'] ?? [],
Expand Down
78 changes: 78 additions & 0 deletions src/PoboClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Pobo\Sdk;

use Pobo\Sdk\DTO\Blog;
use Pobo\Sdk\DTO\Brand;
use Pobo\Sdk\DTO\Category;
use Pobo\Sdk\DTO\DeleteResult;
use Pobo\Sdk\DTO\ImportResult;
Expand Down Expand Up @@ -83,6 +84,24 @@ public function importParameters(array $parameters): ImportResult
return ImportResult::fromArray($response);
}

/**
* @param array<Brand|array<string, mixed>> $brands
* @throws ValidationException
* @throws ApiException
*/
public function importBrands(array $brands): ImportResult
{
$this->validateBulkSize($brands);

$payload = array_map(
fn($brand) => $brand instanceof Brand ? $brand->toArray() : $brand,
$brands
);

$response = $this->request('POST', '/api/v2/rest/brands', $payload);
return ImportResult::fromArray($response);
}

/**
* @param array<Blog|array<string, mixed>> $blogs
* @throws ValidationException
Expand Down Expand Up @@ -139,6 +158,25 @@ public function getCategories(
return PaginatedResponse::fromArray($response, Category::class);
}

/**
* @param array<IncludeContent|string>|null $include Optional content to include: IncludeContent::MARKETPLACE, IncludeContent::NESTED, IncludeContent::SITE_LINK, IncludeContent::RICH_SNIPPET
* @param array<Language|string>|null $lang Languages to include in response. null = only default, [Language::ALL] = all languages
* @return PaginatedResponse<Brand>
* @throws ApiException
*/
public function getBrands(
?int $page = null,
?int $perPage = null,
?\DateTimeInterface $lastUpdateFrom = null,
?bool $isEdited = null,
?array $include = null,
?array $lang = null,
): PaginatedResponse {
$query = $this->buildQueryParams($page, $perPage, $lastUpdateFrom, $isEdited, $include, $lang);
$response = $this->request('GET', '/api/v2/rest/brands' . $query);
return PaginatedResponse::fromArray($response, Brand::class);
}

/**
* @param array<IncludeContent|string>|null $include Optional content to include: IncludeContent::MARKETPLACE, IncludeContent::NESTED, IncludeContent::SITE_LINK, IncludeContent::RICH_SNIPPET
* @param array<Language|string>|null $lang Languages to include in response. null = only default, [Language::ALL] = all languages
Expand Down Expand Up @@ -188,6 +226,21 @@ public function deleteCategories(array $ids): DeleteResult
return DeleteResult::fromArray($response);
}

/**
* @param array<string> $ids
* @throws ValidationException
* @throws ApiException
*/
public function deleteBrands(array $ids): DeleteResult
{
$this->validateBulkSize($ids);

$payload = array_map(fn(string $id) => ['id' => $id], $ids);

$response = $this->request('DELETE', '/api/v2/rest/brands', $payload);
return DeleteResult::fromArray($response);
}

/**
* @param array<string> $ids
* @throws ValidationException
Expand Down Expand Up @@ -253,6 +306,31 @@ public function iterateCategories(
} while ($response->hasMorePages());
}

/**
* @param array<IncludeContent|string>|null $include Optional content to include
* @param array<Language|string>|null $lang Languages to include in response
* @return \Generator<Brand>
* @throws ApiException
*/
public function iterateBrands(
?\DateTimeInterface $lastUpdateFrom = null,
?bool $isEdited = null,
?array $include = null,
?array $lang = null,
): \Generator {
$page = 1;

do {
$response = $this->getBrands($page, self::MAX_BULK_ITEMS, $lastUpdateFrom, $isEdited, $include, $lang);

foreach ($response->data as $brand) {
yield $brand;
}

$page++;
} while ($response->hasMorePages());
}

/**
* @param array<IncludeContent|string>|null $include Optional content to include
* @param array<Language|string>|null $lang Languages to include in response
Expand Down
Loading
Loading