From 2b8fe5764cee04b11ae120708ce5e9df5c7198b1 Mon Sep 17 00:00:00 2001 From: Konstantinos Arvanitakis Date: Fri, 25 Sep 2026 09:10:56 +0300 Subject: [PATCH] Feat: Creating Migration Models And Adapters for file Service --- composer.json | 1 + .../2026_09_25_000001_create_files_table.php | 50 ++++++++ src/File/Adapters/LocalFileAdapter.php | 43 +++++++ src/File/Contracts/FileAdapterInterface.php | 38 ++++++ src/File/Models/File.php | 28 +++++ src/File/Services/FileService.php | 119 ++++++++++++++++++ src/Providers/FileServiceProvider.php | 34 +++++ 7 files changed, 313 insertions(+) create mode 100644 database/migrations/2026_09_25_000001_create_files_table.php create mode 100644 src/File/Adapters/LocalFileAdapter.php create mode 100644 src/File/Contracts/FileAdapterInterface.php create mode 100644 src/File/Models/File.php create mode 100644 src/File/Services/FileService.php create mode 100644 src/Providers/FileServiceProvider.php diff --git a/composer.json b/composer.json index 5dabfe1..931c13d 100644 --- a/composer.json +++ b/composer.json @@ -43,6 +43,7 @@ "Modules\\Core\\Providers\\CatalogServiceProvider", "Modules\\Core\\Providers\\CartServiceProvider", "Modules\\Core\\Providers\\ReviewServiceProvider", + "Modules\\Core\\Providers\\FileServiceProvider", "Modules\\Core\\Providers\\ShippingServiceProvider", "Modules\\Core\\Providers\\OrderServiceProvider", "Modules\\Core\\Providers\\PrivacyServiceProvider" diff --git a/database/migrations/2026_09_25_000001_create_files_table.php b/database/migrations/2026_09_25_000001_create_files_table.php new file mode 100644 index 0000000..9ebefa3 --- /dev/null +++ b/database/migrations/2026_09_25_000001_create_files_table.php @@ -0,0 +1,50 @@ +id(); + $table->string('disk'); + $table->string('path'); + $table->string('original_name')->nullable(); + $table->string('mime')->nullable(); + $table->unsignedBigInteger('size')->nullable(); + $table->string('purpose'); + $table->nullableMorphs('owner'); + $table->timestamps(); + + $table->index(['purpose', 'owner_type', 'owner_id']); + }); + } + + public function down(): void + { + Schema::dropIfExists('file_uploads'); + } +}; diff --git a/src/File/Adapters/LocalFileAdapter.php b/src/File/Adapters/LocalFileAdapter.php new file mode 100644 index 0000000..9e78109 --- /dev/null +++ b/src/File/Adapters/LocalFileAdapter.php @@ -0,0 +1,43 @@ +disk->putFile($directory, $file); + } + + public function exists(string $path): bool + { + return $this->disk->exists($path); + } + + public function delete(string $path): void + { + $this->disk->delete($path); + } + + public function retrieve(string $path, ?string $name = null): StreamedResponse + { + return $this->disk->response($path, $name); + } +} diff --git a/src/File/Contracts/FileAdapterInterface.php b/src/File/Contracts/FileAdapterInterface.php new file mode 100644 index 0000000..eb1740d --- /dev/null +++ b/src/File/Contracts/FileAdapterInterface.php @@ -0,0 +1,38 @@ + 'integer', + ]; + } + + public function owner(): MorphTo + { + return $this->morphTo(); + } +} diff --git a/src/File/Services/FileService.php b/src/File/Services/FileService.php new file mode 100644 index 0000000..daee2c2 --- /dev/null +++ b/src/File/Services/FileService.php @@ -0,0 +1,119 @@ +adapter($disk)->store($file, $directory); + + 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); + } + + 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 + */ + 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]); + } +} diff --git a/src/Providers/FileServiceProvider.php b/src/Providers/FileServiceProvider.php new file mode 100644 index 0000000..531035d --- /dev/null +++ b/src/Providers/FileServiceProvider.php @@ -0,0 +1,34 @@ +app->bind(FileAdapterInterface::class, function ($app, array $params) { + $disk = $params['disk'] ?? 'local'; + + return match ($disk) { + 'local' => new LocalFileAdapter(Storage::disk($disk)), + default => throw new InvalidArgumentException("No FileAdapterInterface available for disk \"{$disk}\"."), + }; + }); + } + + public function boot(): void + { + $this->loadMigrationsFrom(__DIR__.'/../../database/migrations'); + } +}