199 lines
7.0 KiB
PHP
Executable File
199 lines
7.0 KiB
PHP
Executable File
<?php
|
|
namespace App\Services\Catalog;
|
|
|
|
use App\Models\Fornitore;
|
|
use App\Models\Product;
|
|
use App\Models\ProductMedia;
|
|
use App\Support\ArchivioPaths;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Str;
|
|
|
|
class ProductAssetIngestionService
|
|
{
|
|
/**
|
|
* @return array<string,int>
|
|
*/
|
|
public function ingestProductAssets(Product $product, Fornitore $fornitore, bool $dryRun = false): array
|
|
{
|
|
$stats = [
|
|
'images' => 0,
|
|
'documents' => 0,
|
|
'skipped_existing' => 0,
|
|
'skipped_unsupported' => 0,
|
|
'failed' => 0,
|
|
];
|
|
|
|
$catalog = is_array($product->meta['catalog'] ?? null) ? $product->meta['catalog'] : [];
|
|
$links = is_array($catalog['private_source_links'] ?? null) ? $catalog['private_source_links'] : [];
|
|
|
|
foreach ($this->normalizeLinkList($links['image_links'] ?? $links['image_link'] ?? null) as $index => $url) {
|
|
$imageResult = $this->ingestSingleAsset($product, $fornitore, $url, 'image', $dryRun, $index);
|
|
$stats[$imageResult['bucket']]++;
|
|
}
|
|
|
|
foreach ($this->normalizeLinkList($links['document_links'] ?? $links['document_link'] ?? null) as $index => $url) {
|
|
$documentResult = $this->ingestSingleAsset($product, $fornitore, $url, 'document', $dryRun, $index);
|
|
$stats[$documentResult['bucket']]++;
|
|
}
|
|
|
|
return $stats;
|
|
}
|
|
|
|
/**
|
|
* @return array<string,int>
|
|
*/
|
|
public function ingestForFornitore(Fornitore $fornitore, int $limit = 50, bool $dryRun = false): array
|
|
{
|
|
$stats = [
|
|
'products' => 0,
|
|
'images' => 0,
|
|
'documents' => 0,
|
|
'skipped_existing' => 0,
|
|
'skipped_unsupported' => 0,
|
|
'failed' => 0,
|
|
];
|
|
|
|
$products = Product::query()
|
|
->where('default_fornitore_id', (int) $fornitore->id)
|
|
->orderBy('id')
|
|
->limit(max(1, $limit))
|
|
->get(['id', 'internal_code', 'meta']);
|
|
|
|
foreach ($products as $product) {
|
|
$stats['products']++;
|
|
$result = $this->ingestProductAssets($product, $fornitore, $dryRun);
|
|
|
|
foreach ($result as $bucket => $count) {
|
|
$stats[$bucket] += $count;
|
|
}
|
|
}
|
|
|
|
return $stats;
|
|
}
|
|
|
|
/**
|
|
* @return array{bucket:string}
|
|
*/
|
|
private function ingestSingleAsset(Product $product, Fornitore $fornitore, string $url, string $mediaType, bool $dryRun, int $sortOrder = 0): array
|
|
{
|
|
$url = trim($url);
|
|
if ($url === '' || ! str_starts_with($url, 'http')) {
|
|
return ['bucket' => 'skipped_unsupported'];
|
|
}
|
|
|
|
$extension = $this->guessExtension($url, '', $mediaType);
|
|
$folder = $this->buildMediaFolder($fornitore, $product, $mediaType);
|
|
$filename = Str::slug((string) ($product->internal_code ?? ('product-' . $product->id))) . '-' . substr(sha1($url), 0, 12) . '.' . $extension;
|
|
$path = $folder . '/' . $filename;
|
|
|
|
$existing = ProductMedia::query()
|
|
->where('product_id', (int) $product->id)
|
|
->where('media_type', $mediaType)
|
|
->whereIn('disk', ['local', 'public'])
|
|
->where('path', $path)
|
|
->exists();
|
|
|
|
if ($existing) {
|
|
return ['bucket' => 'skipped_existing'];
|
|
}
|
|
|
|
if ($dryRun) {
|
|
return ['bucket' => $mediaType === 'image' ? 'images' : 'documents'];
|
|
}
|
|
|
|
try {
|
|
$response = Http::timeout(20)
|
|
->withHeaders(['User-Agent' => 'NetgesconCatalogBot/1.0'])
|
|
->get($url);
|
|
} catch (\Throwable) {
|
|
return ['bucket' => 'failed'];
|
|
}
|
|
|
|
if (! $response->successful()) {
|
|
return ['bucket' => 'failed'];
|
|
}
|
|
|
|
$mime = strtolower(trim((string) $response->header('Content-Type')));
|
|
if ($mime !== '' && str_contains($mime, ';')) {
|
|
$mime = trim((string) Str::before($mime, ';'));
|
|
}
|
|
|
|
if ($mediaType === 'image' && ! str_starts_with($mime, 'image/')) {
|
|
return ['bucket' => 'skipped_unsupported'];
|
|
}
|
|
|
|
if ($mediaType === 'document' && ! str_contains($mime, 'pdf') && ! str_ends_with(Str::lower(parse_url($url, PHP_URL_PATH) ?: ''), '.pdf')) {
|
|
return ['bucket' => 'skipped_unsupported'];
|
|
}
|
|
|
|
$extension = $this->guessExtension($url, $mime, $mediaType);
|
|
$filename = Str::slug((string) ($product->internal_code ?? ('product-' . $product->id))) . '-' . substr(sha1($url), 0, 12) . '.' . $extension;
|
|
$path = $folder . '/' . $filename;
|
|
|
|
Storage::disk('public')->put($path, $response->body());
|
|
|
|
ProductMedia::query()->create([
|
|
'product_id' => (int) $product->id,
|
|
'media_type' => $mediaType,
|
|
'disk' => 'public',
|
|
'path' => $path,
|
|
'title' => $mediaType === 'image' ? 'Immagine catalogo interna' : 'Scheda prodotto interna',
|
|
'mime_type' => $mime !== '' ? $mime : null,
|
|
'size_bytes' => strlen($response->body()),
|
|
'sort_order' => $sortOrder,
|
|
'meta' => [
|
|
'source_url' => $url,
|
|
'source_system' => 'private_catalog_link',
|
|
'internalized' => true,
|
|
],
|
|
]);
|
|
|
|
return ['bucket' => $mediaType === 'image' ? 'images' : 'documents'];
|
|
}
|
|
|
|
private function buildMediaFolder(Fornitore $fornitore, Product $product, string $mediaType): string
|
|
{
|
|
$base = ArchivioPaths::fornitoreBase($fornitore);
|
|
if (! is_string($base) || $base === '') {
|
|
$base = 'catalogo/fornitori/' . ($fornitore->codice_univoco ?: $fornitore->id);
|
|
}
|
|
|
|
return trim('catalogo-interno/' . trim($base, '/') . '/prodotti/' . ($product->internal_code ?: $product->id) . '/' . $mediaType, '/');
|
|
}
|
|
|
|
private function guessExtension(string $url, string $mime, string $mediaType): string
|
|
{
|
|
$path = Str::lower((string) (parse_url($url, PHP_URL_PATH) ?: ''));
|
|
$ext = pathinfo($path, PATHINFO_EXTENSION);
|
|
if (is_string($ext) && $ext !== '') {
|
|
return $ext;
|
|
}
|
|
|
|
return match (true) {
|
|
str_contains($mime, 'png') => 'png',
|
|
str_contains($mime, 'webp') => 'webp',
|
|
str_contains($mime, 'jpeg'), str_contains($mime, 'jpg') => 'jpg',
|
|
$mediaType === 'document' => 'pdf',
|
|
default => 'bin',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @return array<int,string>
|
|
*/
|
|
private function normalizeLinkList(mixed $links): array
|
|
{
|
|
if (is_array($links)) {
|
|
return array_values(array_filter(array_map(
|
|
static fn(mixed $value): string => trim((string) $value),
|
|
$links,
|
|
), static fn(string $value): bool => $value !== ''));
|
|
}
|
|
|
|
$link = trim((string) $links);
|
|
|
|
return $link !== '' ? [$link] : [];
|
|
}
|
|
}
|