├── config └── image-optimizer.php ├── resources └── lang │ └── en │ └── image-optimizer.php ├── src ├── Facades │ └── ImageOptimizer.php ├── ImageOptimizerServiceProvider.php └── Components │ ├── SpatieMediaLibraryFileUpload.php │ └── BaseFileUpload.php ├── LICENSE.md ├── composer.json ├── CHANGELOG.md └── README.md /config/image-optimizer.php: -------------------------------------------------------------------------------- 1 | 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "joshembling/image-optimizer", 3 | "description": "Optimize your Filament images before they reach your database.", 4 | "keywords": [ 5 | "joshembling", 6 | "laravel", 7 | "filament", 8 | "filamentphp", 9 | "image", 10 | "optimization", 11 | "image-optimizer" 12 | ], 13 | "homepage": "https://github.com/joshembling/image-optimizer", 14 | "support": { 15 | "issues": "https://github.com/joshembling/image-optimizer/issues", 16 | "source": "https://github.com/joshembling/image-optimizer" 17 | }, 18 | "license": "MIT", 19 | "authors": [ 20 | { 21 | "name": "Josh Embling", 22 | "email": "joshembling@gmail.com", 23 | "role": "Developer" 24 | } 25 | ], 26 | "require": { 27 | "php": "^8.2", 28 | "filament/forms": "^3.3", 29 | "illuminate/contracts": "^10.0|^11.0|^12.0", 30 | "intervention/image": "^2.7", 31 | "spatie/laravel-package-tools": "^1.19.0" 32 | }, 33 | "require-dev": { 34 | "laravel/pint": "^1.0", 35 | "nunomaduro/collision": "^7.9", 36 | "orchestra/testbench": "^8.0" 37 | }, 38 | "autoload": { 39 | "psr-4": { 40 | "Joshembling\\ImageOptimizer\\": "src/" 41 | } 42 | }, 43 | "autoload-dev": { 44 | "psr-4": { 45 | "Joshembling\\ImageOptimizer\\Tests\\": "tests/" 46 | } 47 | }, 48 | "scripts": { 49 | "post-autoload-dump": "@php ./vendor/bin/testbench package:discover --ansi", 50 | "format": "vendor/bin/pint" 51 | }, 52 | "config": { 53 | "sort-packages": true 54 | }, 55 | "extra": { 56 | "laravel": { 57 | "providers": [ 58 | "Joshembling\\ImageOptimizer\\ImageOptimizerServiceProvider" 59 | ], 60 | "aliases": { 61 | "ImageOptimizer": "Joshembling\\ImageOptimizer\\Facades\\ImageOptimizer" 62 | } 63 | } 64 | }, 65 | "minimum-stability": "dev", 66 | "prefer-stable": true 67 | } 68 | -------------------------------------------------------------------------------- /src/ImageOptimizerServiceProvider.php: -------------------------------------------------------------------------------- 1 | alias(BaseFileUpload::class, CustomBaseFileUpload::class); 22 | AliasLoader::getInstance()->alias(SpatieMediaLibraryFileUpload::class, CustomSpatieMediaLibraryFileUpload::class); 23 | } 24 | 25 | public function configurePackage(Package $package): void 26 | { 27 | $package->name(static::$name) 28 | ->hasInstallCommand(function (InstallCommand $command) { 29 | $command 30 | ->publishConfigFile() 31 | ->publishMigrations() 32 | ->askToRunMigrations() 33 | ->askToStarRepoOnGitHub('joshembling/image-optimizer'); 34 | }); 35 | 36 | $configFileName = $package->shortName(); 37 | 38 | if (file_exists($package->basePath("/../config/{$configFileName}.php"))) { 39 | $package->hasConfigFile(); 40 | } 41 | 42 | if (file_exists($package->basePath('/../resources/lang'))) { 43 | $package->hasTranslations(); 44 | } 45 | } 46 | 47 | public function packageBooted(): void 48 | { 49 | // Handle Stubs 50 | if (app()->runningInConsole()) { 51 | foreach (app(Filesystem::class)->files(__DIR__ . '/../stubs/') as $file) { 52 | $this->publishes([ 53 | $file->getRealPath() => base_path("stubs/image-optimizer/{$file->getFilename()}"), 54 | ], 'image-optimizer-stubs'); 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `image-optimizer` will be documented in this file. 4 | 5 | ## v1.6.4 - 2025-10-19 6 | 7 | ### What's Changed 8 | 9 | * Fix documentation: Use correct method names for image constraints by @maytham553 in https://github.com/joshembling/image-optimizer/pull/57 10 | 11 | ### New Contributors 12 | 13 | * @maytham553 made their first contribution in https://github.com/joshembling/image-optimizer/pull/57 14 | 15 | **Full Changelog**: https://github.com/joshembling/image-optimizer/compare/v1.6.3...v1.6.4 16 | 17 | ## v1.6.3 - 2025-08-26 18 | 19 | **Full Changelog**: https://github.com/joshembling/image-optimizer/compare/v1.6.2...v1.6.3 20 | 21 | ## v1.6.2 - 2025-06-16 22 | 23 | ### What's Changed 24 | 25 | * fix: Undefined variable $isPasteable by @WillieOng-HK in https://github.com/joshembling/image-optimizer/pull/52 26 | 27 | ### New Contributors 28 | 29 | * @WillieOng-HK made their first contribution in https://github.com/joshembling/image-optimizer/pull/52 30 | 31 | **Full Changelog**: https://github.com/joshembling/image-optimizer/compare/v1.6.1...v1.6.2 32 | 33 | ## v1.6.1 - 2025-06-12 34 | 35 | ### What's Changed 36 | 37 | * fix: match method signature with Filament update by @SupianIDz in https://github.com/joshembling/image-optimizer/pull/50 38 | 39 | ### New Contributors 40 | 41 | * @SupianIDz made their first contribution in https://github.com/joshembling/image-optimizer/pull/50 42 | 43 | **Full Changelog**: https://github.com/joshembling/image-optimizer/compare/v1.6.0...v1.6.1 44 | 45 | ## v1.6.0 - 2025-03-08 46 | 47 | ### What's Changed 48 | 49 | * Laravel 12 compatibility by @FinnPaes in https://github.com/joshembling/image-optimizer/pull/42 50 | 51 | ### New Contributors 52 | 53 | * @FinnPaes made their first contribution in https://github.com/joshembling/image-optimizer/pull/42 54 | 55 | **Full Changelog**: https://github.com/joshembling/image-optimizer/compare/v1.5.0...v1.6.0 56 | 57 | ## v1.5.0 - 2025-02-24 58 | 59 | ### What's Changed 60 | 61 | * Add Conditional Image Resizing Functions by @Autive in https://github.com/joshembling/image-optimizer/pull/40 62 | 63 | ### New Contributors 64 | 65 | * @Autive made their first contribution in https://github.com/joshembling/image-optimizer/pull/40 66 | 67 | **Full Changelog**: https://github.com/joshembling/image-optimizer/compare/v1.4.4...v1.5.0 68 | 69 | ## v1.4.4 - 2025-01-17 70 | 71 | ### What's Changed 72 | 73 | * Fixed maxParallelUploads attribute bug by @lucasvieira2902 in https://github.com/joshembling/image-optimizer/pull/39 74 | 75 | ### New Contributors 76 | 77 | * @lucasvieira2902 made their first contribution in https://github.com/joshembling/image-optimizer/pull/39 78 | 79 | **Full Changelog**: https://github.com/joshembling/image-optimizer/compare/v1.4.3...v1.4.4 80 | 81 | ## v1.4.3 - 2024-11-05 82 | 83 | **Full Changelog**: https://github.com/joshembling/image-optimizer/compare/v1.4.2...v1.4.3 84 | 85 | ## v1.4.2 - 2024-09-17 86 | 87 | ### What's Changed 88 | 89 | * handle afterStateUpdated as array by @cleeimpro in https://github.com/joshembling/image-optimizer/pull/33 90 | 91 | ### New Contributors 92 | 93 | * @cleeimpro made their first contribution in https://github.com/joshembling/image-optimizer/pull/33 94 | 95 | **Full Changelog**: https://github.com/joshembling/image-optimizer/compare/v1.4.1...v1.4.2 96 | 97 | ## v1.4.1 - 2024-06-22 98 | 99 | ### What's Changed 100 | 101 | * fix: undefined class facade ImageOptimizer by @ahmadrio in https://github.com/joshembling/image-optimizer/pull/24 102 | 103 | ### New Contributors 104 | 105 | * @ahmadrio made their first contribution in https://github.com/joshembling/image-optimizer/pull/24 106 | 107 | **Full Changelog**: https://github.com/joshembling/image-optimizer/compare/v1.4.0...v1.4.1 108 | 109 | ## v1.4.0 - 2024-03-17 110 | 111 | - Adds support for Laravel 11 112 | - Minimum PHP requirement now 8.2 113 | 114 | **Full Changelog**: https://github.com/joshembling/image-optimizer/compare/v1.3.1...v1.4.0 115 | 116 | ## v1.3.1 - 2024-02-03 117 | 118 | - Updates documentation around versioning 119 | 120 | **Full Changelog**: https://github.com/joshembling/image-optimizer/compare/v1.3.0...v1.3.1 121 | 122 | ## v1.3.0 - 2024-01-28 123 | 124 | - Fixes https://github.com/joshembling/image-optimizer/issues/9 with upgrades to latest Filament changes in v3.2^ 125 | 126 | **Full Changelog**: https://github.com/joshembling/image-optimizer/compare/v1.2.0...v1.3.0 127 | 128 | ## 1.2.0 - 2023-10-17 129 | 130 | Requires `intervention/image` directly. 131 | 132 | **Full Changelog**: https://github.com/joshembling/image-optimizer/compare/v1.1.1...v1.2.0 133 | 134 | ## 1.1.1 - 2023-09-27 135 | 136 | Fixes FileUpload class import 137 | 138 | **Full Changelog**: https://github.com/joshembling/image-optimizer/compare/v1.1.0...v1.1.1 139 | 140 | ## 1.1.0 - 2023-09-27 141 | 142 | Fixes Spatie Media Library support 143 | 144 | **Full Changelog**: https://github.com/joshembling/image-optimizer/compare/v1.0.4...v1.1.0 145 | 146 | ## 1.0.3 - 2023-09-23 147 | 148 | **Full Changelog**: https://github.com/joshembling/image-optimizer/compare/v1.0.2...v1.0.3 149 | 150 | ## 1.0.2 - 2023-09-23 151 | 152 | Package name and action changes 153 | 154 | ### What's Changed 155 | 156 | - Bump actions/checkout from 3 to 4 by @dependabot in https://github.com/joshembling/image-optimizer/pull/1 157 | - Revert "Bump actions/checkout from 3 to 4" by @joshembling in https://github.com/joshembling/image-optimizer/pull/2 158 | 159 | ### New Contributors 160 | 161 | - @dependabot made their first contribution in https://github.com/joshembling/image-optimizer/pull/1 162 | - @joshembling made their first contribution in https://github.com/joshembling/image-optimizer/pull/2 163 | 164 | **Full Changelog**: https://github.com/joshembling/image-optimizer/compare/v1.0.1...v1.0.2 165 | 166 | ## 1.0.1 - 2023-09-23 167 | 168 | Documentation update 169 | 170 | **Full Changelog**: https://github.com/joshembling/image-optimizer/compare/v1.0.0...v1.0.1 171 | 172 | ## 1.0.0 - 202X-XX-XX 173 | 174 | - initial release 175 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!CAUTION] 2 | > There are no plans to extend this plugin's lifetime beyond Filament v3. Please do not plan to use this in production if you are using or upgrading to Filament v4. 3 | 4 | # Optimize your Filament images before they reach your database. 5 | 6 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/joshembling/image-optimizer.svg?style=flat-square)](https://packagist.org/packages/joshembling/image-optimizer) 7 | [![Total Downloads](https://img.shields.io/packagist/dt/joshembling/image-optimizer.svg?style=flat-square)](https://packagist.org/packages/joshembling/image-optimizer) 8 | 9 | When you currently upload an image using the native Filament component `FileUpload`, the original file is saved without any compression or conversion. 10 | 11 | Additionally, if you upload an image and use conversions with `SpatieMediaLibraryFileUpload`, the original file is saved with its corresponding versions provided on your model. 12 | 13 | What if you'd rather convert and reduce the image(s) before reaching your database/S3 bucket? Especially in the case where you know you'll never need to save the original image sizes the user has uploaded. 14 | 15 | 🤳 **This is where Filament Image Optimizer comes in**. 16 | 17 | You use the same components as you have been doing and have access to two additional methods for maximum optimization, saving you a lot of disk space in the process. 🎉 18 | 19 | ## Contents 20 | 21 | - [Contents](#contents) 22 | - [Installation](#installation) 23 | - [Usage](#usage) 24 | - [Optimizing Images](#optimizing-images) 25 | - [Resizing Images](#resizing-images) 26 | - [Combining Methods](#combining-methods) 27 | - [Multiple Images](#multiple-images) 28 | - [Examples](#examples) 29 | - [Debugging](#debugging) 30 | - [Changelog](#changelog) 31 | - [Contributing](#contributing) 32 | - [Security Vulnerabilities](#security-vulnerabilities) 33 | - [Credits](#credits) 34 | - [Licence](#license) 35 | 36 | ## Installation 37 | 38 | You can install the package via composer, which currently works with the latest Filament version (^3.2) and Laravel 10, 11 & 12: 39 | 40 | ```bash 41 | composer require joshembling/image-optimizer 42 | ``` 43 | 44 | If you are using Filament 3.0 or 3.1 install with: 45 | ```bash 46 | composer require joshembling/image-optimizer:v1.2 47 | ``` 48 | 49 | ## Usage 50 | 51 | ### Filament version 52 | 53 | You must be using [Filament v3.x](https://filamentphp.com/docs/3.x/panels/installation) to have access to this plugin. 54 | 55 | For specific versions that match your PHP, Laravel, Filament and Image Optimizer installations please see the table below: 56 | 57 | | PHP | Laravel version | Filament version | Image Optimizer version | 58 | | ----- | ----- | -----| ----- | 59 | | ^8.1 | ^10.0 | ^3.0 | 1.2 | 60 | | ^8.1 | ^10.0 | ^3.1 | 1.2 | 61 | | ^8.1 | ^10.0 | ^3.2 | ~1.3 | 62 | | ^8.2 | ^10.0, ^11.0 | ^3.2 | ^1.4 | 63 | | ^8.2 | ^10.0, ^11.0, ^12.0 | ^3.2 | ^1.6 | 64 | 65 | ### Server 66 | 67 | [GD Library](https://www.php.net/manual/en/image.installation.php) must be installed on your server to compress images. 68 | 69 | ### Optimizing images 70 | 71 | Before uploading your image, you may choose to optimize it by converting to your chosen format. The file saved to your disk will be the converted version only. 72 | 73 | E.g. I want to convert my image to 'webp': 74 | 75 | `````php 76 | use Filament\Forms\Components\FileUpload; 77 | 78 | FileUpload::make('attachment') 79 | ->image() 80 | ->optimize('webp'), 81 | ````` 82 | 83 | You can do exactly the same using `SpatieMediaLibraryFileUpload`: 84 | 85 | `````php 86 | use Filament\Forms\Components\SpatieMediaLibraryFileUpload; 87 | 88 | SpatieMediaLibraryFileUpload::make('attachment') 89 | ->image() 90 | ->optimize('webp'), 91 | ````` 92 | 93 | ### Resizing images 94 | 95 | You may also want to resize an image by passing in a percentage you would like to reduce the image by. This will also maintain aspect ratio. 96 | 97 | E.g. I'd like to reduce my image (1280px x 720px) by 50%: 98 | 99 | `````php 100 | use Filament\Forms\Components\FileUpload; 101 | 102 | FileUpload::make('attachment') 103 | ->image() 104 | ->resize(50), 105 | ````` 106 | 107 | Uploaded image size is 640px x 360px. 108 | 109 | You can do the same using `SpatieMediaLibraryFileUpload`: 110 | 111 | `````php 112 | use Filament\Forms\Components\SpatieMediaLibraryFileUpload; 113 | 114 | SpatieMediaLibraryFileUpload::make('attachment') 115 | ->image() 116 | ->resize(50), 117 | ````` 118 | 119 | ### Add maximum width and/or height 120 | 121 | You can also add a maximum width and/or height to the image. This will resize the image to the maximum width and/or height, maintaining the aspect ratio. 122 | 123 | `````php 124 | use Filament\Forms\Components\FileUpload; 125 | 126 | FileUpload::make('attachment') 127 | ->image() 128 | ->maxImageWidth(1024) 129 | ->maxImageHeight(768), 130 | ````` 131 | 132 | ### Combining methods 133 | 134 | You can combine these two methods for maximum optimization. 135 | 136 | `````php 137 | use Filament\Forms\Components\FileUpload; 138 | 139 | FileUpload::make('attachment') 140 | ->image() 141 | ->optimize('webp') 142 | ->maxImageWidth(1024) 143 | ->maxImageHeight(768) 144 | ->resize(50), 145 | ````` 146 | 147 | `````php 148 | use Filament\Forms\Components\SpatieMediaLibraryFileUpload; 149 | 150 | SpatieMediaLibraryFileUpload::make('attachment') 151 | ->image() 152 | ->optimize('webp') 153 | ->maxImageWidth(1024) 154 | ->maxImageHeight(768) 155 | ->resize(50), 156 | ````` 157 | 158 | ### Multiple images 159 | 160 | You can also do this with multiple images - all images will be converted to the same format and reduced with the same percentage passed in. Just chain on `multiple()` to your upload: 161 | 162 | `````php 163 | use Filament\Forms\Components\FileUpload; 164 | 165 | FileUpload::make('attachment') 166 | ->image() 167 | ->multiple() 168 | ->optimize('jpg') 169 | ->resize(50), 170 | ````` 171 | 172 | `````php 173 | use Filament\Forms\Components\SpatieMediaLibraryFileUpload; 174 | 175 | SpatieMediaLibraryFileUpload::make('attachment') 176 | ->image() 177 | ->multiple() 178 | ->optimize('jpg') 179 | ->resize(50), 180 | ````` 181 | 182 | ### Examples 183 | 184 | ![Before](images/before.jpg) 185 | 186 | ![After](images/after.jpg) 187 | 188 | ### Debugging 189 | 190 | - If you see a 'not found' exception, including "Method `optimize`" or "Method `resize`", ensure you run `composer update` so that your lock file is in sync with your `composer.json`. 191 | 192 | - You might see a 'Waiting for size' message and an infinite loading state on the component and the likely cause of this is a CORS issue. This can be quickly be resolved by ensuring you are serving and upload images from the same domain. Check your Javascript console for more information. 193 | 194 | ## Changelog 195 | 196 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 197 | 198 | ## Contributing 199 | 200 | Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details. 201 | 202 | ## Security Vulnerabilities 203 | 204 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 205 | 206 | ## Credits 207 | 208 | - [Josh Embling](https://github.com/joshembling) 209 | - [All Contributors](../../contributors) 210 | 211 | ## License 212 | 213 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 214 | -------------------------------------------------------------------------------- /src/Components/SpatieMediaLibraryFileUpload.php: -------------------------------------------------------------------------------- 1 | | Closure | null 31 | */ 32 | protected array | Closure | null $customHeaders = null; 33 | 34 | /** 35 | * @var array | Closure | null 36 | */ 37 | protected array | Closure | null $customProperties = null; 38 | 39 | /** 40 | * @var array> | Closure | null 41 | */ 42 | protected array | Closure | null $manipulations = null; 43 | 44 | /** 45 | * @var array | Closure | null 46 | */ 47 | protected array | Closure | null $properties = null; 48 | 49 | protected ?Closure $filterMedia = null; 50 | 51 | protected function setUp(): void 52 | { 53 | parent::setUp(); 54 | 55 | $this->loadStateFromRelationshipsUsing(static function (SpatieMediaLibraryFileUpload $component, HasMedia $record): void { 56 | /** @var Model&HasMedia $record */ 57 | $files = $record->load('media')->getMedia($component->getCollection()) 58 | ->when( 59 | ! $component->isMultiple(), 60 | fn (Collection $files): Collection => $files->take(1) 61 | ->when( 62 | $component->hasMediaFilter(), 63 | fn (Collection $files) => $component->getFilteredMedia($files) 64 | ), 65 | ) 66 | ->mapWithKeys(function (Media $file): array { 67 | $uuid = $file->getAttributeValue('uuid'); 68 | 69 | return [$uuid => $uuid]; 70 | }) 71 | ->toArray(); 72 | 73 | $component->state($files); 74 | }); 75 | 76 | $this->afterStateHydrated(static function (BaseFileUpload $component, string | array | null $state): void { 77 | if (is_array($state)) { 78 | return; 79 | } 80 | 81 | $component->state([]); 82 | }); 83 | 84 | $this->beforeStateDehydrated(null); 85 | 86 | $this->dehydrated(false); 87 | 88 | $this->getUploadedFileUsing(static function (SpatieMediaLibraryFileUpload $component, string $file): ?array { 89 | if (! $component->getRecord()) { 90 | return null; 91 | } 92 | 93 | /** @var ?Media $media */ 94 | $media = $component->getRecord()->getRelationValue('media')->firstWhere('uuid', $file); 95 | 96 | $url = null; 97 | 98 | if ($component->getVisibility() === 'private') { 99 | try { 100 | $url = $media?->getTemporaryUrl( 101 | now()->addMinutes(5), 102 | ); 103 | } catch (Throwable $exception) { 104 | // This driver does not support creating temporary URLs. 105 | } 106 | } 107 | 108 | if ($component->getConversion() && $media->hasGeneratedConversion($component->getConversion())) { 109 | $url ??= $media?->getUrl($component->getConversion()); 110 | } 111 | 112 | $url ??= $media?->getUrl(); 113 | 114 | return [ 115 | 'name' => $media->getAttributeValue('name') ?? $media->getAttributeValue('file_name'), 116 | 'size' => $media->getAttributeValue('size'), 117 | 'type' => $media->getAttributeValue('mime_type'), 118 | 'url' => $url, 119 | ]; 120 | }); 121 | 122 | $this->saveRelationshipsUsing(static function (SpatieMediaLibraryFileUpload $component) { 123 | $component->deleteAbandonedFiles(); 124 | $component->saveUploadedFiles(); 125 | }); 126 | 127 | $this->saveUploadedFileUsing(static function (SpatieMediaLibraryFileUpload $component, TemporaryUploadedFile $file, ?Model $record): ?string { 128 | if (! method_exists($record, 'addMediaFromString')) { 129 | return $file; 130 | } 131 | 132 | try { 133 | if (! $file->exists()) { 134 | return null; 135 | } 136 | } catch (UnableToCheckFileExistence $exception) { 137 | return null; 138 | } 139 | 140 | $compressedImage = null; 141 | $filename = $component->getUploadedFileNameForStorage($file); 142 | $originalBinaryFile = $file->get(); 143 | $optimize = $component->getOptimization(); 144 | $resize = $component->getResize(); 145 | $maxImageWidth = $component->getMaxImageWidth(); 146 | $maxImageHeight = $component->getMaxImageHeight(); 147 | $shouldResize = false; 148 | $imageHeight = null; 149 | $imageWidth = null; 150 | 151 | if ( 152 | str_contains($file->getMimeType(), 'image') && 153 | ($optimize || $resize || $maxImageWidth || $maxImageHeight) 154 | ) { 155 | $image = InterventionImage::make($originalBinaryFile); 156 | 157 | if ($optimize) { 158 | $quality = $optimize === 'jpeg' || 159 | $optimize === 'jpg' ? 70 : null; 160 | } 161 | 162 | if ($maxImageWidth && $image->width() > $maxImageWidth) { 163 | $shouldResize = true; 164 | $imageWidth = $maxImageWidth; 165 | } 166 | 167 | if ($maxImageHeight && $image->height() > $maxImageHeight) { 168 | $shouldResize = true; 169 | $imageHeight = $maxImageHeight; 170 | } 171 | 172 | if ($resize) { 173 | $shouldResize = true; 174 | 175 | if ($image->height() > $image->width()) { 176 | $imageHeight = $image->height() - ($image->height() * ($resize / 100)); 177 | } else { 178 | $imageWidth = $image->width() - ($image->width() * ($resize / 100)); 179 | } 180 | } 181 | 182 | if ($shouldResize) { 183 | $image->resize($imageWidth, $imageHeight, function ($constraint) { 184 | $constraint->aspectRatio(); 185 | }); 186 | } 187 | 188 | if ($optimize) { 189 | $compressedImage = $image->encode($optimize, $quality); 190 | } else { 191 | $compressedImage = $image->encode(); 192 | } 193 | 194 | $filename = self::formatFileName($filename, $optimize); 195 | } 196 | 197 | /** @var FileAdder $mediaAdder */ 198 | $mediaAdder = $record->addMediaFromString($compressedImage ?? $originalBinaryFile); 199 | 200 | $media = $mediaAdder 201 | ->addCustomHeaders($component->getCustomHeaders()) 202 | ->usingFileName($filename) 203 | ->usingName($component->getMediaName($file) ?? pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME)) 204 | ->storingConversionsOnDisk($component->getConversionsDisk() ?? '') 205 | ->withCustomProperties($component->getCustomProperties()) 206 | ->withManipulations($component->getManipulations()) 207 | ->withResponsiveImagesIf($component->hasResponsiveImages()) 208 | ->withProperties($component->getProperties()) 209 | ->toMediaCollection($component->getCollection(), $component->getDiskName()); 210 | 211 | return $media->getAttributeValue('uuid'); 212 | }); 213 | 214 | $this->reorderUploadedFilesUsing(static function (SpatieMediaLibraryFileUpload $component, array $state): array { 215 | $uuids = array_filter(array_values($state)); 216 | 217 | $mediaClass = config('media-library.media_model', Media::class); 218 | 219 | $mappedIds = $mediaClass::query()->whereIn('uuid', $uuids)->pluck('id', 'uuid')->toArray(); 220 | 221 | $mediaClass::setNewOrder([ 222 | ...array_flip($uuids), 223 | ...$mappedIds, 224 | ]); 225 | 226 | return $state; 227 | }); 228 | } 229 | 230 | public function collection(string | Closure | null $collection): static 231 | { 232 | $this->collection = $collection; 233 | 234 | return $this; 235 | } 236 | 237 | public function conversion(string | Closure | null $conversion): static 238 | { 239 | $this->conversion = $conversion; 240 | 241 | return $this; 242 | } 243 | 244 | public function conversionsDisk(string | Closure | null $disk): static 245 | { 246 | $this->conversionsDisk = $disk; 247 | 248 | return $this; 249 | } 250 | 251 | /** 252 | * @param array | Closure | null $headers 253 | */ 254 | public function customHeaders(array | Closure | null $headers): static 255 | { 256 | $this->customHeaders = $headers; 257 | 258 | return $this; 259 | } 260 | 261 | /** 262 | * @param array | Closure | null $properties 263 | */ 264 | public function customProperties(array | Closure | null $properties): static 265 | { 266 | $this->customProperties = $properties; 267 | 268 | return $this; 269 | } 270 | 271 | public function filterMedia(?Closure $filterMedia): static 272 | { 273 | $this->filterMedia = $filterMedia; 274 | 275 | return $this; 276 | } 277 | 278 | /** 279 | * @param array> | Closure | null $manipulations 280 | */ 281 | public function manipulations(array | Closure | null $manipulations): static 282 | { 283 | $this->manipulations = $manipulations; 284 | 285 | return $this; 286 | } 287 | 288 | /** 289 | * @param array | Closure | null $properties 290 | */ 291 | public function properties(array | Closure | null $properties): static 292 | { 293 | $this->properties = $properties; 294 | 295 | return $this; 296 | } 297 | 298 | public function responsiveImages(bool | Closure $condition = true): static 299 | { 300 | $this->hasResponsiveImages = $condition; 301 | 302 | return $this; 303 | } 304 | 305 | public function deleteAbandonedFiles(): void 306 | { 307 | /** @var Model&HasMedia $record */ 308 | $record = $this->getRecord(); 309 | 310 | $record 311 | ->getMedia($this->getCollection()) 312 | ->whereNotIn('uuid', array_keys($this->getState() ?? [])) 313 | ->when($this->hasMediaFilter(), fn (Collection $files) => $this->getFilteredMedia($files)) 314 | ->each(fn (Media $media) => $media->delete()); 315 | } 316 | 317 | public function getCollection(): string 318 | { 319 | return $this->evaluate($this->collection) ?? 'default'; 320 | } 321 | 322 | public function getConversion(): ?string 323 | { 324 | return $this->evaluate($this->conversion); 325 | } 326 | 327 | public function getConversionsDisk(): ?string 328 | { 329 | return $this->evaluate($this->conversionsDisk); 330 | } 331 | 332 | /** 333 | * @return array 334 | */ 335 | public function getCustomHeaders(): array 336 | { 337 | return $this->evaluate($this->customHeaders) ?? []; 338 | } 339 | 340 | /** 341 | * @return array 342 | */ 343 | public function getCustomProperties(): array 344 | { 345 | return $this->evaluate($this->customProperties) ?? []; 346 | } 347 | 348 | /** 349 | * @return array> 350 | */ 351 | public function getManipulations(): array 352 | { 353 | return $this->evaluate($this->manipulations) ?? []; 354 | } 355 | 356 | /** 357 | * @return array 358 | */ 359 | public function getProperties(): array 360 | { 361 | return $this->evaluate($this->properties) ?? []; 362 | } 363 | 364 | public function getFilteredMedia(Collection $media): Collection 365 | { 366 | return $this->evaluate($this->filterMedia, [ 367 | 'media' => $media, 368 | ]) ?? $media; 369 | } 370 | 371 | public function hasMediaFilter(): bool 372 | { 373 | return $this->filterMedia instanceof Closure; 374 | } 375 | 376 | public function hasResponsiveImages(): bool 377 | { 378 | return (bool) $this->evaluate($this->hasResponsiveImages); 379 | } 380 | 381 | public function mediaName(string | Closure | null $name): static 382 | { 383 | $this->mediaName = $name; 384 | 385 | return $this; 386 | } 387 | 388 | public function getMediaName(TemporaryUploadedFile $file): ?string 389 | { 390 | return $this->evaluate($this->mediaName, [ 391 | 'file' => $file, 392 | ]); 393 | } 394 | } 395 | -------------------------------------------------------------------------------- /src/Components/BaseFileUpload.php: -------------------------------------------------------------------------------- 1 | | Arrayable | Closure | null 28 | */ 29 | protected array | Arrayable | Closure | null $acceptedFileTypes = null; 30 | 31 | protected bool | Closure $isDeletable = true; 32 | 33 | protected bool | Closure $isDownloadable = false; 34 | 35 | protected bool | Closure $isOpenable = false; 36 | 37 | protected bool | Closure $isPreviewable = true; 38 | 39 | protected bool | Closure $isReorderable = false; 40 | 41 | protected string | Closure | null $directory = null; 42 | 43 | protected string | Closure | null $diskName = null; 44 | 45 | protected bool | Closure $isMultiple = false; 46 | 47 | protected int | Closure | null $maxSize = null; 48 | 49 | protected int | Closure | null $minSize = null; 50 | 51 | protected int | Closure | null $maxParallelUploads = null; 52 | 53 | protected int | Closure | null $maxFiles = null; 54 | 55 | protected int | Closure | null $minFiles = null; 56 | 57 | protected string | Closure | null $optimize = null; 58 | 59 | protected int | Closure | null $resize = null; 60 | 61 | protected int | Closure | null $maxImageWidth = null; 62 | 63 | protected int | Closure | null $maxImageHeight = null; 64 | 65 | protected bool | Closure $shouldPreserveFilenames = false; 66 | 67 | protected bool | Closure $shouldMoveFiles = false; 68 | 69 | protected bool | Closure $shouldStoreFiles = true; 70 | 71 | protected bool | Closure $shouldFetchFileInformation = true; 72 | 73 | protected string | Closure | null $fileNamesStatePath = null; 74 | 75 | protected string | Closure $visibility = 'public'; 76 | 77 | protected ?Closure $deleteUploadedFileUsing = null; 78 | 79 | protected ?Closure $getUploadedFileNameForStorageUsing = null; 80 | 81 | protected ?Closure $getUploadedFileUsing = null; 82 | 83 | protected ?Closure $reorderUploadedFilesUsing = null; 84 | 85 | protected ?Closure $saveUploadedFileUsing = null; 86 | 87 | protected bool | Closure $isPasteable = true; 88 | 89 | protected function setUp(): void 90 | { 91 | parent::setUp(); 92 | 93 | $this->afterStateHydrated(static function (BaseFileUpload $component, string | array | null $state): void { 94 | if (blank($state)) { 95 | $component->state([]); 96 | 97 | return; 98 | } 99 | 100 | $shouldFetchFileInformation = $component->shouldFetchFileInformation(); 101 | 102 | $files = collect(Arr::wrap($state)) 103 | ->filter(static function (string $file) use ($component, $shouldFetchFileInformation): bool { 104 | if (blank($file)) { 105 | return false; 106 | } 107 | 108 | if (! $shouldFetchFileInformation) { 109 | return true; 110 | } 111 | 112 | try { 113 | return $component->getDisk()->exists($file); 114 | } catch (UnableToCheckFileExistence $exception) { 115 | return false; 116 | } 117 | }) 118 | ->mapWithKeys(static fn (string $file): array => [((string) Str::uuid()) => $file]) 119 | ->all(); 120 | 121 | $component->state($files); 122 | }); 123 | 124 | $this->afterStateUpdated(static function (BaseFileUpload $component, $state) { 125 | if ($state instanceof TemporaryUploadedFile) { 126 | return; 127 | } 128 | 129 | if (blank($state)) { 130 | return; 131 | } 132 | 133 | if (is_array($state)) { 134 | return; 135 | } 136 | 137 | $component->state([(string) Str::uuid() => $state]); 138 | }); 139 | 140 | $this->beforeStateDehydrated(static function (BaseFileUpload $component): void { 141 | $component->saveUploadedFiles(); 142 | }); 143 | 144 | $this->dehydrateStateUsing(static function (BaseFileUpload $component, ?array $state): string | array | null | TemporaryUploadedFile { 145 | $files = array_values($state ?? []); 146 | 147 | if ($component->isMultiple()) { 148 | return $files; 149 | } 150 | 151 | return $files[0] ?? null; 152 | }); 153 | 154 | $this->getUploadedFileUsing(static function (BaseFileUpload $component, string $file, string | array | null $storedFileNames): ?array { 155 | /** @var FilesystemAdapter $storage */ 156 | $storage = $component->getDisk(); 157 | 158 | $shouldFetchFileInformation = $component->shouldFetchFileInformation(); 159 | 160 | if ($shouldFetchFileInformation) { 161 | try { 162 | if (! $storage->exists($file)) { 163 | return null; 164 | } 165 | } catch (UnableToCheckFileExistence $exception) { 166 | return null; 167 | } 168 | } 169 | 170 | $url = null; 171 | 172 | if ($component->getVisibility() === 'private') { 173 | try { 174 | $url = $storage->temporaryUrl( 175 | $file, 176 | now()->addMinutes(5), 177 | ); 178 | } catch (Throwable $exception) { 179 | // This driver does not support creating temporary URLs. 180 | } 181 | } 182 | 183 | $url ??= $storage->url($file); 184 | 185 | return [ 186 | 'name' => ($component->isMultiple() ? ($storedFileNames[$file] ?? null) : $storedFileNames) ?? basename($file), 187 | 'size' => $shouldFetchFileInformation ? $storage->size($file) : 0, 188 | 'type' => $shouldFetchFileInformation ? $storage->mimeType($file) : null, 189 | 'url' => $url, 190 | ]; 191 | }); 192 | 193 | $this->getUploadedFileNameForStorageUsing(static function (BaseFileUpload $component, TemporaryUploadedFile $file) { 194 | return $component->shouldPreserveFilenames() ? $file->getClientOriginalName() : (Str::ulid() . '.' . $file->getClientOriginalExtension()); 195 | }); 196 | 197 | $this->saveUploadedFileUsing(static function (BaseFileUpload $component, TemporaryUploadedFile $file): ?string { 198 | try { 199 | if (! $file->exists()) { 200 | return null; 201 | } 202 | } catch (UnableToCheckFileExistence $exception) { 203 | return null; 204 | } 205 | 206 | $compressedImage = null; 207 | $filename = $component->getUploadedFileNameForStorage($file); 208 | $optimize = $component->getOptimization(); 209 | $resize = $component->getResize(); 210 | $maxImageWidth = $component->getMaxImageWidth(); 211 | $maxImageHeight = $component->getMaxImageHeight(); 212 | $shouldResize = false; 213 | $imageHeight = null; 214 | $imageWidth = null; 215 | // $originalBinaryFile = $file->get(); 216 | 217 | if ( 218 | str_contains($file->getMimeType(), 'image') && 219 | ($optimize || $resize || $maxImageWidth || $maxImageHeight) 220 | ) { 221 | $image = InterventionImage::make($file); 222 | 223 | if ($optimize) { 224 | $quality = $optimize === 'jpeg' || 225 | $optimize === 'jpg' ? 70 : null; 226 | } 227 | 228 | if ($maxImageWidth && $image->width() > $maxImageWidth) { 229 | $shouldResize = true; 230 | $imageWidth = $maxImageWidth; 231 | } 232 | 233 | if ($maxImageHeight && $image->height() > $maxImageHeight) { 234 | $shouldResize = true; 235 | $imageHeight = $maxImageHeight; 236 | } 237 | 238 | if ($resize) { 239 | $shouldResize = true; 240 | 241 | if ($image->height() > $image->width()) { 242 | $imageHeight = $image->height() - ($image->height() * ($resize / 100)); 243 | } else { 244 | $imageWidth = $image->width() - ($image->width() * ($resize / 100)); 245 | } 246 | } 247 | 248 | if ($shouldResize) { 249 | $image->resize($imageWidth, $imageHeight, function ($constraint) { 250 | $constraint->aspectRatio(); 251 | }); 252 | } 253 | 254 | if ($optimize) { 255 | $compressedImage = $image->encode($optimize, $quality); 256 | } else { 257 | $compressedImage = $image->encode(); 258 | } 259 | 260 | $filename = self::formatFileName($filename, $optimize); 261 | } 262 | 263 | if ($compressedImage) { 264 | Storage::disk($component->getDiskName())->put( 265 | $component->getDirectory() . '/' . $filename, 266 | $compressedImage->getEncoded() 267 | ); 268 | 269 | return $component->getDirectory() . '/' . $filename; 270 | } 271 | 272 | if ( 273 | $component->shouldMoveFiles() && 274 | ($component->getDiskName() == (fn (): string => $this->disk)->call($file)) 275 | ) { 276 | $newPath = trim($component->getDirectory() . '/' . $component->getUploadedFileNameForStorage($file), '/'); 277 | 278 | $component->getDisk()->move((fn (): string => $this->path)->call($file), $newPath); 279 | 280 | return $newPath; 281 | } 282 | 283 | $storeMethod = $component->getVisibility() === 'public' ? 'storePubliclyAs' : 'storeAs'; 284 | 285 | return $file->{$storeMethod}( 286 | $component->getDirectory(), 287 | $component->getUploadedFileNameForStorage($file), 288 | $component->getDiskName() 289 | ); 290 | }); 291 | } 292 | 293 | protected function callAfterStateUpdatedHook(Closure $hook): void 294 | { 295 | $state = $this->getState(); 296 | 297 | $this->evaluate($hook, [ 298 | 'state' => $this->isMultiple() ? $state : Arr::first($state ?? []), 299 | 'old' => $this->isMultiple() ? $this->getOldState() : Arr::first($this->getOldState() ?? []), 300 | ]); 301 | } 302 | 303 | public function callAfterStateUpdated(bool $shouldBubbleToParents = true): static 304 | { 305 | $state = $this->getState(); 306 | 307 | foreach ($this->afterStateUpdated as $callback) { 308 | $this->evaluate($callback, [ 309 | 'state' => $this->isMultiple() ? $state : Arr::first($state ?? []), 310 | ]); 311 | } 312 | 313 | return $this; 314 | } 315 | 316 | /** 317 | * @param array | Arrayable | Closure $types 318 | */ 319 | public function acceptedFileTypes(array | Arrayable | Closure $types): static 320 | { 321 | $this->acceptedFileTypes = $types; 322 | 323 | $this->rule(static function (BaseFileUpload $component) { 324 | $types = implode(',', ($component->getAcceptedFileTypes() ?? [])); 325 | 326 | return "mimetypes:{$types}"; 327 | }); 328 | 329 | return $this; 330 | } 331 | 332 | public function deletable(bool | Closure $condition = true): static 333 | { 334 | $this->isDeletable = $condition; 335 | 336 | return $this; 337 | } 338 | 339 | public function directory(string | Closure | null $directory): static 340 | { 341 | $this->directory = $directory; 342 | 343 | return $this; 344 | } 345 | 346 | public function disk(string | Closure | null $name): static 347 | { 348 | $this->diskName = $name; 349 | 350 | return $this; 351 | } 352 | 353 | public function downloadable(bool | Closure $condition = true): static 354 | { 355 | $this->isDownloadable = $condition; 356 | 357 | return $this; 358 | } 359 | 360 | public function openable(bool | Closure $condition = true): static 361 | { 362 | $this->isOpenable = $condition; 363 | 364 | return $this; 365 | } 366 | 367 | public function reorderable(bool | Closure $condition = true): static 368 | { 369 | $this->isReorderable = $condition; 370 | 371 | return $this; 372 | } 373 | 374 | public function previewable(bool | Closure $condition = true): static 375 | { 376 | $this->isPreviewable = $condition; 377 | 378 | return $this; 379 | } 380 | 381 | /** 382 | * @deprecated Use `downloadable()` instead. 383 | */ 384 | public function enableDownload(bool | Closure $condition = true): static 385 | { 386 | $this->downloadable($condition); 387 | 388 | return $this; 389 | } 390 | 391 | /** 392 | * @deprecated Use `openable()` instead. 393 | */ 394 | public function enableOpen(bool | Closure $condition = true): static 395 | { 396 | $this->openable($condition); 397 | 398 | return $this; 399 | } 400 | 401 | /** 402 | * @deprecated Use `reorderable()` instead. 403 | */ 404 | public function enableReordering(bool | Closure $condition = true): static 405 | { 406 | $this->reorderable($condition); 407 | 408 | return $this; 409 | } 410 | 411 | /** 412 | * @deprecated Use `previewable()` instead. 413 | */ 414 | public function disablePreview(bool | Closure $condition = true): static 415 | { 416 | $this->previewable(fn (BaseFileUpload $component): bool => ! $component->evaluate($condition)); 417 | 418 | return $this; 419 | } 420 | 421 | public function storeFileNamesIn(string | Closure | null $statePath): static 422 | { 423 | $this->fileNamesStatePath = $statePath; 424 | 425 | return $this; 426 | } 427 | 428 | public function preserveFilenames(bool | Closure $condition = true): static 429 | { 430 | $this->shouldPreserveFilenames = $condition; 431 | 432 | return $this; 433 | } 434 | 435 | public function moveFiles(bool | Closure $condition = true): static 436 | { 437 | $this->shouldMoveFiles = $condition; 438 | 439 | return $this; 440 | } 441 | 442 | /** 443 | * @deprecated Use `moveFiles()` instead. 444 | */ 445 | public function moveFile(bool | Closure $condition = true): static 446 | { 447 | $this->moveFiles($condition); 448 | 449 | return $this; 450 | } 451 | 452 | public function maxSize(int | Closure | null $size): static 453 | { 454 | $this->maxSize = $size; 455 | 456 | $this->rule(static function (BaseFileUpload $component): string { 457 | $size = $component->getMaxSize(); 458 | 459 | return "max:{$size}"; 460 | }); 461 | 462 | return $this; 463 | } 464 | 465 | public function minSize(int | Closure | null $size): static 466 | { 467 | $this->minSize = $size; 468 | 469 | $this->rule(static function (BaseFileUpload $component): string { 470 | $size = $component->getMinSize(); 471 | 472 | return "min:{$size}"; 473 | }); 474 | 475 | return $this; 476 | } 477 | 478 | public function maxParallelUploads(int | Closure | null $count): static 479 | { 480 | $this->maxParallelUploads = $count; 481 | 482 | return $this; 483 | } 484 | 485 | public function maxFiles(int | Closure | null $count): static 486 | { 487 | $this->maxFiles = $count; 488 | 489 | return $this; 490 | } 491 | 492 | public function minFiles(int | Closure | null $count): static 493 | { 494 | $this->minFiles = $count; 495 | 496 | return $this; 497 | } 498 | 499 | public function multiple(bool | Closure $condition = true): static 500 | { 501 | $this->isMultiple = $condition; 502 | 503 | return $this; 504 | } 505 | 506 | public function optimize(string | Closure | null $optimize): static 507 | { 508 | $this->optimize = $optimize; 509 | 510 | return $this; 511 | } 512 | 513 | public function resize(int | Closure | null $reductionPercentage): static 514 | { 515 | $this->resize = $reductionPercentage; 516 | 517 | return $this; 518 | } 519 | 520 | public function maxImageWidth(int | Closure | null $width): static 521 | { 522 | $this->maxImageWidth = $width; 523 | 524 | return $this; 525 | } 526 | 527 | public function maxImageHeight(int | Closure | null $height): static 528 | { 529 | $this->maxImageHeight = $height; 530 | 531 | return $this; 532 | } 533 | 534 | public function storeFiles(bool | Closure $condition = true): static 535 | { 536 | $this->shouldStoreFiles = $condition; 537 | 538 | return $this; 539 | } 540 | 541 | /** 542 | * @deprecated Use `storeFiles()` instead. 543 | */ 544 | public function storeFile(bool | Closure $condition = true): static 545 | { 546 | $this->storeFiles($condition); 547 | 548 | return $this; 549 | } 550 | 551 | public function visibility(string | Closure | null $visibility): static 552 | { 553 | $this->visibility = $visibility; 554 | 555 | return $this; 556 | } 557 | 558 | public function deleteUploadedFileUsing(?Closure $callback): static 559 | { 560 | $this->deleteUploadedFileUsing = $callback; 561 | 562 | return $this; 563 | } 564 | 565 | public function getUploadedFileUsing(?Closure $callback): static 566 | { 567 | $this->getUploadedFileUsing = $callback; 568 | 569 | return $this; 570 | } 571 | 572 | public function reorderUploadedFilesUsing(?Closure $callback): static 573 | { 574 | $this->reorderUploadedFilesUsing = $callback; 575 | 576 | return $this; 577 | } 578 | 579 | public function saveUploadedFileUsing(?Closure $callback): static 580 | { 581 | $this->saveUploadedFileUsing = $callback; 582 | 583 | return $this; 584 | } 585 | 586 | public function isDeletable(): bool 587 | { 588 | return (bool) $this->evaluate($this->isDeletable); 589 | } 590 | 591 | public function isDownloadable(): bool 592 | { 593 | return (bool) $this->evaluate($this->isDownloadable); 594 | } 595 | 596 | public function isOpenable(): bool 597 | { 598 | return (bool) $this->evaluate($this->isOpenable); 599 | } 600 | 601 | public function isPreviewable(): bool 602 | { 603 | return (bool) $this->evaluate($this->isPreviewable); 604 | } 605 | 606 | public function isReorderable(): bool 607 | { 608 | return (bool) $this->evaluate($this->isReorderable); 609 | } 610 | 611 | /** 612 | * @return array | null 613 | */ 614 | public function getAcceptedFileTypes(): ?array 615 | { 616 | $types = $this->evaluate($this->acceptedFileTypes); 617 | 618 | if ($types instanceof Arrayable) { 619 | $types = $types->toArray(); 620 | } 621 | 622 | return $types; 623 | } 624 | 625 | public function getDirectory(): ?string 626 | { 627 | return $this->evaluate($this->directory); 628 | } 629 | 630 | public function getDisk(): Filesystem 631 | { 632 | return Storage::disk($this->getDiskName()); 633 | } 634 | 635 | public function getDiskName(): string 636 | { 637 | return $this->evaluate($this->diskName) ?? config('filament.default_filesystem_disk'); 638 | } 639 | 640 | public function getMaxFiles(): ?int 641 | { 642 | return $this->evaluate($this->maxFiles); 643 | } 644 | 645 | public function getMinFiles(): ?int 646 | { 647 | return $this->evaluate($this->minFiles); 648 | } 649 | 650 | public function getMaxSize(): ?int 651 | { 652 | return $this->evaluate($this->maxSize); 653 | } 654 | 655 | public function getMinSize(): ?int 656 | { 657 | return $this->evaluate($this->minSize); 658 | } 659 | 660 | public function getOptimization(): ?string 661 | { 662 | return $this->evaluate($this->optimize); 663 | } 664 | 665 | public function getResize(): ?int 666 | { 667 | return $this->evaluate($this->resize); 668 | } 669 | 670 | public function getMaxImageWidth(): ?int 671 | { 672 | return $this->evaluate($this->maxImageWidth); 673 | } 674 | 675 | public function getMaxImageHeight(): ?int 676 | { 677 | return $this->evaluate($this->maxImageHeight); 678 | } 679 | 680 | public function getMaxParallelUploads(): ?int 681 | { 682 | return $this->evaluate($this->maxParallelUploads); 683 | } 684 | 685 | public function getVisibility(): string 686 | { 687 | return $this->evaluate($this->visibility); 688 | } 689 | 690 | public function shouldPreserveFilenames(): bool 691 | { 692 | return (bool) $this->evaluate($this->shouldPreserveFilenames); 693 | } 694 | 695 | public function shouldMoveFiles(): bool 696 | { 697 | return $this->evaluate($this->shouldMoveFiles); 698 | } 699 | 700 | public function shouldFetchFileInformation(): bool 701 | { 702 | return (bool) $this->evaluate($this->shouldFetchFileInformation); 703 | } 704 | 705 | public function shouldStoreFiles(): bool 706 | { 707 | return $this->evaluate($this->shouldStoreFiles); 708 | } 709 | 710 | public function getFileNamesStatePath(): ?string 711 | { 712 | if (! $this->fileNamesStatePath) { 713 | return null; 714 | } 715 | 716 | return $this->generateRelativeStatePath($this->fileNamesStatePath); 717 | } 718 | 719 | /** 720 | * @return array 721 | */ 722 | public function getValidationRules(): array 723 | { 724 | $rules = [ 725 | $this->getRequiredValidationRule(), 726 | 'array', 727 | ]; 728 | 729 | if (filled($count = $this->getMaxFiles())) { 730 | $rules[] = "max:{$count}"; 731 | } 732 | 733 | if (filled($count = $this->getMinFiles())) { 734 | $rules[] = "min:{$count}"; 735 | } 736 | 737 | $rules[] = function (string $attribute, array $value, Closure $fail): void { 738 | $files = array_filter($value, fn (TemporaryUploadedFile | string $file): bool => $file instanceof TemporaryUploadedFile); 739 | 740 | $name = $this->getName(); 741 | 742 | $validator = Validator::make( 743 | [$name => $files], 744 | ["{$name}.*" => ['file', ...parent::getValidationRules()]], 745 | [], 746 | ["{$name}.*" => $this->getValidationAttribute()], 747 | ); 748 | 749 | if (! $validator->fails()) { 750 | return; 751 | } 752 | 753 | $fail($validator->errors()->first()); 754 | }; 755 | 756 | return $rules; 757 | } 758 | 759 | public function deleteUploadedFile(string $fileKey): static 760 | { 761 | $file = $this->removeUploadedFile($fileKey); 762 | 763 | if (blank($file)) { 764 | return $this; 765 | } 766 | 767 | $callback = $this->deleteUploadedFileUsing; 768 | 769 | if (! $callback) { 770 | return $this; 771 | } 772 | 773 | $this->evaluate($callback, [ 774 | 'file' => $file, 775 | ]); 776 | 777 | return $this; 778 | } 779 | 780 | public function removeUploadedFile(string $fileKey): string | TemporaryUploadedFile | null 781 | { 782 | $files = $this->getState(); 783 | $file = $files[$fileKey] ?? null; 784 | 785 | if (! $file) { 786 | return null; 787 | } 788 | 789 | if (is_string($file)) { 790 | $this->removeStoredFileName($file); 791 | } elseif ($file instanceof TemporaryUploadedFile) { 792 | $file->delete(); 793 | } 794 | 795 | unset($files[$fileKey]); 796 | 797 | $this->state($files); 798 | 799 | return $file; 800 | } 801 | 802 | public function removeStoredFileName(string $file): void 803 | { 804 | $statePath = $this->fileNamesStatePath; 805 | 806 | if (blank($statePath)) { 807 | return; 808 | } 809 | 810 | $this->evaluate(function (BaseFileUpload $component, Get $get, Set $set) use ($file, $statePath) { 811 | if (! $component->isMultiple()) { 812 | $set($statePath, null); 813 | 814 | return; 815 | } 816 | 817 | $fileNames = $get($statePath) ?? []; 818 | 819 | if (array_key_exists($file, $fileNames)) { 820 | unset($fileNames[$file]); 821 | } 822 | 823 | $set($statePath, $fileNames); 824 | }); 825 | } 826 | 827 | /** 828 | * @param array $fileKeys 829 | */ 830 | public function reorderUploadedFiles(array $fileKeys): void 831 | { 832 | if (! $this->isReorderable) { 833 | return; 834 | } 835 | 836 | $fileKeys = array_flip($fileKeys); 837 | 838 | $state = collect($this->getState()) 839 | ->sortBy(static fn ($file, $fileKey) => $fileKeys[$fileKey] ?? null) // $fileKey may not be present in $fileKeys if it was added to the state during the reorder call 840 | ->all(); 841 | 842 | $this->state($state); 843 | } 844 | 845 | /** 846 | * @return array | null 847 | */ 848 | public function getUploadedFiles(): ?array 849 | { 850 | $urls = []; 851 | 852 | foreach ($this->getState() ?? [] as $fileKey => $file) { 853 | if ($file instanceof TemporaryUploadedFile) { 854 | $urls[$fileKey] = null; 855 | 856 | continue; 857 | } 858 | 859 | $callback = $this->getUploadedFileUsing; 860 | 861 | if (! $callback) { 862 | return [$fileKey => null]; 863 | } 864 | 865 | $urls[$fileKey] = $this->evaluate($callback, [ 866 | 'file' => $file, 867 | 'storedFileNames' => $this->getStoredFileNames(), 868 | ]) ?: null; 869 | } 870 | 871 | return $urls; 872 | } 873 | 874 | public function saveUploadedFiles(): void 875 | { 876 | if (blank($this->getState())) { 877 | $this->state([]); 878 | 879 | return; 880 | } 881 | 882 | if (! $this->shouldStoreFiles()) { 883 | return; 884 | } 885 | 886 | $state = array_filter(array_map(function (TemporaryUploadedFile | string $file) { 887 | if (! $file instanceof TemporaryUploadedFile) { 888 | return $file; 889 | } 890 | 891 | $callback = $this->saveUploadedFileUsing; 892 | 893 | if (! $callback) { 894 | $file->delete(); 895 | 896 | return $file; 897 | } 898 | 899 | $storedFile = $this->evaluate($callback, [ 900 | 'file' => $file, 901 | ]); 902 | 903 | if ($storedFile === null) { 904 | return null; 905 | } 906 | 907 | $this->storeFileName($storedFile, $file->getClientOriginalName()); 908 | 909 | $file->delete(); 910 | 911 | return $storedFile; 912 | }, Arr::wrap($this->getState()))); 913 | 914 | if ($this->isReorderable && ($callback = $this->reorderUploadedFilesUsing)) { 915 | $state = $this->evaluate($callback, [ 916 | 'state' => $state, 917 | ]); 918 | } 919 | 920 | $this->state($state); 921 | } 922 | 923 | public function storeFileName(string $file, string $fileName): void 924 | { 925 | $statePath = $this->fileNamesStatePath; 926 | 927 | if (blank($statePath)) { 928 | return; 929 | } 930 | 931 | $this->evaluate(function (BaseFileUpload $component, Get $get, Set $set) use ($file, $fileName, $statePath) { 932 | if (! $component->isMultiple()) { 933 | $set($statePath, $fileName); 934 | 935 | return; 936 | } 937 | 938 | $fileNames = $get($statePath) ?? []; 939 | $fileNames[$file] = $fileName; 940 | 941 | $set($statePath, $fileNames); 942 | }); 943 | } 944 | 945 | /** 946 | * @return string | array | null 947 | */ 948 | public function getStoredFileNames(): string | array | null 949 | { 950 | $state = null; 951 | $statePath = $this->fileNamesStatePath; 952 | 953 | if (filled($statePath)) { 954 | $state = $this->evaluate(fn (Get $get) => $get($statePath)); 955 | } 956 | 957 | if (blank($state) && $this->isMultiple()) { 958 | return []; 959 | } 960 | 961 | return $state; 962 | } 963 | 964 | public function isMultiple(): bool 965 | { 966 | return (bool) $this->evaluate($this->isMultiple); 967 | } 968 | 969 | public function getUploadedFileNameForStorageUsing(Closure $callback): static 970 | { 971 | $this->getUploadedFileNameForStorageUsing = $callback; 972 | 973 | return $this; 974 | } 975 | 976 | public function getUploadedFileNameForStorage(TemporaryUploadedFile $file): string 977 | { 978 | return $this->evaluate($this->getUploadedFileNameForStorageUsing, [ 979 | 'file' => $file, 980 | ]); 981 | } 982 | 983 | /** 984 | * @return array 985 | */ 986 | public function getStateToDehydrate(): array 987 | { 988 | $state = parent::getStateToDehydrate(); 989 | 990 | if ($fileNamesStatePath = $this->getFileNamesStatePath()) { 991 | $state = [ 992 | ...$state, 993 | $fileNamesStatePath => $this->getStoredFileNames(), 994 | ]; 995 | } 996 | 997 | return $state; 998 | } 999 | 1000 | /** 1001 | * @param array> $rules 1002 | */ 1003 | public function dehydrateValidationRules(array &$rules): void 1004 | { 1005 | parent::dehydrateValidationRules($rules); 1006 | 1007 | if ($fileNamesStatePath = $this->getFileNamesStatePath()) { 1008 | $rules[$fileNamesStatePath] = ['nullable']; 1009 | } 1010 | } 1011 | 1012 | public static function formatFilename(string $filename, ?string $format): string 1013 | { 1014 | if (! $format) { 1015 | return $filename; 1016 | } 1017 | 1018 | $extension = strrpos($filename, '.'); 1019 | 1020 | if ($extension !== false) { 1021 | return substr($filename, 0, $extension + 1) . $format; 1022 | } 1023 | 1024 | return $filename; 1025 | } 1026 | 1027 | public function pasteable(bool | Closure $condition = true): static 1028 | { 1029 | $this->isPasteable = $condition; 1030 | 1031 | return $this; 1032 | } 1033 | 1034 | public function isPasteable(): bool 1035 | { 1036 | return (bool) $this->evaluate($this->isPasteable); 1037 | } 1038 | } 1039 | --------------------------------------------------------------------------------