├── .gitignore ├── src ├── Interfaces │ ├── EventUploadInterface.php │ └── UploadFileInterface.php ├── Casts │ ├── FileCastNullable.php │ └── FileCast.php ├── Traits │ └── FileCastRemover.php ├── Classes │ └── FileArgument.php ├── Services │ └── DriverService.php └── FileCastHelper.php ├── tests ├── Models │ ├── OverwriteEventModel.php │ ├── CustomEventModel.php │ ├── User.php │ ├── UserFileCastNullable.php │ └── GlobalEventModel.php ├── TestCase.php ├── Services │ ├── CustomUploadService.php │ └── CustomDriverService.php ├── FileCastTest.php └── FileCastNullableTest.php ├── composer.json ├── phpunit.xml ├── license.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | .phpunit.result.cache 3 | composer.lock 4 | -------------------------------------------------------------------------------- /src/Interfaces/EventUploadInterface.php: -------------------------------------------------------------------------------- 1 | FileCast::class . ':default,default,default,default,' . CustomUploadService::class, 17 | ]; 18 | } 19 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "abdelrahmanbl/laravel-uploadable", 3 | "description": "this package adding behaviour to a model for self uploading images like avatar or any files type.", 4 | "type": "library", 5 | "authors": [ 6 | { 7 | "name": "AbdelrahmanBl", 8 | "email": "abdelrahmangamal990@gmail.com" 9 | } 10 | ], 11 | "scripts": { 12 | "test": "vendor/bin/pest" 13 | }, 14 | "require-dev": { 15 | "orchestra/testbench": "^7.0|^8.0|^9.0", 16 | "pestphp/pest": "^1.20|^2.0" 17 | }, 18 | "autoload": { 19 | "psr-4": { 20 | "Bl\\LaravelUploadable\\": "src/", 21 | "Bl\\LaravelUploadable\\Test\\": "tests/" 22 | } 23 | }, 24 | "config": { 25 | "allow-plugins": { 26 | "pestphp/pest-plugin": true 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | tests 6 | 7 | 8 | 9 | 10 | src/ 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /tests/Models/CustomEventModel.php: -------------------------------------------------------------------------------- 1 | FileCast::class . ':default,default,default,default,' . CustomUploadService::class, 39 | ]; 40 | } 41 | -------------------------------------------------------------------------------- /src/Casts/FileCastNullable.php: -------------------------------------------------------------------------------- 1 | migrate(); 19 | } 20 | 21 | protected function migrate() 22 | { 23 | Schema::create('users', function(Blueprint $table) { 24 | $table->id(); 25 | $table->string('default_avatar')->nullable(); 26 | $table->string('custom_avatar_directory')->nullable(); 27 | $table->string('custom_avatar_disk')->nullable(); 28 | $table->string('custom_avatar_driver')->nullable(); 29 | $table->string('custom_avatar_default_path')->nullable(); 30 | $table->string('custom_avatar_default_path_with_nullable')->nullable(); 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 AbdelrahmanBl 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Traits/FileCastRemover.php: -------------------------------------------------------------------------------- 1 | casts as $attributeName => $parametersString) { 15 | 16 | if(\Str::startsWith($parametersString, FileCast::class)) { 17 | 18 | list($directory, $disk, $driver) = FileCastHelper::getCastingParameters($parametersString); 19 | 20 | // set the driver instance... 21 | $driver = FileCastHelper::getDriverInstance($disk, $driver); 22 | 23 | // set the file path... 24 | $filePath = $instance->getRawOriginal($attributeName); 25 | 26 | // deleting the file from it's path... 27 | if($filePath) { 28 | $driver->delete($filePath); 29 | } 30 | 31 | } 32 | 33 | } 34 | 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Models/User.php: -------------------------------------------------------------------------------- 1 | FileCast::class, 35 | 'custom_avatar_directory' => FileCast::class . ':CustomUser/avatars', 36 | 'custom_avatar_disk' => FileCast::class . ':default,local', 37 | 'custom_avatar_driver' => FileCast::class . ':default,default,' . CustomDriverService::class, 38 | 'custom_avatar_default_path' => FileCast::class . ':default,default,default,custom_default_path.png', 39 | 'custom_avatar_default_path_with_nullable' => FileCast::class . ':default,default,default,nullable', 40 | ]; 41 | } 42 | -------------------------------------------------------------------------------- /src/Classes/FileArgument.php: -------------------------------------------------------------------------------- 1 | argument = $argument; 12 | } 13 | 14 | /** 15 | * set the arugment value. 16 | * 17 | * @param string $value 18 | * @return void 19 | */ 20 | public function setValue($value) 21 | { 22 | $this->argument = $value; 23 | } 24 | 25 | /** 26 | * get the arugment value. 27 | * 28 | * @return string 29 | */ 30 | public function getValue() 31 | { 32 | return $this->argument; 33 | } 34 | 35 | /** 36 | * check if the arugment value is default or not. 37 | * 38 | * @return bool 39 | */ 40 | public function isDefault() 41 | { 42 | return $this->getValue() === 'default'; 43 | } 44 | 45 | /** 46 | * check if the arugment value is custom or not. 47 | * 48 | * @return bool 49 | */ 50 | public function isCustom() 51 | { 52 | return ! $this->isDefault(); 53 | } 54 | 55 | /** 56 | * check if the arugment value is [null|nullable] or not. 57 | * 58 | * @return void 59 | */ 60 | public function isNullable() 61 | { 62 | return in_array($this->getValue(), ['null', 'nullable']); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Services/DriverService.php: -------------------------------------------------------------------------------- 1 | disk = $disk->isDefault() ? '' : $disk->getValue(); 22 | } 23 | 24 | /** 25 | * handle store proccess of the file. 26 | * 27 | * @param UploadedFile $file 28 | * @param string $directory 29 | * @return mixed 30 | */ 31 | public function store(UploadedFile $file, string $directory): mixed 32 | { 33 | return $file->store($directory, $this->disk); 34 | } 35 | 36 | /** 37 | * handle getting the file full url path. 38 | * 39 | * @param string $path 40 | * @return string 41 | */ 42 | public function get(string $path): mixed 43 | { 44 | return Storage::disk($this->disk)->url($path); 45 | } 46 | 47 | /** 48 | * handle deleting a file. 49 | * 50 | * @param string $path 51 | * @return void 52 | */ 53 | public function delete(string $path): void 54 | { 55 | Storage::disk($this->disk)->delete($path); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/Models/UserFileCastNullable.php: -------------------------------------------------------------------------------- 1 | FileCastNullable::class, 42 | 'custom_avatar_directory' => FileCastNullable::class . ':CustomUser/avatars', 43 | 'custom_avatar_disk' => FileCastNullable::class . ':default,local', 44 | 'custom_avatar_driver' => FileCastNullable::class . ':default,default,' . CustomDriverService::class, 45 | 'custom_avatar_default_path' => FileCastNullable::class . ':default,default,default,custom_default_path.png', 46 | 'custom_avatar_default_path_with_nullable' => FileCastNullable::class, 47 | ]; 48 | } 49 | -------------------------------------------------------------------------------- /tests/Services/CustomUploadService.php: -------------------------------------------------------------------------------- 1 | getClientOriginalName(), 32 | $mimeType, 33 | null, 34 | true // Mark the file as already uploaded 35 | ); 36 | 37 | return $uploadedFile; 38 | } 39 | 40 | /** 41 | * Apply after the file cast upload event. 42 | * 43 | * @param UploadedFile $file 44 | * @return void 45 | */ 46 | public function after(UploadedFile $file): void 47 | { 48 | session()->put('CUSTOM_UPLOADED_FILE_PATH', $file->path()); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/Services/CustomDriverService.php: -------------------------------------------------------------------------------- 1 | disk = $disk->getValue(); 23 | } 24 | 25 | /** 26 | * handle store proccess of the file. 27 | * 28 | * @param UploadedFile $file 29 | * @param string $directory 30 | * @return mixed 31 | */ 32 | public function store(UploadedFile $file, string $directory): mixed 33 | { 34 | $uploadedDir = self::$uploadDir . $directory; 35 | 36 | return $file->storePublicly($uploadedDir); 37 | } 38 | 39 | /** 40 | * handle getting the file full url path. 41 | * 42 | * @param string $path 43 | * @return string 44 | */ 45 | public function get(string $path): mixed 46 | { 47 | return url($path); 48 | } 49 | 50 | /** 51 | * handle deleting a file. 52 | * 53 | * @param string $path 54 | * @return void 55 | */ 56 | public function delete(string $path): void 57 | { 58 | $path = storage_path('app/public/' . $path); 59 | 60 | if (file_exists($path)) { 61 | unlink($path); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/Models/GlobalEventModel.php: -------------------------------------------------------------------------------- 1 | FileCast::class, 40 | ]; 41 | 42 | /** 43 | * apply before file cast upload event. 44 | * 45 | * @param UploadedFile $file 46 | * @return UploadedFile 47 | */ 48 | public function beforeFileCastUpload(UploadedFile $file): UploadedFile 49 | { 50 | $compressedFile = gzencode(file_get_contents($file), 9); 51 | 52 | // Save the compressed content to a temporary file 53 | $tempFilePath = tempnam(sys_get_temp_dir(), 'compressed'); 54 | file_put_contents($tempFilePath, $compressedFile); 55 | 56 | // Determine the MIME type of the original file 57 | $mimeType = Storage::mimeType($tempFilePath); 58 | 59 | // Create UploadedFile instance 60 | $uploadedFile = new UploadedFile( 61 | $tempFilePath, 62 | $file->getClientOriginalName(), 63 | $mimeType, 64 | null, 65 | true // Mark the file as already uploaded 66 | ); 67 | 68 | return $uploadedFile; 69 | } 70 | 71 | /** 72 | * apply after file cast upload event. 73 | * 74 | * @param UploadedFile $file 75 | * @return void 76 | */ 77 | public function afterFileCastUpload(UploadedFile $file): void 78 | { 79 | session()->put('GLOBAL_UPLOADED_FILE_PATH', $file->path()); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/FileCastHelper.php: -------------------------------------------------------------------------------- 1 | isDefault() 20 | ? new DriverService($disk) 21 | : new ($driver->getValue())($disk); 22 | } 23 | 24 | /** 25 | * get default directory path or customize it... 26 | * 27 | * @param \Bl\LaravelUploadable\Classes\FileArgument $directory 28 | * @param object $model 29 | * @param string $key 30 | * @return string 31 | */ 32 | public static function getDirectoryPath($directory, $model, $key) 33 | { 34 | // set the default directory... 35 | if($directory->isDefault()) { 36 | $directory->setValue(class_basename($model) . DIRECTORY_SEPARATOR . $key); 37 | } 38 | 39 | return $directory->getValue(); 40 | } 41 | 42 | /** 43 | * get file cast parameters as an array... 44 | * 45 | * @param string $parametersString 46 | * @return array<\Bl\LaravelUploadable\Classes\FileArgument> 47 | */ 48 | public static function getCastingParameters($parametersString) 49 | { 50 | $disk = new FileArgument(); 51 | $driver = new FileArgument(); 52 | $directory = new FileArgument(); 53 | 54 | // unpacking parameters... 55 | $parametersArray = explode(',', $parametersString); 56 | 57 | // reset first element with custom directory value or null when default... 58 | $parametersArray[0] = explode(':', $parametersArray[0])[1] ?? null; 59 | 60 | // set custom directory... 61 | if(! empty($parametersArray[0])) { 62 | $directory->setValue($parametersArray[0]); 63 | } 64 | 65 | // set custom disk... 66 | if(! empty($parametersArray[1])) { 67 | $disk->setValue($parametersArray[1]); 68 | } 69 | 70 | // set custom driver... 71 | if(! empty($parametersArray[2])) { 72 | $driver->setValue($parametersArray[2]); 73 | } 74 | 75 | return [ 76 | $directory, 77 | $disk, 78 | $driver, 79 | ]; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Casts/FileCast.php: -------------------------------------------------------------------------------- 1 | directory = new FileArgument($directory); 36 | 37 | // setting the default driver... 38 | $this->driver = FileCastHelper::getDriverInstance(new FileArgument($disk), new FileArgument($driver)); 39 | 40 | // the default value to return when nullable... 41 | $this->default = new FileArgument($default); 42 | 43 | $this->event = new FileArgument($event); 44 | } 45 | 46 | /** 47 | * Cast the given value. 48 | * 49 | * @param \Illuminate\Database\Eloquent\Model $model 50 | * @param string $key 51 | * @param string $value 52 | * @param array $attributes 53 | * @return array 54 | */ 55 | public function get($model, $key, $value, $attributes) 56 | { 57 | if($value) { 58 | return $this->driver->get($value); 59 | } 60 | 61 | // getting the default path when empty... 62 | if($this->default->isDefault()) { 63 | return asset(config('filesystems.default_url', 'uploadable.jpg')); 64 | } 65 | 66 | // getting null value when empty... 67 | if($this->default->isNullable()) { 68 | return null; 69 | } 70 | 71 | // getting customized path when empty... 72 | return asset($this->default->getValue()); 73 | } 74 | 75 | /** 76 | * Prepare the given value for storage. 77 | * 78 | * @param \Illuminate\Database\Eloquent\Model $model 79 | * @param string $key 80 | * @param \Illuminate\Http\UploadedFile $value 81 | * @param array $attributes 82 | * @return string 83 | */ 84 | public function set($model, $key, $value, $attributes) 85 | { 86 | if($value instanceof UploadedFile) { 87 | 88 | // handle delete old file if exists... 89 | if(array_key_exists($key, $attributes) && ! empty($attributes[$key])) { 90 | $this->driver->delete($attributes[$key]); 91 | } 92 | 93 | // set custom event service... 94 | if($this->event->isCustom()) { 95 | $this->customEventService = new ($this->event->getValue())(); 96 | } 97 | 98 | // handle before file upload events... 99 | $value = $this->handleBeforeUpload($value, $model); 100 | 101 | // storing the file in the directory... 102 | $storedPath = $this->driver->store( 103 | $value, 104 | FileCastHelper::getDirectoryPath($this->directory, $model, $key) 105 | ); 106 | 107 | // handle after file upload events... 108 | $this->handleAfterUpload($value, $model); 109 | 110 | // overwrite the model with the stored path... 111 | $model->{$key} = $storedPath; 112 | 113 | return $storedPath; 114 | 115 | } 116 | 117 | // return old value or null to skip the field update... 118 | return $attributes[$key] ?? NULL; 119 | 120 | } 121 | 122 | /** 123 | * handle before uploading the file. 124 | * 125 | * @param \Illuminate\Http\UploadedFile $file 126 | * @param \Illuminate\Database\Eloquent\Model $model 127 | * @return \Illuminate\Http\UploadedFile 128 | */ 129 | public function handleBeforeUpload($file, $model) 130 | { 131 | // handle custom event service... 132 | if(isset($this->customEventService)) { 133 | return $this->customEventService->before($file); 134 | } 135 | 136 | // handle global event service... 137 | if(method_exists($model, 'beforeFileCastUpload')) { 138 | return $model->beforeFileCastUpload($file); 139 | } 140 | 141 | return $file; 142 | } 143 | 144 | public function handleAfterUpload($file, $model) 145 | { 146 | // handle custom event service... 147 | if(isset($this->customEventService)) { 148 | $this->customEventService->after($file); 149 | return; 150 | } 151 | 152 | // handle global event service... 153 | if(method_exists($model, 'afterFileCastUpload')) { 154 | $model->afterFileCastUpload($file); 155 | return; 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /tests/FileCastTest.php: -------------------------------------------------------------------------------- 1 | create([ 16 | 'default_avatar' => UploadedFile::fake()->image('avatar') 17 | ]); 18 | 19 | $avatarLink = url('storage' . DIRECTORY_SEPARATOR . $user->getRawOriginal('default_avatar')); 20 | 21 | $avatarPath = storage_path('app/public' . DIRECTORY_SEPARATOR . $user->getRawOriginal('default_avatar')); 22 | 23 | $this->assertEquals($avatarLink, $user->default_avatar); 24 | 25 | $this->assertFileExists($avatarPath); 26 | 27 | $user->delete(); 28 | 29 | $this->assertFileDoesNotExist($avatarPath); 30 | } 31 | 32 | public function test_it_can_create_avatar_with_custom_directory() 33 | { 34 | $user = User::query()->create([ 35 | 'custom_avatar_directory' => UploadedFile::fake()->image('avatar') 36 | ]); 37 | 38 | $avatarLink = url('storage' . DIRECTORY_SEPARATOR . $user->getRawOriginal('custom_avatar_directory')); 39 | 40 | $avatarPath = storage_path('app/public' . DIRECTORY_SEPARATOR . $user->getRawOriginal('custom_avatar_directory')); 41 | 42 | $this->assertEquals($avatarLink, $user->custom_avatar_directory); 43 | 44 | $this->assertFileExists($avatarPath); 45 | 46 | $user->delete(); 47 | 48 | $this->assertFileDoesNotExist($avatarPath); 49 | } 50 | 51 | public function test_it_can_create_avatar_with_custom_disk() 52 | { 53 | $user = User::query()->create([ 54 | 'custom_avatar_disk' => UploadedFile::fake()->image('avatar') 55 | ]); 56 | 57 | $avatarLink = '/storage' . DIRECTORY_SEPARATOR . $user->getRawOriginal('custom_avatar_disk'); 58 | 59 | $avatarPath = storage_path('app' . DIRECTORY_SEPARATOR . $user->getRawOriginal('custom_avatar_disk')); 60 | 61 | $this->assertEquals($avatarLink, $user->custom_avatar_disk); 62 | 63 | $this->assertFileExists($avatarPath); 64 | 65 | $user->delete(); 66 | 67 | $this->assertFileDoesNotExist($avatarPath); 68 | } 69 | 70 | public function test_it_can_create_avatar_with_custom_driver() 71 | { 72 | $user = User::query()->create([ 73 | 'custom_avatar_driver' => UploadedFile::fake()->image('avatar') 74 | ]); 75 | 76 | $avatarLink = url($user->getRawOriginal('custom_avatar_driver')); 77 | 78 | $avatarPath = storage_path('app/public' . DIRECTORY_SEPARATOR . $user->getRawOriginal('custom_avatar_driver')); 79 | 80 | $this->assertEquals($avatarLink, $user->custom_avatar_driver); 81 | 82 | $this->assertFileExists($avatarPath); 83 | 84 | $user->delete(); 85 | 86 | $this->assertFileDoesNotExist($avatarPath); 87 | } 88 | 89 | public function test_it_can_create_avatar_with_custom_default_path() 90 | { 91 | $user = User::query()->create([ 92 | 'custom_avatar_default_path' => NULL 93 | ]); 94 | 95 | $avatarLink = url('custom_default_path.png'); 96 | 97 | $this->assertEquals($avatarLink, $user->custom_avatar_default_path); 98 | } 99 | 100 | public function test_it_can_create_avatar_with_custom_default_path_with_nullable() 101 | { 102 | $user = User::query()->create([ 103 | 'custom_avatar_default_path_with_nullable' => NULL 104 | ]); 105 | 106 | $this->assertEquals(NULL, $user->custom_avatar_default_path_with_nullable); 107 | } 108 | 109 | public function test_it_can_overwrite_old_avatar_when_updating() 110 | { 111 | $user = User::query()->create([ 112 | 'default_avatar' => UploadedFile::fake()->image('avatar') 113 | ]); 114 | 115 | $oldAvatarLink = url('storage' . DIRECTORY_SEPARATOR . $user->getRawOriginal('default_avatar')); 116 | 117 | $oldAvatarPath = storage_path('app/public' . DIRECTORY_SEPARATOR . $user->getRawOriginal('default_avatar')); 118 | 119 | $this->assertFileExists($oldAvatarPath); 120 | 121 | $user->update([ 122 | 'default_avatar' => UploadedFile::fake()->image('avatar') 123 | ]); 124 | 125 | $newAvatarLink = url('storage' . DIRECTORY_SEPARATOR . $user->getRawOriginal('default_avatar')); 126 | 127 | $newAvatarPath = storage_path('app/public' . DIRECTORY_SEPARATOR . $user->getRawOriginal('default_avatar')); 128 | 129 | $this->assertNotEquals($oldAvatarLink, $newAvatarLink); 130 | 131 | $this->assertFileDoesNotExist($oldAvatarPath); 132 | 133 | $this->assertFileExists($newAvatarPath); 134 | 135 | $user->delete(); 136 | 137 | $this->assertFileDoesNotExist($newAvatarPath); 138 | } 139 | 140 | public function test_it_can_apply_global_events() 141 | { 142 | $image = UploadedFile::fake()->image('avatar'); 143 | 144 | $user = GlobalEventModel::query()->create([ 145 | 'default_avatar' => $image, 146 | ]); 147 | 148 | $avatarPath = storage_path('app/public' . DIRECTORY_SEPARATOR . $user->getRawOriginal('default_avatar')); 149 | 150 | $this->assertLessThan($image->getSize(), filesize($avatarPath)); 151 | 152 | $this->assertFileEquals($avatarPath, session()->pull('GLOBAL_UPLOADED_FILE_PATH')); 153 | } 154 | 155 | public function test_it_can_apply_custom_events() 156 | { 157 | $image = UploadedFile::fake()->image('avatar'); 158 | 159 | $user = CustomEventModel::query()->create([ 160 | 'default_avatar' => $image, 161 | ]); 162 | 163 | $avatarPath = storage_path('app/public' . DIRECTORY_SEPARATOR . $user->getRawOriginal('default_avatar')); 164 | 165 | $this->assertLessThan($image->getSize(), filesize($avatarPath)); 166 | 167 | $this->assertFileEquals($avatarPath, session()->pull('CUSTOM_UPLOADED_FILE_PATH')); 168 | } 169 | 170 | public function test_it_can_apply_custom_overwrite_global_events() 171 | { 172 | $image = UploadedFile::fake()->image('avatar'); 173 | 174 | $user = OverwriteEventModel::query()->create([ 175 | 'default_avatar' => $image, 176 | ]); 177 | 178 | $avatarPath = storage_path('app/public' . DIRECTORY_SEPARATOR . $user->getRawOriginal('default_avatar')); 179 | 180 | $this->assertLessThan($image->getSize(), filesize($avatarPath)); 181 | 182 | $this->assertNull(session()->pull('GLOBAL_UPLOADED_FILE_PATH')); 183 | 184 | $this->assertFileEquals($avatarPath, session()->pull('CUSTOM_UPLOADED_FILE_PATH')); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /tests/FileCastNullableTest.php: -------------------------------------------------------------------------------- 1 | create([ 16 | 'default_avatar' => UploadedFile::fake()->image('avatar') 17 | ]); 18 | 19 | $avatarLink = url('storage' . DIRECTORY_SEPARATOR . $user->getRawOriginal('default_avatar')); 20 | 21 | $avatarPath = storage_path('app/public' . DIRECTORY_SEPARATOR . $user->getRawOriginal('default_avatar')); 22 | 23 | $this->assertEquals($avatarLink, $user->default_avatar); 24 | 25 | $this->assertFileExists($avatarPath); 26 | 27 | $user->delete(); 28 | 29 | $this->assertFileDoesNotExist($avatarPath); 30 | } 31 | 32 | public function test_it_can_create_avatar_with_custom_directory() 33 | { 34 | $user = UserFileCastNullable::query()->create([ 35 | 'custom_avatar_directory' => UploadedFile::fake()->image('avatar') 36 | ]); 37 | 38 | $avatarLink = url('storage' . DIRECTORY_SEPARATOR . $user->getRawOriginal('custom_avatar_directory')); 39 | 40 | $avatarPath = storage_path('app/public' . DIRECTORY_SEPARATOR . $user->getRawOriginal('custom_avatar_directory')); 41 | 42 | $this->assertEquals($avatarLink, $user->custom_avatar_directory); 43 | 44 | $this->assertFileExists($avatarPath); 45 | 46 | $user->delete(); 47 | 48 | $this->assertFileDoesNotExist($avatarPath); 49 | } 50 | 51 | public function test_it_can_create_avatar_with_custom_disk() 52 | { 53 | $user = UserFileCastNullable::query()->create([ 54 | 'custom_avatar_disk' => UploadedFile::fake()->image('avatar') 55 | ]); 56 | 57 | $avatarLink = '/storage' . DIRECTORY_SEPARATOR . $user->getRawOriginal('custom_avatar_disk'); 58 | 59 | $avatarPath = storage_path('app' . DIRECTORY_SEPARATOR . $user->getRawOriginal('custom_avatar_disk')); 60 | 61 | $this->assertEquals($avatarLink, $user->custom_avatar_disk); 62 | 63 | $this->assertFileExists($avatarPath); 64 | 65 | $user->delete(); 66 | 67 | $this->assertFileDoesNotExist($avatarPath); 68 | } 69 | 70 | public function test_it_can_create_avatar_with_custom_driver() 71 | { 72 | $user = UserFileCastNullable::query()->create([ 73 | 'custom_avatar_driver' => UploadedFile::fake()->image('avatar') 74 | ]); 75 | 76 | $avatarLink = url($user->getRawOriginal('custom_avatar_driver')); 77 | 78 | $avatarPath = storage_path('app/public' . DIRECTORY_SEPARATOR . $user->getRawOriginal('custom_avatar_driver')); 79 | 80 | $this->assertEquals($avatarLink, $user->custom_avatar_driver); 81 | 82 | $this->assertFileExists($avatarPath); 83 | 84 | $user->delete(); 85 | 86 | $this->assertFileDoesNotExist($avatarPath); 87 | } 88 | 89 | public function test_it_can_create_avatar_with_custom_default_path() 90 | { 91 | $user = UserFileCastNullable::query()->create([ 92 | 'custom_avatar_default_path' => NULL 93 | ]); 94 | 95 | $avatarLink = url('custom_default_path.png'); 96 | 97 | $this->assertEquals($avatarLink, $user->custom_avatar_default_path); 98 | } 99 | 100 | public function test_it_can_create_avatar_with_custom_default_path_with_nullable() 101 | { 102 | $user = UserFileCastNullable::query()->create([ 103 | 'custom_avatar_default_path_with_nullable' => NULL 104 | ]); 105 | 106 | $this->assertEquals(NULL, $user->custom_avatar_default_path_with_nullable); 107 | } 108 | 109 | public function test_it_can_overwrite_old_avatar_when_updating() 110 | { 111 | $user = UserFileCastNullable::query()->create([ 112 | 'default_avatar' => UploadedFile::fake()->image('avatar') 113 | ]); 114 | 115 | $oldAvatarLink = url('storage' . DIRECTORY_SEPARATOR . $user->getRawOriginal('default_avatar')); 116 | 117 | $oldAvatarPath = storage_path('app/public' . DIRECTORY_SEPARATOR . $user->getRawOriginal('default_avatar')); 118 | 119 | $this->assertFileExists($oldAvatarPath); 120 | 121 | $user->update([ 122 | 'default_avatar' => UploadedFile::fake()->image('avatar') 123 | ]); 124 | 125 | $newAvatarLink = url('storage' . DIRECTORY_SEPARATOR . $user->getRawOriginal('default_avatar')); 126 | 127 | $newAvatarPath = storage_path('app/public' . DIRECTORY_SEPARATOR . $user->getRawOriginal('default_avatar')); 128 | 129 | $this->assertNotEquals($oldAvatarLink, $newAvatarLink); 130 | 131 | $this->assertFileDoesNotExist($oldAvatarPath); 132 | 133 | $this->assertFileExists($newAvatarPath); 134 | 135 | $user->delete(); 136 | 137 | $this->assertFileDoesNotExist($newAvatarPath); 138 | } 139 | 140 | public function test_it_can_apply_global_events() 141 | { 142 | $image = UploadedFile::fake()->image('avatar'); 143 | 144 | $user = GlobalEventModel::query()->create([ 145 | 'default_avatar' => $image, 146 | ]); 147 | 148 | $avatarPath = storage_path('app/public' . DIRECTORY_SEPARATOR . $user->getRawOriginal('default_avatar')); 149 | 150 | $this->assertLessThan($image->getSize(), filesize($avatarPath)); 151 | 152 | $this->assertFileEquals($avatarPath, session()->pull('GLOBAL_UPLOADED_FILE_PATH')); 153 | } 154 | 155 | public function test_it_can_apply_custom_events() 156 | { 157 | $image = UploadedFile::fake()->image('avatar'); 158 | 159 | $user = CustomEventModel::query()->create([ 160 | 'default_avatar' => $image, 161 | ]); 162 | 163 | $avatarPath = storage_path('app/public' . DIRECTORY_SEPARATOR . $user->getRawOriginal('default_avatar')); 164 | 165 | $this->assertLessThan($image->getSize(), filesize($avatarPath)); 166 | 167 | $this->assertFileEquals($avatarPath, session()->pull('CUSTOM_UPLOADED_FILE_PATH')); 168 | } 169 | 170 | public function test_it_can_apply_custom_overwrite_global_events() 171 | { 172 | $image = UploadedFile::fake()->image('avatar'); 173 | 174 | $user = OverwriteEventModel::query()->create([ 175 | 'default_avatar' => $image, 176 | ]); 177 | 178 | $avatarPath = storage_path('app/public' . DIRECTORY_SEPARATOR . $user->getRawOriginal('default_avatar')); 179 | 180 | $this->assertLessThan($image->getSize(), filesize($avatarPath)); 181 | 182 | $this->assertNull(session()->pull('GLOBAL_UPLOADED_FILE_PATH')); 183 | 184 | $this->assertFileEquals($avatarPath, session()->pull('CUSTOM_UPLOADED_FILE_PATH')); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # laravel-uploadable 2 | Laravel Uploadable for adding behaviour to a model for self uploading file like avatar or any file type. 3 | 4 | ## Introduction 5 | This package can help you to upload image or any type of file to a specific destination in your filesystem, you can determine a path for a directory to save your uploaded file for each field in your table with minimal configurations or you can use the default store directory of the package. 6 | 7 | ## Installation 8 | ``` 9 | composer require abdelrahmanbl/laravel-uploadable 10 | ``` 11 | ## About Upload 12 | This package uses the [Laravel File Storage](https://laravel.com/docs/11.x/filesystem) to keep the file management. The file will be stored inside the default disk. For example, if you are using the public disk, to access the image or file, you need to create a symbolic link inside your project: 13 | ``` 14 | php artisan storage:link 15 | ``` 16 | And then, configure your default filesystem from .env file 17 | ``` 18 | APP_URL=https://your-domain.com 19 | FILESYSTEM_DISK=public # or your prefered disk 20 | ``` 21 | You can add `default_url` to the filesystems config file to overwrite the default file url. 22 | The default file url is asset('uploadable.jpg'). 23 | ## Usage 24 | To use this package, import the FileCast in your model And then configure the $casts of your model with the FileCast class. 25 | ``` 26 | use Bl\LaravelUploadable\Casts\FileCast; 27 | 28 | class User extends model 29 | { 30 | /** 31 | * Don't forget 32 | * Add all the fields for file or image to the model $fillable when you don't use model $guarded. 33 | * 34 | * @var array 35 | */ 36 | protected $fillable = [ 37 | ..., 38 | 'avatar', 39 | ]; 40 | 41 | /** 42 | * The attributes that should be cast. 43 | * 44 | * @var array 45 | */ 46 | protected $casts = [ 47 | ..., 48 | 'avatar' => FileCast::class, 49 | ]; 50 | } 51 | ``` 52 | ## Customize The Directory 53 | ``` 54 | 'avatar' => FileCast::class . ':User/avatar', # this is the default value ( the attribute key name inside the model basename ) 55 | ``` 56 | ## Customize The Disk 57 | ``` 58 | 'avatar' => FileCast::class . ':default,s3', # here we customized the disk to s3. 59 | ``` 60 | ## Customize The Driver 61 | ``` 62 | 'avatar' => FileCast::class . ':default,default,' . CustomDriverService::class, 63 | ``` 64 | > **_Note:_** your customer driver service must implement `Bl\LaravelUploadable\Interfaces\UploadFileInterface` 65 | and has a constructor with parameter $disk 66 | ``` 67 | use Bl\LaravelUploadable\Interfaces\UploadFileInterface; 68 | use Illuminate\Http\UploadedFile; 69 | 70 | class CustomDriverService implements UploadFileInterface 71 | { 72 | protected $disk; 73 | 74 | /** 75 | * __construct 76 | * 77 | * @param \Bl\LaravelUploadable\Classes\FileArgument $disk 78 | * @return void 79 | */ 80 | public function __construct($disk) 81 | { 82 | $this->disk = $disk->getValue(); 83 | } 84 | 85 | /** 86 | * handle store proccess of the file. 87 | * 88 | * @param UploadedFile $file 89 | * @param string $directory 90 | * @return mixed 91 | */ 92 | public function store(UploadedFile $file, string $directory): mixed 93 | { 94 | // ... 95 | } 96 | 97 | /** 98 | * handle getting the file full url path. 99 | * 100 | * @param string $path 101 | * @return string 102 | */ 103 | public function get(string $path): mixed 104 | { 105 | // ... 106 | } 107 | 108 | /** 109 | * handle deleting a file. 110 | * 111 | * @param string $path 112 | * @return void 113 | */ 114 | public function delete(string $path): void 115 | { 116 | // ... 117 | } 118 | } 119 | ``` 120 | ## Customize The Default Path 121 | ``` 122 | 'avatar' => FileCast::class . ':default,default,default,null', # customize the default value to null. 123 | 'avatar' => FileCast::class . ':default,default,default,nullable', # customize the default value to null. 124 | 'avatar' => FileCast::class . ':default,default,default,avatar.png', # customize the default value to asset('avatar.png'). 125 | ``` 126 | That's all! After this configuration, you can send file data from the client side with the same name of each file field of the model. The package will make the magic! 127 | ## Example 128 | In frontend you can create a form-data with field name avatar. 129 | 130 | ``` 131 |
132 | @csrf 133 |
134 | 135 | 136 |
137 |
138 | 141 |
142 |
143 | ``` 144 | In backend you can pass all the data to the User model. 145 | ``` 146 | /** 147 | * Handle the incoming request. 148 | */ 149 | public function store(UploadRequest $request) 150 | { 151 | $user = \App\Models\User::query()->create( 152 | $request->validated() // or you can use $request->all() if you don't make a validation 153 | ); 154 | 155 | // this get a link of the image that uploaded. 156 | $user->avatar; # https://domain.com/storage/User/avatar/U4q6En4mOHMJj0.png 157 | } 158 | ``` 159 | You can update the file manually to the User model. 160 | ``` 161 | /** 162 | * Handle the incoming request. 163 | */ 164 | public function store(UploadRequest $request) 165 | { 166 | $user = \App\Models\User::query()->first(); 167 | $user->avatar = $request->file('avatar'); 168 | $user->save(); 169 | 170 | // this get a link of the image that uploaded. 171 | $user->avatar; # https://domain.com/storage/User/avatar/U4q6En4mOHMJj0.png 172 | } 173 | ``` 174 | > **_Note:_** when update a field with a file the package will automatic delete the old file and put the new one. 175 | ## Delete The File 176 | You can use the FileCastRemover trait in your model and when you deleting the model instance all the related files will be deleted automatically. 177 | ``` 178 | 179 | use Bl\LaravelUploadable\Traits\FileCastRemover; 180 | 181 | class User extends Model 182 | { 183 | use FileCastRemover; 184 | 185 | /** 186 | * The attributes that are mass assignable. 187 | * 188 | * @var array 189 | */ 190 | protected $fillable = [ 191 | 'avatar', 192 | ]; 193 | 194 | /** 195 | * The attributes that should be cast. 196 | * 197 | * @var array 198 | */ 199 | protected $casts = [ 200 | 'avatar' => FileCast::class, 201 | ]; 202 | } 203 | ``` 204 | And once the model instance is deleted all it's related files will be removed. 205 | ``` 206 | /** 207 | * Remove the user. 208 | */ 209 | public function destroy(User $user) 210 | { 211 | $user->delete(); 212 | } 213 | ``` 214 | ## Apply The Events 215 | You can apply events either before or after the file upload. In addition to, you can apply that globally or for custom field. 216 | ### Global Events 217 | - For apply global events before the file upload you should define a method called `beforeFileCastUpload` in your model and it take one paramter with type `\Illuminate\Http\UploadedFile` and return the same type. 218 | - For apply global events after the file uploaded you should define a void method called `afterFileCastUpload` in your model 219 | and it take one paramter with type `\Illuminate\Http\UploadedFile`. 220 | ``` 221 | use Illuminate\Http\UploadedFile; 222 | 223 | class User extends model 224 | { 225 | /** 226 | * Apply before the file cast upload event. 227 | * 228 | * @param UploadedFile $file 229 | * @return UploadedFile 230 | */ 231 | public function beforeFileCastUpload(UploadedFile $file): UploadedFile 232 | { 233 | return $file; 234 | } 235 | 236 | /** 237 | * Apply after the file cast upload event. 238 | * 239 | * @param UploadedFile $file 240 | * @return void 241 | */ 242 | public function afterFileCastUpload(UploadedFile $file): void 243 | { 244 | dd($file); 245 | } 246 | } 247 | ``` 248 | ### Custom Events 249 | For apply custom events you should create a service that implement `Bl\LaravelUploadable\Interfaces\EventUploadInterface` and path it as a parameter. 250 | ``` 251 | 'avatar' => FileCast::class . ':default,default,default,default,' . CustomUploadService::class 252 | ``` 253 | ``` 254 | use Bl\LaravelUploadable\Interfaces\EventUploadInterface; 255 | use Illuminate\Http\UploadedFile; 256 | 257 | class CustomUploadService implements EventUploadInterface 258 | { 259 | /** 260 | * Apply before the file cast upload event. 261 | * 262 | * @param UploadedFile $file 263 | * @return UploadedFile 264 | */ 265 | public function before(UploadedFile $file): UploadedFile 266 | { 267 | return $file; 268 | } 269 | 270 | /** 271 | * Apply after the file cast upload event. 272 | * 273 | * @param UploadedFile $file 274 | * @return void 275 | */ 276 | public function after(UploadedFile $file): void 277 | { 278 | dd($file); 279 | } 280 | } 281 | ``` 282 | > **_Note:_** when applying global and custom events in your model the priority go to the custom event. 283 | ## Contributing 284 | Feel free to comment, open issues and send PR's. Enjoy it!! 285 | ## License 286 | The MIT License (MIT). Please see [License File](license.md) for more information. 287 | --------------------------------------------------------------------------------