Files
ladill-woo-manager/app/Services/Woo/CatalogWriteService.php
T
isaaccladandCursor 551473f8e5
Deploy Ladill Woo Manager / deploy (push) Successful in 1m16s
Preserve WooCommerce HTML in product descriptions with a rich text editor.
Stop stripping tags on sync so paragraphs and bold book titles round-trip to Woo.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 00:47:01 +00:00

223 lines
8.0 KiB
PHP

<?php
namespace App\Services\Woo;
use App\Models\WooCategory;
use App\Models\WooProduct;
use App\Models\WooStore;
use App\Support\WooHtml;
class CatalogWriteService
{
public function __construct(
private PluginClient $client,
private ProductIngestService $productIngest,
private CategoryIngestService $categoryIngest,
) {}
/** @param array<string, mixed> $input */
public function createProduct(WooStore $store, array $input): WooProduct
{
$payload = $this->productPayload($input);
$response = $this->client->post($store, 'products', $payload);
if (is_array($response) && isset($response['id'])) {
return $this->productIngest->ingest($store, $response);
}
return $this->createLocalProduct($store, $payload);
}
/** @param array<string, mixed> $input */
public function updateProduct(WooProduct $product, array $input): WooProduct
{
$product->loadMissing('store');
$store = $product->store;
abort_if(! $store, 404);
$payload = $this->productPayload($input);
$response = $this->client->put($store, 'products/'.$product->external_id, $payload);
if (is_array($response) && isset($response['id'])) {
return $this->productIngest->ingest($store, $response);
}
$product->update($this->localProductAttributes($payload));
return $product->fresh();
}
/** @param array<string, mixed> $input */
public function createCategory(WooStore $store, array $input): WooCategory
{
$payload = $this->categoryPayload($input);
$response = $this->client->post($store, 'categories', $payload);
if (is_array($response) && isset($response['id'])) {
return $this->categoryIngest->ingest($store, $response);
}
return $this->createLocalCategory($store, $payload);
}
/** @param array<string, mixed> $input */
public function updateCategory(WooCategory $category, array $input): WooCategory
{
$category->loadMissing('store');
$store = $category->store;
abort_if(! $store, 404);
$payload = $this->categoryPayload($input);
$response = $this->client->put($store, 'categories/'.$category->external_id, $payload);
if (is_array($response) && isset($response['id'])) {
return $this->categoryIngest->ingest($store, $response);
}
$category->update($this->localCategoryAttributes($payload));
return $category->fresh();
}
public function deleteCategory(WooCategory $category): bool
{
$category->loadMissing('store');
$store = $category->store;
abort_if(! $store, 404);
if ($this->client->delete($store, 'categories/'.$category->external_id)) {
$category->delete();
return true;
}
$category->delete();
return true;
}
/** @param array<string, mixed> $input */
private function productPayload(array $input): array
{
$categories = collect($input['category_external_ids'] ?? [])
->map(fn ($id) => ['id' => (int) $id])
->filter(fn (array $row) => $row['id'] > 0)
->values()
->all();
return array_filter([
'name' => (string) ($input['name'] ?? ''),
'sku' => $input['sku'] ?? null,
'status' => (string) ($input['status'] ?? WooProduct::STATUS_DRAFT),
'regular_price' => $this->majorPrice($input['regular_price'] ?? null),
'sale_price' => $this->majorPrice($input['sale_price'] ?? null),
'manage_stock' => (bool) ($input['manage_stock'] ?? false),
'stock_quantity' => isset($input['stock_quantity']) ? (int) $input['stock_quantity'] : null,
'short_description' => WooHtml::sanitize($input['short_description'] ?? null),
'description' => WooHtml::sanitize($input['description'] ?? null),
'categories' => $categories,
'images' => $input['images'] ?? null,
], fn ($value) => $value !== null && $value !== '');
}
/** @param array<string, mixed> $input */
private function categoryPayload(array $input): array
{
$parent = (int) ($input['parent_external_id'] ?? 0);
return array_filter([
'name' => (string) ($input['name'] ?? ''),
'slug' => $input['slug'] ?? null,
'description' => WooHtml::sanitize($input['description'] ?? null),
'parent' => $parent > 0 ? $parent : 0,
], fn ($value) => $value !== null && $value !== '');
}
/** @param array<string, mixed> $payload */
private function createLocalProduct(WooStore $store, array $payload): WooProduct
{
$externalId = (int) (WooProduct::query()->where('woo_store_id', $store->id)->max('external_id') ?? 0) + 1;
return WooProduct::query()->create(array_merge(
$this->localProductAttributes($payload),
[
'woo_store_id' => $store->id,
'external_id' => $externalId,
'slug' => \Illuminate\Support\Str::slug((string) ($payload['name'] ?? 'product')).'-'.$externalId,
],
));
}
/** @param array<string, mixed> $payload */
private function createLocalCategory(WooStore $store, array $payload): WooCategory
{
$externalId = (int) (WooCategory::query()->where('woo_store_id', $store->id)->max('external_id') ?? 0) + 1;
$parent = (int) ($payload['parent'] ?? 0);
return WooCategory::query()->create(array_merge(
$this->localCategoryAttributes($payload),
[
'woo_store_id' => $store->id,
'external_id' => $externalId,
'parent_external_id' => $parent > 0 ? $parent : null,
'slug' => (string) ($payload['slug'] ?? \Illuminate\Support\Str::slug((string) ($payload['name'] ?? 'category')).'-'.$externalId),
],
));
}
/** @param array<string, mixed> $payload */
private function localProductAttributes(array $payload): array
{
$categoryIds = collect($payload['categories'] ?? [])
->map(fn (array $row) => (int) ($row['id'] ?? 0))
->filter(fn (int $id) => $id > 0)
->values()
->all();
return [
'name' => (string) ($payload['name'] ?? 'Product'),
'sku' => $payload['sku'] ?? null,
'status' => (string) ($payload['status'] ?? WooProduct::STATUS_DRAFT),
'regular_price_minor' => $this->minorPrice($payload['regular_price'] ?? null),
'sale_price_minor' => $this->minorPrice($payload['sale_price'] ?? null),
'manage_stock' => (bool) ($payload['manage_stock'] ?? false),
'stock_quantity' => isset($payload['stock_quantity']) ? (int) $payload['stock_quantity'] : null,
'short_description' => $payload['short_description'] ?? null,
'description' => $payload['description'] ?? null,
'category_external_ids' => $categoryIds,
'images' => $payload['images'] ?? null,
];
}
/** @param array<string, mixed> $payload */
private function localCategoryAttributes(array $payload): array
{
$parent = (int) ($payload['parent'] ?? 0);
return [
'name' => (string) ($payload['name'] ?? 'Category'),
'slug' => (string) ($payload['slug'] ?? \Illuminate\Support\Str::slug((string) ($payload['name'] ?? 'category'))),
'description' => $payload['description'] ?? null,
'parent_external_id' => $parent > 0 ? $parent : null,
];
}
private function majorPrice(mixed $value): ?string
{
if ($value === null || $value === '') {
return null;
}
return number_format(((float) $value), 2, '.', '');
}
private function minorPrice(mixed $value): ?int
{
if ($value === null || $value === '') {
return null;
}
return (int) round(((float) $value) * 100);
}
}