136 lines
4.5 KiB
PHP
136 lines
4.5 KiB
PHP
<?php
|
|
|
|
namespace Modules\Core\File\Services;
|
|
|
|
use Illuminate\Contracts\Container\Container;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Carbon;
|
|
use Illuminate\Support\Collection;
|
|
use Modules\Core\File\Contracts\FileAdapterInterface;
|
|
use Modules\Core\File\Models\File;
|
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
|
|
|
/**
|
|
* Orchestrates the `files` table (ownership, purpose, cleanup — see that
|
|
* migration's own docblock) on top of whichever FileAdapterInterface a
|
|
* disk resolves to (see Providers\FileServiceProvider's contextual
|
|
* binding) — never touches a disk or a raw path itself. Knows nothing
|
|
* about custom fields, carts, or orders specifically; every caller
|
|
* (3dealer's custom-field upload flow today, some other future
|
|
* file-upload need tomorrow) supplies its own `purpose` string and owner
|
|
* model, and scopes its own queries by them.
|
|
*
|
|
* `purpose` also doubles as the storage directory a file lands under
|
|
* (store() passes it straight through as the adapter's own $directory) —
|
|
* one string to name both, rather than every caller supplying two
|
|
* near-identical values for what's really the same distinction ("which
|
|
* kind of upload is this").
|
|
*/
|
|
class FileService
|
|
{
|
|
public function __construct(
|
|
private readonly Container $container,
|
|
) {}
|
|
|
|
public function store(UploadedFile $file, string $purpose, string $disk = 'local'): File
|
|
{
|
|
$path = $this->adapter($disk)->store($file, $purpose);
|
|
|
|
return File::create([
|
|
'disk' => $disk,
|
|
'path' => $path,
|
|
'original_name' => $file->getClientOriginalName(),
|
|
'mime' => $file->getMimeType(),
|
|
'size' => $file->getSize(),
|
|
'purpose' => $purpose,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Re-points an existing File at its owner — called once an owner
|
|
* actually exists (e.g. a shopper's picked-but-not-yet-added photo
|
|
* gets a CartLine the moment it's added to the cart). Never creates a
|
|
* second row for the same physical file.
|
|
*/
|
|
public function attachOwner(File $file, Model $owner): File
|
|
{
|
|
$file->update([
|
|
'owner_type' => $owner->getMorphClass(),
|
|
'owner_id' => $owner->getKey(),
|
|
]);
|
|
|
|
return $file;
|
|
}
|
|
|
|
public function retrieve(File $file): StreamedResponse
|
|
{
|
|
return $this->adapter($file->disk)->retrieve($file->path, $file->original_name);
|
|
}
|
|
|
|
/**
|
|
* Same file as retrieve(), forced as a download (Content-Disposition:
|
|
* attachment) rather than served inline — for a button distinct from
|
|
* a preview link/thumbnail pointing at the same File.
|
|
*/
|
|
public function download(File $file): StreamedResponse
|
|
{
|
|
return $this->adapter($file->disk)->download($file->path, $file->original_name);
|
|
}
|
|
|
|
public function exists(File $file): bool
|
|
{
|
|
return $this->adapter($file->disk)->exists($file->path);
|
|
}
|
|
|
|
public function delete(File $file): void
|
|
{
|
|
$this->adapter($file->disk)->delete($file->path);
|
|
|
|
$file->delete();
|
|
}
|
|
|
|
/**
|
|
* @return Collection<int, File>
|
|
*/
|
|
public function list(string $purpose, ?Model $owner = null): Collection
|
|
{
|
|
return File::query()
|
|
->where('purpose', $purpose)
|
|
->when($owner, fn ($query) => $query
|
|
->where('owner_type', $owner->getMorphClass())
|
|
->where('owner_id', $owner->getKey()))
|
|
->get();
|
|
}
|
|
|
|
/**
|
|
* Deletes every File of the given purpose that has no owner yet and
|
|
* is older than $olderThan — the grace period covers a shopper still
|
|
* on the page with a picked-but-not-yet-added file. An owned File
|
|
* (whatever the owner type) is never touched here; callers that want
|
|
* owned files gone too should delete() them explicitly wherever that
|
|
* ownership itself ends (e.g. a CartLine being removed).
|
|
*
|
|
* @return int number of files deleted
|
|
*/
|
|
public function pruneUnowned(string $purpose, Carbon $olderThan): int
|
|
{
|
|
$files = File::query()
|
|
->where('purpose', $purpose)
|
|
->whereNull('owner_type')
|
|
->where('created_at', '<', $olderThan)
|
|
->get();
|
|
|
|
foreach ($files as $file) {
|
|
$this->delete($file);
|
|
}
|
|
|
|
return $files->count();
|
|
}
|
|
|
|
private function adapter(string $disk): FileAdapterInterface
|
|
{
|
|
return $this->container->make(FileAdapterInterface::class, ['disk' => $disk]);
|
|
}
|
|
}
|