Add WooCommerce-style featured image and product gallery flow.
Deploy Ladill Woo Manager / deploy (push) Successful in 24s
Deploy Ladill Woo Manager / deploy (push) Successful in 24s
Sync full image sets through the plugin, with upload/URL support in the product editor and thumbnails in the list. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -7,6 +7,7 @@ use App\Models\WooProduct;
|
||||
use App\Models\WooStore;
|
||||
use App\Services\Woo\CatalogSyncService;
|
||||
use App\Services\Woo\CatalogWriteService;
|
||||
use App\Services\Woo\ProductMediaService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
@@ -17,6 +18,7 @@ class ProductController extends Controller
|
||||
public function __construct(
|
||||
private CatalogWriteService $write,
|
||||
private CatalogSyncService $sync,
|
||||
private ProductMediaService $media,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): View
|
||||
@@ -64,6 +66,7 @@ class ProductController extends Controller
|
||||
{
|
||||
$validated = $this->validateProduct($request);
|
||||
$store = $this->authorizedStore($request, (int) $validated['woo_store_id']);
|
||||
$validated['images'] = $this->media->buildImagesFromRequest($request, $store);
|
||||
|
||||
$this->write->createProduct($store, $validated);
|
||||
|
||||
@@ -90,6 +93,7 @@ class ProductController extends Controller
|
||||
abort_if($product->store?->user_id !== $request->user()->id, 403);
|
||||
|
||||
$validated = $this->validateProduct($request, $product);
|
||||
$validated['images'] = $this->media->buildImagesFromRequest($request, $product->store);
|
||||
$this->write->updateProduct($product, $validated);
|
||||
|
||||
return redirect()->route('woo.products.index', ['store' => $product->woo_store_id])
|
||||
@@ -129,6 +133,14 @@ class ProductController extends Controller
|
||||
'description' => ['nullable', 'string', 'max:20000'],
|
||||
'category_external_ids' => ['nullable', 'array'],
|
||||
'category_external_ids.*' => ['integer', 'min:1'],
|
||||
'product_images' => ['nullable', 'string'],
|
||||
'featured_image_upload' => ['nullable', 'image', 'max:10240'],
|
||||
'featured_image_src' => ['nullable', 'url', 'max:2000'],
|
||||
'featured_image_alt' => ['nullable', 'string', 'max:255'],
|
||||
'gallery_uploads' => ['nullable', 'array'],
|
||||
'gallery_uploads.*' => ['image', 'max:10240'],
|
||||
'gallery_image_srcs' => ['nullable', 'array'],
|
||||
'gallery_image_srcs.*' => ['url', 'max:2000'],
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -74,6 +74,31 @@ class WooProduct extends Model
|
||||
return self::STATUSES[$this->status] ?? ucfirst((string) $this->status);
|
||||
}
|
||||
|
||||
/** @return array{id?: int, src?: string, alt?: string, position?: int}|null */
|
||||
public function featuredImage(): ?array
|
||||
{
|
||||
$images = (array) $this->images;
|
||||
|
||||
return $images[0] ?? null;
|
||||
}
|
||||
|
||||
/** @return list<array{id?: int, src?: string, alt?: string, position?: int}> */
|
||||
public function galleryImages(): array
|
||||
{
|
||||
$images = array_values((array) $this->images);
|
||||
|
||||
return array_slice($images, 1);
|
||||
}
|
||||
|
||||
public function thumbnailUrl(): ?string
|
||||
{
|
||||
$featured = $this->featuredImage();
|
||||
|
||||
return is_array($featured) && ! empty($featured['src'])
|
||||
? (string) $featured['src']
|
||||
: null;
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
public function categoryNames(): array
|
||||
{
|
||||
|
||||
@@ -115,6 +115,7 @@ class CatalogWriteService
|
||||
'short_description' => $input['short_description'] ?? null,
|
||||
'description' => $input['description'] ?? null,
|
||||
'categories' => $categories,
|
||||
'images' => $input['images'] ?? null,
|
||||
], fn ($value) => $value !== null && $value !== '');
|
||||
}
|
||||
|
||||
@@ -183,6 +184,7 @@ class CatalogWriteService
|
||||
'short_description' => $payload['short_description'] ?? null,
|
||||
'description' => $payload['description'] ?? null,
|
||||
'category_external_ids' => $categoryIds,
|
||||
'images' => $payload['images'] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Services\Woo;
|
||||
|
||||
use App\Models\WooStore;
|
||||
use Illuminate\Http\Client\Response;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
@@ -32,6 +33,37 @@ class PluginClient
|
||||
return $this->raw($store, 'DELETE', $path)->successful();
|
||||
}
|
||||
|
||||
/** @return array<string, mixed>|null */
|
||||
public function uploadMedia(WooStore $store, UploadedFile $file, string $alt = ''): ?array
|
||||
{
|
||||
$site = rtrim((string) $store->site_url, '/');
|
||||
$url = $site.'/wp-json/ladill-woo/v1/media';
|
||||
|
||||
$response = Http::acceptJson()
|
||||
->timeout(60)
|
||||
->withHeaders(['X-Ladill-Store' => $store->public_id])
|
||||
->attach(
|
||||
'file',
|
||||
file_get_contents($file->getRealPath()) ?: '',
|
||||
$file->getClientOriginalName() ?: 'upload.jpg',
|
||||
)
|
||||
->post($url, array_filter(['alt' => $alt !== '' ? $alt : null]));
|
||||
|
||||
if (! $response->successful()) {
|
||||
Log::warning('Woo plugin media upload failed', [
|
||||
'store_id' => $store->id,
|
||||
'status' => $response->status(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @var array<string, mixed>|null $json */
|
||||
$json = $response->json();
|
||||
|
||||
return is_array($json) ? $json : null;
|
||||
}
|
||||
|
||||
/** @return array<string, mixed>|null */
|
||||
private function request(WooStore $store, string $method, string $path, array $data = []): ?array
|
||||
{
|
||||
|
||||
@@ -20,12 +20,14 @@ class ProductIngestService
|
||||
$sale = $this->priceMinor($payload['sale_price'] ?? null);
|
||||
|
||||
$images = collect((array) ($payload['images'] ?? []))
|
||||
->map(fn (array $image) => [
|
||||
->map(fn (array $image, int $index) => [
|
||||
'id' => (int) ($image['id'] ?? 0),
|
||||
'src' => (string) ($image['src'] ?? ''),
|
||||
'alt' => (string) ($image['alt'] ?? ''),
|
||||
'position' => (int) ($image['position'] ?? $index),
|
||||
])
|
||||
->filter(fn (array $image) => $image['src'] !== '')
|
||||
->filter(fn (array $image) => $image['src'] !== '' || $image['id'] > 0)
|
||||
->sortBy('position')
|
||||
->values()
|
||||
->all();
|
||||
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Woo;
|
||||
|
||||
use App\Models\WooStore;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
|
||||
class ProductMediaService
|
||||
{
|
||||
public function __construct(private PluginClient $client) {}
|
||||
|
||||
/** @return list<array{id?: int, src: string, alt?: string, position?: int}> */
|
||||
public function buildImagesFromRequest(Request $request, WooStore $store): array
|
||||
{
|
||||
/** @var list<array<string, mixed>> $images */
|
||||
$images = json_decode((string) $request->input('product_images', '[]'), true) ?? [];
|
||||
if (! is_array($images)) {
|
||||
$images = [];
|
||||
}
|
||||
|
||||
if ($request->hasFile('featured_image_upload')) {
|
||||
$uploaded = $this->upload($store, $request->file('featured_image_upload'), (string) $request->input('featured_image_alt', ''));
|
||||
if ($uploaded !== null) {
|
||||
$images[0] = $uploaded;
|
||||
}
|
||||
} elseif ($src = trim((string) $request->input('featured_image_src', ''))) {
|
||||
$images[0] = [
|
||||
'src' => $src,
|
||||
'alt' => trim((string) $request->input('featured_image_alt', '')),
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($request->file('gallery_uploads', []) as $file) {
|
||||
if ($file instanceof UploadedFile && $file->isValid()) {
|
||||
$uploaded = $this->upload($store, $file, '');
|
||||
if ($uploaded !== null) {
|
||||
$images[] = $uploaded;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ((array) $request->input('gallery_image_srcs', []) as $src) {
|
||||
if (! is_string($src)) {
|
||||
continue;
|
||||
}
|
||||
$src = trim($src);
|
||||
if ($src !== '') {
|
||||
$images[] = ['src' => $src, 'alt' => ''];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->normalizeImages($images);
|
||||
}
|
||||
|
||||
/** @return array{id: int, src: string, alt: string}|null */
|
||||
public function upload(WooStore $store, UploadedFile $file, string $alt = ''): ?array
|
||||
{
|
||||
$response = $this->client->uploadMedia($store, $file, $alt);
|
||||
|
||||
if (! is_array($response) || empty($response['src'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => (int) ($response['id'] ?? 0),
|
||||
'src' => (string) $response['src'],
|
||||
'alt' => (string) ($response['alt'] ?? $alt),
|
||||
];
|
||||
}
|
||||
|
||||
/** @param list<array<string, mixed>> $images */
|
||||
private function normalizeImages(array $images): array
|
||||
{
|
||||
$normalized = [];
|
||||
|
||||
foreach (array_values($images) as $position => $image) {
|
||||
if (! is_array($image)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$src = trim((string) ($image['src'] ?? ''));
|
||||
$id = (int) ($image['id'] ?? 0);
|
||||
|
||||
if ($src === '' && $id <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$row = [
|
||||
'alt' => trim((string) ($image['alt'] ?? '')),
|
||||
'position' => $position,
|
||||
];
|
||||
|
||||
if ($id > 0) {
|
||||
$row['id'] = $id;
|
||||
}
|
||||
if ($src !== '') {
|
||||
$row['src'] = $src;
|
||||
}
|
||||
|
||||
$normalized[] = $row;
|
||||
}
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user