From 3cf18fb73854ded31003ec243e531dd7b3e30eda Mon Sep 17 00:00:00 2001 From: Moamen Eltouny Date: Thu, 12 Dec 2024 02:51:56 +0200 Subject: [PATCH 1/5] Update composer.json for PHP and Laravel version compatibility; add .gitignore --- .gitignore | 2 ++ composer.json | 11 +++++------ 2 files changed, 7 insertions(+), 6 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dfd6caa --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/vendor +composer.lock \ No newline at end of file diff --git a/composer.json b/composer.json index 0923c74..717db3f 100644 --- a/composer.json +++ b/composer.json @@ -11,14 +11,13 @@ "authors": [ { "name": "Moamen Eltouny (Raggi)", - "email": "raggi@raggitech.com" + "email": "raggigroup@gmail.com" } ], "require": { - "php": ">=7.2", - "laravel/framework": ">=6.0", - "pharaonic/laravel-uploader": ">=1.0", - "pharaonic/laravel-helpers": ">=1.0" + "php": ">=8.0", + "laravel/framework": ">=9.0", + "pharaonic/laravel-uploader": "^4.0" }, "autoload": { "psr-4": { @@ -34,4 +33,4 @@ }, "minimum-stability": "dev", "prefer-stable": true -} +} \ No newline at end of file From d63ab153ba525747eb5c6b529c59374a08cb5012 Mon Sep 17 00:00:00 2001 From: Moamen Eltouny Date: Thu, 12 Dec 2024 03:00:21 +0200 Subject: [PATCH 2/5] Add migration for creating files table with relationships --- .../2021_02_01_000003_create_files_table.php | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 database/migrations/2021_02_01_000003_create_files_table.php diff --git a/database/migrations/2021_02_01_000003_create_files_table.php b/database/migrations/2021_02_01_000003_create_files_table.php new file mode 100644 index 0000000..ce50c41 --- /dev/null +++ b/database/migrations/2021_02_01_000003_create_files_table.php @@ -0,0 +1,40 @@ +bigIncrements('id'); + $table->string('field'); + + $table->unsignedBigInteger('upload_id'); + + $table->string('model_type'); + $table->string('model_id'); + + $table->timestamps(); + + $table->foreign('upload_id')->references('id')->on('uploads')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('files'); + } +}; From 49c566377c7ad51605dcce86f86990d0d82de15e Mon Sep 17 00:00:00 2001 From: Moamen Eltouny Date: Thu, 12 Dec 2024 03:00:35 +0200 Subject: [PATCH 3/5] Remove File model, HasFiles trait, configuration, and migration for files table --- src/File.php | 59 ------ src/HasFiles.php | 171 ------------------ src/config/files.php | 8 - .../2021_02_01_000003_create_files_table.php | 40 ---- 4 files changed, 278 deletions(-) delete mode 100644 src/File.php delete mode 100644 src/HasFiles.php delete mode 100644 src/config/files.php delete mode 100644 src/database/migrations/2021_02_01_000003_create_files_table.php diff --git a/src/File.php b/src/File.php deleted file mode 100644 index 25eb38d..0000000 --- a/src/File.php +++ /dev/null @@ -1,59 +0,0 @@ - - */ -class File extends Model -{ - - /** - * Fillable Columns - * - * @var array - */ - protected $fillable = ['field', 'upload_id']; - - /** - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo - */ - public function file() - { - return $this->belongsTo(Upload::class, 'upload_id'); - } - - /** - * Get Url Directly - * - * @return string - */ - public function getUrlAttribute() - { - return $this->file->url; - } - - /** - * Get Thumbnail - * - * @return string - */ - public function getThumbnailAttribute() - { - return $this->file->thumbnail ?? null; - } - - /** - * Get the owning model. - */ - public function model() - { - return $this->morphTo(); - } -} diff --git a/src/HasFiles.php b/src/HasFiles.php deleted file mode 100644 index f9398fa..0000000 --- a/src/HasFiles.php +++ /dev/null @@ -1,171 +0,0 @@ - - */ -trait HasFiles -{ - use HasCustomAttributes; - - /** - * Files Atrributes on Save/Create - * - * @var array - */ - protected static $filesAttributesAction = []; - - /** - * @return void - */ - public function initializeHasFiles() - { - $attrs = get_class_vars(self::class); - $attrs = array_merge(config('Pharaonic.files.fields', []), $attrs['filesAttributes'] ?? []); - - foreach ($attrs as $attr) - $this->fillable[] = $attr; - } - - protected static function bootHasFiles() - { - $attrs = get_class_vars(self::class); - $attrs = array_merge(config('Pharaonic.files.fields', []), $attrs['filesAttributes'] ?? []); - - // Created - self::creating(function ($model) use ($attrs) { - foreach ($model->getAttributes() as $name => $value) { - if (in_array($name, $attrs)) { - self::$filesAttributesAction[$name] = $value; - unset($model->{$name}); - } - } - }); - - // Created - self::created(function ($model) { - if (count(self::$filesAttributesAction) > 0) { - foreach (self::$filesAttributesAction as $name => $file) - if ($file instanceof UploadedFile) - $model->setAttribute($name, $model->_setFileAttribute($name, $file)); - } - }); - - // Retrieving - self::retrieved(function ($model) use ($attrs) { - try { - foreach ($attrs as $attr) $model->addGetterAttribute($attr, '_getFileAttribute'); - foreach ($attrs as $attr) $model->addSetterAttribute($attr, '_setFileAttribute'); - } catch (\Throwable $e) { - throw new Exception('You have to use Pharaonic\Laravel\Helpers\Traits\HasCustomAttributes as a trait in ' . get_class($model)); - } - }); - - - // Deleting - self::deleting(function ($model) { - $model->clearFiles(); - }); - } - - /** - * Getting File - */ - public function _getFileAttribute($key) - { - if ($this->isFileAttribute($key)) { - if ($this->relationLoaded('files')) { - $file = $this->files; - } else { - $file = $this->files(); - - if (isset($this->filesOptions) && isset($this->filesOptions[$key]) && isset($this->filesOptions[$key]['thumbnail'])) - $file = $file->with('file.thumbnail'); - } - - $file = $this->relationLoaded('files') ? $this->files : $this->files(); - $file = $file->where('field', $key)->first(); - return $file ? $file->file : null; - } - } - - /** - * Uploading File - */ - public function _setFileAttribute($key, $value) - { - if ($this->isFileAttribute($key)) { - $file = $this->files()->where('field', $key)->first(); - - if ($file) { - $options = $this->filesOptions[$key] ?? []; - $options['file'] = $file->file; - - $newFile = upload($value, $options); - $file->update(['upload_id' => $newFile->id]); - - return $file; - } else { - $file = upload($value, $this->filesOptions[$key] ?? []); - - $this->files()->create([ - 'field' => $key, - 'upload_id' => $file->id, - ]); - - return $file; - } - } - - return null; - } - - /** - * Getting files attributes - */ - public function getFilesAttributes(): array - { - $fields = isset($this->filesAttributes) && is_array($this->filesAttributes) ? $this->filesAttributes : []; - return array_merge(config('Pharaonic.files.fields', []), $fields); - } - - /** - * Check if file attribute - */ - public function isFileAttribute(string $key): bool - { - return in_array($key, $this->getFilesAttributes()); - } - - /** - * Get All Files - */ - public function files() - { - $hasThumbnails = false; - - foreach ($this->filesOptions ?? [] as $options) - if (isset($options['thumbnail'])) - $hasThumbnails = true; - - return $this->morphMany(File::class, 'model')->with($hasThumbnails ? 'file.thumbnail' : 'file'); - } - - /** - * Clear All Files - */ - public function clearFiles() - { - foreach ($this->files()->get() as $file) { - $file->file->delete(); - } - } -} diff --git a/src/config/files.php b/src/config/files.php deleted file mode 100644 index f69e7bc..0000000 --- a/src/config/files.php +++ /dev/null @@ -1,8 +0,0 @@ - [ - 'image', 'picture', 'cover', - 'video', 'audio', - 'file' - ] -]; \ No newline at end of file diff --git a/src/database/migrations/2021_02_01_000003_create_files_table.php b/src/database/migrations/2021_02_01_000003_create_files_table.php deleted file mode 100644 index d5b2a73..0000000 --- a/src/database/migrations/2021_02_01_000003_create_files_table.php +++ /dev/null @@ -1,40 +0,0 @@ -bigIncrements('id'); - $table->string('field'); - - $table->unsignedBigInteger('upload_id'); - - $table->string('model_type'); - $table->string('model_id'); - - $table->timestamps(); - - $table->foreign('upload_id')->references('id')->on('uploads')->onDelete('cascade'); - }); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Schema::dropIfExists('files'); - } -} From 23336e8d32049d4618ed22a19d335e219c91a83c Mon Sep 17 00:00:00 2001 From: Moamen Eltouny Date: Thu, 12 Dec 2024 03:00:57 +0200 Subject: [PATCH 4/5] Add File model, FileObserver, and update FilesServiceProvider for migration loading --- src/FilesServiceProvider.php | 16 ++++---- src/Models/File.php | 69 ++++++++++++++++++++++++++++++++++ src/Observers/FileObserver.php | 19 ++++++++++ src/Traits/HasFiles.php | 8 ++++ 4 files changed, 103 insertions(+), 9 deletions(-) create mode 100644 src/Models/File.php create mode 100644 src/Observers/FileObserver.php create mode 100644 src/Traits/HasFiles.php diff --git a/src/FilesServiceProvider.php b/src/FilesServiceProvider.php index efca69c..e30603d 100644 --- a/src/FilesServiceProvider.php +++ b/src/FilesServiceProvider.php @@ -3,10 +3,11 @@ namespace Pharaonic\Laravel\Files; use Illuminate\Support\ServiceProvider; +use Pharaonic\Laravel\Files\Models\File; +use Pharaonic\Laravel\Files\Observers\FileObserver; class FilesServiceProvider extends ServiceProvider { - /** * Register services. * @@ -14,11 +15,7 @@ class FilesServiceProvider extends ServiceProvider */ public function register() { - // Config Merge - $this->mergeConfigFrom(__DIR__ . '/config/files.php', 'laravel-has-files'); - - // Migration Loading - $this->loadMigrationsFrom(__DIR__ . '/database/migrations'); + $this->loadMigrationsFrom(__DIR__ . '/../database/migrations'); } /** @@ -28,11 +25,12 @@ public function register() */ public function boot() { + // Observers + File::observe(FileObserver::class); + // Publishes $this->publishes([ - __DIR__ . '/config/files.php' => config_path('Pharaonic/files.php'), - __DIR__ . '/database/migrations/2021_02_01_000003_create_files_table.php' => database_path('migrations/2021_02_01_000003_create_files_table.php'), + __DIR__ . '/../database/migrations' => database_path('migrations'), ], ['pharaonic', 'laravel-has-files']); - } } diff --git a/src/Models/File.php b/src/Models/File.php new file mode 100644 index 0000000..4d8f02f --- /dev/null +++ b/src/Models/File.php @@ -0,0 +1,69 @@ +belongsTo(Upload::class, 'upload_id'); + } + + /** + * @return \Illuminate\Database\Eloquent\Relations\MorphTo + */ + public function model() + { + return $this->morphTo(); + } + + /** + * Get Url Directly + * + * @return string + */ + public function getUrlAttribute() + { + return $this->file->url; + } + + /** + * Get Thumbnail Object + * + * @return Upload|null + */ + public function getThumbnailAttribute() + { + return $this->file->thumbnail ?? null; + } +} diff --git a/src/Observers/FileObserver.php b/src/Observers/FileObserver.php new file mode 100644 index 0000000..53f1fdc --- /dev/null +++ b/src/Observers/FileObserver.php @@ -0,0 +1,19 @@ +file->delete(); + } +} diff --git a/src/Traits/HasFiles.php b/src/Traits/HasFiles.php new file mode 100644 index 0000000..681aada --- /dev/null +++ b/src/Traits/HasFiles.php @@ -0,0 +1,8 @@ + Date: Thu, 12 Dec 2024 15:32:16 +0200 Subject: [PATCH 5/5] Update file handling features: enhance File model, add ModelObserver, and implement FilesHandler trait --- composer.json | 7 +- src/Models/File.php | 65 ++++++++++++++--- src/Observers/FileObserver.php | 19 ++++- src/Observers/ModelObserver.php | 50 +++++++++++++ src/Traits/FilesHandler.php | 123 ++++++++++++++++++++++++++++++++ src/Traits/HasFiles.php | 46 +++++++++++- 6 files changed, 293 insertions(+), 17 deletions(-) create mode 100644 src/Observers/ModelObserver.php create mode 100644 src/Traits/FilesHandler.php diff --git a/composer.json b/composer.json index 717db3f..d895435 100644 --- a/composer.json +++ b/composer.json @@ -16,8 +16,9 @@ ], "require": { "php": ">=8.0", - "laravel/framework": ">=9.0", - "pharaonic/laravel-uploader": "^4.0" + "laravel/framework": ">=10.0", + "pharaonic/laravel-uploader": "^4.0", + "pharaonic/laravel-assistant": "^1.0" }, "autoload": { "psr-4": { @@ -33,4 +34,4 @@ }, "minimum-stability": "dev", "prefer-stable": true -} \ No newline at end of file +} diff --git a/src/Models/File.php b/src/Models/File.php index 4d8f02f..36d2eff 100644 --- a/src/Models/File.php +++ b/src/Models/File.php @@ -12,13 +12,20 @@ * @property string $model_type * @property string $model_id * @property-read string $url - * @property-read Upload $file + * @property-read Upload $upload * @property-read Upload $thumbnail * @property \Carbon\Carbon $created_at * @property \Carbon\Carbon $updated_at */ class File extends Model { + /** + * The Caller Model instance. + * + * @var Model + */ + public $caller; + /** * The attributes that are mass assignable. * @@ -34,9 +41,9 @@ class File extends Model /** * @return \Illuminate\Database\Eloquent\Relations\BelongsTo */ - public function file() + public function upload() { - return $this->belongsTo(Upload::class, 'upload_id'); + return $this->belongsTo(Upload::class); } /** @@ -48,22 +55,58 @@ public function model() } /** - * Get Url Directly + * Get an attribute from the model. + * + * @param string $key + * @return mixed + */ + public function getAttribute($key) + { + $value = parent::getAttribute($key); + + if (!$value) { + return $this->upload->{$key}; + } + + return $value; + } + + /** + * Handle dynamic method calls into the model. * - * @return string + * @param string $method + * @param array $parameters + * @return mixed */ - public function getUrlAttribute() + public function __call($method, $parameters) { - return $this->file->url; + if (in_array($method, [ + 'size', + 'thumbnail', + 'visibility', + 'public', + 'private', + 'getUrlAttribute', + 'getTemporaryUrlAttribute', + 'url', + 'temporaryUrl' + ])) { + return $this->upload->{$method}(...$parameters); + } + + return parent::__call($method, $parameters); } /** - * Get Thumbnail Object + * Set the Caller Model instance. * - * @return Upload|null + * @param Model $model + * @return void */ - public function getThumbnailAttribute() + public function setCaller(Model &$caller) { - return $this->file->thumbnail ?? null; + $this->caller = $caller; + + return $this; } } diff --git a/src/Observers/FileObserver.php b/src/Observers/FileObserver.php index 53f1fdc..8e6b4d3 100644 --- a/src/Observers/FileObserver.php +++ b/src/Observers/FileObserver.php @@ -14,6 +14,21 @@ class FileObserver */ public function deleting(File $file) { - $file->file->delete(); - } + $attribute = $file->caller->getAttributables()[$file->field]; + + if ($attribute->getOriginal() === $file) { + $attribute->reset($attribute->getValue()); + } else { + $attribute->reset(null); + } + + $file->caller->setRelation( + 'files', + $file->caller + ->files + ->filter(fn($f) => $f->field != $file->field) + ); + + $file->upload->delete(); + } } diff --git a/src/Observers/ModelObserver.php b/src/Observers/ModelObserver.php new file mode 100644 index 0000000..49c7366 --- /dev/null +++ b/src/Observers/ModelObserver.php @@ -0,0 +1,50 @@ +getDirtyFiles() as $attribute) { + if ($attribute->isDirty() && $attribute->getOriginal()) { + $attribute->getOriginal()->delete(); + } + + $upload = upload( + $attribute->get(), + $model->getFileOptions($attribute->getName()) + ); + + $file = $model->files()->create([ + 'field' => $attribute->getName(), + 'upload_id' => $upload->id + ]) + ->setRelation('upload', $upload) + ->setCaller($model); + + $attribute->reset($file); + } + } + + /** + * Handle the model "deleting" event. + * + * @param Model $model + * @return void + */ + public function deleting(Model $model) + { + foreach ($model->getFilesAttributes() as $attribute) { + $model->{$attribute}?->delete(); + } + } +} diff --git a/src/Traits/FilesHandler.php b/src/Traits/FilesHandler.php new file mode 100644 index 0000000..c297a67 --- /dev/null +++ b/src/Traits/FilesHandler.php @@ -0,0 +1,123 @@ +getFilesAttributes()); + } + + /** + * Getting files attributes + * + * @return array + */ + public function getFilesAttributes(): array + { + return array_keys($this->filesData); + } + + /** + * Getting file options. + * + * @param string $key + * @return array + */ + public function getFileOptions(string $key) + { + return $this->filesData[$key] ?? []; + } + + /** + * @return \Illuminate\Database\Eloquent\Relations\MorphMany + */ + public function files() + { + return $this + ->morphMany(File::class, 'model') + ->with('upload') + ->when( + $this->hasThumbnailRelationship, + fn($q) => $q->with('upload.thumbnail') + ); + } + + /** + * Assign the files to it's attributes. + * + * @return void + */ + public function loadFiles() + { + if (!$this->filesRelationLoaded) { + $this->files->each(function ($file) { + $this->{$file->field} = $file->setCaller($this); + }); + + $this->filesRelationLoaded = true; + } + } + + /** + * Getting a specific file. + * + * @param string $key + * @return \Pharaonic\Laravel\Files\Models\File|null + */ + public function getFile(string $key) + { + $this->loadFiles(); + + if ($this->isFileAttribute($key)) { + return $this->attributables[$key]?->getValue() + ?? $this->files->where('field', $key)->first() + ?? null; + } + + return null; + } + + /** + * Get Dirty Files + * + * @return array + */ + public function getDirtyFiles() + { + return array_filter( + $this->getDirtyAttributables(), + fn($attributable) => $this->isFileAttribute($attributable->getName()) + ); + } +} diff --git a/src/Traits/HasFiles.php b/src/Traits/HasFiles.php index 681aada..303b3e9 100644 --- a/src/Traits/HasFiles.php +++ b/src/Traits/HasFiles.php @@ -2,7 +2,51 @@ namespace Pharaonic\Laravel\Files\Traits; +use Pharaonic\Laravel\Assistant\Traits\Eloquent\Attributable; +use Pharaonic\Laravel\Files\Observers\ModelObserver; + trait HasFiles { - // + use Attributable, FilesHandler; + + /** + * Initialize Has Files + * + * @return void + */ + public function initializeHasFiles() + { + if (property_exists($this, 'files')) { + foreach ($this->files as $key => $value) { + $attribute = is_numeric($key) ? $value : $key; + $options = is_numeric($key) ? [] : $value; + + $this->fillable[] = $attribute; + $this->filesData[$attribute] = $options; + + $this->addAttributable( + $attribute, + null, + fn() => $this->getFile($attribute) + ); + + // Check Thumbnail Relationship Existence + if (isset($options['thumbnail'])) { + $this->hasThumbnailRelationship = true; + } + } + + unset($this->files); + } + } + + /** + * Boot the HasFiles Trait + * + * @return void + */ + public static function bootHasFiles() + { + static::observe(ModelObserver::class); + } }