$itemIds */ public function handle( array $itemIds, string $descriptionMode, bool $separateDocuments, int $userId, ): File { $items = CommonCatalogItem::query() ->with('imageFile') ->whereIn('id', $itemIds) ->orderBy('series') ->orderBy('article') ->get(); if ($items->isEmpty()) { throw new RuntimeException('Не выбраны позиции для экспорта технических описаний.'); } if (! array_key_exists($descriptionMode, TechnicalDescriptionService::DESCRIPTION_MODES)) { throw new RuntimeException('Выбран неизвестный вариант технического описания.'); } PhpWordSettings::setOutputEscapingEnabled(true); return $separateDocuments ? $this->separateDocuments($items, $descriptionMode, $userId) : $this->combinedDocument($items, $descriptionMode, $userId); } /** @param Collection $items */ private function combinedDocument(Collection $items, string $descriptionMode, int $userId): File { $filename = 'Технические_описания_'.now()->format('Ymd_His').'.docx'; $path = $this->generatedPath($userId, $filename); Storage::disk('local')->makeDirectory(dirname($path)); $processor = new TemplateProcessor($this->templatePath(self::COMBINED_TEMPLATE)); $processor->cloneBlock('product_block', $items->count(), true, true); foreach ($items->values() as $index => $item) { $this->fillProduct($processor, $item, $descriptionMode, $index + 1, $index + 1); } $processor->saveAs(Storage::disk('local')->path($path)); return $this->registerFile($path, $filename, 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', $userId); } /** @param Collection $items */ private function separateDocuments(Collection $items, string $descriptionMode, int $userId): File { $filename = 'Технические_описания_'.now()->format('Ymd_His').'.zip'; $path = $this->generatedPath($userId, $filename); $temporaryDirectory = 'generated/technical-descriptions/tmp/'.Str::uuid(); Storage::disk('local')->makeDirectory(dirname($path)); Storage::disk('local')->makeDirectory($temporaryDirectory); $zip = new ZipArchive; if ($zip->open(Storage::disk('local')->path($path), ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) { throw new RuntimeException('Не удалось создать ZIP-архив технических описаний.'); } try { foreach ($items->values() as $index => $item) { $documentName = sprintf( '%s_%04d.docx', $this->safeFilename((string) $item->article), $index + 1, ); $documentPath = $temporaryDirectory.'/'.$documentName; $processor = new TemplateProcessor($this->templatePath(self::SINGLE_TEMPLATE)); $processor->cloneBlock('product_block', 1, true, true); $this->fillProduct($processor, $item, $descriptionMode, 1, 1); $processor->setValue('page_br#1', ''); $processor->saveAs(Storage::disk('local')->path($documentPath)); $zip->addFile(Storage::disk('local')->path($documentPath), $documentName); } } finally { $zip->close(); Storage::disk('local')->deleteDirectory($temporaryDirectory); } return $this->registerFile($path, $filename, 'application/zip', $userId); } private function fillProduct( TemplateProcessor $processor, CommonCatalogItem $item, string $descriptionMode, int $variableIndex, int $number, ): void { $suffix = '#'.$variableIndex; $processor->setValue('product_group'.$suffix, $item->product_group ?: $item->kind ?: ''); $processor->setValue('num'.$suffix, (string) $number); $processor->setValue('name_for_form'.$suffix, $item->print_name ?: $item->calculator_name ?: ''); $processor->setValue('price'.$suffix, $this->technicalDescriptions->formattedPrice($item)); $processor->setValue( 'description'.$suffix, $this->technicalDescriptions->description($item, $descriptionMode), ); $imagePath = $item->imageFile?->path; if ($imagePath && Storage::disk('public')->exists($imagePath)) { $processor->setImageValue('image'.$suffix, [ 'path' => Storage::disk('public')->path($imagePath), 'width' => 270, 'height' => 180, 'ratio' => true, ]); } else { $processor->setValue('image'.$suffix, ''); } } private function registerFile(string $path, string $filename, string $mimeType, int $userId): File { $file = File::query()->create([ 'link' => '', 'path' => $path, 'user_id' => $userId, 'original_name' => $filename, 'mime_type' => $mimeType, 'is_generated' => true, ]); $file->update([ 'link' => route('common-catalog.technical-description.download', $file), ]); return $file; } private function generatedPath(int $userId, string $filename): string { return "generated/technical-descriptions/{$userId}/".Str::uuid()."/{$filename}"; } private function templatePath(string $filename): string { $path = base_path('templates/technical-descriptions/'.$filename); if (! is_file($path)) { throw new RuntimeException("Шаблон технического описания {$filename} не найден."); } return $path; } private function safeFilename(string $value): string { $value = preg_replace('/[^\pL\pN._-]+/u', '_', trim($value)) ?? ''; return $value === '' ? 'позиция' : mb_substr($value, 0, 100); } }