├── .gitignore ├── README.md ├── composer.json ├── config └── filecaster.php └── src ├── FileCaster.php ├── FileCasterServiceProvider.php └── Helpers └── FileWrapper.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /composer.lock 3 | .DS_Store 4 | .env 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # laravel-filecaster 2 | 3 | A simple file casting for laravel model to handle file upload and retrieve 4 | 5 | --- 6 | 7 | ## Installation 8 | 9 | ```sh 10 | composer require sabbir268/laravel-filecaster 11 | ``` 12 | 13 | ## Configuration 14 | 15 | **Cast Class Alias** 16 | Add in aliases array (optional) 17 | 18 | ```php 19 | 'aliases' => Facade::defaultAliases()->merge([ 20 | // ... 21 | 'filecast' => Sabbir268\LaravelFileCaster\FileCaster::class, 22 | // ... 23 | ])->toArray(), 24 | ``` 25 | 26 | Publish config file 27 | 28 | ```sh 29 | php artisan vendor:publish --provider="Sabbir268\LaravelFileCaster\FileCasterServiceProvider" 30 | ``` 31 | 32 | ## Use from Model 33 | 34 | #### Import FileCaster class 35 | 36 | ```php 37 | use Sabbir268\LaravelFileCaster\FileCaster; 38 | ``` 39 | 40 | ### Example: manage "image" file upload and retrieve 41 | 42 | Let's assume, we have Blog model and there is a column `image` to store image file. 43 | 44 | ```php 45 | use App\Models; 46 | use Sabbir268\LaravelFileCaster\FileCaster; 47 | 48 | class Blog extends Model 49 | { 50 | // ... 51 | protected $casts = [ 52 | 'image' => FileCaster::class, 53 | ]; 54 | // ... 55 | } 56 | 57 | ``` 58 | 59 | Or, you can use `filecast` as alias of `Sabbir268\LaravelFileCaster\FileCaster` class. 60 | 61 | ```php 62 | use App\Models; 63 | 64 | class Blog extends Model 65 | { 66 | // ... 67 | protected $casts = [ 68 | 'image' => 'filecast', 69 | ]; 70 | // ... 71 | } 72 | ``` 73 | 74 | Now when you will create a new Blog model instance and has a file from request assign to `image` property, it will automatically upload the file to `storage/app/public/{class_name}/{id}` directory and store the file name with path in `image` column. 75 | 76 | ```php 77 | $blog = new Blog(); 78 | $blog->image = $request->file('image'); 79 | $blog->save(); 80 | ``` 81 | 82 | And when you will retrieve the model instance, it will automatically retrieve the file path from `image` column and you can use it as like as a string. 83 | 84 | ```php 85 | $blog = Blog::find(1); 86 | echo $blog->image; // output: /storage/blog/1/image.jpg 87 | ``` 88 | 89 | #### There several methods/property you can use to retrieve the file information. 90 | 91 | ```php 92 | // get file name 93 | $blog->image->name; // output: image.jpg 94 | 95 | // get file extension 96 | $blog->image->extension; // output: jpg 97 | 98 | // get file size 99 | $blog->image->size; // output: 1024 100 | 101 | // get image width 102 | $blog->image->width; // output: 200 103 | 104 | // get image height 105 | 106 | $blog->image->height; // output: 200 107 | 108 | // get file mime type 109 | $blog->image->mime; // output: image/jpeg 110 | 111 | // get file http url 112 | $blog->image->url; // output: http://example.com/storage/blog/1/image.jpg 113 | 114 | // get file full path 115 | $blog->image->path; // output: /var/www/html/storage/app/public/blog/1/image.jpg 116 | 117 | // get storage directory 118 | $blog->image->dir; // output: /var/www/html/storage/app/public/blog/1 119 | 120 | // check if file exists 121 | $blog->image->exists; // output: true 122 | ``` 123 | 124 | If you want to get manipulated image url, you can use `ur('WIDTHxHEIGHT')` method. 125 | 126 | ```php 127 | $blog->image->url('200x200'); // output: http://example.com/storage/cache/200x200/blog-2-image1.jpg 128 | ``` 129 | 130 | Note: It will create a manipulated image in storage cache directory. You will need `gd` or `imagick` extension installed in your server. 131 | 132 | If you want to delete the file, you can use `remove()` method. 133 | 134 | ```php 135 | $blog->image->remove(); // output: true 136 | ``` 137 | 138 | 139 | 140 | 141 | ## Contribution 142 | 143 | You're open to create any pull request. 144 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sabbir268/laravel-filecaster", 3 | "description": "A simple casting class for laravel model to handle file upload and retrieve", 4 | "keywords": [ 5 | "file", 6 | "file-upload", 7 | "file-casting", 8 | "file-retrieve", 9 | "file-upload-laravel", 10 | "file-casting-laravel", 11 | "file-retrieve-laravel", 12 | "laravel-file-upload", 13 | "laravel-casting" 14 | ], 15 | "homepage": "https://github.com/sabbir268/laravel-filecaster", 16 | "license": "MIT", 17 | "autoload": { 18 | "psr-4": { 19 | "Sabbir268\\LaravelFileCaster\\": "src/" 20 | } 21 | }, 22 | "authors": [{ 23 | "name": "SabbirHossain", 24 | "email": "sabbir.hossain2668@gmail.com" 25 | }], 26 | "minimum-stability": "dev", 27 | "require": { 28 | "php": "^7.4|^8.0", 29 | "intervention/image": "^2.7" 30 | }, 31 | "suggest": { 32 | "ext-gd": "to use GD library based image processing.", 33 | "ext-imagick": "to use Imagick based image processing." 34 | }, 35 | "extra": { 36 | "laravel": { 37 | "providers": [ 38 | "Sabbir268\\LaravelFileCaster\\FileCasterServiceProvider" 39 | ], 40 | "aliases": { 41 | "FileCaster": "Sabbir268\\LaravelFileCaster\\FileCaster\\Facades" 42 | } 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /config/filecaster.php: -------------------------------------------------------------------------------- 1 | 'public', 14 | 15 | /** 16 | * File storage 17 | * 18 | * Here you may specify the file storage that should be used 19 | * Supported storages: "local", "cloud" 20 | */ 21 | 22 | 'storage' => 'local', 23 | 24 | 25 | /* 26 | |-------------------------------------------------------------------------- 27 | | Image Driver 28 | |-------------------------------------------------------------------------- 29 | | 30 | | Intervention Image supports "GD Library" and "Imagick" to process images 31 | | internally. You may choose one of them according to your PHP 32 | | configuration. By default PHP's "GD Library" implementation is used. 33 | | 34 | | Supported: "gd", "imagick" 35 | | 36 | */ 37 | 38 | 'driver' => 'gd', 39 | 40 | 41 | /* 42 | |-------------------------------------------------------------------------- 43 | | File path options 44 | |-------------------------------------------------------------------------- 45 | | 46 | | Here you may specify the file path that should be used 47 | | Supported path options: "by_model_name_and_id", "defined_path_in_model" 48 | | 49 | | "by_model_name_and_id" will create directory with model name and id like: "user/1" 50 | | "defined_path_in_model" will create directory which is defined in model, in model there must be public a property $fileUploadPath 51 | | 52 | */ 53 | 54 | 'path' => 'by_model_name_and_id', 55 | 56 | /* 57 | |-------------------------------------------------------------------------- 58 | | File name 59 | |-------------------------------------------------------------------------- 60 | | 61 | | Here you may specify the file name that should be used 62 | | Supported names: "original_file_name", "hash_name" 63 | | 64 | */ 65 | 'file_name' => 'original_file_name', 66 | ]; 67 | -------------------------------------------------------------------------------- /src/FileCaster.php: -------------------------------------------------------------------------------- 1 | disk = config('filecaster.disk') ?? 'public'; 21 | $this->namePrefix = $namePrefix; 22 | } 23 | 24 | /** 25 | * Cast the given value. 26 | * 27 | * @param array $attributes 28 | */ 29 | public function get(Model $model, string $key, mixed $value, array $attributes): mixed 30 | { 31 | $class = $this->getClassName($model); 32 | return new FileWrapper($value, $model, $key); 33 | } 34 | 35 | /** 36 | * Prepare the given value for storage. 37 | * 38 | * @param array $attributes 39 | */ 40 | public function set(Model $model, string $key, mixed $value, array $attributes): mixed 41 | { 42 | if (is_file($value)) { 43 | if (isset($attributes[$key])) { 44 | if (Storage::disk($this->disk)->exists($attributes[$key])) { 45 | Storage::disk($this->disk)->delete($attributes[$key]); 46 | } 47 | } 48 | $file = $value; 49 | $class = $this->getClassName($model); 50 | $id = $this->getId($attributes, $model); 51 | 52 | $filenameWithExt = $this->getFileName($file); 53 | $path = $this->filePath($model, $attributes); 54 | $value = $file->storeAs($path, $filenameWithExt, $this->disk); 55 | return $value; 56 | } else { 57 | return $value; 58 | } 59 | } 60 | 61 | protected function getId($attributes = null, $modelName = null) 62 | { 63 | if (isset($attributes['id'])) { 64 | return $attributes['id']; 65 | } else { 66 | $model = $modelName::orderBy('id', 'desc')->first(); 67 | if ($model) { 68 | return $model->id + 1; 69 | } else { 70 | return 1; 71 | } 72 | } 73 | } 74 | 75 | /** 76 | * @param Model $model 77 | * @return mixed 78 | */ 79 | protected function getClassName(Model $model): string 80 | { 81 | return strtolower(substr(get_class($model), strrpos(get_class($model), '\\') + 1)); 82 | } 83 | 84 | /** 85 | * @param Model $model 86 | * @param array $attributes 87 | * @return mixed 88 | */ 89 | protected function filePath(Model $model, $attributes) 90 | { 91 | $definedPath = config('filecaster.path'); 92 | if ($definedPath == 'by_model_name_and_id') { 93 | return $this->pathByModelNameAndId($model, $attributes); 94 | } elseif ($definedPath == 'defined_path_in_model') { 95 | return $this->pathByDefinedPathInModel($model); 96 | } else { 97 | throw new \Exception("Invalid path defined in config"); 98 | } 99 | } 100 | 101 | /** 102 | * @param Model $model 103 | * @param array $attributes 104 | * @return mixed 105 | */ 106 | protected function pathByModelNameAndId(Model $model, $attributes) 107 | { 108 | $class = $this->getClassName($model); 109 | $id = $this->getId($attributes, $model); 110 | $path = $class . '/' . $id; 111 | return $path; 112 | } 113 | 114 | /** 115 | * @param Model $model 116 | * @return mixed 117 | */ 118 | protected function pathByDefinedPathInModel(Model $model) 119 | { 120 | if (!isset($model->fileUploadPath)) { 121 | throw new \Exception("Model does not have a variable named fileUploadPath"); 122 | } 123 | return $model->fileUploadPath; 124 | } 125 | 126 | /** 127 | * @param File $file 128 | * @return mixed 129 | */ 130 | protected function getFileName($file) 131 | { 132 | $fileName = config('filecaster.file_name'); 133 | $namePrefix = $this->namePrefix; 134 | $name = ''; 135 | if ($fileName == 'original_file_name') { 136 | $name = $file->getClientOriginalName(); 137 | } elseif ($fileName == 'hash_name') { 138 | $name = $file->hashName(); 139 | } else { 140 | throw new \Exception("Invalid file name defined in config"); 141 | } 142 | if ($namePrefix && $namePrefix != '') { 143 | $name = $namePrefix . '-' . $name; 144 | } 145 | return $name; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/FileCasterServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->bind('laravel-filecaster', function () { 16 | return new FileCaster(); 17 | }); 18 | 19 | $this->mergeConfigFrom( 20 | __DIR__ . '/../config/filecaster.php', 21 | 'filecaster' 22 | ); 23 | } 24 | 25 | /** 26 | * Bootstrap any application services. 27 | */ 28 | public function boot(): void 29 | { 30 | $this->publishes([ 31 | __DIR__ . '/../config/filecaster.php' => config_path('filecaster.php'), 32 | ]); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Helpers/FileWrapper.php: -------------------------------------------------------------------------------- 1 | value = $value; 27 | $this->model = $model; 28 | $this->key = $key; 29 | 30 | $this->driver = config('filecaster.driver'); 31 | $this->disk = config('filecaster.disk') ? config('filecaster.disk') : 'public'; 32 | if ($this->disk == 's3') { 33 | $this->storageType = 'cloud'; 34 | } else { 35 | $this->storageType = config('filecaster.storage') ?? 'local'; 36 | } 37 | 38 | $this->methods = [ 39 | 'url' => [$this, 'url'], 40 | 'name' => [$this, 'name'], 41 | 'path' => [$this, 'path'], 42 | 'dir' => [$this, 'dir'], 43 | 'size' => [$this, 'size'], 44 | 'extension' => [$this, 'extension'], 45 | 'mime' => [$this, 'mime'], 46 | 'lastModified' => [$this, 'lastModified'], 47 | 'exists' => [$this, 'exists'], 48 | 'height' => [$this, 'height'], 49 | 'width' => [$this, 'width'], 50 | ]; 51 | // check if value is empty or null 52 | if ((empty($this->value) || is_null($this->value))) { 53 | return $this->value = ''; 54 | } 55 | 56 | if (!Storage::disk($this->disk)->exists($this->value)) { 57 | return $this->value = ''; 58 | } 59 | } 60 | 61 | public function __get($name) 62 | { 63 | if (empty($this->value) || is_null($this->value)) { 64 | return $this->value = ''; 65 | } 66 | 67 | if (isset($this->methods[$name]) && is_callable($this->methods[$name])) { 68 | return $this->methods[$name](); 69 | } 70 | throw new \Exception("Property or method '$name' does not exist."); 71 | } 72 | 73 | public function __toString(): String 74 | { 75 | return $this->value ? $this->value : ''; 76 | } 77 | 78 | /** 79 | * @param string $dimension width x height (e.g. 200x200) 80 | * @return mixed 81 | */ 82 | public function url($dimension = null, $weight = null): mixed 83 | { 84 | if ((empty($this->value) || is_null($this->value))) { 85 | return $this->value = ''; 86 | } 87 | 88 | if (!$dimension) { 89 | $file = Storage::disk($this->disk)->url($this->value); 90 | } else { 91 | $file = $this->resize($dimension, $this->value, $weight); 92 | } 93 | 94 | return $file; 95 | } 96 | 97 | 98 | /** 99 | * @return string 100 | */ 101 | protected function name(): String 102 | { 103 | $file = Storage::disk($this->disk)->url($this->value); 104 | $filenameWithExt = basename($file); 105 | 106 | return $filenameWithExt; 107 | } 108 | 109 | /** 110 | * @return string 111 | */ 112 | protected function path(): String 113 | { 114 | return Storage::disk($this->disk)->path($this->value); 115 | } 116 | 117 | /** 118 | * @return string 119 | */ 120 | protected function dir(): String 121 | { 122 | $file = Storage::disk($this->disk)->path($this->value); 123 | $path = dirname($file); 124 | 125 | return $path; 126 | } 127 | 128 | /** 129 | * @return string 130 | */ 131 | protected function size(): String 132 | { 133 | return Storage::disk($this->disk)->size($this->value); 134 | } 135 | 136 | /** 137 | * @return string 138 | */ 139 | protected function height(): String 140 | { 141 | $file = Storage::disk($this->disk)->path($this->value); 142 | if (!in_array($this->extension, $this->supportedExtensions)) { 143 | return ''; 144 | } 145 | $height = isset(getimagesize($file)[1]) ? getimagesize($file)[1] : ''; 146 | return $height; 147 | } 148 | 149 | /** 150 | * @return string 151 | */ 152 | protected function width(): String 153 | { 154 | $file = Storage::disk($this->disk)->path($this->value); 155 | if (!in_array($this->extension, $this->supportedExtensions)) { 156 | return ''; 157 | } 158 | $width = isset(getimagesize($file)[0]) ? getimagesize($file)[0] : ''; 159 | return $width; 160 | } 161 | 162 | /** 163 | * @return string 164 | */ 165 | protected function extension(): String 166 | { 167 | $file = Storage::disk($this->disk)->path($this->value); 168 | $extension = pathinfo($file, PATHINFO_EXTENSION); 169 | return $extension; 170 | } 171 | 172 | /** 173 | * @return string 174 | */ 175 | protected function mime(): String 176 | { 177 | return Storage::disk($this->disk)->mimeType($this->value); 178 | } 179 | 180 | protected function lastModified(): String 181 | { 182 | $lastModified = Storage::disk($this->disk)->lastModified($this->value); 183 | return date('Y-m-d H:i:s', $lastModified); 184 | } 185 | 186 | 187 | /* 188 | * @return bool 189 | */ 190 | protected function exists(): bool 191 | { 192 | if (!$this->value || ($this->value && !Storage::disk($this->disk)->exists($this->value))) { 193 | return false; 194 | } 195 | 196 | return true; 197 | } 198 | 199 | 200 | /** 201 | * @return bool 202 | */ 203 | 204 | public function remove(): bool 205 | { 206 | if ((empty($this->value) || is_null($this->value))) { 207 | return true; 208 | } 209 | 210 | $eraseFile = Storage::disk($this->disk)->delete($this->value); 211 | if ($eraseFile) { 212 | if (is_object($this->model)) { 213 | $this->model->update([$this->key => null]); 214 | } 215 | } else { 216 | return false; 217 | } 218 | return true; 219 | } 220 | 221 | 222 | /* 223 | * @param string $size 224 | * @param string $path 225 | * 226 | * @return mixed 227 | */ 228 | protected function resize($size = null, $path = null, $weight = null): mixed 229 | { 230 | $name = $this->name; 231 | $extension = $this->extension; 232 | $filename = Str::beforeLast($name, '.' . $extension); 233 | 234 | $cacheFolder = $size; 235 | $fit = false; 236 | $parentDir = Str::beforeLast($path, '/'); 237 | 238 | if (!$weight) { 239 | $weight = 100; 240 | } 241 | 242 | if (!is_null($size)) { 243 | if (strpos($size, 'f') !== false) { 244 | $size = str_replace('f', '', $size); 245 | $fit = true; 246 | } 247 | 248 | $size = explode('x', $size); 249 | 250 | if (empty($size['0'])) { 251 | $size['0'] = null; 252 | } 253 | 254 | if (empty($size['1'])) { 255 | $size['1'] = null; 256 | } 257 | 258 | 259 | $width = $size['0']; 260 | $height = $size['1']; 261 | 262 | 263 | if ($this->storageType == 'cloud') { 264 | $imagePath = tempnam(sys_get_temp_dir(), 'image'); 265 | copy(Storage::disk($this->disk)->url($this->value), $imagePath); 266 | } else { 267 | $imagePath = Storage::disk($this->disk)->path($this->value); 268 | } 269 | 270 | 271 | 272 | if (!in_array($extension, $this->supportedExtensions)) { 273 | return Storage::disk($this->disk)->url($path); 274 | } 275 | if (!extension_loaded($this->driver)) { 276 | throw new \Exception($this->driver . "driver not installed"); 277 | } 278 | 279 | Image::configure(['driver' => $this->driver]); 280 | 281 | $imgThumb = "cache/{$cacheFolder}/"; 282 | $cacheFileName = str_replace('/', '-', $parentDir) . '-' . $filename; 283 | $savePath = $imgThumb . $cacheFileName . '.' . $extension; 284 | if (!Storage::disk($this->disk)->exists($savePath)) { 285 | if (!$fit) { 286 | $theimg = Image::make($imagePath)->resize($width, $height, function ($constraint) { 287 | $constraint->aspectRatio(); 288 | $constraint->upsize(); 289 | })->stream($extension, $weight); 290 | } else { 291 | $theimg = Image::make($imagePath)->fit($width, $height, function ($constraint) { 292 | // $constraint->aspectRatio(); 293 | // $constraint->upsize(); 294 | })->stream($extension, $weight); 295 | } 296 | Storage::disk($this->disk)->put($savePath, $theimg); 297 | } 298 | return Storage::disk($this->disk)->url($savePath); 299 | } else { 300 | return Storage::disk($this->disk)->url($path); 301 | } 302 | } 303 | } 304 | --------------------------------------------------------------------------------