379 lines
13 KiB
PHP
379 lines
13 KiB
PHP
<?php
|
|
|
|
namespace App\Jobs;
|
|
|
|
use App\Models\Brand;
|
|
use App\Models\Font;
|
|
use App\Models\Product;
|
|
use App\Models\Range;
|
|
use App\Models\Settings;
|
|
use App\Models\Tenant;
|
|
use Carsdotcom\JsonSchemaValidation\SchemaValidator;
|
|
use Illuminate\Bus\Queueable;
|
|
use Illuminate\Contracts\Database\Eloquent\Builder;
|
|
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
use Illuminate\Foundation\Bus\Dispatchable;
|
|
use Illuminate\Queue\InteractsWithQueue;
|
|
use Illuminate\Queue\SerializesModels;
|
|
use Illuminate\Support\Arr;
|
|
use Illuminate\Support\Carbon;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\File;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Str;
|
|
use RuntimeException;
|
|
|
|
class ShowroomExport implements ShouldQueue, ShouldBeUnique
|
|
{
|
|
use Dispatchable;
|
|
use InteractsWithQueue;
|
|
use Queueable;
|
|
use SerializesModels;
|
|
|
|
/**
|
|
* Export dir
|
|
*
|
|
* @var string
|
|
*/
|
|
private string $exportFolder = 'app/export';
|
|
|
|
/**
|
|
* Working dir
|
|
*
|
|
* @var string
|
|
*/
|
|
private string $tempFolder = 'app/temp/export';
|
|
|
|
/**
|
|
* Working dir
|
|
*
|
|
* @var string
|
|
*/
|
|
private string $tenantId = '';
|
|
|
|
/**
|
|
* The number of seconds after which the job's unique lock will be released.
|
|
*
|
|
* @var integer
|
|
*/
|
|
public int $uniqueFor = 3600;
|
|
|
|
/**
|
|
* Create a new job instance.
|
|
*
|
|
* @param string $tenantId
|
|
*
|
|
* @throws RuntimeException If there's no Tenant for the given id.
|
|
*/
|
|
public function __construct(string $tenantId)
|
|
{
|
|
if (!Tenant::query()->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<string,array<int,\App\Models\Brand>|array<int,\App\Models\Font>> $data
|
|
* @return array{
|
|
* settings: string,
|
|
* brands: array<int, string>,
|
|
* fonts: array<int, string>,
|
|
* ranges: array<int, string>,
|
|
* products: array<int, string>
|
|
* }
|
|
*/
|
|
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<string> $fonts */
|
|
$fonts = collect(Arr::get($data, 'fonts'))->pluck('font')->filter()->all();
|
|
/** @var \Illuminate\Support\Collection<int, string> $brandList */
|
|
$brandList = $items->pluck(0);
|
|
/** @var \Illuminate\Support\Collection<int, string> $rangeList */
|
|
$rangeList = $items->pluck(1)->map(fn(Collection $ranges) => $ranges
|
|
->map(fn(Collection $range) => $range->pluck(0)))->flatten();
|
|
/** @var \Illuminate\Support\Collection<int, string> $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<string,array<int,\App\Models\Brand>|array<int,\App\Models\Font>> $data
|
|
* @return array<'version'|'settings'|'fonts'|'items',string|array<string,number|string|boolean>|\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<int, string>,fonts: array<string>,ranges: array<int, string>,products: array<int, string>} $filesToZip
|
|
* @return array<string>
|
|
* @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<int,\App\Models\Brand>|array<int,\App\Models\Font>>
|
|
*/
|
|
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()
|
|
];
|
|
}
|
|
}
|