This commit is contained in:
816
app/Http/Controllers/Admin/ProductController.php
Normal file
816
app/Http/Controllers/Admin/ProductController.php
Normal file
@@ -0,0 +1,816 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Category;
|
||||
use App\Models\Product;
|
||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class ProductController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
return view('admin.products.index', [
|
||||
'products' => Product::query()->with('category')->latest('id')->paginate(20),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
$categories = Category::query()->where('is_active', true)->orderBy('name')->get();
|
||||
|
||||
return view('admin.products.create', [
|
||||
'categories' => $categories,
|
||||
'categorySpecsById' => $this->categorySpecDefinitionsById($categories),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$data = $this->validateProduct($request);
|
||||
unset($data['image'], $data['gallery_images'], $data['remove_image'], $data['remove_gallery_paths']);
|
||||
$data['slug'] = $this->makeUniqueSlug($data['slug'] ?: $data['name']);
|
||||
$category = Category::query()->find($data['category_id']);
|
||||
$data['specs'] = $this->parseSpecs(
|
||||
(array) $request->input('specs', []),
|
||||
$request->input('specs_text'),
|
||||
$category?->slug
|
||||
);
|
||||
|
||||
$imagePath = $request->hasFile('image') ? $this->storeImage($request) : null;
|
||||
$galleryPaths = $request->hasFile('gallery_images')
|
||||
? $this->storeGalleryImages($request)
|
||||
: [];
|
||||
|
||||
[$imagePath, $galleryPaths] = $this->normalizeProductImageSet($imagePath, $galleryPaths);
|
||||
|
||||
$data['image_path'] = $imagePath;
|
||||
$data['gallery_paths'] = $galleryPaths;
|
||||
|
||||
if ($data['image_path'] === null) {
|
||||
unset($data['image_path']);
|
||||
}
|
||||
|
||||
if ($data['gallery_paths'] === []) {
|
||||
unset($data['gallery_paths']);
|
||||
}
|
||||
|
||||
Product::create($data);
|
||||
|
||||
return redirect()->route('admin.products.index')->with('status', 'Товар создан.');
|
||||
}
|
||||
|
||||
public function edit(Product $product)
|
||||
{
|
||||
$categories = Category::query()->where('is_active', true)->orderBy('name')->get();
|
||||
|
||||
return view('admin.products.edit', [
|
||||
'product' => $product,
|
||||
'categories' => $categories,
|
||||
'categorySpecsById' => $this->categorySpecDefinitionsById($categories),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, Product $product): RedirectResponse
|
||||
{
|
||||
$data = $this->validateProduct($request, $product);
|
||||
unset($data['image'], $data['gallery_images'], $data['remove_image'], $data['remove_gallery_paths']);
|
||||
$data['slug'] = $this->makeUniqueSlug($data['slug'] ?: $data['name'], $product->id);
|
||||
$category = Category::query()->find($data['category_id']);
|
||||
$data['specs'] = $this->parseSpecs(
|
||||
(array) $request->input('specs', []),
|
||||
$request->input('specs_text'),
|
||||
$category?->slug
|
||||
);
|
||||
|
||||
$oldImagePath = $product->image_path;
|
||||
$oldGalleryPaths = $this->normalizeImagePaths((array) ($product->gallery_paths ?? []));
|
||||
$removeImage = $request->boolean('remove_image');
|
||||
$removeGalleryPaths = $this->normalizeImagePaths((array) $request->input('remove_gallery_paths', []));
|
||||
|
||||
$nextImagePath = $oldImagePath;
|
||||
$nextGalleryPaths = $oldGalleryPaths;
|
||||
|
||||
if ($request->hasFile('image')) {
|
||||
$nextImagePath = $this->storeImage($request);
|
||||
} elseif ($removeImage) {
|
||||
$nextImagePath = null;
|
||||
if ($oldImagePath) {
|
||||
$removeGalleryPaths[] = $oldImagePath;
|
||||
}
|
||||
}
|
||||
|
||||
if ($request->hasFile('gallery_images')) {
|
||||
$nextGalleryPaths = $this->storeGalleryImages($request);
|
||||
} elseif ($removeGalleryPaths !== []) {
|
||||
$removablePaths = array_values(array_intersect($oldGalleryPaths, $removeGalleryPaths));
|
||||
$nextGalleryPaths = array_values(array_filter(
|
||||
$oldGalleryPaths,
|
||||
fn (string $path) => !in_array($path, $removablePaths, true)
|
||||
));
|
||||
}
|
||||
|
||||
[$nextImagePath, $nextGalleryPaths] = $this->normalizeProductImageSet($nextImagePath, $nextGalleryPaths);
|
||||
|
||||
$data['image_path'] = $nextImagePath;
|
||||
$data['gallery_paths'] = $nextGalleryPaths;
|
||||
|
||||
$product->update($data);
|
||||
|
||||
$activePaths = $this->normalizeImagePaths(
|
||||
array_merge(
|
||||
[$product->image_path],
|
||||
(array) ($product->gallery_paths ?? [])
|
||||
)
|
||||
);
|
||||
|
||||
if ($oldImagePath && !in_array($oldImagePath, $activePaths, true)) {
|
||||
$this->deleteImage($oldImagePath);
|
||||
}
|
||||
|
||||
foreach ($oldGalleryPaths as $oldPath) {
|
||||
if (!in_array($oldPath, $activePaths, true)) {
|
||||
$this->deleteImage($oldPath);
|
||||
}
|
||||
}
|
||||
|
||||
return redirect()->route('admin.products.index')->with('status', 'Товар обновлен.');
|
||||
}
|
||||
|
||||
public function destroy(Product $product): RedirectResponse
|
||||
{
|
||||
$this->deleteImages(array_merge([$product->image_path], (array) ($product->gallery_paths ?? [])));
|
||||
$product->delete();
|
||||
|
||||
return redirect()->route('admin.products.index')->with('status', 'Товар удален.');
|
||||
}
|
||||
|
||||
public function exportCsv(): StreamedResponse
|
||||
{
|
||||
$specKeys = $this->allSpecKeys();
|
||||
$headers = array_merge(
|
||||
[
|
||||
'id',
|
||||
'category_slug',
|
||||
'category_name',
|
||||
'name',
|
||||
'slug',
|
||||
'sku',
|
||||
'price',
|
||||
'old_price',
|
||||
'stock',
|
||||
'short_description',
|
||||
'description',
|
||||
'is_active',
|
||||
],
|
||||
$specKeys,
|
||||
['specs_json']
|
||||
);
|
||||
|
||||
return response()->streamDownload(function () use ($headers, $specKeys) {
|
||||
$output = fopen('php://output', 'wb');
|
||||
if (!$output) {
|
||||
return;
|
||||
}
|
||||
|
||||
fwrite($output, "\xEF\xBB\xBF");
|
||||
fputcsv($output, $headers, ';');
|
||||
|
||||
Product::query()
|
||||
->with('category')
|
||||
->orderBy('id')
|
||||
->chunk(200, function (EloquentCollection $products) use ($output, $specKeys) {
|
||||
foreach ($products as $product) {
|
||||
$specs = (array) ($product->specs ?? []);
|
||||
$row = [
|
||||
$product->id,
|
||||
$product->category?->slug ?? '',
|
||||
$product->category?->name ?? '',
|
||||
$product->name,
|
||||
$product->slug,
|
||||
$product->sku,
|
||||
$product->price,
|
||||
$product->old_price,
|
||||
$product->stock,
|
||||
$product->short_description,
|
||||
$product->description,
|
||||
$product->is_active ? 1 : 0,
|
||||
];
|
||||
|
||||
foreach ($specKeys as $key) {
|
||||
$row[] = $specs[$key] ?? '';
|
||||
}
|
||||
|
||||
$row[] = $specs === [] ? '' : json_encode($specs, JSON_UNESCAPED_UNICODE);
|
||||
|
||||
fputcsv($output, $row, ';');
|
||||
}
|
||||
});
|
||||
|
||||
fclose($output);
|
||||
}, 'products-' . now()->format('Ymd-His') . '.csv', [
|
||||
'Content-Type' => 'text/csv; charset=UTF-8',
|
||||
]);
|
||||
}
|
||||
|
||||
public function importCsv(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'csv_file' => ['required', 'file', 'mimes:csv,txt', 'max:5120'],
|
||||
]);
|
||||
|
||||
$path = $request->file('csv_file')?->getRealPath();
|
||||
if (!$path) {
|
||||
return back()->withErrors(['csv_file' => 'Не удалось прочитать файл.']);
|
||||
}
|
||||
|
||||
[$handle, $delimiter] = $this->openCsv($path);
|
||||
if (!$handle) {
|
||||
return back()->withErrors(['csv_file' => 'Не удалось открыть CSV файл.']);
|
||||
}
|
||||
|
||||
$headerRow = fgetcsv($handle, 0, $delimiter);
|
||||
if ($headerRow === false) {
|
||||
fclose($handle);
|
||||
return back()->withErrors(['csv_file' => 'CSV файл пустой.']);
|
||||
}
|
||||
|
||||
$headers = array_map(fn ($header) => $this->normalizeCsvHeader((string) $header), $headerRow);
|
||||
if (!in_array('name', $headers, true) || !in_array('price', $headers, true)) {
|
||||
fclose($handle);
|
||||
return back()->withErrors(['csv_file' => 'В CSV должны быть колонки name и price.']);
|
||||
}
|
||||
|
||||
if (!in_array('category_slug', $headers, true) && !in_array('category_name', $headers, true)) {
|
||||
fclose($handle);
|
||||
return back()->withErrors(['csv_file' => 'В CSV должна быть category_slug или category_name.']);
|
||||
}
|
||||
|
||||
$categories = Category::query()->get();
|
||||
$categoriesBySlug = $categories->keyBy(fn (Category $category) => mb_strtolower($category->slug));
|
||||
$categoriesByName = $categories->keyBy(fn (Category $category) => mb_strtolower($category->name));
|
||||
$allSpecKeys = $this->allSpecKeys();
|
||||
|
||||
$created = 0;
|
||||
$updated = 0;
|
||||
$skipped = 0;
|
||||
|
||||
while (($row = fgetcsv($handle, 0, $delimiter)) !== false) {
|
||||
if ($this->rowIsEmpty($row)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$rowData = $this->mapCsvRow($headers, $row);
|
||||
$category = $this->resolveCategory($rowData, $categoriesBySlug, $categoriesByName);
|
||||
|
||||
$name = trim((string) ($rowData['name'] ?? ''));
|
||||
$price = $this->parseDecimal($rowData['price'] ?? null);
|
||||
$stock = $this->parseInteger($rowData['stock'] ?? 0);
|
||||
|
||||
if (!$category || $name === '' || $price === null || $stock === null) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$sku = $this->trimToNull((string) ($rowData['sku'] ?? ''));
|
||||
$slugInput = trim((string) ($rowData['slug'] ?? ''));
|
||||
|
||||
$existing = null;
|
||||
if ($slugInput !== '') {
|
||||
$existing = Product::query()->where('slug', Str::slug($slugInput))->first();
|
||||
}
|
||||
|
||||
if (!$existing && $sku !== null) {
|
||||
$existing = Product::query()->where('sku', $sku)->first();
|
||||
}
|
||||
|
||||
$slug = $this->makeUniqueSlug($slugInput !== '' ? $slugInput : $name, $existing?->id);
|
||||
$specs = $this->extractSpecsFromCsvRow($rowData, $allSpecKeys, $category->slug);
|
||||
|
||||
$payload = [
|
||||
'category_id' => $category->id,
|
||||
'name' => $name,
|
||||
'slug' => $slug,
|
||||
'sku' => $this->trimToNull((string) ($rowData['sku'] ?? '')),
|
||||
'price' => $price,
|
||||
'old_price' => $this->parseDecimal($rowData['old_price'] ?? null),
|
||||
'stock' => $stock,
|
||||
'short_description' => $this->trimToNull((string) ($rowData['short_description'] ?? '')),
|
||||
'description' => $this->trimToNull((string) ($rowData['description'] ?? '')),
|
||||
'is_active' => $this->parseBooleanValue($rowData['is_active'] ?? null, true),
|
||||
'specs' => $specs,
|
||||
];
|
||||
|
||||
if ($existing) {
|
||||
$existing->update($payload);
|
||||
$updated++;
|
||||
} else {
|
||||
Product::create($payload);
|
||||
$created++;
|
||||
}
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
|
||||
return redirect()
|
||||
->route('admin.products.index')
|
||||
->with('status', "Импорт завершен: создано {$created}, обновлено {$updated}, пропущено {$skipped}.");
|
||||
}
|
||||
|
||||
private function validateProduct(Request $request, ?Product $product = null): array
|
||||
{
|
||||
return $request->validate([
|
||||
'category_id' => ['required', 'integer', 'exists:categories,id'],
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'slug' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:255',
|
||||
Rule::unique('products', 'slug')->ignore($product?->id),
|
||||
],
|
||||
'sku' => ['nullable', 'string', 'max:255'],
|
||||
'price' => ['required', 'numeric', 'min:0'],
|
||||
'old_price' => ['nullable', 'numeric', 'min:0'],
|
||||
'stock' => ['required', 'integer', 'min:0'],
|
||||
'short_description' => ['nullable', 'string', 'max:255'],
|
||||
'description' => ['nullable', 'string'],
|
||||
'image' => ['nullable', 'image', 'mimes:jpg,jpeg,png,webp', 'max:5120'],
|
||||
'gallery_images' => ['nullable', 'array', 'max:12'],
|
||||
'gallery_images.*' => ['image', 'mimes:jpg,jpeg,png,webp', 'max:5120'],
|
||||
'remove_image' => ['nullable', 'boolean'],
|
||||
'remove_gallery_paths' => ['nullable', 'array'],
|
||||
'remove_gallery_paths.*' => ['string', 'max:255'],
|
||||
'is_active' => ['nullable', 'boolean'],
|
||||
'specs' => ['nullable', 'array'],
|
||||
'specs.*' => ['nullable', 'string', 'max:255'],
|
||||
'specs_text' => ['nullable', 'string'],
|
||||
]) + ['is_active' => $request->boolean('is_active')];
|
||||
}
|
||||
|
||||
private function makeUniqueSlug(string $value, ?int $ignoreId = null): string
|
||||
{
|
||||
$base = Str::slug($value);
|
||||
if ($base === '') {
|
||||
$base = 'product';
|
||||
}
|
||||
|
||||
$slug = $base;
|
||||
$index = 2;
|
||||
|
||||
while (
|
||||
Product::query()
|
||||
->where('slug', $slug)
|
||||
->when($ignoreId, fn ($query) => $query->whereKeyNot($ignoreId))
|
||||
->exists()
|
||||
) {
|
||||
$slug = "{$base}-{$index}";
|
||||
$index++;
|
||||
}
|
||||
|
||||
return $slug;
|
||||
}
|
||||
|
||||
private function parseSpecs(array $specsInput = [], ?string $specsText = null, ?string $categorySlug = null): array
|
||||
{
|
||||
$specs = [];
|
||||
|
||||
foreach ($specsInput as $key => $value) {
|
||||
$normalizedKey = $this->normalizeSpecKey((string) $key);
|
||||
$normalizedValue = trim((string) $value);
|
||||
|
||||
if ($normalizedKey === '' || $normalizedValue === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$specs[$normalizedKey] = $normalizedValue;
|
||||
}
|
||||
|
||||
if ($specsText !== null && trim($specsText) !== '') {
|
||||
$specs = array_merge($this->parseSpecsFromText($specsText), $specs);
|
||||
}
|
||||
|
||||
return $this->filterSpecsByCategory($specs, $categorySlug);
|
||||
}
|
||||
|
||||
private function parseSpecsFromText(string $specsText): array
|
||||
{
|
||||
$rows = preg_split('/\r\n|\r|\n/', $specsText);
|
||||
$specs = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$line = trim($row);
|
||||
if ($line === '' || !str_contains($line, ':')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
[$key, $value] = array_map('trim', explode(':', $line, 2));
|
||||
$key = $this->normalizeSpecKey($key);
|
||||
|
||||
if ($key === '' || $value === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$specs[$key] = $value;
|
||||
}
|
||||
|
||||
return $specs;
|
||||
}
|
||||
|
||||
private function normalizeSpecKey(string $key): string
|
||||
{
|
||||
return Str::of($key)
|
||||
->lower()
|
||||
->replace(' ', '_')
|
||||
->replace('-', '_')
|
||||
->trim()
|
||||
->toString();
|
||||
}
|
||||
|
||||
private function categorySpecDefinitionsById(Collection $categories): array
|
||||
{
|
||||
$specDefinitions = $this->specDefinitions();
|
||||
$savedValuesByCategory = $this->savedSpecValuesByCategory($categories);
|
||||
|
||||
return $categories
|
||||
->mapWithKeys(function (Category $category) use ($specDefinitions, $savedValuesByCategory) {
|
||||
$definitions = $specDefinitions[$category->slug] ?? [];
|
||||
$categoryValues = $savedValuesByCategory[$category->id] ?? [];
|
||||
|
||||
$prepared = array_map(function (array $definition) use ($categoryValues) {
|
||||
$key = (string) ($definition['key'] ?? '');
|
||||
if ($key === '') {
|
||||
return $definition;
|
||||
}
|
||||
|
||||
$presetOptions = collect((array) ($definition['options'] ?? []))
|
||||
->map(fn ($value) => trim((string) $value))
|
||||
->filter()
|
||||
->values();
|
||||
|
||||
$savedOptions = collect($categoryValues[$key] ?? [])
|
||||
->map(fn ($value) => trim((string) $value))
|
||||
->filter()
|
||||
->values();
|
||||
|
||||
$definition['options'] = $presetOptions
|
||||
->concat($savedOptions)
|
||||
->unique()
|
||||
->values()
|
||||
->all();
|
||||
|
||||
return $definition;
|
||||
}, $definitions);
|
||||
|
||||
return [$category->id => $prepared];
|
||||
})
|
||||
->all();
|
||||
}
|
||||
|
||||
private function specDefinitions(): array
|
||||
{
|
||||
return config('product_specs.categories', []);
|
||||
}
|
||||
|
||||
private function allSpecKeys(): array
|
||||
{
|
||||
return collect($this->specDefinitions())
|
||||
->flatten(1)
|
||||
->pluck('key')
|
||||
->filter()
|
||||
->unique()
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
private function savedSpecValuesByCategory(Collection $categories): array
|
||||
{
|
||||
$categoryIds = $categories->pluck('id')->filter()->values();
|
||||
if ($categoryIds->isEmpty()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$values = [];
|
||||
|
||||
Product::query()
|
||||
->select(['id', 'category_id', 'specs'])
|
||||
->whereIn('category_id', $categoryIds->all())
|
||||
->whereNotNull('specs')
|
||||
->chunkById(200, function (EloquentCollection $products) use (&$values) {
|
||||
foreach ($products as $product) {
|
||||
$specs = (array) ($product->specs ?? []);
|
||||
if ($specs === []) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($specs as $key => $value) {
|
||||
if (!is_scalar($value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$normalizedKey = $this->normalizeSpecKey((string) $key);
|
||||
$normalizedValue = trim((string) $value);
|
||||
|
||||
if ($normalizedKey === '' || $normalizedValue === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$values[$product->category_id][$normalizedKey][$normalizedValue] = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
foreach ($values as $categoryId => $specValues) {
|
||||
foreach ($specValues as $key => $valueSet) {
|
||||
$options = array_keys($valueSet);
|
||||
natcasesort($options);
|
||||
$values[$categoryId][$key] = array_values($options);
|
||||
}
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
private function filterSpecsByCategory(array $specs, ?string $categorySlug): array
|
||||
{
|
||||
if (!$categorySlug) {
|
||||
return $specs;
|
||||
}
|
||||
|
||||
$allowedKeys = collect($this->specDefinitions()[$categorySlug] ?? [])
|
||||
->pluck('key')
|
||||
->all();
|
||||
|
||||
if ($allowedKeys === []) {
|
||||
return $specs;
|
||||
}
|
||||
|
||||
return collect($specs)->only($allowedKeys)->all();
|
||||
}
|
||||
|
||||
private function openCsv(string $path): array
|
||||
{
|
||||
$handle = fopen($path, 'rb');
|
||||
if (!$handle) {
|
||||
return [null, ';'];
|
||||
}
|
||||
|
||||
$sample = fgets($handle);
|
||||
rewind($handle);
|
||||
|
||||
$delimiter = substr_count((string) $sample, ';') >= substr_count((string) $sample, ',') ? ';' : ',';
|
||||
|
||||
return [$handle, $delimiter];
|
||||
}
|
||||
|
||||
private function normalizeCsvHeader(string $header): string
|
||||
{
|
||||
$value = str_replace("\xEF\xBB\xBF", '', $header);
|
||||
|
||||
return Str::of($value)
|
||||
->trim()
|
||||
->lower()
|
||||
->replace(' ', '_')
|
||||
->replace('-', '_')
|
||||
->toString();
|
||||
}
|
||||
|
||||
private function rowIsEmpty(array $row): bool
|
||||
{
|
||||
foreach ($row as $value) {
|
||||
if (trim((string) $value) !== '') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function mapCsvRow(array $headers, array $row): array
|
||||
{
|
||||
$row = array_pad($row, count($headers), '');
|
||||
$mapped = [];
|
||||
|
||||
foreach ($headers as $index => $header) {
|
||||
if ($header === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$mapped[$header] = trim((string) ($row[$index] ?? ''));
|
||||
}
|
||||
|
||||
return $mapped;
|
||||
}
|
||||
|
||||
private function resolveCategory(array $rowData, Collection $bySlug, Collection $byName): ?Category
|
||||
{
|
||||
$slug = mb_strtolower(trim((string) ($rowData['category_slug'] ?? '')));
|
||||
if ($slug !== '' && $bySlug->has($slug)) {
|
||||
return $bySlug->get($slug);
|
||||
}
|
||||
|
||||
$name = mb_strtolower(trim((string) ($rowData['category_name'] ?? '')));
|
||||
if ($name !== '' && $byName->has($name)) {
|
||||
return $byName->get($name);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function extractSpecsFromCsvRow(array $rowData, array $allSpecKeys, ?string $categorySlug): array
|
||||
{
|
||||
$specs = [];
|
||||
|
||||
foreach ($allSpecKeys as $key) {
|
||||
$value = trim((string) ($rowData[$key] ?? ''));
|
||||
if ($value !== '') {
|
||||
$specs[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
$specsJson = trim((string) ($rowData['specs_json'] ?? ''));
|
||||
if ($specsJson !== '') {
|
||||
$decoded = json_decode($specsJson, true);
|
||||
|
||||
if (is_array($decoded)) {
|
||||
foreach ($decoded as $key => $value) {
|
||||
if (!is_scalar($value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$normalizedKey = $this->normalizeSpecKey((string) $key);
|
||||
$normalizedValue = trim((string) $value);
|
||||
|
||||
if ($normalizedKey === '' || $normalizedValue === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$specs[$normalizedKey] = $normalizedValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->filterSpecsByCategory($specs, $categorySlug);
|
||||
}
|
||||
|
||||
private function parseDecimal(mixed $value): ?float
|
||||
{
|
||||
$string = trim((string) $value);
|
||||
if ($string === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$normalized = str_replace([' ', ','], ['', '.'], $string);
|
||||
if (!is_numeric($normalized)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$decimal = (float) $normalized;
|
||||
|
||||
return $decimal < 0 ? null : $decimal;
|
||||
}
|
||||
|
||||
private function parseInteger(mixed $value): ?int
|
||||
{
|
||||
$string = trim((string) $value);
|
||||
if ($string === '') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!preg_match('/^-?\d+$/', $string)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$integer = (int) $string;
|
||||
|
||||
return $integer < 0 ? null : $integer;
|
||||
}
|
||||
|
||||
private function parseBooleanValue(mixed $value, bool $default = true): bool
|
||||
{
|
||||
$string = mb_strtolower(trim((string) $value));
|
||||
if ($string === '') {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return match ($string) {
|
||||
'1', 'true', 'yes', 'y', 'да', 'active', 'on' => true,
|
||||
'0', 'false', 'no', 'n', 'нет', 'inactive', 'off' => false,
|
||||
default => $default,
|
||||
};
|
||||
}
|
||||
|
||||
private function storeImage(Request $request): string
|
||||
{
|
||||
$file = $request->file('image');
|
||||
if (!$file instanceof UploadedFile) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $this->storeUploadedImage($file);
|
||||
}
|
||||
|
||||
private function storeGalleryImages(Request $request): array
|
||||
{
|
||||
$files = $request->file('gallery_images', []);
|
||||
if (!is_array($files)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$paths = [];
|
||||
foreach ($files as $file) {
|
||||
if (!$file instanceof UploadedFile) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$path = $this->storeUploadedImage($file);
|
||||
if ($path !== '') {
|
||||
$paths[] = $path;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->normalizeImagePaths($paths);
|
||||
}
|
||||
|
||||
private function storeUploadedImage(UploadedFile $file): string
|
||||
{
|
||||
$extension = strtolower((string) $file->getClientOriginalExtension());
|
||||
if ($extension === '') {
|
||||
$extension = 'jpg';
|
||||
}
|
||||
|
||||
$name = now()->format('YmdHis') . '-' . Str::uuid() . '.' . $extension;
|
||||
|
||||
return (string) $file->storeAs('products', $name, 'public');
|
||||
}
|
||||
|
||||
private function deleteImage(?string $path): void
|
||||
{
|
||||
if (!$path) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (str_starts_with($path, 'uploads/')) {
|
||||
$filePath = public_path(ltrim($path, '/'));
|
||||
if (File::exists($filePath)) {
|
||||
File::delete($filePath);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Storage::disk('public')->delete($path);
|
||||
}
|
||||
|
||||
private function deleteImages(array $paths): void
|
||||
{
|
||||
foreach ($this->normalizeImagePaths($paths) as $path) {
|
||||
$this->deleteImage($path);
|
||||
}
|
||||
}
|
||||
|
||||
private function normalizeImagePaths(array $paths): array
|
||||
{
|
||||
return collect($paths)
|
||||
->filter(fn ($path) => is_string($path) && trim($path) !== '')
|
||||
->map(fn (string $path) => trim($path))
|
||||
->unique()
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
private function normalizeProductImageSet(?string $imagePath, array $galleryPaths): array
|
||||
{
|
||||
$normalizedImagePath = $this->trimToNull((string) $imagePath);
|
||||
$normalizedGalleryPaths = $this->normalizeImagePaths($galleryPaths);
|
||||
|
||||
if ($normalizedImagePath === null && $normalizedGalleryPaths !== []) {
|
||||
$normalizedImagePath = array_shift($normalizedGalleryPaths);
|
||||
}
|
||||
|
||||
if ($normalizedImagePath !== null) {
|
||||
$normalizedGalleryPaths = array_values(array_filter(
|
||||
$normalizedGalleryPaths,
|
||||
fn (string $path) => $path !== $normalizedImagePath
|
||||
));
|
||||
}
|
||||
|
||||
return [$normalizedImagePath, $normalizedGalleryPaths];
|
||||
}
|
||||
|
||||
private function trimToNull(string $value): ?string
|
||||
{
|
||||
$trimmed = trim($value);
|
||||
|
||||
return $trimmed === '' ? null : $trimmed;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user