where('id', $tenantId)->first()) { throw new RuntimeException(\sprintf('No Tenant found for id : %s', $tenantId)); } $this->exportFolder = \storage_path($this->exportFolder); $this->tempFolder = \storage_path($this->tempFolder); $this->tenantId = $tenantId; } /** * Execute the job. * * @return void * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException If a schema file could not be found. */ public function handle(): void { $data = $this->getData(); $associatedFileList = $this->getAssociatedFileList($data); $fileContent = $this->getFile($data); $this->assertSchemaIsValid('app.schema.json', json_encode($fileContent)); $filesToZip = $this->preparePicturesForZip($associatedFileList); $dataFilePath = "{$this->tempFolder}/data.json"; // * Make json File add add to files to zip. File::put($dataFilePath, json_encode($fileContent), true); if (!File::exists($this->exportFolder)) { File::makeDirectory($this->exportFolder, 0700, true); } // * Empty too old files collect(File::allFiles($this->exportFolder))->each(function (\SplFileInfo $file) { $timestamp = $file->getCTime() ?: $file->getMTime() ?: $file->getATime(); if (!$timestamp) { throw new \RuntimeException('Failed to get modified time'); } if (now()->subDays(7)->gt(Carbon::createFromTimestamp($timestamp))) { Log::info('Removed exported file ' . $file->getRealPath()); File::delete($file->getRealPath()); } }); $zipFile = $this->makeZipFile($filesToZip, $dataFilePath); $successMessage = "File exported in {$zipFile}."; if (app()->runningInConsole()) { $output = new \Symfony\Component\Console\Output\ConsoleOutput(); $output->writeln($successMessage); } Log::info($successMessage); } /** * Make zip file and get its path. * * @param array $filesToZip * @param string $dataFilePath * @return string * @throws \RuntimeException In case of failure. */ protected function makeZipFile(array $filesToZip, string $dataFilePath): string { File::ensureDirectoryExists($this->exportFolder); $zip = new \ZipArchive(); $zipFileName = 'showroom-bo-export_' . now()->isoFormat('DD-MM-YYYY_HH-mm-ss') . '.zip'; $zipFilePath = "{$this->exportFolder}/{$zipFileName}"; if ($zip->open($zipFilePath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) { throw new \RuntimeException("Failed to open zip file {$zipFilePath}"); } foreach ($filesToZip as $file) { if (! is_file($file)) { throw new \RuntimeException("File to zip does not exist: {$file}"); } $fileToAdd = Str::of($file)->replace($this->tempFolder, '') ->start('/')->explode('/')->filter()->implode('/'); if (! $zip->addFile($file, $fileToAdd)) { throw new \RuntimeException("Failed to add file {$file} into zip"); } } if (! is_file($dataFilePath) || ! $zip->addFile($dataFilePath, basename($dataFilePath))) { throw new \RuntimeException("Failed to add file {$dataFilePath} into zip"); } if (! $zip->close()) { throw new \RuntimeException( "Failed to write zip {$zipFilePath}: " . $zip->getStatusString() ); } return $zipFilePath; } /** * Assert schema is valid * * @param string $schemaFileName * @param string $fileContent * @return void * @throws \Carsdotcom\JsonSchemaValidation\Exceptions\JsonSchemaValidationException In case of failure. */ protected function assertSchemaIsValid(string $schemaFileName, string $fileContent): void { SchemaValidator::validateEncodedStringOrThrow($fileContent, $schemaFileName); } /** * Get associated files list to export * * @param array|array> $data * @return array{ * settings: string, * brands: array, * fonts: array, * ranges: array, * products: array * } */ protected function getAssociatedFileList(array $data): array { $groupBy = fn(array $path) => Str::of(Arr::get($path, 0))->explode('/')->get(2); $items = collect(Arr::get($data, 'brands'))->map(fn(Brand $brand) => [ $brand->picture, // @phpstan-ignore-next-line $brand->ranges->map(fn(Range $range) => [ $range->picture, // @phpstan-ignore-next-line $range->products->map(fn(Product $product) => [ $product->picture ])->collect()->groupBy($groupBy) ])->collect()->groupBy($groupBy) ])->collect(); /** @var string $logo */ $logo = Arr::get($this->getSettings(), 'logo'); /** @var array $fonts */ $fonts = collect(Arr::get($data, 'fonts'))->pluck('font')->filter()->all(); /** @var \Illuminate\Support\Collection $brandList */ $brandList = $items->pluck(0); /** @var \Illuminate\Support\Collection $rangeList */ $rangeList = $items->pluck(1)->map(fn(Collection $ranges) => $ranges ->map(fn(Collection $range) => $range->pluck(0)))->flatten(); /** @var \Illuminate\Support\Collection $productList */ $productList = $items->pluck(1)->map(fn(Collection $ranges) => $ranges ->map(fn(Collection $range) => $range->pluck(1)))->flatten(); return [ 'settings' => $logo, 'brands' => $brandList->all(), 'fonts' => $fonts, 'ranges' => $rangeList->all(), 'products' => $productList->all(), ]; } /** * Get export file content * * @param array|array> $data * @return array<'version'|'settings'|'fonts'|'items',string|array|\App\Models\Font|\App\Models\Brand> */ protected function getFile(array $data): array { // phpcs:enable $settings = $this->getSettings(); $tableName = $settings->getTable(); $settings->logo = Str::start( \pathinfo($settings->logo, \PATHINFO_BASENAME), "pictures/{$tableName}/" ); return [ 'version' => $this->getVersion(), 'settings' => Arr::except($settings->toArray(), ['id']), 'fonts' => Arr::map(Arr::get($data, 'fonts'), function (Font $font) { $font->font = $font->font ? Str::start( \pathinfo($font->font, \PATHINFO_BASENAME), "fonts/" ) : $font->font; return $font; }), 'items' => Arr::map(Arr::get($data, 'brands'), function (Brand $brand) { $this->setPictureExportPath($brand); $brand->setRelation( 'ranges', // @phpstan-ignore argument.type $brand->ranges->map(function (Range $range) { $this->setPictureExportPath($range); // @phpstan-ignore argument.type $range->setRelation('products', $range->products->map(function (Product $product) { $this->setPictureExportPath($product); return $product; })); return $range; }) ); return $brand; }) ]; } // * Private Methods /** * Get the files that should be zipped * * @phpcs:ignore * @param array{settings: string,brands: array,fonts: array,ranges: array,products: array} $filesToZip * @return array * @throws \RuntimeException If temp folder caanot be cleared in order to work. */ private function preparePicturesForZip(array $filesToZip): array { $newPaths = []; if (!File::exists($this->tempFolder)) { File::makeDirectory($this->tempFolder, 0700, true); } File::deleteDirectory($this->tempFolder, true); if (!File::isEmptyDirectory($this->tempFolder)) { throw new \RuntimeException("{$this->tempFolder} is not empty, refuse to prepare export"); } $copyPath = fn(string $targetPath) => function (string $path) use ($targetPath): string { $targetFilePath = Str::of($targetPath)->append('/')->append(\pathinfo($path, \PATHINFO_BASENAME)); File::copy(\public_path($path), $targetFilePath); return $targetFilePath; }; $toCopy = ['settings', 'brands', 'ranges', 'products']; foreach ($toCopy as $element) { $targetPath = Str::of($this->tempFolder)->append("/pictures/{$element}"); File::makeDirectory($targetPath, 0700, true); $newPaths[$element] = collect($filesToZip[$element])->map($copyPath($targetPath))->all(); } // * 'fonts' $targetPath = Str::of($this->tempFolder)->append("/fonts"); File::makeDirectory($targetPath, 0700, true); $newPaths['fonts'] = collect($filesToZip['fonts'])->map(function ($path) use ($targetPath) { $targetFilePath = (string) $targetPath->append('/')->append(\pathinfo($path, \PATHINFO_BASENAME)); File::copy(\public_path($path), $targetFilePath); return $targetFilePath; })->all(); return collect($newPaths)->flatten()->all(); } /** * Set picture export path, where it will be stored in zip file. * * @param \App\Models\Brand|\App\Models\Range|\App\Models\Product $model * @return void */ private function setPictureExportPath(Brand|Range|Product|Font &$model): void { $tableName = $model->getTable(); $model->picture = Str::start( \pathinfo($model->picture, \PATHINFO_BASENAME), "pictures/{$tableName}/" ); } /** * Get app version * * @return string */ private function getVersion(): string { return Cache::get('composer')->version; } /** * Get app settings * * @return \App\Models\Settings */ private function getSettings(): Settings { return Settings::getFromCache($this->tenantId); } /** * Get app data * * @return array<'brands'|'fonts',array|array> */ private function getData(): array { return [ 'brands' => Brand::query() ->where([['status', true], ['tenant_id', $this->tenantId]]) ->whereHas('ranges') ->with('ranges', function (Builder $q) { return $q->where('status', true) ->whereHas('products') ->with('products', function (Builder $q) { return $q->where('status', true); }); }) ->get() ->all(), 'fonts' => Font::query() ->where('tenant_id', $this->tenantId) ->get() ->all() ]; } }