| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180 |
- <?php
- declare(strict_types=1);
- namespace App\Services\Export;
- use App\Models\CommonCatalogItem;
- use App\Models\File;
- use App\Services\TechnicalDescriptionService;
- use Illuminate\Support\Collection;
- use Illuminate\Support\Facades\Storage;
- use Illuminate\Support\Str;
- use PhpOffice\PhpWord\Settings as PhpWordSettings;
- use PhpOffice\PhpWord\TemplateProcessor;
- use RuntimeException;
- use ZipArchive;
- class ExportTechnicalDescriptionsService
- {
- private const COMBINED_TEMPLATE = 'массовый.docx';
- private const SINGLE_TEMPLATE = 'одиночный.docx';
- public function __construct(
- private readonly TechnicalDescriptionService $technicalDescriptions,
- ) {}
- /** @param list<int> $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<int, CommonCatalogItem> $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<int, CommonCatalogItem> $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);
- }
- }
|