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..d895435 100644 --- a/composer.json +++ b/composer.json @@ -11,14 +11,14 @@ "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": ">=10.0", + "pharaonic/laravel-uploader": "^4.0", + "pharaonic/laravel-assistant": "^1.0" }, "autoload": { "psr-4": { diff --git a/src/database/migrations/2021_02_01_000003_create_files_table.php b/database/migrations/2021_02_01_000003_create_files_table.php similarity index 95% rename from src/database/migrations/2021_02_01_000003_create_files_table.php rename to database/migrations/2021_02_01_000003_create_files_table.php index d5b2a73..ce50c41 100644 --- a/src/database/migrations/2021_02_01_000003_create_files_table.php +++ b/database/migrations/2021_02_01_000003_create_files_table.php @@ -4,7 +4,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -class CreateFilesTable extends Migration +return new class extends Migration { /** * Run the migrations. @@ -37,4 +37,4 @@ public function down() { Schema::dropIfExists('files'); } -} +}; 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/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/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/Models/File.php b/src/Models/File.php new file mode 100644 index 0000000..36d2eff --- /dev/null +++ b/src/Models/File.php @@ -0,0 +1,112 @@ +belongsTo(Upload::class); + } + + /** + * @return \Illuminate\Database\Eloquent\Relations\MorphTo + */ + public function model() + { + return $this->morphTo(); + } + + /** + * 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. + * + * @param string $method + * @param array $parameters + * @return mixed + */ + public function __call($method, $parameters) + { + if (in_array($method, [ + 'size', + 'thumbnail', + 'visibility', + 'public', + 'private', + 'getUrlAttribute', + 'getTemporaryUrlAttribute', + 'url', + 'temporaryUrl' + ])) { + return $this->upload->{$method}(...$parameters); + } + + return parent::__call($method, $parameters); + } + + /** + * Set the Caller Model instance. + * + * @param Model $model + * @return void + */ + public function setCaller(Model &$caller) + { + $this->caller = $caller; + + return $this; + } +} diff --git a/src/Observers/FileObserver.php b/src/Observers/FileObserver.php new file mode 100644 index 0000000..8e6b4d3 --- /dev/null +++ b/src/Observers/FileObserver.php @@ -0,0 +1,34 @@ +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 new file mode 100644 index 0000000..303b3e9 --- /dev/null +++ b/src/Traits/HasFiles.php @@ -0,0 +1,52 @@ +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); + } +} 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