├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── composer.json ├── config └── imageup.php └── src ├── Exceptions └── InvalidUploadFieldException.php ├── HasImageUploads.php └── ImageUpServiceProvider.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-imageup` will be documented in this file 4 | 5 | ## 1.1.0 - 2020-09-24 6 | - Laravel 8 support 7 | 8 | ## 1.0.9 - 2020-03-22 9 | - Laravel 7 support 10 | 11 | ## 1.0.7 - 2019-09-06 12 | - Laravel 6 support 13 | 14 | ## 1.0.6 - 2019-09-06 15 | - Laravel 5.8 support 16 | 17 | ## 1.0.5 - 2018-11-03 18 | - Added support to upload non image file also 19 | - Can disable/enable auto upload dynamiclly by calling `$model->disableAutoUpload()` and enable it back `$model->enableAutoUpload()` 20 | - Improved tests & Code cleanup 21 | 22 | ## 1.0.4 - 2018-11-01 23 | - Added support to customize filename and relative path dynamically 24 | 25 | ## 1.0.3 - 2018-10-21 26 | - Added `before_save` and `after_save` hooks 27 | 28 | ## 1.0.2 - 2018-10-02 29 | - Added `before_save` and `after_save` hooks 30 | 31 | ## 1.0.1 - 2018-09-29 32 | - s3 storage bug #5 and #6 fixed to get imageUrl and delete old images 33 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | We accept contributions via Pull Requests on [Github](https://github.com/spatie/eloquent-sortable). 6 | 7 | 8 | ## Pull Requests 9 | 10 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer). 11 | 12 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 13 | 14 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 15 | 16 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 17 | 18 | - **Create feature branches** - Don't ask us to pull from your master branch. 19 | 20 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 21 | 22 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 23 | 24 | 25 | ## Running Tests 26 | 27 | ``` bash 28 | $ vendor/bin/phpunit 29 | ``` 30 | 31 | 32 | **Happy coding**! 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Mohd Saqueib Ansari 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Laravel ImageUp 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/qcod/laravel-imageup.svg)](https://packagist.org/packages/qcod/laravel-imageup) 4 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE.md) 5 | [![Build Status](https://img.shields.io/travis/qcod/laravel-imageup/master.svg)](https://travis-ci.org/qcod/laravel-imageup) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/qcod/laravel-imageup.svg)](https://packagist.org/packages/qcod/laravel-imageup) 7 | 8 | The `qcod/laravel-imageup` is a trait which gives you auto upload, resize and crop for image feature with tons of customization. 9 | 10 | ### Installation 11 | 12 | You can install the package via composer: 13 | 14 | ```bash 15 | $ composer require qcod/laravel-imageup 16 | ``` 17 | 18 | The package will automatically register itself. In case you need to manually register it you can by adding it in `config/app.php` providers array: 19 | 20 | ```php 21 | QCod\ImageUp\ImageUpServiceProvider::class 22 | ``` 23 | 24 | You can optionally publish the config file with: 25 | 26 | ```bash 27 | php artisan vendor:publish --provider="QCod\ImageUp\ImageUpServiceProvider" --tag="config" 28 | ``` 29 | 30 | It will create [`config/imageup.php`](#config-file) with all the settings. 31 | 32 | ### Getting Started 33 | 34 | To use this trait you just need to add `use HasImageUploads` on the Eloquent model and define all the fields which needs to store the images in database. 35 | 36 | **Model** 37 | ```php 38 | Image field should be as as `VARCHAR` in database table to store the path of uploaded image. 59 | 60 | **In Controller** 61 | ```php 62 | all()); 69 | } 70 | } 71 | ``` 72 | > Make sure to run `php artisan storage:link` to see the images from public storage disk 73 | 74 | That's it, with above setup when ever you hit store method with post request and if `cover` or `avatar` named file is present on request() it will be auto uploaded. 75 | 76 | ## Upload Field options 77 | 78 | ImageUp gives you tons of customization on how the upload and resize will be handled from defined field options, following are the things you can customize: 79 | 80 | ```php 81 | [ 103 | // width to resize image after upload 104 | 'width' => 200, 105 | 106 | // height to resize image after upload 107 | 'height' => 100, 108 | 109 | // set true to crop image with the given width/height and you can also pass arr [x,y] coordinate for crop. 110 | 'crop' => true, 111 | 112 | // what disk you want to upload, default config('imageup.upload_disk') 113 | 'disk' => 'public', 114 | 115 | // a folder path on the above disk, default config('imageup.upload_directory') 116 | 'path' => 'avatars', 117 | 118 | // placeholder image if image field is empty 119 | 'placeholder' => '/images/avatar-placeholder.svg', 120 | 121 | // validation rules when uploading image 122 | 'rules' => 'image|max:2000', 123 | 124 | // override global auto upload setting coming from config('imageup.auto_upload_images') 125 | 'auto_upload' => false, 126 | 127 | // if request file is don't have same name, default will be the field name 128 | 'file_input' => 'photo', 129 | 130 | // if field (here "avatar") don't exist in database or you wan't this field in database 131 | 'update_database' => false, 132 | 133 | // a hook that is triggered before the image is saved 134 | 'before_save' => BlurFilter::class, 135 | 136 | // a hook that is triggered after the image is saved 137 | 'after_save' => CreateWatermarkImage::class 138 | ], 139 | 'cover' => [ 140 | //... 141 | ] 142 | ]; 143 | 144 | // any other than image file type for upload 145 | protected static $fileFields = [ 146 | 'resume' => [ 147 | // what disk you want to upload, default config('imageup.upload_disk') 148 | 'disk' => 'public', 149 | 150 | // a folder path on the above disk, default config('imageup.upload_directory') 151 | 'path' => 'docs', 152 | 153 | // validation rules when uploading file 154 | 'rules' => 'mimes:doc,pdf,docx|max:1000', 155 | 156 | // override global auto upload setting coming from config('imageup.auto_upload_images') 157 | 'auto_upload' => false, 158 | 159 | // if request file is don't have same name, default will be the field name 160 | 'file_input' => 'cv', 161 | 162 | // a hook that is triggered before the file is saved 163 | 'before_save' => HookForBeforeSave::class, 164 | 165 | // a hook that is triggered after the file is saved 166 | 'after_save' => HookForAfterSave::class 167 | ], 168 | 'cover_letter' => [ 169 | //... 170 | ] 171 | ]; 172 | } 173 | ``` 174 | ### Customize filename 175 | 176 | In some case you will need to customize the saved filename. By default it will be `$file->hashName()` generated hash. 177 | 178 | You can do it by adding a method on the model with `{fieldName}UploadFilePath` naming convention: 179 | 180 | ```php 181 | class User extends Model { 182 | use HasImageUploads; 183 | 184 | // assuming `users` table has 'cover', 'avatar' columns 185 | // mark all the columns as image fields 186 | protected static $imageFields = [ 187 | 'cover', 'avatar' 188 | ]; 189 | 190 | // override cover file name 191 | protected function coverUploadFilePath($file) { 192 | return $this->id . '-cover-image.jpg'; 193 | } 194 | } 195 | ``` 196 | 197 | Above will always save uploaded cover image as `uploads/1-cover-image.jpg`. 198 | 199 | > Make sure to return only relative path from override method. 200 | 201 | Request file will be passed as `$file` param in this method, so you can get the extension or original file name etc to build the filename. 202 | 203 | ```php 204 | // override cover file name 205 | protected function coverUploadFilePath($file) { 206 | return $this->id .'-'. $file->getClientOriginalName(); 207 | } 208 | 209 | /** Some of methods on file */ 210 | // $file->getClientOriginalExtension() 211 | // $file->getRealPath() 212 | // $file->getSize() 213 | // $file->getMimeType() 214 | ``` 215 | 216 | ## Available methods 217 | 218 | You are not limited to use auto upload image feature only. This trait will give you following methods which you can use to manually upload and resize image. 219 | 220 | **Note:** Make sure you have disabled auto upload by setting `protected $autoUploadImages = false;` 221 | on model or dynamiclly by calling `$model->disableAutoUpload()`. You can also disable it for specifig field by calling `$model->setImagesField(['cover' => ['auto_upload' => false]);` 222 | otherwise you will be not seeing your manual uploads, since it will be overwritten by auto upload upon model save. 223 | 224 | #### $model->uploadImage($imageFile, $field = null) / $model->uploadFile($docFile, $field = null) 225 | 226 | Upload image/file for given $field, if $field is null it will upload to first image/file option defined in array. 227 | 228 | ```php 229 | $user = User::findOrFail($id); 230 | $user->uploadImage(request()->file('cover'), 'cover'); 231 | $user->uploadFile(request()->file('resume'), 'resume'); 232 | ``` 233 | 234 | #### $model->setImagesField($fieldsOptions) / $model->setFilesField($fieldsOptions) 235 | 236 | You can also set the image/file fields dynamically by calling `$model->setImagesField($fieldsOptions) / $model->setFilesField($fieldsOptions)` with field options, it will replace fields defined on model property. 237 | 238 | ```php 239 | $user = User::findOrFail($id); 240 | 241 | $fieldOptions = [ 242 | 'cover' => [ 'width' => 1000 ], 243 | 'avatar' => [ 'width' => 120, 'crop' => true ], 244 | ]; 245 | 246 | // override image fields defined on model 247 | $user->setImagesField($fieldOptions); 248 | 249 | $fileFieldOption = [ 250 | 'resume' => ['path' => 'resumes'] 251 | ]; 252 | 253 | // override file fields defined on model 254 | $user->setFilesField($fileFieldOption); 255 | ``` 256 | 257 | #### $model->hasImageField($field) / $model->hasFileField($field) 258 | 259 | To check if field is defined as image/file field. 260 | 261 | #### $model->deleteImage($filePath) / $model->deleteFile($filePath) 262 | 263 | Delete any image/file if it exists. 264 | 265 | #### $model->resizeImage($imageFile, $fieldOptions) 266 | 267 | If you have image already you can call this method to resize it with the same options we have used for image fields. 268 | 269 | ```php 270 | $user = User::findOrFail($id); 271 | 272 | // resize image, it will give you resized image, you need to save it 273 | $imageFile = '/images/some-big-image.jpg'; 274 | $image = $user->resizeImage($imageFile, [ 'width' => 120, 'crop' => true ]); 275 | 276 | // or you can use uploaded file 277 | $imageFile = request()->file('avatar'); 278 | $image = $user->resizeImage($imageFile, [ 'width' => 120, 'crop' => true ]); 279 | ``` 280 | 281 | #### $model->cropTo($x, $y)->resizeImage($imageFile, $field = null) 282 | 283 | You can use this `cropTo()` method to set the x and y coordinates of cropping. It will be very useful if you are getting coordinate from some sort of font-end image cropping library. 284 | 285 | ```php 286 | $user = User::findOrFail($id); 287 | 288 | // uploaded file from request 289 | $imageFile = request()->file('avatar'); 290 | 291 | // coordinates from request 292 | $coords = request()->only(['crop_x', 'crop_y']); 293 | 294 | // resizing will give you intervention image back 295 | $image = $user->cropTo($coords) 296 | ->resizeImage($imageFile, [ 'width' => 120, 'crop' => true ]); 297 | 298 | // or you can do upload and resize like this, it will override field options crop setting 299 | $user->cropTo($coords) 300 | ->uploadImage(request()->file('cover'), 'avatar'); 301 | ``` 302 | 303 | #### $model->imageUrl($field) / $model->fileUrl($field) 304 | 305 | Gives uploaded file url for given image/file field. 306 | 307 | ```php 308 | $user = User::findOrFail($id); 309 | 310 | // in your view 311 | 312 | // http://www.example.com/storage/uploads/iGqUEbCPTv7EuqkndE34CNitlJbFhuxEWmgN9JIh.jpeg 313 | ``` 314 | 315 | #### $model->imageTag($field, $attribute = '') 316 | 317 | It gives you `` tag for a field. 318 | 319 | ```html 320 | {!! $model->imageTag('avatar') !!} 321 | 322 | 323 | {!! $model->imageTag('avatar', 'class="float-left mr-3"') !!} 324 | 325 | ``` 326 | 327 | ### Hooks 328 | Hooks allow you to apply different type of customizations or any other logic that you want to take place before or after the image is saved. 329 | 330 | ##### Definition types 331 | You can define hooks by specifying a class name 332 | 333 | ```php 334 | protected static $imageFields = [ 335 | 'avatar' => [ 336 | 'before_save' => BlurFilter::class, 337 | ], 338 | 'cover' => [ 339 | //... 340 | ] 341 | ]; 342 | ``` 343 | 344 | The hook class must have a method named `handle` that will be called when the hook is triggered. 345 | An instance of the intervention image will be passed to the `handle` method. 346 | 347 | ```php 348 | class BlurFilter { 349 | public function handle($image) { 350 | $image->blur(10); 351 | } 352 | } 353 | ``` 354 | 355 | The class based hooks are resolved through laravel ioc container, which allows you to inject any dependencies through the constructor. 356 | 357 | > Keep in mind you will be getting resized image in `before` and `after` save hook handler if you have defined field option with `width` or `height`. 358 | Sure you can get original image from `request()->file('avatar')` any time you want. 359 | 360 | The second type off hook definition is callback hooks. 361 | ```php 362 | $user->setImagesField([ 363 | 'avatar' => [ 364 | 'before_save' => function($image) { 365 | $image->blur(10); 366 | }, 367 | ], 368 | 'cover' => [ 369 | //... 370 | ] 371 | ]); 372 | ``` 373 | 374 | The callback will receive the intervention image instance argument as well. 375 | 376 | ##### Hook types 377 | There are two types of hooks a `before_save` and `after_save` hooks. 378 | 379 | The `before_save` hook is called just before the image is saved to the disk. 380 | Any changes made to the intervention image instance within the hook will be applied to the output image. 381 | 382 | ```php 383 | $user->setImagesField([ 384 | 'avatar' => [ 385 | 'width' => 100, 386 | 'height' => 100, 387 | 'before_save' => function($image) { 388 | // The image will be 50 * 50, this will override the 100 * 100 389 | $image->resize(50, 50); 390 | }, 391 | ] 392 | ]); 393 | ``` 394 | 395 | The `after_save` hook is called right after the image was saved to the disk. 396 | 397 | ```php 398 | $user->setImagesField([ 399 | 'logo' => [ 400 | 'after_save' => function($image) { 401 | // Create a watermark image and save it 402 | }, 403 | ] 404 | ]); 405 | ``` 406 | 407 | ### Config file 408 | 409 | ```php 410 | 'public', 418 | 419 | /** 420 | * Default Image upload directory on the disc 421 | * eg. 'uploads' or 'user/avatar' 422 | */ 423 | 'upload_directory' => 'uploads', 424 | 425 | /** 426 | * Auto upload images from incoming Request if same named field or 427 | * file_input field on option present upon model update and create. 428 | * can be override in individual field options 429 | */ 430 | 'auto_upload_images' => true, 431 | 432 | /** 433 | * It will auto delete images once record is deleted from database 434 | */ 435 | 'auto_delete_images' => true, 436 | 437 | /** 438 | * Set an image quality 439 | */ 440 | 'resize_image_quality' => 80 441 | ]; 442 | ``` 443 | 444 | ### Changelog 445 | 446 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 447 | 448 | ### Testing 449 | The package contains some integration/smoke tests, set up with Orchestra. The tests can be run via phpunit. 450 | 451 | ```bash 452 | $ composer test 453 | ``` 454 | 455 | ### Contributing 456 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 457 | 458 | ### Security 459 | 460 | If you discover any security related issues, please email saquibweb@gmail.com instead of using the issue tracker. 461 | 462 | ### Credits 463 | - [Mohd Saqueib Ansari](https://github.com/saqueib) 464 | - [Melek Rebai aka shadoWalker89](https://github.com/shadoWalker89) 465 | - [João Roberto P. Borges](https://github.com/joaorobertopb) 466 | 467 | ### About QCode.in 468 | QCode.in (https://www.qcode.in) is blog by [Saqueib](https://github.com/saqueib) which covers All about Full Stack Web Development. 469 | 470 | ### License 471 | 472 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 473 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qcod/laravel-imageup", 3 | "description": "Auto Image upload, resize and crop for Laravel eloquent model using Intervention image", 4 | "homepage": "https://github.com/qcod/laravel-imageup", 5 | "type": "library", 6 | "license": "MIT", 7 | "keywords": [ 8 | "laravel", 9 | "image upload", 10 | "image crop", 11 | "image resize", 12 | "intervention image", 13 | "eloquent", 14 | "model" 15 | ], 16 | "authors": [ 17 | { 18 | "name": "Mohd Saqueib Ansari", 19 | "email": "saquibweb@gmail.com" 20 | } 21 | ], 22 | "require": { 23 | "php": "^7.3|^8.0", 24 | "laravel/framework": "~5.4.0|~5.5.0|~5.6.0|~5.7.0|~5.8.0|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 25 | "intervention/image": "^2.4|^3.4" 26 | }, 27 | "require-dev": { 28 | "orchestra/testbench": "^4.0|^5.0|^7.0|^8.0|^9.0|^10.0", 29 | "phpunit/phpunit": "^8.0|^9.0|^10.5|^11.5.3", 30 | "dms/phpunit-arraysubset-asserts": "^0.2.0|^0.3|^0.4|^0.5" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "QCod\\ImageUp\\": "src/" 35 | } 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "QCod\\ImageUp\\Tests\\": "tests/" 40 | } 41 | }, 42 | "extra": { 43 | "laravel": { 44 | "providers": [ 45 | "QCod\\ImageUp\\ImageUpServiceProvider" 46 | ] 47 | } 48 | }, 49 | "scripts": { 50 | "test": "vendor/bin/phpunit" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /config/imageup.php: -------------------------------------------------------------------------------- 1 | 'public', 9 | 10 | /** 11 | * Default Image upload directory on the disc 12 | * eg. 'uploads' or 'user/avatar' 13 | */ 14 | 'upload_directory' => 'uploads', 15 | 16 | /** 17 | * Auto upload images from incoming Request if same named field or 18 | * file_input field on option is present upon model update and create. 19 | * Can be overridden in individual field options 20 | */ 21 | 'auto_upload_images' => true, 22 | 23 | /** 24 | * It will auto delete images once a record is deleted from the database 25 | */ 26 | 'auto_delete_images' => true, 27 | 28 | /** 29 | * Set an image quality 30 | */ 31 | 'resize_image_quality' => 80 32 | ]; 33 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidUploadFieldException.php: -------------------------------------------------------------------------------- 1 | disableAutoUpload) { 63 | $model->autoUpload(); 64 | } 65 | }); 66 | 67 | // delete event 68 | static::deleted(function ($model) { 69 | $model->autoDeleteImage(); 70 | }); 71 | } 72 | 73 | /** 74 | * Get absolute file url for a field 75 | * 76 | * @param string|null $field 77 | * 78 | * @return mixed|string 79 | * @throws InvalidUploadFieldException 80 | */ 81 | public function fileUrl(?string $field = null): string 82 | { 83 | return $this->imageUrl($field); 84 | } 85 | 86 | /** 87 | * Get absolute url for a field 88 | * 89 | * @param string|null $field 90 | * 91 | * @return mixed|string 92 | * @throws InvalidUploadFieldException 93 | */ 94 | public function imageUrl(?string $field = null): string 95 | { 96 | $this->uploadFieldName = $this->getUploadFieldName($field); 97 | $this->uploadFieldOptions = $this->getUploadFieldOptions($this->uploadFieldName); 98 | 99 | // get the model attribute value 100 | if (Arr::get($this->uploadFieldOptions, 'update_database', true)) { 101 | $attributeValue = $this->getOriginal($this->uploadFieldName); 102 | } else { 103 | $attributeValue = $this->getFileUploadPath($field); 104 | } 105 | 106 | // check for placeholder defined in option 107 | $placeholderImage = Arr::get($this->uploadFieldOptions, 'placeholder'); 108 | 109 | return (empty($attributeValue) && $placeholderImage) 110 | ? $placeholderImage 111 | : $this->getStorageDisk()->url($attributeValue); 112 | } 113 | 114 | /** 115 | * Get the upload field name 116 | * 117 | * @param null $field 118 | * 119 | * @return mixed|null 120 | */ 121 | public function getUploadFieldName($field = null) 122 | { 123 | if (!is_null($field)) { 124 | return $field; 125 | } 126 | 127 | $imagesFields = $this->getDefinedUploadFields(); 128 | $fieldKey = Arr::first(array_keys($imagesFields)); 129 | 130 | // return first field name 131 | return is_int($fieldKey) 132 | ? $imagesFields[$fieldKey] 133 | : $fieldKey; 134 | } 135 | 136 | /** 137 | * Get all the image and file fields defined on model 138 | * 139 | * @return array 140 | */ 141 | public function getDefinedUploadFields(): array 142 | { 143 | $fields = static::$imageFields ?? $this->imagesFields; 144 | 145 | return array_merge($this->getDefinedFileFields(), $fields); 146 | } 147 | 148 | /** 149 | * Get all the file fields defined on model 150 | * 151 | * @return array 152 | */ 153 | public function getDefinedFileFields(): array 154 | { 155 | return static::$fileFields ?? $this->filesFields; 156 | } 157 | 158 | /** 159 | * Get upload field options 160 | * 161 | * @param $field 162 | * 163 | * @return array 164 | * @throws InvalidUploadFieldException 165 | */ 166 | public function getUploadFieldOptions($field = null): array 167 | { 168 | // get first option if no field provided 169 | if (is_null($field)) { 170 | $imagesFields = $this->getDefinedUploadFields(); 171 | 172 | if (!$imagesFields) { 173 | throw new InvalidUploadFieldException( 174 | 'No upload fields are defined in $imageFields/$fileFields array on model.' 175 | ); 176 | } 177 | 178 | $fieldKey = Arr::first(array_keys($imagesFields)); 179 | return is_int($fieldKey) ? [] : Arr::first($imagesFields); 180 | } 181 | 182 | // check if provided filed defined 183 | if (!$this->hasImageField($field)) { 184 | throw new InvalidUploadFieldException( 185 | 'Image/File field `' . $field . '` is not defined in $imageFields/$fileFields array on model.' 186 | ); 187 | } 188 | 189 | return Arr::get($this->getDefinedUploadFields(), $field, []); 190 | } 191 | 192 | /** 193 | * Check if image filed is defined 194 | * 195 | * @param string $field 196 | * 197 | * @return bool 198 | */ 199 | public function hasImageField(string $field): bool 200 | { 201 | return $this->hasUploadField($field, $this->getDefinedUploadFields()); 202 | } 203 | 204 | /** 205 | * Check is upload field is defined 206 | * 207 | * @param string $field 208 | * @param array $definedField 209 | * 210 | * @return bool 211 | */ 212 | private function hasUploadField(string $field, array $definedField): bool 213 | { 214 | // check for string key 215 | if (Arr::has($definedField, $field)) { 216 | return true; 217 | } 218 | 219 | // check for value 220 | $found = false; 221 | foreach ($definedField as $key => $val) { 222 | $found = (is_numeric($key) && $val === $field); 223 | 224 | if ($found) { 225 | break; 226 | } 227 | } 228 | 229 | return $found; 230 | } 231 | 232 | /** 233 | * Get the full path to upload file 234 | * 235 | * @param UploadedFile $file 236 | * 237 | * @return string 238 | */ 239 | protected function getFileUploadPath(UploadedFile $file): string 240 | { 241 | // check if path override is defined for current file 242 | $pathOverrideMethod = Str::camel(strtolower($this->uploadFieldName) . 'UploadFilePath'); 243 | 244 | if (method_exists($this, $pathOverrideMethod)) { 245 | return $this->getImageUploadPath() . '/' . $this->$pathOverrideMethod($file); 246 | } 247 | 248 | return $this->getImageUploadPath() . '/' . $file->hashName(); 249 | } 250 | 251 | /** 252 | * Get image upload path 253 | * 254 | * @return string 255 | */ 256 | protected function getImageUploadPath(): string 257 | { 258 | // check for disk option 259 | if ($pathInOption = Arr::get($this->uploadFieldOptions, 'path')) { 260 | return $pathInOption; 261 | } 262 | 263 | return property_exists($this, 'imagesUploadPath') 264 | ? trim($this->imagesUploadPath, '/') 265 | : trim(config('imageup.upload_directory', 'uploads'), '/'); 266 | } 267 | 268 | /** 269 | * Get storage disk 270 | * 271 | * @return Filesystem 272 | */ 273 | protected function getStorageDisk(): Filesystem 274 | { 275 | return Storage::disk($this->getImageUploadDisk()); 276 | } 277 | 278 | /** 279 | * Get image upload disk 280 | * 281 | * @return string 282 | */ 283 | protected function getImageUploadDisk(): string 284 | { 285 | // check for disk option 286 | if ($diskInOption = Arr::get($this->uploadFieldOptions, 'disk')) { 287 | return $diskInOption; 288 | } 289 | 290 | return property_exists($this, 'imagesUploadDisk') 291 | ? $this->imagesUploadDisk 292 | : config('imageup.upload_disk', 'public'); 293 | } 294 | 295 | /** 296 | * Get html image tag for a field if image present 297 | * 298 | * @param string|null $field 299 | * @param string $attributes 300 | * 301 | * @return string 302 | */ 303 | public function imageTag(?string $field = null, string $attributes = ''): string 304 | { 305 | // if no field found just return empty string 306 | if (!$this->hasImageField($field) || $this->hasFileField($field)) { 307 | return ''; 308 | } 309 | 310 | try { 311 | return ''; 312 | } catch (Exception $exception) { 313 | } 314 | return ''; 315 | } 316 | 317 | /** 318 | * Check if file filed is defined 319 | * 320 | * @param string $field 321 | * 322 | * @return bool 323 | */ 324 | public function hasFileField(string $field): bool 325 | { 326 | return $this->hasUploadField($field, $this->getDefinedFileFields()); 327 | } 328 | 329 | /** 330 | * Upload a file 331 | * 332 | * @param $file 333 | * @param string|null $field 334 | * 335 | * @return $this 336 | * @throws InvalidUploadFieldException 337 | */ 338 | public function uploadFile($file, ?string $field = null): self 339 | { 340 | return $this->uploadImage($file, $field); 341 | } 342 | 343 | /** 344 | * Upload and resize image 345 | * 346 | * @param UploadedFile $imageFile 347 | * @param string|null $field 348 | * 349 | * @return $this 350 | * @throws InvalidUploadFieldException|\Exception 351 | */ 352 | public function uploadImage(UploadedFile $imageFile, ?string $field = null): self 353 | { 354 | $this->uploadFieldName = $this->getUploadFieldName($field); 355 | $this->uploadFieldOptions = $this->getUploadFieldOptions($this->uploadFieldName); 356 | 357 | // validate it 358 | $this->validateImage($imageFile, $this->uploadFieldName, $this->uploadFieldOptions); 359 | 360 | // handle upload 361 | $filePath = $this->hasFileField($this->uploadFieldName) 362 | ? $this->handleFileUpload($imageFile) 363 | : $this->handleImageUpload($imageFile); 364 | 365 | // hold old file 366 | $currentFile = $this->getOriginal($this->uploadFieldName); 367 | 368 | // update the model with field name 369 | $this->updateModel($filePath, $this->uploadFieldName); 370 | 371 | // delete old file 372 | if (!empty($currentFile) && $currentFile != $filePath) { 373 | $this->deleteImage($currentFile); 374 | } 375 | 376 | return $this; 377 | } 378 | 379 | /** 380 | * Validate image file with given rules in option 381 | * 382 | * @param $file 383 | * @param string $fieldName 384 | * @param array $imageOptions 385 | * 386 | * @throws \Illuminate\Validation\ValidationException 387 | */ 388 | protected function validateImage($file, string $fieldName, array $imageOptions): void 389 | { 390 | if ($rules = Arr::get($imageOptions, 'rules')) { 391 | $this->validationFactory()->make( 392 | [$fieldName => $file], 393 | [$fieldName => $rules] 394 | )->validate(); 395 | } 396 | } 397 | 398 | /** 399 | * Get a validation factory instance. 400 | * 401 | * @return \Illuminate\Contracts\Validation\Factory 402 | */ 403 | protected function validationFactory(): Factory 404 | { 405 | return app(Factory::class); 406 | } 407 | 408 | /** 409 | * Process file upload 410 | * 411 | * @param UploadedFile $file 412 | * 413 | * @return string 414 | * @throws \Exception 415 | */ 416 | public function handleFileUpload(UploadedFile $file): string 417 | { 418 | // Trigger before save hook 419 | $this->triggerBeforeSaveHook($file); 420 | 421 | $filePath = $this->getFileUploadPath($file); 422 | 423 | $this->getStorageDisk()->put($filePath, file_get_contents($file), 'public'); 424 | 425 | // Trigger after save hook 426 | $this->triggerAfterSaveHook($file); 427 | 428 | return $filePath; 429 | } 430 | 431 | /** 432 | * Trigger user defined before save hook. 433 | * 434 | * @param $image 435 | * 436 | * @return $this 437 | * @throws \Exception 438 | */ 439 | protected function triggerBeforeSaveHook($image): self 440 | { 441 | if (isset($this->uploadFieldOptions['before_save'])) { 442 | $this->triggerHook($this->uploadFieldOptions['before_save'], $image); 443 | } 444 | 445 | return $this; 446 | } 447 | 448 | /** 449 | * This will try to trigger the hook depending on the user definition. 450 | * 451 | * @param $hook 452 | * @param $image 453 | * 454 | * @throws \Exception 455 | */ 456 | protected function triggerHook($hook, $image) 457 | { 458 | if (is_callable($hook)) { 459 | $hook($image); 460 | } 461 | 462 | // We assume that the user is passing the hook class name 463 | if (is_string($hook)) { 464 | $instance = app($hook); 465 | $instance->handle($image); 466 | } 467 | } 468 | 469 | /** 470 | * Trigger user defined after save hook. 471 | * 472 | * @param $image 473 | * 474 | * @return $this 475 | * @throws \Exception 476 | */ 477 | protected function triggerAfterSaveHook($image): self 478 | { 479 | if (isset($this->uploadFieldOptions['after_save'])) { 480 | $this->triggerHook($this->uploadFieldOptions['after_save'], $image); 481 | } 482 | 483 | return $this; 484 | } 485 | 486 | /** 487 | * Process image upload 488 | * 489 | * @param $imageFile 490 | * 491 | * @return string 492 | * @throws \Exception 493 | */ 494 | protected function handleImageUpload($imageFile): string 495 | { 496 | // resize the image with given option 497 | $image = $this->resizeImage($imageFile, $this->uploadFieldOptions); 498 | 499 | // save the uploaded file on disk 500 | return $this->saveImage($imageFile, $image); 501 | } 502 | 503 | /** 504 | * Resize image based on options 505 | * 506 | * @param $imageFile 507 | * @param array $imageFieldOptions 508 | * 509 | * @return \Intervention\Image\Image 510 | */ 511 | public function resizeImage($imageFile, array $imageFieldOptions): \Intervention\Image\Image 512 | { 513 | $image = Image::make($imageFile); 514 | 515 | // check if resize needed 516 | if (!$this->needResizing($imageFieldOptions)) { 517 | return $image; 518 | } 519 | 520 | // resize it according to options 521 | $width = Arr::get($imageFieldOptions, 'width'); 522 | $height = Arr::get($imageFieldOptions, 'height'); 523 | $cropHeight = empty($height) ? $width : $height; 524 | $crop = $this->getCropOption($imageFieldOptions); 525 | 526 | // crop it if option is set to true 527 | if ($crop === true) { 528 | $image->fit($width, $cropHeight, function ($constraint) { 529 | $constraint->upsize(); 530 | }); 531 | 532 | return $image; 533 | } 534 | 535 | // crop with x,y coordinate array 536 | if (is_array($crop) && count($crop) == 2) { 537 | [$x, $y] = $crop; 538 | 539 | $image->crop($width, $cropHeight, $x, $y); 540 | 541 | return $image; 542 | } 543 | 544 | // or resize it with given width and height 545 | $image->resize($width, $height, function ($constraint) { 546 | $constraint->aspectRatio(); 547 | $constraint->upsize(); 548 | }); 549 | 550 | return $image; 551 | } 552 | 553 | /** 554 | * Check if image need resizing from options 555 | * 556 | * @param array $imageFieldOptions 557 | * 558 | * @return bool 559 | */ 560 | protected function needResizing(array $imageFieldOptions): bool 561 | { 562 | return Arr::has($imageFieldOptions, 'width') || Arr::has($imageFieldOptions, 'height'); 563 | } 564 | 565 | /** 566 | * Get the crop option 567 | * 568 | * @param array $imageFieldOptions 569 | * 570 | * @return array|boolean 571 | */ 572 | protected function getCropOption(array $imageFieldOptions) 573 | { 574 | $crop = Arr::get($imageFieldOptions, 'crop', false); 575 | 576 | // check for crop override 577 | if (isset($this->cropCoordinates) && count($this->cropCoordinates) == 2) { 578 | $crop = $this->cropCoordinates; 579 | } 580 | 581 | return $crop; 582 | } 583 | 584 | /** 585 | * Save the image to disk 586 | * 587 | * @param UploadedFile $imageFile 588 | * @param $image 589 | * 590 | * @return string 591 | * @throws \Exception 592 | */ 593 | protected function saveImage(UploadedFile $imageFile, $image): string 594 | { 595 | // Trigger before save hook 596 | $this->triggerBeforeSaveHook($image); 597 | 598 | $imageQuality = Arr::get( 599 | $this->uploadFieldOptions, 600 | 'resize_image_quality', 601 | config('imageup.resize_image_quality') 602 | ); 603 | 604 | $imagePath = $this->getFileUploadPath($imageFile); 605 | 606 | $this->getStorageDisk()->put( 607 | $imagePath, 608 | (string) $image->encode(null, $imageQuality), 609 | 'public' 610 | ); 611 | 612 | // Trigger after save hook 613 | $this->triggerAfterSaveHook($image); 614 | 615 | // clean up 616 | $image->destroy(); 617 | 618 | return $imagePath; 619 | } 620 | 621 | /** 622 | * update the model field 623 | * 624 | * @param string $imagePath 625 | * @param string $imageFieldName 626 | */ 627 | protected function updateModel(string $imagePath, string $imageFieldName): void 628 | { 629 | // check if update_database = false (default: true) 630 | $imagesFields = $this->getDefinedUploadFields(); 631 | $actualField = Arr::get($imagesFields, $imageFieldName); 632 | $updateAuthorized = Arr::get($actualField, 'update_database', true); 633 | 634 | // update model (if update_database=true or not set) 635 | if ($updateAuthorized) { 636 | $this->attributes[$imageFieldName] = $imagePath; 637 | $dispatcher = $this->getEventDispatcher(); 638 | self::unsetEventDispatcher(); 639 | $this->save(); 640 | self::setEventDispatcher($dispatcher); 641 | } 642 | } 643 | 644 | /** 645 | * Delete an Image 646 | * 647 | * @param string $filePath 648 | */ 649 | public function deleteImage(string $filePath): void 650 | { 651 | $this->deleteUploadedFile($filePath); 652 | } 653 | 654 | /** 655 | * Delete a file from disk 656 | * 657 | * @param string $filePath 658 | */ 659 | private function deleteUploadedFile(string $filePath): void 660 | { 661 | if ($this->getStorageDisk()->exists($filePath)) { 662 | $this->getStorageDisk()->delete($filePath); 663 | } 664 | } 665 | 666 | /** 667 | * Override Crop X and Y coordinates 668 | * 669 | * @param int $x 670 | * @param int $y 671 | * 672 | * @return $this 673 | */ 674 | public function cropTo(int $x, int $y): self 675 | { 676 | $this->cropCoordinates = [$x, $y]; 677 | return $this; 678 | } 679 | 680 | /** 681 | * Setter for model image fields 682 | * 683 | * @param array $fieldsOptions 684 | * 685 | * @return $this 686 | */ 687 | public function setImagesField(array $fieldsOptions): self 688 | { 689 | if (isset(static::$imageFields)) { 690 | static::$imageFields = array_merge($this->getDefinedUploadFields(), $fieldsOptions); 691 | } else { 692 | $this->imagesFields = $fieldsOptions; 693 | } 694 | 695 | return $this; 696 | } 697 | 698 | /** 699 | * Setter for model file fields 700 | * 701 | * @param array $fieldsOptions 702 | * 703 | * @return $this 704 | */ 705 | public function setFilesField(array $fieldsOptions): self 706 | { 707 | if (isset(static::$fileFields)) { 708 | static::$fileFields = array_merge($this->getDefinedUploadFields(), $fieldsOptions); 709 | } else { 710 | $this->filesFields = $fieldsOptions; 711 | } 712 | 713 | return $this; 714 | } 715 | 716 | /** 717 | * Delete a file 718 | * 719 | * @param string $filePath 720 | */ 721 | public function deleteFile(string $filePath): void 722 | { 723 | $this->deleteUploadedFile($filePath); 724 | } 725 | 726 | /** 727 | * Disable auto upload 728 | * 729 | * @return $this 730 | */ 731 | public function disableAutoUpload(): self 732 | { 733 | $this->disableAutoUpload = true; 734 | return $this; 735 | } 736 | 737 | /** 738 | * Enable auto upload 739 | * 740 | * @return $this 741 | */ 742 | public function enableAutoUpload(): self 743 | { 744 | $this->disableAutoUpload = false; 745 | return $this; 746 | } 747 | 748 | /** 749 | * Auto image upload handler 750 | * 751 | * @throws InvalidUploadFieldException 752 | * @throws \Exception 753 | */ 754 | protected function autoUpload(): void 755 | { 756 | foreach ($this->getDefinedUploadFields() as $key => $val) { 757 | $field = is_int($key) ? $val : $key; 758 | $options = Arr::wrap($val); 759 | 760 | // check if global upload is allowed, then in override in option 761 | $autoUploadAllowed = Arr::get($options, 'auto_upload', $this->canAutoUploadImages()); 762 | 763 | if (!$autoUploadAllowed) { 764 | continue; 765 | } 766 | 767 | // get the input file name 768 | $requestFileName = Arr::get($options, 'file_input', $field); 769 | 770 | // if request has the file upload it 771 | if (request()->hasFile($requestFileName)) { 772 | $this->uploadImage( 773 | request()->file($requestFileName), 774 | $field 775 | ); 776 | } 777 | } 778 | } 779 | 780 | /** 781 | * Check if auto upload is allowed 782 | * 783 | * @return bool 784 | */ 785 | protected function canAutoUploadImages(): bool 786 | { 787 | return property_exists($this, 'autoUploadImages') 788 | ? $this->autoUploadImages 789 | : config('imageup.auto_upload_images', false); 790 | } 791 | 792 | /** 793 | * Auto delete image handler 794 | */ 795 | protected function autoDeleteImage(): void 796 | { 797 | if (config('imageup.auto_delete_images')) { 798 | foreach ($this->getDefinedUploadFields() as $field => $options) { 799 | $field = is_numeric($field) ? $options : $field; 800 | if (!is_null($this->getOriginal($field))) { 801 | $this->deleteImage($this->getOriginal($field)); 802 | } 803 | } 804 | } 805 | } 806 | } 807 | -------------------------------------------------------------------------------- /src/ImageUpServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 17 | __DIR__.'/../config/imageup.php' => config_path('imageup.php') 18 | ], 'config'); 19 | 20 | $this->mergeConfigFrom( 21 | __DIR__.'/../config/imageup.php', 22 | 'imageup' 23 | ); 24 | } 25 | 26 | /** 27 | * Register bindings in the container. 28 | * 29 | * @return void 30 | */ 31 | public function register() 32 | { 33 | } 34 | } 35 | --------------------------------------------------------------------------------