diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 506763f..0ad534a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,7 +13,7 @@ jobs: strategy: fail-fast: false matrix: - php: ['8.1', '8.2', '8.3'] + php: ['8.1', '8.2', '8.3', '8.4', '8.5'] name: PHP ${{ matrix.php }} diff --git a/README.md b/README.md index db5a1a1..0ab487a 100644 --- a/README.md +++ b/README.md @@ -200,6 +200,38 @@ $result = $client->importBlogs($blogs); echo sprintf('Imported: %d, Updated: %d', $result->imported, $result->updated); ``` +## Delete + +### Delete Products + +```php +$result = $client->deleteProducts(['PROD-001', 'PROD-002', 'PROD-003']); + +echo sprintf('Deleted: %d, Skipped: %d', $result->deleted, $result->skipped); + +if ($result->hasErrors() === true) { + foreach ($result->errors as $error) { + echo sprintf('ID %s: %s', $error['id'], implode(', ', $error['errors'])); + } +} +``` + +### Delete Categories + +```php +$result = $client->deleteCategories(['CAT-001', 'CAT-002']); +echo sprintf('Deleted: %d', $result->deleted); +``` + +### Delete Blogs + +```php +$result = $client->deleteBlogs(['BLOG-001', 'BLOG-002']); +echo sprintf('Deleted: %d', $result->deleted); +``` + +> **Note:** Delete performs a soft-delete. + ## Export ### Export Products @@ -400,6 +432,9 @@ $name->toArray(); // ['default' => '...', 'cs' => '...', ...] | `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)` | Get products page | | `getCategories(?int $page, ?int $perPage, ?DateTime $lastUpdateFrom, ?bool $isEdited)` | Get categories page | | `getBlogs(?int $page, ?int $perPage, ?DateTime $lastUpdateFrom, ?bool $isEdited)` | Get blogs page | @@ -412,6 +447,7 @@ $name->toArray(); // ['default' => '...', 'cs' => '...', ...] | Limit | Value | |------------------------------|--------------| | Max items per import request | 100 | +| Max items per delete request | 100 | | Max items per export page | 100 | | Product/Category ID length | 255 chars | | Name length | 250 chars | diff --git a/src/DTO/DeleteResult.php b/src/DTO/DeleteResult.php new file mode 100644 index 0000000..c1842b4 --- /dev/null +++ b/src/DTO/DeleteResult.php @@ -0,0 +1,37 @@ +}> $errors + */ + public function __construct( + public readonly bool $success, + public readonly int $deleted, + public readonly int $skipped, + public readonly array $errors = [], + ) { + } + + public function hasErrors(): bool + { + return $this->errors !== []; + } + + /** + * @param array $data + */ + public static function fromArray(array $data): self + { + return new self( + success: $data['success'] ?? false, + deleted: $data['deleted'] ?? 0, + skipped: $data['skipped'] ?? 0, + errors: $data['errors'] ?? [], + ); + } +} diff --git a/src/Exception/ValidationException.php b/src/Exception/ValidationException.php index e20a491..b696d7c 100644 --- a/src/Exception/ValidationException.php +++ b/src/Exception/ValidationException.php @@ -20,7 +20,7 @@ public static function tooManyItems(int $count, int $max): self { return new self( sprintf('Too many items: %d provided, maximum is %d', $count, $max), - ['bulk' => [sprintf('Maximum %d items allowed for bulk import', $max)]] + ['bulk' => [sprintf('Maximum %d items allowed per request', $max)]] ); } diff --git a/src/PoboClient.php b/src/PoboClient.php index 0e0819f..446daf8 100644 --- a/src/PoboClient.php +++ b/src/PoboClient.php @@ -6,6 +6,7 @@ use Pobo\Sdk\DTO\Blog; use Pobo\Sdk\DTO\Category; +use Pobo\Sdk\DTO\DeleteResult; use Pobo\Sdk\DTO\ImportResult; use Pobo\Sdk\DTO\PaginatedResponse; use Pobo\Sdk\DTO\Parameter; @@ -140,6 +141,51 @@ public function getBlogs( return PaginatedResponse::fromArray($response, Blog::class); } + /** + * @param array $ids + * @throws ValidationException + * @throws ApiException + */ + public function deleteProducts(array $ids): DeleteResult + { + $this->validateBulkSize($ids); + + $payload = array_map(fn(string $id) => ['id' => $id], $ids); + + $response = $this->request('DELETE', '/api/v2/rest/products', $payload); + return DeleteResult::fromArray($response); + } + + /** + * @param array $ids + * @throws ValidationException + * @throws ApiException + */ + public function deleteCategories(array $ids): DeleteResult + { + $this->validateBulkSize($ids); + + $payload = array_map(fn(string $id) => ['id' => $id], $ids); + + $response = $this->request('DELETE', '/api/v2/rest/categories', $payload); + return DeleteResult::fromArray($response); + } + + /** + * @param array $ids + * @throws ValidationException + * @throws ApiException + */ + public function deleteBlogs(array $ids): DeleteResult + { + $this->validateBulkSize($ids); + + $payload = array_map(fn(string $id) => ['id' => $id], $ids); + + $response = $this->request('DELETE', '/api/v2/rest/blogs', $payload); + return DeleteResult::fromArray($response); + } + /** * @return \Generator * @throws ApiException @@ -275,6 +321,11 @@ private function request(string $method, string $endpoint, ?array $data = null): if ($data !== null) { curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); } + } elseif ($method === 'DELETE') { + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); + if ($data !== null) { + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); + } } $response = curl_exec($ch); diff --git a/tests/DTO/DeleteResultTest.php b/tests/DTO/DeleteResultTest.php new file mode 100644 index 0000000..6931880 --- /dev/null +++ b/tests/DTO/DeleteResultTest.php @@ -0,0 +1,86 @@ + true, + 'deleted' => 3, + 'skipped' => 1, + 'errors' => [], + ]; + + $result = DeleteResult::fromArray($data); + + $this->assertTrue($result->success); + $this->assertSame(3, $result->deleted); + $this->assertSame(1, $result->skipped); + $this->assertSame([], $result->errors); + $this->assertFalse($result->hasErrors()); + } + + public function testFromArrayWithErrors(): void + { + $data = [ + 'success' => true, + 'deleted' => 2, + 'skipped' => 1, + 'errors' => [ + [ + 'index' => 2, + 'id' => 'PROD-999', + 'errors' => ['Product not found'], + ], + ], + ]; + + $result = DeleteResult::fromArray($data); + + $this->assertTrue($result->hasErrors()); + $this->assertCount(1, $result->errors); + $this->assertSame(2, $result->errors[0]['index']); + $this->assertSame('PROD-999', $result->errors[0]['id']); + } + + public function testFromArrayWithDefaults(): void + { + $result = DeleteResult::fromArray([]); + + $this->assertFalse($result->success); + $this->assertSame(0, $result->deleted); + $this->assertSame(0, $result->skipped); + $this->assertSame([], $result->errors); + } + + public function testHasErrorsReturnsFalseForEmptyErrors(): void + { + $result = new DeleteResult( + success: true, + deleted: 1, + skipped: 0, + errors: [], + ); + + $this->assertFalse($result->hasErrors()); + } + + public function testHasErrorsReturnsTrueForNonEmptyErrors(): void + { + $result = new DeleteResult( + success: true, + deleted: 0, + skipped: 1, + errors: [['index' => 0, 'id' => 'X', 'errors' => ['Not found']]], + ); + + $this->assertTrue($result->hasErrors()); + } +} diff --git a/tests/PoboClientTest.php b/tests/PoboClientTest.php index 323ea62..49f02f4 100644 --- a/tests/PoboClientTest.php +++ b/tests/PoboClientTest.php @@ -7,6 +7,7 @@ use PHPUnit\Framework\TestCase; use Pobo\Sdk\DTO\Blog; use Pobo\Sdk\DTO\Category; +use Pobo\Sdk\DTO\DeleteResult; use Pobo\Sdk\DTO\LocalizedString; use Pobo\Sdk\DTO\Parameter; use Pobo\Sdk\DTO\ParameterValue; @@ -246,4 +247,84 @@ public function testBlogDtoIsConvertedToArray(): void $this->assertSame('news', $array['category']); $this->assertTrue($array['is_visible']); } + + public function testDeleteProductsThrowsExceptionForEmptyArray(): void + { + $this->expectException(ValidationException::class); + $this->expectExceptionMessage('Payload cannot be empty'); + + $this->client->deleteProducts([]); + } + + public function testDeleteProductsThrowsExceptionForTooManyItems(): void + { + $ids = []; + for ($i = 0; $i < 101; $i++) { + $ids[] = sprintf('PROD-%03d', $i); + } + + $this->expectException(ValidationException::class); + $this->expectExceptionMessage('Too many items: 101 provided, maximum is 100'); + + $this->client->deleteProducts($ids); + } + + public function testDeleteCategoriesThrowsExceptionForEmptyArray(): void + { + $this->expectException(ValidationException::class); + $this->expectExceptionMessage('Payload cannot be empty'); + + $this->client->deleteCategories([]); + } + + public function testDeleteCategoriesThrowsExceptionForTooManyItems(): void + { + $ids = []; + for ($i = 0; $i < 101; $i++) { + $ids[] = sprintf('CAT-%03d', $i); + } + + $this->expectException(ValidationException::class); + $this->expectExceptionMessage('Too many items: 101 provided, maximum is 100'); + + $this->client->deleteCategories($ids); + } + + public function testDeleteBlogsThrowsExceptionForEmptyArray(): void + { + $this->expectException(ValidationException::class); + $this->expectExceptionMessage('Payload cannot be empty'); + + $this->client->deleteBlogs([]); + } + + public function testDeleteBlogsThrowsExceptionForTooManyItems(): void + { + $ids = []; + for ($i = 0; $i < 101; $i++) { + $ids[] = sprintf('BLOG-%03d', $i); + } + + $this->expectException(ValidationException::class); + $this->expectExceptionMessage('Too many items: 101 provided, maximum is 100'); + + $this->client->deleteBlogs($ids); + } + + public function testDeleteResultFromArray(): void + { + $result = DeleteResult::fromArray([ + 'success' => true, + 'deleted' => 2, + 'skipped' => 1, + 'errors' => [ + ['index' => 2, 'id' => 'PROD-999', 'errors' => ['Product not found']], + ], + ]); + + $this->assertTrue($result->success); + $this->assertSame(2, $result->deleted); + $this->assertSame(1, $result->skipped); + $this->assertTrue($result->hasErrors()); + } }