> $sourceProducts * @return array> */ public function handle( array $sourceProducts, ?string $imagesDirectory, int $userId, bool $overwrite = false, bool $createMissing = false, bool $applyToVariants = false, bool $dryRun = false, ): array { $stats = [ 'source' => count($sourceProducts), 'matched' => 0, 'updated' => 0, 'created' => 0, 'unchanged' => 0, 'missing' => 0, 'ambiguous' => 0, 'images_imported' => 0, 'images_missing' => 0, 'missing_articles' => [], 'ambiguous_articles' => [], ]; foreach ($sourceProducts as $source) { $article = trim((string) ($source['article'] ?? '')); if ($article === '') { continue; } $items = CommonCatalogItem::query()->where('article', $article)->get(); if ($items->isEmpty()) { if (! $createMissing) { $stats['missing']++; $stats['missing_articles'][] = $article; continue; } $items = collect([new CommonCatalogItem([ 'article' => $article, 'calculator_name' => $this->nullableString($source['name'] ?? null), ])]); } elseif ($items->count() > 1 && ! $applyToVariants) { $stats['ambiguous']++; $stats['ambiguous_articles'][] = $article; continue; } foreach ($items as $item) { $stats['matched']++; $payload = $this->payload($source); $changes = $this->changes($item, $payload, $overwrite); $isNew = ! $item->exists; $imageSource = $this->imageSource($source, $imagesDirectory); $shouldImportImage = $item->image_file_id === null && $imageSource !== null; if ($dryRun) { if ($isNew) { $stats['created']++; } elseif ($changes !== [] || $shouldImportImage) { $stats['updated']++; } else { $stats['unchanged']++; } if ($shouldImportImage) { $stats['images_imported']++; } elseif ($item->image_file_id === null && ($source['image_path'] ?? '') !== '') { $stats['images_missing']++; } continue; } if ($changes !== []) { $item->fill($changes); } if ($isNew) { $item->save(); $stats['created']++; } elseif ($item->isDirty()) { $item->save(); $stats['updated']++; } else { $stats['unchanged']++; } if ($item->image_file_id === null && ($source['image_path'] ?? '') !== '') { if ($imageSource === null) { $stats['images_missing']++; } else { $this->importImage($item, $imageSource, $userId); $stats['images_imported']++; if (! $isNew && $changes === []) { $stats['unchanged']--; $stats['updated']++; } } } } } $stats['missing_articles'] = array_values(array_unique($stats['missing_articles'])); $stats['ambiguous_articles'] = array_values(array_unique($stats['ambiguous_articles'])); return $stats; } /** @param array $source */ private function payload(array $source): array { return [ 'series' => $this->nullableString($source['series'] ?? null), 'calculator_name' => $this->nullableString($source['name'] ?? null), 'print_name' => $this->nullableString($source['name_for_form'] ?? null), 'product_group' => $this->nullableString($source['product_group'] ?? null), 'characteristics' => $this->nullableString($source['characteristics'] ?? null), 'technical_description' => $this->nullableString($source['tech_description'] ?? null), 'technical_description_short' => $this->nullableString($source['tech_description_short'] ?? null), ]; } private function changes(CommonCatalogItem $item, array $payload, bool $overwrite): array { return array_filter( $payload, function (mixed $value, string $field) use ($item, $overwrite): bool { if ($value === null) { return false; } return $overwrite || trim((string) $item->getAttribute($field)) === ''; }, ARRAY_FILTER_USE_BOTH, ); } /** @param array $source */ private function imageSource(array $source, ?string $imagesDirectory): ?string { $imageName = basename(str_replace('\\', '/', (string) ($source['image_path'] ?? ''))); if ($imageName === '' || $imagesDirectory === null || $imagesDirectory === '') { return null; } $path = rtrim($imagesDirectory, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.$imageName; return is_file($path) && is_readable($path) ? $path : null; } private function importImage(CommonCatalogItem $item, string $sourcePath, int $userId): void { $contents = file_get_contents($sourcePath); if (! is_string($contents) || $contents === '') { throw new RuntimeException("Не удалось прочитать изображение {$sourcePath}."); } $mimeType = (string) (new \finfo(FILEINFO_MIME_TYPE))->buffer($contents); $extension = match ($mimeType) { 'image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp', default => throw new RuntimeException("Неподдерживаемый MIME-тип изображения {$mimeType}."), }; $path = "common_catalog/items/{$item->id}/image/".Str::uuid().".{$extension}"; if (! Storage::disk('public')->put($path, $contents)) { throw new RuntimeException("Не удалось сохранить изображение для артикула {$item->article}."); } $file = File::query()->create([ 'link' => url('/storage/'.$path), 'path' => $path, 'user_id' => $userId, 'original_name' => basename($sourcePath), 'mime_type' => $mimeType, ]); app(FileService::class)->ensureThumbnail($file); $item->update(['image_file_id' => $file->id]); } private function nullableString(mixed $value): ?string { $value = trim((string) $value); return $value === '' ? null : $value; } }