├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── workflows
│ └── unit-tests.yml
├── .gitignore
├── LICENSE
├── build
└── .gitkeep
├── composer.json
├── config
└── config.php
├── database
└── migrations
│ ├── 2016_01_01_000000_create_guided_images_table.php
│ └── 2016_01_01_000001_create_guided_imageables_table.php
├── docs
├── examples
│ ├── Image.php
│ └── ImageController.php
└── images
│ └── logo.png
├── grumphp.yml
├── phpunit.xml
├── pint.json
├── readme.md
├── routes
└── web.php
├── src
├── Concern
│ ├── Guide.php
│ ├── Guided.php
│ └── GuidedRepository.php
├── Console
│ └── Command
│ │ └── ClearSkimDirectories.php
├── Contract
│ ├── ConfigProvider.php
│ ├── FileHelper.php
│ ├── GuidedImage.php
│ ├── ImageDemand.php
│ ├── ImageDispenser.php
│ ├── ImageGuide.php
│ ├── ImageManager.php
│ └── ImageUploader.php
├── Demand
│ ├── Dummy.php
│ ├── ExistingImage.php
│ ├── Image.php
│ ├── Resize.php
│ └── Thumbnail.php
├── Exception
│ ├── BadImplementation.php
│ └── UrlUploadFailed.php
├── Helper
│ └── FileHelper.php
├── Model
│ └── UploadedImage.php
├── Result.php
├── Service
│ ├── ConfigProvider.php
│ ├── ImageDispenser.php
│ ├── ImageManager.php
│ └── ImageUploader.php
└── ServiceProvider.php
└── tests
├── Feature
└── .gitkeep
├── Fixtures
└── Model
│ └── GuidedImage.php
├── TestCase.php
├── Unit
├── Demand
│ ├── DummyTest.php
│ ├── ExistingImageTest.php
│ ├── ImageTest.php
│ ├── ResizeTest.php
│ ├── TestCase.php
│ └── ThumbnailTest.php
├── Service
│ ├── ImageDispenserTest.php
│ └── ImageUploaderTest.php
└── TestCase.php
└── bootstrap.php
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: reliq
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: enhancement, request
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/workflows/unit-tests.yml:
--------------------------------------------------------------------------------
1 | name: Unit Tests
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | run:
7 | runs-on: ubuntu-latest
8 | strategy:
9 | max-parallel: 10
10 | matrix:
11 | laravel-version: ['^11.1', '^12.1']
12 | preference: ['stable']
13 | php-version: ['8.2', '8.3', '8.4']
14 | exclude:
15 | - laravel-version: ^12.1
16 | php-version: 8.2
17 | name: Laravel ${{ matrix.laravel-version }} (${{ matrix.preference }}) on PHP ${{ matrix.php-version }}
18 | steps:
19 | - name: Checkout
20 | uses: actions/checkout@v2
21 | - name: Setup PHP
22 | uses: shivammathur/setup-php@v2
23 | with:
24 | php-version: ${{ matrix.php-version }}
25 | extensions: mbstring, xdebug
26 | coverage: xdebug
27 | env:
28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
29 | - name: Install dependencies
30 | run: |
31 | composer require --no-update --no-interaction "illuminate/support:${{ matrix.laravel-version }}"
32 | composer update --prefer-${{ matrix.preference }} --no-interaction --prefer-dist --no-suggest --no-scripts --optimize-autoloader
33 | - name: Lint composer.json
34 | run: composer validate
35 | - name: Run Tests
36 | run: composer test:ci
37 | - name: Upload Coverage
38 | uses: codecov/codecov-action@v1
39 | with:
40 | token: ${{ secrets.CODECOV_TOKEN }} #required
41 | file: ./build/coverage.xml #optional
42 | flags: unittests #optional
43 | name: codecov-umbrella #optional
44 | fail_ci_if_error: true #optional (default = false)
45 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 |
4 | # User-specific files
5 | *.suo
6 | *.user
7 | *.userosscache
8 | *.sln.docstates
9 |
10 | # User-specific files (MonoDevelop/Xamarin Studio)
11 | *.userprefs
12 |
13 | # Build results
14 | [Dd]ebug/
15 | [Dd]ebugPublic/
16 | [Rr]elease/
17 | [Rr]eleases/
18 | x64/
19 | x86/
20 | bld/
21 | [Bb]in/
22 | [Oo]bj/
23 | [Ll]og/
24 |
25 | # Visual Studio 2015 cache/options directory
26 | .vs/
27 | # Uncomment if you have tasks that create the project's static files in wwwroot
28 | #wwwroot/
29 |
30 | # MSTest test Results
31 | [Tt]est[Rr]esult*/
32 | [Bb]uild[Ll]og.*
33 |
34 | # NUNIT
35 | *.VisualState.xml
36 | TestResult.xml
37 |
38 | # Build Results of an ATL Project
39 | [Dd]ebugPS/
40 | [Rr]eleasePS/
41 | dlldata.c
42 |
43 | # DNX
44 | project.lock.json
45 | artifacts/
46 |
47 | *_i.c
48 | *_p.c
49 | *_i.h
50 | *.ilk
51 | *.meta
52 | *.obj
53 | *.pch
54 | *.pdb
55 | *.pgc
56 | *.pgd
57 | *.rsp
58 | *.sbr
59 | *.tlb
60 | *.tli
61 | *.tlh
62 | *.tmp
63 | *.tmp_proj
64 | *.log
65 | *.vspscc
66 | *.vssscc
67 | .builds
68 | *.pidb
69 | *.svclog
70 | *.scc
71 |
72 | # Chutzpah Test files
73 | _Chutzpah*
74 |
75 | # Visual C++ cache files
76 | ipch/
77 | *.aps
78 | *.ncb
79 | *.opendb
80 | *.opensdf
81 | *.sdf
82 | *.cachefile
83 | *.VC.db
84 | *.VC.VC.opendb
85 |
86 | # Visual Studio profiler
87 | *.psess
88 | *.vsp
89 | *.vspx
90 | *.sap
91 |
92 | # TFS 2012 Local Workspace
93 | $tf/
94 |
95 | # Guidance Automation Toolkit
96 | *.gpState
97 |
98 | # ReSharper is a .NET coding add-in
99 | _ReSharper*/
100 | *.[Rr]e[Ss]harper
101 | *.DotSettings.user
102 |
103 | # JustCode is a .NET coding add-in
104 | .JustCode
105 |
106 | # TeamCity is a build add-in
107 | _TeamCity*
108 |
109 | # DotCover is a Code Coverage Tool
110 | *.dotCover
111 |
112 | # NCrunch
113 | _NCrunch_*
114 | .*crunch*.local.xml
115 | nCrunchTemp_*
116 |
117 | # MightyMoose
118 | *.mm.*
119 | AutoTest.Net/
120 |
121 | # Web workbench (sass)
122 | .sass-cache/
123 |
124 | # Installshield output folder
125 | [Ee]xpress/
126 |
127 | # DocProject is a documentation generator add-in
128 | DocProject/buildhelp/
129 | DocProject/Help/*.HxT
130 | DocProject/Help/*.HxC
131 | DocProject/Help/*.hhc
132 | DocProject/Help/*.hhk
133 | DocProject/Help/*.hhp
134 | DocProject/Help/Html2
135 | DocProject/Help/html
136 |
137 | # Click-Once directory
138 | publish/
139 |
140 | # Publish Web Output
141 | *.[Pp]ublish.xml
142 | *.azurePubxml
143 | # TODO: Comment the next line if you want to checkin your web deploy settings
144 | # but database connection strings (with potential passwords) will be unencrypted
145 | *.pubxml
146 | *.publishproj
147 |
148 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
149 | # checkin your Azure Web App publish settings, but sensitive information contained
150 | # in these scripts will be unencrypted
151 | PublishScripts/
152 |
153 | # NuGet Packages
154 | *.nupkg
155 | # The packages folder can be ignored because of Package Restore
156 | **/packages/*
157 | # except build/, which is used as an MSBuild target.
158 | !**/packages/build/
159 | # Uncomment if necessary however generally it will be regenerated when needed
160 | #!**/packages/repositories.config
161 | # NuGet v3's project.json files produces more ignoreable files
162 | *.nuget.props
163 | *.nuget.targets
164 |
165 | # Microsoft Azure Build Output
166 | csx/
167 | *.build.csdef
168 |
169 | # Microsoft Azure Emulator
170 | ecf/
171 | rcf/
172 |
173 | # Windows Store app package directories and files
174 | AppPackages/
175 | BundleArtifacts/
176 | Package.StoreAssociation.xml
177 | _pkginfo.txt
178 |
179 | # Visual Studio cache files
180 | # files ending in .cache can be ignored
181 | *.[Cc]ache
182 | # but keep track of directories ending in .cache
183 | !*.[Cc]ache/
184 |
185 | # Others
186 | ClientBin/
187 | ~$*
188 | *~
189 | *.dbmdl
190 | *.dbproj.schemaview
191 | *.pfx
192 | *.publishsettings
193 | node_modules/
194 | orleans.codegen.cs
195 |
196 | # Since there are multiple workflows, uncomment next line to ignore bower_components
197 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
198 | #bower_components/
199 |
200 | # RIA/Silverlight projects
201 | Generated_Code/
202 |
203 | # Backup & report files from converting an old project file
204 | # to a newer Visual Studio version. Backup files are not needed,
205 | # because we have git ;-)
206 | _UpgradeReport_Files/
207 | Backup*/
208 | UpgradeLog*.XML
209 | UpgradeLog*.htm
210 |
211 | # SQL Server files
212 | *.mdf
213 | *.ldf
214 |
215 | # Business Intelligence projects
216 | *.rdl.data
217 | *.bim.layout
218 | *.bim_*.settings
219 |
220 | # Microsoft Fakes
221 | FakesAssemblies/
222 |
223 | # GhostDoc plugin setting file
224 | *.GhostDoc.xml
225 |
226 | # Node.js Tools for Visual Studio
227 | .ntvs_analysis.dat
228 |
229 | # Visual Studio 6 build log
230 | *.plg
231 |
232 | # Visual Studio 6 workspace options file
233 | *.opt
234 |
235 | # Visual Studio LightSwitch build output
236 | **/*.HTMLClient/GeneratedArtifacts
237 | **/*.DesktopClient/GeneratedArtifacts
238 | **/*.DesktopClient/ModelManifest.xml
239 | **/*.Server/GeneratedArtifacts
240 | **/*.Server/ModelManifest.xml
241 | _Pvt_Extensions
242 |
243 | # Paket dependency manager
244 | .paket/paket.exe
245 | paket-files/
246 |
247 | # FAKE - F# Make
248 | .fake/
249 |
250 | # JetBrains Rider
251 | .idea/
252 | *.sln.iml
253 |
254 | /build/*
255 | vendor
256 | composer.lock
257 | !.gitkeep
258 | .vscode
259 | .DS_Store
260 | *cache
261 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Reliq Arts
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/build/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reliqarts/laravel-guided-image/3f3d19dd06cd1d9e4c5f800fcf62d5a7380e1071/build/.gitkeep
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reliqarts/laravel-guided-image",
3 | "description": "Simplified and ready image manipulation for Laravel via intervention image.",
4 | "keywords": [
5 | "image",
6 | "route",
7 | "generation",
8 | "laravel",
9 | "photo",
10 | "laravel5",
11 | "resize",
12 | "thumb",
13 | "dummy",
14 | "crop"
15 | ],
16 | "type": "library",
17 | "license": "MIT",
18 | "authors": [
19 | {
20 | "name": "reliq",
21 | "email": "reliq@reliqarts.com"
22 | }
23 | ],
24 | "require": {
25 | "php": "^8.2",
26 | "illuminate/support": "^11.1 || ^12.1",
27 | "intervention/image": "^3.7",
28 | "reliqarts/laravel-common": "^8.0",
29 | "ext-json": "*",
30 | "ext-fileinfo": "*",
31 | "anhskohbo/no-captcha": "@dev"
32 | },
33 | "require-dev": {
34 | "laravel/pint": "^1.15",
35 | "orchestra/testbench": "^9.0 || ^10.0",
36 | "phpro/grumphp": "^2.5",
37 | "phpspec/prophecy-phpunit": "^2.0",
38 | "phpunit/phpunit": "^11.0",
39 | "yieldstudio/grumphp-laravel-pint": "^1.0"
40 | },
41 | "autoload": {
42 | "psr-4": {
43 | "ReliqArts\\GuidedImage\\": "src/",
44 | "ReliqArts\\GuidedImage\\Tests\\": "tests/"
45 | }
46 | },
47 | "scripts": {
48 | "test": "phpunit",
49 | "test:ci": "phpunit --colors=auto --coverage-clover=build/coverage.xml",
50 | "test:unit": "phpunit --testsuite=Unit --verbose --coverage-clover=build/coverage.xml"
51 | },
52 | "config": {
53 | "sort-packages": true,
54 | "allow-plugins": {
55 | "phpro/grumphp": true
56 | }
57 | },
58 | "extra": {
59 | "laravel": {
60 | "providers": [
61 | "ReliqArts\\GuidedImage\\ServiceProvider"
62 | ]
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/config/config.php:
--------------------------------------------------------------------------------
1 | false,
6 |
7 | // Set the model to be guided.
8 | 'model' => env('GUIDED_IMAGE_MODEL', 'Image'),
9 |
10 | // Set the guided model namespace.
11 | 'model_namespace' => env('GUIDED_IMAGE_MODEL_NAMESPACE', 'App\\Models\\'),
12 |
13 | // Set the model to be guided.
14 | 'database' => [
15 | // Guided image table.
16 | 'image_table' => env('GUIDED_IMAGE_TABLE', 'images'),
17 |
18 | // Guided imageables table.
19 | 'imageables_table' => env('GUIDED_IMAGEABLES_TABLE', 'imageables'),
20 | ],
21 |
22 | // image encoding @see: http://image.intervention.io/api/encode
23 | 'encoding' => [
24 | 'mime_type' => env('GUIDED_IMAGE_ENCODING_MIME_TYPE', 'image/png'),
25 | 'quality' => env('GUIDED_IMAGE_ENCODING_QUALITY', 90),
26 | ],
27 |
28 | // Route related options.
29 | 'routes' => [
30 | // Define controllers here which guided routes should be added onto:
31 | 'controllers' => [
32 | env('GUIDED_IMAGE_CONTROLLER', 'ImageController'),
33 | ],
34 |
35 | // Set the prefix that should be used for routes
36 | 'prefix' => env('GUIDED_IMAGE_ROUTE_PREFIX', 'image'),
37 |
38 | // Set the bindings for guided routes.
39 | 'bindings' => [
40 | // public
41 | 'public' => [
42 | 'middleware' => 'web',
43 | ],
44 |
45 | // admin
46 | 'admin' => [
47 | // 'middleware' => 'admin',
48 | ],
49 | ],
50 | ],
51 |
52 | // allowed extensions
53 | 'allowed_extensions' => ['gif', 'jpg', 'jpeg', 'png'],
54 |
55 | // image rules for validation
56 | 'rules' => 'required|mimes:png,gif,jpeg|max:2048',
57 |
58 | // storage
59 | 'storage' => [
60 | // disk for in-built caching mechanism; MUST BE A LOCAL DISK, cloud disks such as s3 are not supported here.
61 | 'cache_disk' => env('GUIDED_IMAGE_CACHE_DISK', 'local'),
62 |
63 | // upload disk
64 | 'upload_disk' => env('GUIDED_IMAGE_UPLOAD_DISK', 'public'),
65 |
66 | // Where uploaded images should be stored. This is relative to the application's public directory.
67 | 'upload_dir' => env('GUIDED_IMAGE_UPLOAD_DIR', 'uploads/images'),
68 |
69 | // generate upload sub directories (e.g. 2019/05)
70 | 'generate_upload_date_sub_directories' => env('GUIDED_IMAGE_GENERATE_UPLOAD_DATE_SUB_DIRECTORIES', false),
71 |
72 | // Temporary storage directory for images already generated.
73 | // This directory will live inside your application's storage directory.
74 | 'cache_dir' => env('GUIDED_IMAGE_CACHE_DIR', 'images'),
75 |
76 | // Generated thumbnails will be temporarily kept inside this directory.
77 | 'cache_sub_dir_thumbs' => env('GUIDED_IMAGE_CACHE_SUB_DIR_THUMBS', '.thumb'),
78 |
79 | // Generated resized images will be temporarily kept inside this directory.
80 | 'cache_sub_dir_resized' => env('GUIDED_IMAGE_CACHE_SUB_DIR_RESIZED', '.resized'),
81 | ],
82 |
83 | // headers
84 | 'headers' => [
85 | // cache days
86 | 'cache_days' => env('GUIDED_IMAGE_CACHE_DAYS', 366),
87 |
88 | // any additional headers for guided images
89 | 'additional' => [],
90 | ],
91 |
92 | 'dispenser' => [
93 | // whether raw image should be served as fallback if NotReadableException occurs
94 | 'raw_image_fallback_enabled' => env('GUIDED_IMAGE_DISPENSER_RAW_IMAGE_FALLBACK_ENABLED', false),
95 | ],
96 | ];
97 |
--------------------------------------------------------------------------------
/database/migrations/2016_01_01_000000_create_guided_images_table.php:
--------------------------------------------------------------------------------
1 | configProvider = resolve(ConfigProvider::class);
21 | }
22 |
23 | /**
24 | * Run the migrations.
25 | */
26 | public function up()
27 | {
28 | $tableName = $this->configProvider->getImagesTableName();
29 |
30 | if (!Schema::hasTable($tableName)) {
31 | Schema::create($tableName, function (Blueprint $table) {
32 | $table->increments('id');
33 | $table->string('name', 191);
34 | $table->string('mime_type', 20);
35 | $table->string('extension', 10);
36 | $table->integer('size');
37 | $table->integer('height');
38 | $table->integer('width');
39 | $table->string('location');
40 | $table->string('full_path');
41 | $table->timestamps();
42 | });
43 | }
44 | }
45 |
46 | /**
47 | * Reverse the migrations.
48 | */
49 | public function down()
50 | {
51 | $tableName = $this->configProvider->getImagesTableName();
52 |
53 | Schema::dropIfExists($tableName);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/database/migrations/2016_01_01_000001_create_guided_imageables_table.php:
--------------------------------------------------------------------------------
1 | configProvider = resolve(ConfigProvider::class);
21 | }
22 |
23 | /**
24 | * Run the migrations.
25 | */
26 | public function up()
27 | {
28 | $tableName = $this->configProvider->getImageablesTableName();
29 | $imagesTableName = $this->configProvider->getImagesTableName();
30 |
31 | if (Schema::hasTable($tableName)) {
32 | return;
33 | }
34 |
35 | Schema::create($tableName, function (Blueprint $table) use ($imagesTableName) {
36 | $table->increments('id');
37 | $table->integer('image_id')->unsigned();
38 | $table->foreign('image_id')
39 | ->references('id')
40 | ->on($imagesTableName)
41 | ->onDelete('CASCADE');
42 | $table->integer('imageable_id');
43 | $table->string('imageable_type');
44 | });
45 | }
46 |
47 | /**
48 | * Reverse the migrations.
49 | */
50 | public function down()
51 | {
52 | $tableName = $this->configProvider->getImageablesTableName();
53 |
54 | Schema::dropIfExists($tableName);
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/docs/examples/Image.php:
--------------------------------------------------------------------------------
1 | morphedByMany('App\Post', 'imageable');
37 | }
38 |
39 | /**
40 | * {@inheritdoc}
41 | */
42 | public function isSafeForDelete(int $safeAmount = 1): bool
43 | {
44 | /** @noinspection PhpUndefinedClassInspection */
45 | $posts = Post::withTrashed()->where('image_id', $this->id)->get();
46 | $posts = $this->posts->merge($posts);
47 | $usage = $posts->count();
48 |
49 | return $usage <= $safeAmount;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/docs/examples/ImageController.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | tests/Feature
12 |
13 |
14 | tests/Unit
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | src
30 |
31 |
32 | src/Concerns
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/pint.json:
--------------------------------------------------------------------------------
1 | {
2 | "preset": "psr12"
3 | }
4 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Laravel Guided Image
2 |
3 | Guided Image is an image utility package for Laravel based on Intervention Image.
4 |
5 | [](http://laravel.com)
6 | [](https://travis-ci.com/reliqarts/laravel-guided-image)
7 | [](https://scrutinizer-ci.com/g/reliqarts/laravel-guided-image/)
8 | [](https://codecov.io/gh/reliqarts/laravel-guided-image)
9 | [](https://codeclimate.com/github/reliqarts/laravel-guided-image/maintainability)
10 | [](https://packagist.org/packages/reliqarts/laravel-guided-image)
11 | [](https://packagist.org/packages/reliqarts/laravel-guided-image)
12 | [](//packagist.org/packages/reliqarts/laravel-guided-image)
13 |
14 |
15 |
16 | [](#)
17 |
18 | ## Key Features
19 |
20 | - On-the-fly image resizing
21 | - On-the-fly thumbnail generation
22 | - Image uploading
23 | - Smart image reuse; mitigating against double uploads and space resource waste.
24 |
25 | Guided Image can be integrated seamlessly with your existing image model.
26 |
27 | ### Guided Routes
28 |
29 | The package provides routes for generating resized/cropped/dummy images.
30 | - Routes are configurable you you may set any middleware and prefix you want.
31 | - Generated images are *cached to disk* to avoid regenerating frequently accessed images and reduce overhead.
32 |
33 | ### Image file reuse
34 |
35 | For situations where different instances of models use the same image.
36 | - The package provides a safe removal feature which allows images to be detached and only deleted from disk if not being used elsewhere.
37 | - An overridable method is used to determine when an image should be considered *safe* to delete.
38 |
39 | ## Installation & Usage
40 |
41 | ### Installation
42 |
43 | Install via composer; in console:
44 | ```
45 | composer require reliqarts/laravel-guided-image
46 | ```
47 | or require in *composer.json*:
48 | ```json
49 | {
50 | "require": {
51 | "reliqarts/laravel-guided-image": "^5.0"
52 | }
53 | }
54 | ```
55 | then run `composer update` in your terminal to pull it in.
56 |
57 | Finally, publish package resources and configuration:
58 |
59 | ```
60 | php artisan vendor:publish --provider="ReliqArts\GuidedImage\ServiceProvider"
61 | ```
62 |
63 | You may opt to publish only configuration by using the `guidedimage-config` tag:
64 |
65 | ```
66 | php artisan vendor:publish --provider="ReliqArts\GuidedImage\ServiceProvider" --tag="guidedimage-config"
67 | ```
68 |
69 | ### Setup
70 |
71 | Set the desired environment variables so the package knows your image model, controller(s), etc.
72 |
73 | Example environment config:
74 | ```
75 | GUIDED_IMAGE_MODEL=Image
76 | GUIDED_IMAGE_CONTROLLER=ImageController
77 | GUIDED_IMAGE_ROUTE_PREFIX=image
78 | GUIDED_IMAGE_SKIM_DIR=images
79 | ```
80 |
81 | These variables, and more are explained within the [config](https://github.com/ReliqArts/laravel-guided-image/blob/master/config/config.php) file.
82 |
83 | And... it's ready! :ok_hand:
84 |
85 | ### Usage
86 |
87 | To *use* Guided Image you must do just that from your *Image* model. :smirk:
88 |
89 | Implement the `ReliqArts\GuidedImage\Contract\GuidedImage` contract and use the `ReliqArts\GuidedImage\Concern\Guided` trait, e.g:
90 |
91 | ```php
92 | use Illuminate\Database\Eloquent\Model;
93 | use ReliqArts\GuidedImage\Concern\Guided;
94 | use ReliqArts\GuidedImage\Contract\GuidedImage;
95 |
96 | class Image extends Model implements GuidedImage
97 | {
98 | use Guided;
99 |
100 | // ... properties and methods
101 | }
102 | ```
103 | See example [here](https://github.com/ReliQArts/laravel-guided-image/blob/master/docs/examples/Image.php).
104 |
105 | Implement the `ReliqArts\GuidedImage\Contract\ImageGuide` contract and use the `ReliqArts\GuidedImage\Concern\Guide` trait from your *ImageController*, e.g:
106 |
107 | ```php
108 | use ReliqArts\GuidedImage\Contract\ImageGuide;
109 | use ReliqArts\GuidedImage\Concern\Guide;
110 |
111 | class ImageController extends Controller implements ImageGuide
112 | {
113 | use Guide;
114 | }
115 | ```
116 | See example [here](https://github.com/ReliQArts/laravel-guided-image/blob/master/docs/examples/ImageController.php).
117 |
118 | #### Features
119 |
120 | ##### Safely Remove Image (dissociate & conditionally delete the image)
121 |
122 | An guided image instance is removed by calling the *remove* method. e.g:
123 |
124 | ```php
125 | $oldImage->remove($force);
126 | ```
127 | `$force` is optional and is `false` by default.
128 |
129 | ##### Link Generation
130 |
131 | You may retrieve guided links to resized or cropped images like so:
132 |
133 | ```php
134 | // resized image:
135 | $linkToImage = $image->routeResized([
136 | '550', // width
137 | '_', // height, 'null' is OK
138 | '_', // keep aspect ratio? true by default, 'null' is OK
139 | '_', // allow upsize? false by default, 'null' is OK
140 | ]);
141 |
142 | // thumbnail:
143 | $linkToImage = $image->routeThumbnail([
144 | 'crop', // method: crop|fit
145 | '550', // width
146 | '_', // height
147 | ]);
148 | ```
149 | **NB:** In the above example `_` resolves to `null`.
150 |
151 | Have a look at the [GuidedImage contract](https://github.com/ReliQArts/laravel-guided-image/blob/master/src/Contract/GuidedImage.php) for more info on model functions.
152 |
153 | For more info on controller functions see the [ImageGuide contract](https://github.com/reliqarts/laravel-guided-image/blob/master/src/Contract/ImageGuide.php).
154 |
155 | ##### Routes
156 |
157 | Your actually routes will depend heavily on your custom configuration. Here is an example of what the routes may look like:
158 |
159 | ```
160 | || GET|HEAD | image/.dum//{width}-{height}/{color?}/{fill?} | image.dummy | App\Http\Controllers\ImageController@dummy | web |
161 | || GET|HEAD | image/.res/{image}//{width}-{height}/{aspect?}/{upSize?}| image.resize | App\Http\Controllers\ImageController@resized | web |
162 | || GET|HEAD | image/.tmb/{image}//m.{method}/{width}-{height} | image.thumb | App\Http\Controllers\ImageController@thumb | web |
163 | || GET|HEAD | image/empty-cache | image.empty-cache | App\Http\Controllers\ImageController@emptyCache | web |
164 |
165 | ```
166 |
--------------------------------------------------------------------------------
/routes/web.php:
--------------------------------------------------------------------------------
1 | getGuidedModelName(true);
19 |
20 | // get controllers for routes and create the routes for each
21 | foreach ($configProvider->getControllersForRoutes() as $controllerName) {
22 | // if controller name's empty skip
23 | if (! $controllerName) {
24 | continue;
25 | }
26 |
27 | // if controller name doesn't contain namespace, add it
28 | if (! str_contains($controllerName, '\\')) {
29 | $controllerName = sprintf('App\\Http\\Controllers\\%s', $controllerName);
30 | }
31 |
32 | // the public route group
33 | Route::group(
34 | $configProvider->getRouteGroupBindings(),
35 | static function () use ($configProvider, $controllerName, $modelName) {
36 | // $guidedModel thumbnail
37 | Route::get(
38 | sprintf('.tmb/{%s}//m.{method}/{width}-{height}', $modelName),
39 | sprintf('%s@thumb', $controllerName)
40 | )->name(sprintf('%s.thumb', $modelName));
41 |
42 | // Resized $guidedModel
43 | Route::get(
44 | sprintf('.res/{%s}//{width}-{height}/{aspect?}/{upSize?}', $modelName),
45 | sprintf('%s@resized', $controllerName)
46 | )->name(sprintf('%s.resize', $modelName));
47 |
48 | // admin route group
49 | Route::group(
50 | $configProvider->getRouteGroupBindings([], 'admin'),
51 | static function () use ($controllerName, $modelName) {
52 | // Used to empty directory photo cache (skimDir)
53 | Route::get(
54 | 'empty-cache',
55 | sprintf('%s@emptyCache', $controllerName)
56 | )->name(sprintf('%s.empty-cache', $modelName));
57 | }
58 | );
59 | }
60 | );
61 | }
62 |
--------------------------------------------------------------------------------
/src/Concern/Guide.php:
--------------------------------------------------------------------------------
1 | emptyCache()) {
28 | return response()->json(
29 | new Result(true, '', ['Cache successfully cleared.'])
30 | );
31 | }
32 |
33 | return response()->json(
34 | new Result(false, $errorMessage, [$errorMessage])
35 | );
36 | }
37 |
38 | public function resized(
39 | ImageDispenser $imageDispenser,
40 | Request $request,
41 | GuidedImage $guidedImage,
42 | mixed $width,
43 | mixed $height,
44 | mixed $aspect = true,
45 | mixed $upSize = false
46 | ): Response {
47 | $demand = new Resize($request, $guidedImage, $width, $height, $aspect, $upSize);
48 |
49 | return $imageDispenser->getResizedImage($demand);
50 | }
51 |
52 | public function thumb(
53 | ImageDispenser $imageDispenser,
54 | Request $request,
55 | GuidedImage $guidedImage,
56 | mixed $method,
57 | mixed $width,
58 | mixed $height
59 | ): Response {
60 | $demand = new Thumbnail($request, $guidedImage, $method, $width, $height);
61 |
62 | return $imageDispenser->getImageThumbnail($demand);
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/Concern/Guided.php:
--------------------------------------------------------------------------------
1 | getUrl();
53 | }
54 |
55 | /** @var ConfigProvider $configProvider */
56 | $configProvider = resolve(ConfigProvider::class);
57 | $guidedModelName = $configProvider->getGuidedModelName(true);
58 |
59 | array_unshift($params, $this->id);
60 |
61 | return route(sprintf('%s.%s', $guidedModelName, $type), $params);
62 | }
63 |
64 | /**
65 | * Whether image is safe for deleting.
66 | * Since a single image may be re-used this method is used to determine
67 | * when an image can be safely deleted from disk.
68 | *
69 | * @param int $safeAmount a photo is safe to delete if it is used by $safe_num amount of records
70 | *
71 | * @return bool whether image is safe for delete
72 | */
73 | public function isSafeForDelete(int $safeAmount = 1): bool
74 | {
75 | return $safeAmount === 1;
76 | }
77 |
78 | /**
79 | * Get link to resized photo.
80 | *
81 | * @param array $params parameters to pass to route
82 | */
83 | public function routeResized(array $params = []): string
84 | {
85 | return $this->getRoutedUrl(Resize::ROUTE_TYPE_NAME, $params);
86 | }
87 |
88 | /**
89 | * Get link to photo thumbnail.
90 | *
91 | * @param array $params parameters to pass to route
92 | */
93 | public function routeThumbnail(array $params = []): string
94 | {
95 | return $this->getRoutedUrl(Thumbnail::ROUTE_TYPE_NAME, $params);
96 | }
97 |
98 | /**
99 | * Get class.
100 | */
101 | public function getClassName(): string
102 | {
103 | return get_class($this);
104 | }
105 |
106 | /**
107 | * Get URL/path to image.
108 | *
109 | * @param bool $diskRelative whether to return `full path` (relative to disk),
110 | * hence skipping call to Storage facade
111 | *
112 | * @uses \Illuminate\Support\Facades\Storage
113 | */
114 | public function getUrl(bool $diskRelative = false): string
115 | {
116 | $path = urldecode($this->getFullPath());
117 |
118 | if ($diskRelative) {
119 | return $path;
120 | }
121 |
122 | /** @var ConfigProvider $configProvider */
123 | $configProvider = resolve(ConfigProvider::class);
124 | $diskName = $configProvider->getUploadDiskName();
125 |
126 | return Storage::disk($diskName)->url($path);
127 | }
128 |
129 | /**
130 | * Get ready image title.
131 | */
132 | public function getTitle(): string
133 | {
134 | return Str::title(preg_replace('/[\\-_]/', ' ', $this->getName()));
135 | }
136 |
137 | /**
138 | * Get full path.
139 | */
140 | public function getFullPath(): string
141 | {
142 | return $this->full_path ?? '';
143 | }
144 |
145 | /**
146 | * Get name.
147 | */
148 | public function getName(): string
149 | {
150 | return $this->name ?? '';
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/src/Concern/GuidedRepository.php:
--------------------------------------------------------------------------------
1 | upload($file);
25 | }
26 |
27 | /**
28 | * Removes image from database, and disk, if not in use.
29 | *
30 | * @param bool $force override safety constraints
31 | *
32 | * @throws Exception
33 | */
34 | public function remove(bool $force = false): Result
35 | {
36 | /** @var ConfigProvider $configProvider */
37 | $configProvider = resolve(ConfigProvider::class);
38 | $diskName = $configProvider->getUploadDiskName();
39 | $path = urldecode($this->getFullPath());
40 |
41 | if (!($force || $this->isSafeForDelete())) {
42 | return new Result(
43 | false,
44 | 'Not safe to delete, hence file not removed.'
45 | );
46 | }
47 |
48 | if (Storage::disk($diskName)->delete($path)) {
49 | $this->delete();
50 | }
51 |
52 | return new Result(true);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/Console/Command/ClearSkimDirectories.php:
--------------------------------------------------------------------------------
1 | emptyCache()) {
32 | $this->line(PHP_EOL . '✔ Success! Guided Image cache cleared.');
33 |
34 | return true;
35 | }
36 |
37 | $this->line(PHP_EOL . '✘ Couldn\'t clear Guided Image cache.');
38 |
39 | return false;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Contract/ConfigProvider.php:
--------------------------------------------------------------------------------
1 | file('image');
86 | */
87 | public static function upload(UploadedFile $imageFile): Result;
88 | }
89 |
--------------------------------------------------------------------------------
/src/Contract/ImageDemand.php:
--------------------------------------------------------------------------------
1 | isValueConsideredNull($this->color) ? self::DEFAULT_COLOR : $this->color;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Demand/ExistingImage.php:
--------------------------------------------------------------------------------
1 | request;
24 | }
25 |
26 | final public function getGuidedImage(): GuidedImage
27 | {
28 | return $this->guidedImage;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Demand/Image.php:
--------------------------------------------------------------------------------
1 | isValueConsideredNull($this->width) ? null : (int) $this->width;
18 | }
19 |
20 | final public function getHeight(): ?int
21 | {
22 | return $this->isValueConsideredNull($this->height) ? null : (int) $this->height;
23 | }
24 |
25 | final public function isValueConsideredNull(mixed $value): bool
26 | {
27 | return in_array($value, static::NULLS, true);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Demand/Resize.php:
--------------------------------------------------------------------------------
1 | isValueConsideredNull($this->maintainAspectRatio);
29 | }
30 |
31 | public function allowUpSizing(): bool
32 | {
33 | return ! $this->isValueConsideredNull($this->allowUpSizing);
34 | }
35 |
36 | public function returnObject(): bool
37 | {
38 | return ! $this->isValueConsideredNull($this->returnObject);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Demand/Thumbnail.php:
--------------------------------------------------------------------------------
1 | method === self::METHOD_FIT) {
41 | return self::METHOD_COVER;
42 | }
43 |
44 | return $this->method;
45 | }
46 |
47 | public function isValid(): bool
48 | {
49 | return in_array($this->method, self::METHODS, true);
50 | }
51 |
52 | public function returnObject(): bool
53 | {
54 | return $this->returnObject;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Exception/BadImplementation.php:
--------------------------------------------------------------------------------
1 | getMessage()),
20 | $previousException->getCode(),
21 | $previousException
22 | );
23 | $instance->url = $url;
24 |
25 | return $instance;
26 | }
27 |
28 | public function getUrl(): string
29 | {
30 | return $this->url;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Helper/FileHelper.php:
--------------------------------------------------------------------------------
1 | '3g2',
72 | 'video/3gp' => '3gp',
73 | 'video/3gpp' => '3gp',
74 | 'application/x-compressed' => '7zip',
75 | 'audio/x-acc' => 'aac',
76 | 'audio/ac3' => 'ac3',
77 | 'application/postscript' => 'ai',
78 | 'audio/x-aiff' => 'aif',
79 | 'audio/aiff' => 'aif',
80 | 'audio/x-au' => 'au',
81 | 'video/x-msvideo' => 'avi',
82 | 'video/msvideo' => 'avi',
83 | 'video/avi' => 'avi',
84 | 'application/x-troff-msvideo' => 'avi',
85 | 'application/macbinary' => 'bin',
86 | 'application/mac-binary' => 'bin',
87 | 'application/x-binary' => 'bin',
88 | 'application/x-macbinary' => 'bin',
89 | 'image/bmp' => 'bmp',
90 | 'image/x-bmp' => 'bmp',
91 | 'image/x-bitmap' => 'bmp',
92 | 'image/x-xbitmap' => 'bmp',
93 | 'image/x-win-bitmap' => 'bmp',
94 | 'image/x-windows-bmp' => 'bmp',
95 | 'image/ms-bmp' => 'bmp',
96 | 'image/x-ms-bmp' => 'bmp',
97 | 'application/bmp' => 'bmp',
98 | 'application/x-bmp' => 'bmp',
99 | 'application/x-win-bitmap' => 'bmp',
100 | 'application/cdr' => 'cdr',
101 | 'application/coreldraw' => 'cdr',
102 | 'application/x-cdr' => 'cdr',
103 | 'application/x-coreldraw' => 'cdr',
104 | 'image/cdr' => 'cdr',
105 | 'image/x-cdr' => 'cdr',
106 | 'zz-application/zz-winassoc-cdr' => 'cdr',
107 | 'application/mac-compactpro' => 'cpt',
108 | 'application/pkix-crl' => 'crl',
109 | 'application/pkcs-crl' => 'crl',
110 | 'application/x-x509-ca-cert' => 'crt',
111 | 'application/pkix-cert' => 'crt',
112 | 'text/css' => 'css',
113 | 'text/x-comma-separated-values' => 'csv',
114 | 'text/comma-separated-values' => 'csv',
115 | 'application/vnd.msexcel' => 'csv',
116 | 'application/x-director' => 'dcr',
117 | 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx',
118 | 'application/x-dvi' => 'dvi',
119 | 'message/rfc822' => 'eml',
120 | 'application/x-msdownload' => 'exe',
121 | 'video/x-f4v' => 'f4v',
122 | 'audio/x-flac' => 'flac',
123 | 'video/x-flv' => 'flv',
124 | 'image/gif' => 'gif',
125 | 'application/gpg-keys' => 'gpg',
126 | 'application/x-gtar' => 'gtar',
127 | 'application/x-gzip' => 'gzip',
128 | 'application/mac-binhex40' => 'hqx',
129 | 'application/mac-binhex' => 'hqx',
130 | 'application/x-binhex40' => 'hqx',
131 | 'application/x-mac-binhex40' => 'hqx',
132 | 'text/html' => 'html',
133 | 'image/x-icon' => 'ico',
134 | 'image/x-ico' => 'ico',
135 | 'image/vnd.microsoft.icon' => 'ico',
136 | 'text/calendar' => 'ics',
137 | 'application/java-archive' => 'jar',
138 | 'application/x-java-application' => 'jar',
139 | 'application/x-jar' => 'jar',
140 | 'image/jp2' => 'jp2',
141 | 'video/mj2' => 'jp2',
142 | 'image/jpx' => 'jp2',
143 | 'image/jpm' => 'jp2',
144 | 'image/jpeg' => 'jpeg',
145 | 'image/pjpeg' => 'jpeg',
146 | 'application/x-javascript' => 'js',
147 | 'application/json' => 'json',
148 | 'text/json' => 'json',
149 | 'application/vnd.google-earth.kml+xml' => 'kml',
150 | 'application/vnd.google-earth.kmz' => 'kmz',
151 | 'text/x-log' => 'log',
152 | 'audio/x-m4a' => 'm4a',
153 | 'audio/mp4' => 'm4a',
154 | 'application/vnd.mpegurl' => 'm4u',
155 | 'audio/midi' => 'mid',
156 | 'application/vnd.mif' => 'mif',
157 | 'video/quicktime' => 'mov',
158 | 'video/x-sgi-movie' => 'movie',
159 | 'audio/mpeg' => 'mp3',
160 | 'audio/mpg' => 'mp3',
161 | 'audio/mpeg3' => 'mp3',
162 | 'audio/mp3' => 'mp3',
163 | 'video/mp4' => 'mp4',
164 | 'video/mpeg' => 'mpeg',
165 | 'application/oda' => 'oda',
166 | 'audio/ogg' => 'ogg',
167 | 'video/ogg' => 'ogg',
168 | 'application/ogg' => 'ogg',
169 | 'font/otf' => 'otf',
170 | 'application/x-pkcs10' => 'p10',
171 | 'application/pkcs10' => 'p10',
172 | 'application/x-pkcs12' => 'p12',
173 | 'application/x-pkcs7-signature' => 'p7a',
174 | 'application/pkcs7-mime' => 'p7c',
175 | 'application/x-pkcs7-mime' => 'p7c',
176 | 'application/x-pkcs7-certreqresp' => 'p7r',
177 | 'application/pkcs7-signature' => 'p7s',
178 | 'application/pdf' => 'pdf',
179 | 'application/octet-stream' => 'pdf',
180 | 'application/x-x509-user-cert' => 'pem',
181 | 'application/x-pem-file' => 'pem',
182 | 'application/pgp' => 'pgp',
183 | 'application/x-httpd-php' => 'php',
184 | 'application/php' => 'php',
185 | 'application/x-php' => 'php',
186 | 'text/php' => 'php',
187 | 'text/x-php' => 'php',
188 | 'application/x-httpd-php-source' => 'php',
189 | 'image/png' => 'png',
190 | 'image/x-png' => 'png',
191 | 'application/powerpoint' => 'ppt',
192 | 'application/vnd.ms-powerpoint' => 'ppt',
193 | 'application/vnd.ms-office' => 'ppt',
194 | 'application/msword' => 'doc',
195 | 'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx',
196 | 'application/x-photoshop' => 'psd',
197 | 'image/vnd.adobe.photoshop' => 'psd',
198 | 'audio/x-realaudio' => 'ra',
199 | 'audio/x-pn-realaudio' => 'ram',
200 | 'application/x-rar' => 'rar',
201 | 'application/rar' => 'rar',
202 | 'application/x-rar-compressed' => 'rar',
203 | 'audio/x-pn-realaudio-plugin' => 'rpm',
204 | 'application/x-pkcs7' => 'rsa',
205 | 'text/rtf' => 'rtf',
206 | 'text/richtext' => 'rtx',
207 | 'video/vnd.rn-realvideo' => 'rv',
208 | 'application/x-stuffit' => 'sit',
209 | 'application/smil' => 'smil',
210 | 'text/srt' => 'srt',
211 | 'image/svg+xml' => 'svg',
212 | 'application/x-shockwave-flash' => 'swf',
213 | 'application/x-tar' => 'tar',
214 | 'application/x-gzip-compressed' => 'tgz',
215 | 'image/tiff' => 'tiff',
216 | 'font/ttf' => 'ttf',
217 | 'text/plain' => 'txt',
218 | 'text/x-vcard' => 'vcf',
219 | 'application/videolan' => 'vlc',
220 | 'text/vtt' => 'vtt',
221 | 'audio/x-wav' => 'wav',
222 | 'audio/wave' => 'wav',
223 | 'audio/wav' => 'wav',
224 | 'application/wbxml' => 'wbxml',
225 | 'video/webm' => 'webm',
226 | 'image/webp' => 'webp',
227 | 'audio/x-ms-wma' => 'wma',
228 | 'application/wmlc' => 'wmlc',
229 | 'video/x-ms-wmv' => 'wmv',
230 | 'video/x-ms-asf' => 'wmv',
231 | 'font/woff' => 'woff',
232 | 'font/woff2' => 'woff2',
233 | 'application/xhtml+xml' => 'xhtml',
234 | 'application/excel' => 'xl',
235 | 'application/msexcel' => 'xls',
236 | 'application/x-msexcel' => 'xls',
237 | 'application/x-ms-excel' => 'xls',
238 | 'application/x-excel' => 'xls',
239 | 'application/x-dos_ms_excel' => 'xls',
240 | 'application/xls' => 'xls',
241 | 'application/x-xls' => 'xls',
242 | 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx',
243 | 'application/vnd.ms-excel' => 'xlsx',
244 | 'application/xml' => 'xml',
245 | 'text/xml' => 'xml',
246 | 'text/xsl' => 'xsl',
247 | 'application/xspf+xml' => 'xspf',
248 | 'application/x-compress' => 'z',
249 | 'application/x-zip' => 'zip',
250 | 'application/zip' => 'zip',
251 | 'application/x-zip-compressed' => 'zip',
252 | 'application/s-compressed' => 'zip',
253 | 'multipart/x-zip' => 'zip',
254 | 'text/x-scriptzsh' => 'zsh',
255 | ];
256 |
257 | return $mimeMap[$mime] ?? null;
258 | }
259 |
260 | /**
261 | * @throws FileException
262 | * @throws FileNotFoundException
263 | */
264 | public function createUploadedFile(string $tempFile, string $originalName, string $mimeType): UploadedFile
265 | {
266 | return new UploadedFile($tempFile, $originalName, $mimeType);
267 | }
268 | }
269 |
--------------------------------------------------------------------------------
/src/Model/UploadedImage.php:
--------------------------------------------------------------------------------
1 | fileHelper = $fileHelper;
32 | $this->file = $file;
33 | $this->destination = $destination;
34 | }
35 |
36 | public function getFile(): UploadedFile
37 | {
38 | return $this->file;
39 | }
40 |
41 | public function getDestination(): string
42 | {
43 | return $this->destination;
44 | }
45 |
46 | public function getFilename(): string
47 | {
48 | return str_replace(' ', '_', $this->file->getClientOriginalName());
49 | }
50 |
51 | public function getSize(): int
52 | {
53 | return $this->file->getSize();
54 | }
55 |
56 | /**
57 | * Get the instance as an array.
58 | */
59 | public function toArray(): array
60 | {
61 | $image = [
62 | self::KEY_SIZE => $this->getSize(),
63 | self::KEY_NAME => $this->getFilename(),
64 | self::KEY_MIME_TYPE => $this->file->getMimeType(),
65 | self::KEY_EXTENSION => $this->file->getClientOriginalExtension(),
66 | self::KEY_LOCATION => $this->getDestination(),
67 | ];
68 | $image[self::KEY_FULL_PATH] = urlencode(
69 | sprintf('%s/%s', $this->getDestination(), $this->getFilename())
70 | );
71 | [$image[self::KEY_WIDTH], $image[self::KEY_HEIGHT]] = $this->fileHelper->getImageSize(
72 | $this->file->getRealPath()
73 | );
74 |
75 | return $image;
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/Result.php:
--------------------------------------------------------------------------------
1 | configAccessor = $configAccessor;
103 | }
104 |
105 | public function getAllowedExtensions(): array
106 | {
107 | return $this->configAccessor->get(self::CONFIG_KEY_ALLOWED_EXTENSIONS, self::DEFAULT_ALLOWED_EXTENSIONS);
108 | }
109 |
110 | /**
111 | * {@inheritdoc}
112 | *
113 | * @return array of controller FQCNs for route binding
114 | */
115 | public function getControllersForRoutes(): array
116 | {
117 | return $this->configAccessor->get(self::CONFIG_KEY_ROUTES_CONTROLLERS, []);
118 | }
119 |
120 | /**
121 | * {@inheritdoc}
122 | *
123 | * @return string prefix
124 | */
125 | public function getRoutePrefix(): string
126 | {
127 | return $this->configAccessor->get(self::CONFIG_KEY_ROUTES_PREFIX, self::DEFAULT_ROUTES_PREFIX);
128 | }
129 |
130 | /**
131 | * {@inheritdoc}
132 | *
133 | * @return string model name
134 | */
135 | public function getGuidedModelName(bool $lowered = false): string
136 | {
137 | $model = $this->configAccessor->get(self::CONFIG_KEY_GUIDED_MODEL, self::DEFAULT_GUIDED_MODEL);
138 |
139 | return $lowered ? strtolower($model) : $model;
140 | }
141 |
142 | /**
143 | * {@inheritdoc}
144 | *
145 | * @return string model namespace
146 | */
147 | public function getGuidedModelNamespace(bool $lowered = false): string
148 | {
149 | $modelNamespace = $this->configAccessor->get(
150 | self::CONFIG_KEY_GUIDED_MODEL_NAMESPACE,
151 | self::DEFAULT_GUIDED_MODEL_NAMESPACE
152 | );
153 |
154 | return $lowered ? strtolower($modelNamespace) : $modelNamespace;
155 | }
156 |
157 | /**
158 | * {@inheritdoc}
159 | */
160 | public function getRouteGroupBindings(array $bindings = [], string $groupKey = self::ROUTE_GROUP_KEY_PUBLIC): array
161 | {
162 | $defaults = $groupKey === self::ROUTE_GROUP_KEY_PUBLIC ? [self::KEY_PREFIX => $this->getRoutePrefix()] : [];
163 |
164 | $bindings = array_merge(
165 | $this->configAccessor->get(sprintf(self::CONFIG_KEY_ROUTES_BINDINGS_WITH_GROUP, $groupKey), []),
166 | $bindings
167 | );
168 |
169 | return array_merge($defaults, $bindings);
170 | }
171 |
172 | public function getImageRules(): string
173 | {
174 | return $this->configAccessor->get(self::CONFIG_KEY_IMAGE_RULES, self::DEFAULT_IMAGE_RULES);
175 | }
176 |
177 | /**
178 | * {@inheritdoc}
179 | */
180 | public function getImagesTableName(): string
181 | {
182 | return $this->configAccessor->get(self::CONFIG_KEY_IMAGES_TABLE, self::DEFAULT_IMAGES_TABLE);
183 | }
184 |
185 | /**
186 | * {@inheritdoc}
187 | */
188 | public function getImageablesTableName(): string
189 | {
190 | return $this->configAccessor->get(self::CONFIG_KEY_IMAGEABLES_TABLE, self::DEFAULT_IMAGEABLES_TABLE);
191 | }
192 |
193 | public function getUploadDirectory(): string
194 | {
195 | return $this->configAccessor->get(self::CONFIG_KEY_STORAGE_UPLOAD_DIRECTORY, self::DEFAULT_UPLOAD_DIRECTORY);
196 | }
197 |
198 | public function generateUploadDateSubDirectories(): bool
199 | {
200 | return (bool) $this->configAccessor->get(
201 | self::CONFIG_KEY_STORAGE_GENERATE_UPLOAD_DATE_SUB_DIRECTORIES,
202 | self::DEFAULT_STORAGE_GENERATE_UPLOAD_DATE_SUB_DIRECTORIES
203 | );
204 | }
205 |
206 | public function getResizedCachePath(): string
207 | {
208 | $cacheDir = $this->getCacheDirectory();
209 | $cacheResizedSubDir = $this->configAccessor->get(
210 | self::CONFIG_KEY_STORAGE_CACHE_SUB_DIR_RESIZED,
211 | self::DEFAULT_STORAGE_CACHE_SUB_DIR_RESIZED
212 | );
213 |
214 | return sprintf('%s/%s', $cacheDir, $cacheResizedSubDir);
215 | }
216 |
217 | public function getThumbsCachePath(): string
218 | {
219 | $cacheDir = $this->getCacheDirectory();
220 | $cacheThumbsSubDir = $this->configAccessor->get(
221 | self::CONFIG_KEY_STORAGE_CACHE_SUB_DIR_THUMBS,
222 | self::DEFAULT_STORAGE_CACHE_SUB_DIR_THUMBS
223 | );
224 |
225 | return sprintf('%s/%s', $cacheDir, $cacheThumbsSubDir);
226 | }
227 |
228 | public function getCacheDaysHeader(): int
229 | {
230 | return (int) $this->configAccessor->get(self::CONFIG_KEY_HEADERS_CACHE_DAYS, self::DEFAULT_HEADER_CACHE_DAYS);
231 | }
232 |
233 | public function getAdditionalHeaders(): array
234 | {
235 | return $this->configAccessor->get(self::CONFIG_KEY_HEADERS_ADDITIONAL, self::DEFAULT_ADDITIONAL_HEADERS);
236 | }
237 |
238 | public function getCacheDirectory(): string
239 | {
240 | return $this->configAccessor->get(
241 | self::CONFIG_KEY_STORAGE_CACHE_DIR,
242 | self::DEFAULT_STORAGE_CACHE_DIR
243 | );
244 | }
245 |
246 | public function getImageEncodingMimeType(): string
247 | {
248 | return $this->configAccessor->get(
249 | self::CONFIG_KEY_IMAGE_ENCODING_MIME_TYPE,
250 | self::DEFAULT_IMAGE_ENCODING_MIME_TYPE
251 | );
252 | }
253 |
254 | public function getImageEncodingQuality(): int
255 | {
256 | return (int) $this->configAccessor->get(
257 | self::CONFIG_KEY_IMAGE_ENCODING_QUALITY,
258 | self::DEFAULT_IMAGE_ENCODING_QUALITY
259 | );
260 | }
261 |
262 | public function getCacheDiskName(): string
263 | {
264 | return $this->configAccessor->get(self::CONFIG_KEY_STORAGE_CACHE_DISK, self::DEFAULT_STORAGE_CACHE_DISK);
265 | }
266 |
267 | public function getUploadDiskName(): string
268 | {
269 | return $this->configAccessor->get(self::CONFIG_KEY_STORAGE_UPLOAD_DISK, self::DEFAULT_STORAGE_UPLOAD_DISK);
270 | }
271 |
272 | public function isRawImageFallbackEnabled(): bool
273 | {
274 | return $this->configAccessor->get(
275 | self::CONFIG_KEY_DISPENSER_RAW_IMAGE_FALLBACK_ENABLED,
276 | self::DEFAULT_DISPENSER_RAW_IMAGE_FALLBACK_ENABLED
277 | );
278 | }
279 | }
280 |
--------------------------------------------------------------------------------
/src/Service/ImageDispenser.php:
--------------------------------------------------------------------------------
1 | cacheDisk = $filesystemManager->disk($configProvider->getCacheDiskName());
64 | $this->uploadDisk = $filesystemManager->disk($configProvider->getUploadDiskName());
65 | $this->imageEncodingMimeType = $configProvider->getImageEncodingMimeType();
66 | $this->imageEncodingQuality = $configProvider->getImageEncodingQuality();
67 |
68 | $this->prepCacheDirectories();
69 | }
70 |
71 | /**
72 | * {@inheritdoc}
73 | *
74 | * @throws RuntimeException
75 | */
76 | public function getDummyImage(Dummy $demand): ImageInterface
77 | {
78 | return $this->imageManager->create(
79 | $demand->getWidth(),
80 | $demand->getHeight()
81 | )->fill($demand->getColor());
82 | }
83 |
84 | /**
85 | * {@inheritdoc}
86 | *
87 | * @return ImageInterface|SymfonyResponse|void
88 | *
89 | * @throws InvalidArgumentException
90 | * @throws RuntimeException
91 | */
92 | public function getResizedImage(Resize $demand)
93 | {
94 | $guidedImage = $demand->getGuidedImage();
95 | $width = $demand->getWidth();
96 | $height = $demand->getHeight();
97 | $cacheFilePath = sprintf(
98 | '%s/%d-%d-_-%d_%d_%s',
99 | $this->resizedCachePath,
100 | $width,
101 | $height,
102 | $demand->maintainAspectRatio() ? 1 : 0,
103 | $demand->allowUpSizing() ? 1 : 0,
104 | $guidedImage->getName()
105 | );
106 |
107 | try {
108 | if ($this->cacheDisk->exists($cacheFilePath)) {
109 | $image = $this->makeImageWithEncoding($this->cacheDisk->path($cacheFilePath));
110 | } else {
111 | $image = $this->makeImageWithEncoding($this->getImageFullPath($guidedImage));
112 | $sizingMethod = $demand->allowUpSizing() ? 'resize' : 'resizeDown';
113 | if ($demand->maintainAspectRatio()) {
114 | $sizingMethod = $demand->allowUpSizing() ? 'scale' : 'scaleDown';
115 | }
116 |
117 | $image->{$sizingMethod}($width, $height);
118 | $image->save($this->cacheDisk->path($cacheFilePath));
119 | }
120 |
121 | if ($demand->returnObject()) {
122 | return $image;
123 | }
124 |
125 | return new Response(
126 | $this->cacheDisk->get($cacheFilePath),
127 | self::RESPONSE_HTTP_OK,
128 | $this->getImageHeaders($cacheFilePath, $demand, $image) ?: []
129 | );
130 | } catch (RuntimeException $exception) {
131 | return $this->handleRuntimeException($exception, $guidedImage);
132 | } catch (FileNotFoundException $exception) {
133 | $this->logger->error(
134 | sprintf(
135 | 'Exception was encountered while building resized image; %s',
136 | $exception->getMessage()
137 | ),
138 | [
139 | self::KEY_IMAGE_URL => $guidedImage->getUrl(),
140 | self::KEY_CACHE_FILE => $cacheFilePath,
141 | ]
142 | );
143 |
144 | abort(self::RESPONSE_HTTP_NOT_FOUND);
145 | }
146 | }
147 |
148 | /**
149 | * {@inheritdoc}
150 | *
151 | * @return ImageInterface|SymfonyResponse|never
152 | *
153 | * @throws InvalidArgumentException
154 | * @throws RuntimeException
155 | *
156 | * @noinspection PhpVoidFunctionResultUsedInspection
157 | */
158 | public function getImageThumbnail(Thumbnail $demand)
159 | {
160 | if (! $demand->isValid()) {
161 | $this->logger->warning(
162 | sprintf('Invalid demand for thumbnail image. Method: %s', $demand->getMethod()),
163 | [
164 | 'method' => $demand->getMethod(),
165 | ]
166 | );
167 |
168 | return abort(self::RESPONSE_HTTP_NOT_FOUND);
169 | }
170 |
171 | $guidedImage = $demand->getGuidedImage();
172 | $width = $demand->getWidth();
173 | $height = $demand->getHeight();
174 | $method = $demand->getMethod();
175 | $cacheFilePath = sprintf(
176 | '%s/%d-%d-_-%s_%s',
177 | $this->thumbsCachePath,
178 | $width,
179 | $height,
180 | $method,
181 | $guidedImage->getName()
182 | );
183 |
184 | try {
185 | if ($this->cacheDisk->exists($cacheFilePath)) {
186 | $image = $this->makeImageWithEncoding($this->cacheDisk->path($cacheFilePath));
187 | } else {
188 | /** @var ImageInterface $image */
189 | $image = $this->imageManager
190 | ->read($this->getImageFullPath($guidedImage))
191 | ->{$method}(
192 | $width,
193 | $height
194 | );
195 |
196 | $image->save($this->cacheDisk->path($cacheFilePath));
197 | }
198 |
199 | if ($demand->returnObject()) {
200 | return $image;
201 | }
202 |
203 | return new Response(
204 | $this->cacheDisk->get($cacheFilePath),
205 | self::RESPONSE_HTTP_OK,
206 | $this->getImageHeaders($cacheFilePath, $demand, $image) ?: []
207 | );
208 | } catch (RuntimeException $exception) {
209 | return $this->handleRuntimeException($exception, $guidedImage);
210 | } catch (FileNotFoundException $exception) {
211 | $this->logger->error(
212 | sprintf(
213 | 'Exception was encountered while building thumbnail; %s',
214 | $exception->getMessage()
215 | ),
216 | [
217 | self::KEY_IMAGE_URL => $guidedImage->getUrl(),
218 | self::KEY_CACHE_FILE => $cacheFilePath,
219 | ]
220 | );
221 |
222 | abort(self::RESPONSE_HTTP_NOT_FOUND);
223 | }
224 | }
225 |
226 | public function emptyCache(): bool
227 | {
228 | return $this->cacheDisk->deleteDirectory($this->resizedCachePath)
229 | && $this->cacheDisk->deleteDirectory($this->thumbsCachePath);
230 | }
231 |
232 | /**
233 | * Get image headers. Improved caching
234 | * If the image has not been modified say 304 Not Modified.
235 | *
236 | * @return array image headers
237 | */
238 | private function getImageHeaders(string $relativeCacheFilePath, ExistingImage $demand, ImageInterface $image): array
239 | {
240 | $request = $demand->getRequest();
241 | $fullCacheFilePath = $this->cacheDisk->path($relativeCacheFilePath);
242 | $lastModified = $this->cacheDisk->lastModified($relativeCacheFilePath);
243 | $modifiedSince = $request->header('If-Modified-Since', '');
244 | $etagHeader = trim($request->header('If-None-Match', ''));
245 | $etagFile = $this->fileHelper->hashFile($fullCacheFilePath);
246 | $originalImageRelativePath = $demand->getGuidedImage()->getUrl(true);
247 |
248 | // check if image hasn't changed
249 | if ($etagFile === $etagHeader || strtotime($modifiedSince) === $lastModified) {
250 | // Say not modified and kill script
251 | header('HTTP/1.1 304 Not Modified');
252 | header(sprintf('ETag: %s', $etagFile));
253 | exit();
254 | }
255 |
256 | // adjust headers and return
257 | return array_merge(
258 | $this->getDefaultHeaders(),
259 | [
260 | 'Content-Type' => $image->origin()->mediaType(),
261 | 'Content-Disposition' => sprintf('inline; filename=%s', basename($originalImageRelativePath)),
262 | 'Last-Modified' => date(DATE_RFC822, $lastModified),
263 | 'Etag' => $etagFile,
264 | ]
265 | );
266 | }
267 |
268 | private function prepCacheDirectories(): void
269 | {
270 | $this->resizedCachePath = $this->configProvider->getResizedCachePath();
271 | $this->thumbsCachePath = $this->configProvider->getThumbsCachePath();
272 |
273 | if (! $this->cacheDisk->exists($this->thumbsCachePath)) {
274 | $this->cacheDisk->makeDirectory($this->thumbsCachePath);
275 | }
276 |
277 | if (! $this->cacheDisk->exists($this->resizedCachePath)) {
278 | $this->cacheDisk->makeDirectory($this->resizedCachePath);
279 | }
280 | }
281 |
282 | private function getDefaultHeaders(): array
283 | {
284 | $maxAge = self::ONE_DAY_IN_SECONDS * $this->configProvider->getCacheDaysHeader();
285 |
286 | return array_merge(
287 | [
288 | 'X-Guided-Image' => true,
289 | 'Cache-Control' => sprintf('public, max-age=%s', $maxAge),
290 | ],
291 | $this->configProvider->getAdditionalHeaders()
292 | );
293 | }
294 |
295 | /**
296 | * @throws RuntimeException
297 | */
298 | private function makeImageWithEncoding(mixed $data): ImageInterface
299 | {
300 | $encoder = new AutoEncoder(
301 | $this->imageEncodingMimeType ?: self::DEFAULT_IMAGE_ENCODING_MIME_TYPE,
302 | quality: $this->imageEncodingQuality ?: self::DEFAULT_IMAGE_ENCODING_QUALITY,
303 | );
304 |
305 | $encodedImage = $this->imageManager
306 | ->read($data)
307 | ->encode($encoder);
308 |
309 | return $this->imageManager->read($encodedImage->toFilePointer());
310 | }
311 |
312 | private function getImageFullPath(GuidedImage $guidedImage): string
313 | {
314 | return $this->uploadDisk->path($guidedImage->getUrl(true));
315 | }
316 |
317 | /**
318 | * @throws RuntimeException
319 | */
320 | private function handleRuntimeException(
321 | RuntimeException $exception,
322 | GuidedImage $guidedImage
323 | ): BinaryFileResponse {
324 | $errorMessage = 'Intervention image creation failed with RuntimeException;';
325 | $context = ['imageUrl' => $this->getImageFullPath($guidedImage)];
326 |
327 | if (! $this->configProvider->isRawImageFallbackEnabled()) {
328 | $this->logger->error(
329 | sprintf('%s %s. Raw image fallback is disabled.', $errorMessage, $exception->getMessage()),
330 | $context
331 | );
332 |
333 | abort(self::RESPONSE_HTTP_NOT_FOUND);
334 | }
335 |
336 | return response()->file(
337 | $this->uploadDisk->path($guidedImage->getUrl(true)),
338 | array_merge(
339 | $this->getDefaultHeaders(),
340 | ['X-Guided-Image-Fallback' => true]
341 | )
342 | );
343 | }
344 | }
345 |
--------------------------------------------------------------------------------
/src/Service/ImageManager.php:
--------------------------------------------------------------------------------
1 | manager = $manager ?? InterventionImageManager::gd();
23 | }
24 |
25 | /**
26 | * @throws RuntimeException
27 | */
28 | public function create(int $width, int $height): ImageInterface
29 | {
30 | return $this->manager->create($width, $height);
31 | }
32 |
33 | /**
34 | * @throws RuntimeException
35 | */
36 | public function read(mixed $input, array|string|DecoderInterface $decoders = []): ImageInterface
37 | {
38 | return $this->manager->read($input, $decoders);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Service/ImageUploader.php:
--------------------------------------------------------------------------------
1 | configProvider = $configProvider;
61 | $this->uploadDisk = $filesystemManager->disk($configProvider->getUploadDiskName());
62 | $this->fileHelper = $fileHelper;
63 | $this->validationFactory = $validationFactory;
64 | $this->guidedImage = $guidedImage;
65 | $this->logger = $logger;
66 | }
67 |
68 | /**
69 | * {@inheritdoc}
70 | *
71 | * @noinspection StaticInvocationViaThisInspection
72 | */
73 | public function upload(UploadedFile $imageFile, bool $isUrlUpload = false): Result
74 | {
75 | if (! $this->validate($imageFile, $isUrlUpload)) {
76 | return new Result(false, self::ERROR_INVALID_IMAGE);
77 | }
78 |
79 | $uploadedImage = new UploadedImage($this->fileHelper, $imageFile, $this->getUploadDestination());
80 |
81 | $existing = $this->guidedImage
82 | ->where(UploadedImage::KEY_NAME, $uploadedImage->getFilename())
83 | ->where(UploadedImage::KEY_SIZE, $uploadedImage->getSize())
84 | ->first();
85 |
86 | if (! empty($existing)) {
87 | $result = new Result(true);
88 |
89 | return $result
90 | ->addMessage(self::MESSAGE_IMAGE_REUSED)
91 | ->setExtra($existing);
92 | }
93 |
94 | try {
95 | $this->uploadDisk->putFileAs(
96 | $uploadedImage->getDestination(),
97 | $uploadedImage->getFile(),
98 | $uploadedImage->getFilename(),
99 | self::UPLOAD_VISIBILITY
100 | );
101 |
102 | $this->guidedImage->unguard();
103 | $instance = $this->guidedImage->create($uploadedImage->toArray());
104 | $this->guidedImage->reguard();
105 |
106 | $result = new Result(true, '', [], $instance);
107 | } catch (Exception $exception) {
108 | $this->logger->error(
109 | $exception->getMessage(),
110 | [
111 | 'uploaded image' => $uploadedImage->toArray(),
112 | 'trace' => $exception->getTraceAsString(),
113 | ]
114 | );
115 |
116 | $result = new Result(false, $exception->getMessage());
117 | }
118 |
119 | return $result;
120 | }
121 |
122 | /**
123 | * @throws UrlUploadFailed
124 | */
125 | public function uploadFromUrl(string $url): Result
126 | {
127 | try {
128 | $filenameForbiddenChars = ['?', '&', '%', '='];
129 | $tempFile = $this->fileHelper->tempName(
130 | $this->fileHelper->getSystemTempDirectory(),
131 | self::TEMP_FILE_PREFIX
132 | );
133 | $imageContents = $this->fileHelper->getContents($url);
134 |
135 | $this->fileHelper->putContents($tempFile, $imageContents);
136 |
137 | $mimeType = $this->fileHelper->getMimeType($tempFile);
138 | $originalName = sprintf(
139 | '%s.%s',
140 | str_replace($filenameForbiddenChars, '', basename($url)),
141 | $this->fileHelper->mime2Ext($mimeType)
142 | );
143 | $uploadedFile = $this->fileHelper->createUploadedFile($tempFile, $originalName, $mimeType);
144 | $result = $this->upload($uploadedFile, true);
145 |
146 | $this->fileHelper->unlink($tempFile);
147 |
148 | return $result;
149 | } catch (FileNotFoundException|FileException $exception) {
150 | throw UrlUploadFailed::forUrl($url, $exception);
151 | }
152 | }
153 |
154 | private function validate(UploadedFile $imageFile, bool $isUrlUpload): bool
155 | {
156 | if ($isUrlUpload) {
157 | return $this->validateFileExtension($imageFile);
158 | }
159 |
160 | return $this->validatePostUpload($imageFile);
161 | }
162 |
163 | private function validatePostUpload(UploadedFile $imageFile): bool
164 | {
165 | $validator = $this->validationFactory->make(
166 | [self::KEY_FILE => $imageFile],
167 | [self::KEY_FILE => $this->configProvider->getImageRules()]
168 | );
169 |
170 | return $this->validateFileExtension($imageFile) && ! $validator->fails();
171 | }
172 |
173 | private function validateFileExtension(UploadedFile $imageFile): bool
174 | {
175 | return in_array(
176 | strtolower($imageFile->getClientOriginalExtension()),
177 | $this->configProvider->getAllowedExtensions(),
178 | true
179 | );
180 | }
181 |
182 | private function getUploadDestination(): string
183 | {
184 | $destination = $this->configProvider->getUploadDirectory();
185 |
186 | if (! $this->configProvider->generateUploadDateSubDirectories()) {
187 | return $destination;
188 | }
189 |
190 | $uploadSubDirectories = date(self::UPLOAD_DATE_SUB_DIRECTORIES_PATTERN);
191 | $destination = sprintf('%s/%s', $destination, $uploadSubDirectories);
192 |
193 | return str_replace('//', '/', $destination);
194 | }
195 | }
196 |
--------------------------------------------------------------------------------
/src/ServiceProvider.php:
--------------------------------------------------------------------------------
1 | handleRoutes();
59 | $this->handleConfig();
60 | $this->handleCommands();
61 | $this->handleMigrations();
62 | }
63 |
64 | /**
65 | * @throws InvalidArgumentException
66 | */
67 | public function register(): void
68 | {
69 | $this->configProvider = new ConfigProvider(
70 | new ReliqArtsConfigProvider(
71 | resolve(ConfigRepository::class),
72 | $this->getConfigKey()
73 | )
74 | );
75 |
76 | $guidedModelFQCN = $this->configProvider->getGuidedModelNamespace()
77 | .$this->configProvider->getGuidedModelName();
78 |
79 | $this->app->bind(GuidedImage::class, $guidedModelFQCN);
80 |
81 | $this->app->singleton(
82 | ConfigProviderContract::class,
83 | function (): ConfigProviderContract {
84 | return $this->configProvider;
85 | }
86 | );
87 |
88 | $this->app->singleton(
89 | ImageManagerContract::class,
90 | ImageManager::class
91 | );
92 |
93 | $this->app->singleton(
94 | FileHelperContract::class,
95 | FileHelper::class
96 | );
97 |
98 | $logger = $this->createLogger();
99 |
100 | $this->app->singleton(
101 | ImageUploaderContract::class,
102 | fn (): ImageUploaderContract => new ImageUploader(
103 | $this->configProvider,
104 | resolve(FilesystemManager::class),
105 | resolve(FileHelperContract::class),
106 | resolve(ValidationFactory::class),
107 | resolve(GuidedImage::class),
108 | $logger,
109 | )
110 | );
111 |
112 | $this->app->singleton(
113 | ImageDispenserContract::class,
114 | fn (): ImageDispenserContract => new ImageDispenser(
115 | $this->configProvider,
116 | resolve(FilesystemManager::class),
117 | resolve(ImageManagerContract::class),
118 | $logger,
119 | resolve(FileHelperContract::class)
120 | )
121 | );
122 | }
123 |
124 | public function provides(): array
125 | {
126 | return array_merge(
127 | $this->commands,
128 | [
129 | GuidedImage::class,
130 | ]
131 | );
132 | }
133 |
134 | protected function handleConfig(): void
135 | {
136 | $configFile = sprintf('%s/config/config.php', $this->getAssetDirectory());
137 | $configKey = $this->getConfigKey();
138 |
139 | $this->mergeConfigFrom($configFile, $configKey);
140 |
141 | $this->publishes(
142 | [$configFile => config_path(sprintf('%s.php', $configKey))],
143 | sprintf('%s-config', $configKey)
144 | );
145 | }
146 |
147 | /**
148 | * Command files.
149 | */
150 | private function handleCommands(): void
151 | {
152 | if ($this->app->runningInConsole()) {
153 | $this->commands($this->commands);
154 | }
155 | }
156 |
157 | /**
158 | * @throws BindingResolutionException
159 | */
160 | private function handleRoutes(): void
161 | {
162 | $router = $this->app->make('router');
163 | $modelName = $this->configProvider->getGuidedModelName();
164 |
165 | if (! $this->app->routesAreCached()) {
166 | $router->model(strtolower($modelName), $this->configProvider->getGuidedModelNamespace().$modelName);
167 |
168 | require_once sprintf('%s/routes/web.php', $this->getAssetDirectory());
169 | }
170 | }
171 |
172 | private function handleMigrations(): void
173 | {
174 | $this->loadMigrationsFrom(sprintf('%s/database/migrations', $this->getAssetDirectory()));
175 | }
176 |
177 | /**
178 | * @throws InvalidArgumentException
179 | */
180 | private function createLogger(): LoggerContract
181 | {
182 | /** @var LoggerFactory $loggerFactory */
183 | $loggerFactory = resolve(LoggerFactory::class);
184 |
185 | return $loggerFactory->create($this->getLoggerName(), $this->getLogFileBasename());
186 | }
187 | }
188 |
--------------------------------------------------------------------------------
/tests/Feature/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reliqarts/laravel-guided-image/3f3d19dd06cd1d9e4c5f800fcf62d5a7380e1071/tests/Feature/.gitkeep
--------------------------------------------------------------------------------
/tests/Fixtures/Model/GuidedImage.php:
--------------------------------------------------------------------------------
1 | setBasePath(__DIR__ . '/..');
20 |
21 | // set app config
22 | $app['config']->set('database.default', 'testing');
23 | }
24 |
25 | /**
26 | * @param Application $app
27 | *
28 | * @return array
29 | */
30 | protected function getPackageProviders($app)
31 | {
32 | return [
33 | ServiceProvider::class,
34 | ];
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/tests/Unit/Demand/DummyTest.php:
--------------------------------------------------------------------------------
1 | getColor());
31 | }
32 |
33 | public static function colorDataProvider(): array
34 | {
35 | return [
36 | ['0f0', '0f0'],
37 | ['n', Dummy::DEFAULT_COLOR],
38 | ['_', Dummy::DEFAULT_COLOR],
39 | ['false', Dummy::DEFAULT_COLOR],
40 | ['null', Dummy::DEFAULT_COLOR],
41 | [false, Dummy::DEFAULT_COLOR],
42 | [null, Dummy::DEFAULT_COLOR],
43 | ];
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/tests/Unit/Demand/ExistingImageTest.php:
--------------------------------------------------------------------------------
1 | request->reveal(),
25 | $this->getExistingImageDemand()->getRequest()
26 | );
27 | }
28 |
29 | /**
30 | * @throws Exception
31 | */
32 | public function testGetGuidedImage(): void
33 | {
34 | self::assertSame(
35 | $this->guidedImage->reveal(),
36 | $this->getExistingImageDemand()
37 | ->getGuidedImage()
38 | );
39 | }
40 |
41 | /**
42 | * @throws Exception
43 | */
44 | private function getExistingImageDemand(): ExistingImage
45 | {
46 | return new Thumbnail(
47 | $this->request->reveal(),
48 | $this->guidedImage->reveal(),
49 | 'crop',
50 | self::DIMENSION,
51 | self::DIMENSION
52 | );
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/tests/Unit/Demand/ImageTest.php:
--------------------------------------------------------------------------------
1 | getImageDemand($width, self::DIMENSION);
26 |
27 | self::assertSame($expectedResult, $demand->getWidth());
28 | }
29 |
30 | /**
31 | * @throws Exception
32 | */
33 | #[DataProvider('widthAndHeightDataProvider')]
34 | public function testGetHeight($height, ?int $expectedResult): void
35 | {
36 | $demand = $this->getImageDemand(self::DIMENSION, $height);
37 |
38 | self::assertSame($expectedResult, $demand->getHeight());
39 | }
40 |
41 | public static function widthAndHeightDataProvider(): array
42 | {
43 | return [
44 | [200, 200],
45 | [false, null],
46 | [null, null],
47 | ['null', null],
48 | ['false', null],
49 | ['_', null],
50 | ['n', null],
51 | ['0', null],
52 | ];
53 | }
54 |
55 | private function getImageDemand($width, $height): Image
56 | {
57 | return new Dummy($width, $height);
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/tests/Unit/Demand/ResizeTest.php:
--------------------------------------------------------------------------------
1 | request->reveal(),
28 | $this->guidedImage->reveal(),
29 | $width,
30 | self::DIMENSION
31 | );
32 |
33 | self::assertSame($expectedResult, $demand->getWidth());
34 | }
35 |
36 | /**
37 | * @throws Exception
38 | */
39 | #[DataProvider('resizeDimensionDataProvider')]
40 | public function testGetHeight($height, ?int $expectedResult): void
41 | {
42 | $demand = new Resize(
43 | $this->request->reveal(),
44 | $this->guidedImage->reveal(),
45 | self::DIMENSION,
46 | $height
47 | );
48 |
49 | self::assertSame($expectedResult, $demand->getHeight());
50 | }
51 |
52 | /**
53 | * @throws Exception
54 | */
55 | #[DataProvider('resizeFlagDataProvider')]
56 | public function testMaintainAspectRatio($maintainAspectRatio, bool $expectedResult): void
57 | {
58 | $demand = new Resize(
59 | $this->request->reveal(),
60 | $this->guidedImage->reveal(),
61 | self::DIMENSION,
62 | self::DIMENSION,
63 | $maintainAspectRatio
64 | );
65 |
66 | self::assertSame($expectedResult, $demand->maintainAspectRatio());
67 | }
68 |
69 | /**
70 | * @throws Exception
71 | */
72 | #[DataProvider('resizeFlagDataProvider')]
73 | public function testAllowUpSizing($upSize, bool $expectedResult): void
74 | {
75 | $demand = new Resize(
76 | $this->request->reveal(),
77 | $this->guidedImage->reveal(),
78 | self::DIMENSION,
79 | self::DIMENSION,
80 | true,
81 | $upSize
82 | );
83 |
84 | self::assertSame($expectedResult, $demand->allowUpSizing());
85 | }
86 |
87 | /**
88 | * @throws Exception
89 | */
90 | #[DataProvider('resizeFlagDataProvider')]
91 | public function testReturnObject($returnObject, bool $expectedResult): void
92 | {
93 | $demand = new Resize(
94 | $this->request->reveal(),
95 | $this->guidedImage->reveal(),
96 | self::DIMENSION,
97 | self::DIMENSION,
98 | true,
99 | null,
100 | $returnObject
101 | );
102 |
103 | self::assertSame($expectedResult, $demand->returnObject());
104 | }
105 |
106 | public static function resizeDimensionDataProvider(): array
107 | {
108 | return [
109 | [self::DIMENSION, self::DIMENSION],
110 | ['n', null],
111 | ['_', null],
112 | ['false', null],
113 | ['null', null],
114 | [false, null],
115 | [null, null],
116 | ];
117 | }
118 |
119 | public static function resizeFlagDataProvider(): array
120 | {
121 | return [
122 | [true, true],
123 | ['n', false],
124 | ['_', false],
125 | ['false', false],
126 | ['null', false],
127 | [false, false],
128 | [null, false],
129 | ];
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/tests/Unit/Demand/TestCase.php:
--------------------------------------------------------------------------------
1 | request = $this->prophesize(Request::class);
31 | $this->guidedImage = $this->prophesize(GuidedImage::class);
32 | }
33 |
34 | public function nullValueProvider(): array
35 | {
36 | return [
37 | ['_'],
38 | ['n'],
39 | ['null'],
40 | [false],
41 | [null],
42 | ];
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/tests/Unit/Demand/ThumbnailTest.php:
--------------------------------------------------------------------------------
1 | request->reveal(),
26 | $this->guidedImage->reveal(),
27 | $method,
28 | self::DIMENSION,
29 | self::DIMENSION
30 | );
31 |
32 | self::assertSame($expectedResult, $demand->isValid());
33 | }
34 |
35 | public static function isValidDataProvider(): array
36 | {
37 | return [
38 | ['crop', true],
39 | ['cover', true],
40 | ['fit', true],
41 | ['grab', false],
42 | ['spook', false],
43 | ];
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/tests/Unit/Service/ImageDispenserTest.php:
--------------------------------------------------------------------------------
1 | cacheDisk = $this->prophesize(FilesystemAdapter::class);
115 | $this->imageManager = $this->prophesize(ImageManager::class);
116 | $this->logger = $this->prophesize(Logger::class);
117 | $this->request = $this->prophesize(Request::class);
118 | $this->guidedImage = $this->prophesize(GuidedImage::class);
119 | $this->cacheResized = self::CACHE_RESIZED_SUB_DIRECTORY;
120 | $this->cacheThumbs = self::CACHE_THUMBS_SUB_DIRECTORY;
121 |
122 | $configProvider = $this->prophesize(ConfigProvider::class);
123 | $fileHelper = $this->prophesize(FileHelper::class);
124 | $filesystemManager = $this->prophesize(FilesystemManager::class);
125 | $uploadDisk = $this->prophesize(FilesystemAdapter::class);
126 |
127 | $configProvider
128 | ->getCacheDiskName()
129 | ->shouldBeCalledTimes(1)
130 | ->willReturn(self::CACHE_DISK_NAME);
131 | $configProvider
132 | ->getUploadDiskName()
133 | ->shouldBeCalledTimes(1)
134 | ->willReturn(self::UPLOAD_DISK_NAME);
135 | $configProvider
136 | ->getResizedCachePath()
137 | ->shouldBeCalledTimes(1)
138 | ->willReturn(self::CACHE_RESIZED_SUB_DIRECTORY);
139 | $configProvider
140 | ->getThumbsCachePath()
141 | ->shouldBeCalledTimes(1)
142 | ->willReturn(self::CACHE_THUMBS_SUB_DIRECTORY);
143 | $configProvider
144 | ->getCacheDaysHeader()
145 | ->willReturn(2);
146 | $configProvider
147 | ->getAdditionalHeaders()
148 | ->willReturn([]);
149 | $configProvider
150 | ->getImageEncodingMimeType()
151 | ->willReturn(self::IMAGE_ENCODING_FORMAT);
152 | $configProvider
153 | ->getImageEncodingQuality()
154 | ->willReturn(self::IMAGE_ENCODING_QUALITY);
155 | $configProvider
156 | ->isRawImageFallbackEnabled()
157 | ->willReturn(false);
158 |
159 | $filesystemManager
160 | ->disk(self::CACHE_DISK_NAME)
161 | ->shouldBeCalledTimes(1)
162 | ->willReturn($this->cacheDisk);
163 | $filesystemManager
164 | ->disk(self::UPLOAD_DISK_NAME)
165 | ->shouldBeCalledTimes(1)
166 | ->willReturn($uploadDisk);
167 |
168 | $this->cacheDisk
169 | ->exists($this->cacheResized)
170 | ->shouldBeCalledTimes(1)
171 | ->willReturn(false);
172 | $this->cacheDisk
173 | ->makeDirectory($this->cacheResized)
174 | ->shouldBeCalledTimes(1)
175 | ->willReturn(true);
176 | $this->cacheDisk
177 | ->exists($this->cacheThumbs)
178 | ->shouldBeCalledTimes(1)
179 | ->willReturn(false);
180 | $this->cacheDisk
181 | ->makeDirectory($this->cacheThumbs)
182 | ->shouldBeCalledTimes(1)
183 | ->willReturn(true);
184 | $this->cacheDisk
185 | ->lastModified(Argument::type('string'))
186 | ->willReturn(self::LAST_MODIFIED);
187 |
188 | $uploadDisk
189 | ->path(self::IMAGE_PATH)
190 | ->willReturn(self::IMAGE_PATH);
191 |
192 | $fileHelper
193 | ->hashFile(Argument::type('string'))
194 | ->willReturn(self::FILE_HASH);
195 |
196 | $this->request
197 | ->header(Argument::cetera())
198 | ->willReturn('');
199 |
200 | $this->guidedImage
201 | ->getName()
202 | ->willReturn(self::IMAGE_NAME);
203 | $this->guidedImage
204 | ->getUrl(true)
205 | ->willReturn(self::IMAGE_PATH);
206 |
207 | $this->subject = new ImageDispenser(
208 | $configProvider->reveal(),
209 | $filesystemManager->reveal(),
210 | $this->imageManager->reveal(),
211 | $this->logger->reveal(),
212 | $fileHelper->reveal()
213 | );
214 | }
215 |
216 | /**
217 | * @throws RuntimeException
218 | */
219 | public function testEmptyCache(): void
220 | {
221 | $this->cacheDisk
222 | ->deleteDirectory($this->cacheResized)
223 | ->shouldBeCalledTimes(1)
224 | ->willReturn(true);
225 | $this->cacheDisk
226 | ->deleteDirectory($this->cacheThumbs)
227 | ->shouldBeCalledTimes(1)
228 | ->willReturn(true);
229 |
230 | $result = $this->subject->emptyCache();
231 |
232 | self::assertTrue($result);
233 | }
234 |
235 | /**
236 | * @throws RuntimeException
237 | */
238 | public function testGetDummyImage(): void
239 | {
240 | $width = self::IMAGE_WIDTH;
241 | $height = self::IMAGE_HEIGHT;
242 | $color = 'fee';
243 | $image = $this->getImageMock();
244 |
245 | $this->imageManager
246 | ->create($width, $height)
247 | ->shouldBeCalledTimes(1)
248 | ->willReturn($image);
249 |
250 | $result = $this->subject->getDummyImage(
251 | new Dummy($width, $height, $color)
252 | );
253 |
254 | self::assertSame($image, $result);
255 | }
256 |
257 | /**
258 | * @throws RuntimeException
259 | * @throws InvalidArgumentException
260 | */
261 | public function testGetResizedImage(): void
262 | {
263 | $width = self::IMAGE_WIDTH;
264 | $height = self::IMAGE_HEIGHT;
265 | $image = $this->getImageMock();
266 | $demand = new Resize(
267 | $this->request->reveal(),
268 | $this->guidedImage->reveal(),
269 | $width,
270 | $height
271 | );
272 | $cacheFile = $this->getCacheFilename($width, $height);
273 | $imageContent = self::IMAGE_CONTENT_RAW;
274 |
275 | $this->cacheDisk
276 | ->exists($cacheFile)
277 | ->shouldBeCalledTimes(1)
278 | ->willReturn(false);
279 | $this->cacheDisk
280 | ->path($cacheFile)
281 | ->shouldBeCalledTimes(2)
282 | ->willReturn($cacheFile);
283 | $this->cacheDisk
284 | ->get($cacheFile)
285 | ->shouldBeCalledTimes(1)
286 | ->willReturn($imageContent);
287 |
288 | $this->imageManager
289 | ->read($cacheFile)
290 | ->shouldNotBeCalled();
291 | $this->imageManager
292 | ->read(Argument::in([self::IMAGE_PATH, self::FOO_RESOURCE]))
293 | ->shouldBeCalledTimes(2)
294 | ->willReturn($image);
295 |
296 | $result = $this->subject->getResizedImage($demand);
297 |
298 | self::assertInstanceOf(Response::class, $result);
299 | self::assertSame(self::RESPONSE_HTTP_OK, $result->getStatusCode());
300 | self::assertSame($imageContent, $result->getOriginalContent());
301 | }
302 |
303 | /**
304 | * @throws RuntimeException|InvalidArgumentException
305 | */
306 | public function testGetResizedImageWhenImageInstanceIsExpected(): void
307 | {
308 | $width = self::IMAGE_WIDTH;
309 | $height = self::IMAGE_HEIGHT;
310 | $image = $this->getImageMock();
311 | $demand = new Resize(
312 | $this->request->reveal(),
313 | $this->guidedImage->reveal(),
314 | $width,
315 | $height,
316 | returnObject: true
317 | );
318 | $cacheFile = $this->getCacheFilename($width, $height);
319 |
320 | $this->cacheDisk
321 | ->exists($cacheFile)
322 | ->shouldBeCalledTimes(1)
323 | ->willReturn(false);
324 | $this->cacheDisk
325 | ->path($cacheFile)
326 | ->shouldBeCalledTimes(1)
327 | ->willReturn($cacheFile);
328 | $this->cacheDisk
329 | ->get($cacheFile)
330 | ->shouldNotBeCalled();
331 |
332 | $this->imageManager
333 | ->read($cacheFile)
334 | ->shouldNotBeCalled();
335 | $this->imageManager
336 | ->read(Argument::in([self::IMAGE_PATH, self::FOO_RESOURCE]))
337 | ->shouldBeCalledTimes(2)
338 | ->willReturn($image);
339 |
340 | $result = $this->subject->getResizedImage($demand);
341 |
342 | self::assertSame($image, $result);
343 | }
344 |
345 | /**
346 | * @throws Exception
347 | */
348 | public function testGetResizedImageWhenCacheFileExists(): void
349 | {
350 | $width = self::IMAGE_WIDTH;
351 | $height = self::IMAGE_HEIGHT;
352 | $image = $this->getImageMock();
353 | $cacheFile = $this->getCacheFilename($width, $height);
354 |
355 | $this->cacheDisk
356 | ->exists($cacheFile)
357 | ->shouldBeCalledTimes(1)
358 | ->willReturn(true);
359 | $this->cacheDisk
360 | ->path($cacheFile)
361 | ->shouldBeCalledTimes(2)
362 | ->willReturn($cacheFile);
363 | $this->cacheDisk
364 | ->get($cacheFile)
365 | ->shouldBeCalledTimes(1)
366 | ->willReturn(self::IMAGE_CONTENT_RAW);
367 |
368 | $this->imageManager
369 | ->read(Argument::in([$cacheFile, self::FOO_RESOURCE]))
370 | ->shouldBeCalledTimes(2)
371 | ->willReturn($image);
372 | $this->imageManager
373 | ->read(self::IMAGE_PATH)
374 | ->shouldNotBeCalled();
375 |
376 | $result = $this->subject->getResizedImage(
377 | new Resize(
378 | $this->request->reveal(),
379 | $this->guidedImage->reveal(),
380 | $width,
381 | $height
382 | )
383 | );
384 |
385 | self::assertInstanceOf(Response::class, $result);
386 | self::assertSame(self::RESPONSE_HTTP_OK, $result->getStatusCode());
387 | self::assertSame(self::IMAGE_CONTENT_RAW, $result->getOriginalContent());
388 | }
389 |
390 | /**
391 | * @throws Exception
392 | */
393 | public function testGetResizedWhenImageRetrievalFails(): void
394 | {
395 | $width = self::IMAGE_WIDTH;
396 | $height = self::IMAGE_HEIGHT;
397 | $image = $this->getImageMock();
398 | $demand = new Resize(
399 | $this->request->reveal(),
400 | $this->guidedImage->reveal(),
401 | $width,
402 | $height
403 | );
404 | $cacheFile = $this->getCacheFilename($width, $height);
405 |
406 | $this->cacheDisk
407 | ->exists($cacheFile)
408 | ->shouldBeCalledTimes(1)
409 | ->willReturn(false);
410 | $this->cacheDisk
411 | ->path($cacheFile)
412 | ->shouldBeCalledTimes(1)
413 | ->willReturn($cacheFile);
414 | $this->cacheDisk
415 | ->get($cacheFile)
416 | ->shouldBeCalledTimes(1)
417 | ->willThrow(FileNotFoundException::class);
418 |
419 | $this->imageManager
420 | ->read($cacheFile)
421 | ->shouldNotBeCalled();
422 | $this->imageManager
423 | ->read(Argument::in([self::IMAGE_PATH, self::FOO_RESOURCE]))
424 | ->shouldBeCalledTimes(2)
425 | ->willReturn($image);
426 |
427 | $this->guidedImage
428 | ->getUrl()
429 | ->shouldBeCalledTimes(1)
430 | ->willReturn(self::IMAGE_PATH);
431 |
432 | $this->logger
433 | ->error(
434 | Argument::containingString('Exception'),
435 | Argument::type('array')
436 | )
437 | ->shouldBeCalledTimes(1);
438 |
439 | $this->expectException(NotFoundHttpException::class);
440 |
441 | $this->subject->getResizedImage($demand);
442 | }
443 |
444 | /**
445 | * @throws Exception
446 | */
447 | public function testGetImageThumbnail(): void
448 | {
449 | $width = self::IMAGE_WIDTH;
450 | $height = self::IMAGE_HEIGHT;
451 | $image = $this->getImageMock();
452 | $demand = new Thumbnail(
453 | $this->request->reveal(),
454 | $this->guidedImage->reveal(),
455 | self::THUMBNAIL_METHOD_CROP,
456 | $width,
457 | $height
458 | );
459 | $cacheFile = sprintf(
460 | self::CACHE_FILE_FORMAT_THUMBNAIL,
461 | $this->cacheThumbs,
462 | $width,
463 | $height,
464 | $demand->getMethod(),
465 | self::IMAGE_NAME
466 | );
467 | $imageContent = self::IMAGE_CONTENT_RAW;
468 |
469 | $this->cacheDisk
470 | ->exists($cacheFile)
471 | ->shouldBeCalledTimes(1)
472 | ->willReturn(false);
473 | $this->cacheDisk
474 | ->get($cacheFile)
475 | ->shouldBeCalledTimes(1)
476 | ->willReturn($imageContent);
477 | $this->cacheDisk
478 | ->path($cacheFile)
479 | ->shouldBeCalledTimes(2)
480 | ->willReturn($cacheFile);
481 |
482 | $this->imageManager
483 | ->read($cacheFile)
484 | ->shouldNotBeCalled();
485 | $this->imageManager
486 | ->read(self::IMAGE_PATH)
487 | ->shouldBeCalledTimes(1)
488 | ->willReturn($image);
489 |
490 | $result = $this->subject->getImageThumbnail($demand);
491 |
492 | self::assertInstanceOf(Response::class, $result);
493 | self::assertSame(self::RESPONSE_HTTP_OK, $result->getStatusCode());
494 | self::assertSame($imageContent, $result->getOriginalContent());
495 | }
496 |
497 | /**
498 | * @throws Exception
499 | */
500 | public function testGetImageThumbnailWhenImageInstanceIsExpected(): void
501 | {
502 | $width = self::IMAGE_WIDTH;
503 | $height = self::IMAGE_HEIGHT;
504 | $image = $this->getImageMock();
505 | $cacheFile = sprintf(
506 | self::CACHE_FILE_FORMAT_THUMBNAIL,
507 | $this->cacheThumbs,
508 | $width,
509 | $height,
510 | self::THUMBNAIL_METHOD_CROP,
511 | self::IMAGE_NAME
512 | );
513 |
514 | $this->cacheDisk
515 | ->exists($cacheFile)
516 | ->shouldBeCalledTimes(1)
517 | ->willReturn(false);
518 | $this->cacheDisk
519 | ->path($cacheFile)
520 | ->shouldBeCalledTimes(1)
521 | ->willReturn($cacheFile);
522 | $this->cacheDisk
523 | ->get($cacheFile)
524 | ->shouldNotBeCalled();
525 |
526 | $this->imageManager
527 | ->read($cacheFile)
528 | ->shouldNotBeCalled();
529 | $this->imageManager
530 | ->read(Argument::in([self::IMAGE_PATH, self::FOO_RESOURCE]))
531 | ->shouldBeCalledTimes(1)
532 | ->willReturn($image);
533 |
534 | $result = $this->subject->getImageThumbnail(
535 | new Thumbnail(
536 | $this->request->reveal(),
537 | $this->guidedImage->reveal(),
538 | self::THUMBNAIL_METHOD_CROP,
539 | $width,
540 | $height,
541 | true
542 | )
543 | );
544 |
545 | self::assertSame($image, $result);
546 | }
547 |
548 | /**
549 | * @throws Exception
550 | */
551 | public function testGetImageThumbnailWhenCacheFileExists(): void
552 | {
553 | $width = self::IMAGE_WIDTH;
554 | $height = self::IMAGE_HEIGHT;
555 | $image = $this->getImageMock();
556 | $cacheFile = sprintf(
557 | self::CACHE_FILE_FORMAT_THUMBNAIL,
558 | $this->cacheThumbs,
559 | $width,
560 | $height,
561 | self::THUMBNAIL_METHOD_CROP,
562 | self::IMAGE_NAME
563 | );
564 | $imageContent = self::IMAGE_CONTENT_RAW;
565 |
566 | $this->cacheDisk
567 | ->exists($cacheFile)
568 | ->shouldBeCalledTimes(1)
569 | ->willReturn(true);
570 | $this->cacheDisk
571 | ->path($cacheFile)
572 | ->shouldBeCalledTimes(2)
573 | ->willReturn($cacheFile);
574 | $this->cacheDisk
575 | ->get($cacheFile)
576 | ->shouldBeCalledTimes(1)
577 | ->willReturn($imageContent);
578 |
579 | $this->imageManager
580 | ->read(Argument::in([$cacheFile, self::FOO_RESOURCE]))
581 | ->shouldBeCalledTimes(2)
582 | ->willReturn($image);
583 | $this->imageManager
584 | ->read(self::IMAGE_PATH)
585 | ->shouldNotBeCalled();
586 |
587 | $result = $this->subject->getImageThumbnail(
588 | new Thumbnail(
589 | $this->request->reveal(),
590 | $this->guidedImage->reveal(),
591 | self::THUMBNAIL_METHOD_CROP,
592 | $width,
593 | $height
594 | )
595 | );
596 |
597 | self::assertInstanceOf(Response::class, $result);
598 | self::assertSame(self::RESPONSE_HTTP_OK, $result->getStatusCode());
599 | self::assertSame($imageContent, $result->getOriginalContent());
600 | }
601 |
602 | /**
603 | * @throws Exception
604 | */
605 | public function testGetImageThumbnailWhenDemandIsInvalid(): void
606 | {
607 | $width = self::IMAGE_WIDTH;
608 | $height = self::IMAGE_HEIGHT;
609 | $demand = new Thumbnail(
610 | $this->request->reveal(),
611 | $this->guidedImage->reveal(),
612 | 'invalid',
613 | $width,
614 | $height
615 | );
616 | $cacheFile = sprintf(
617 | self::CACHE_FILE_FORMAT_THUMBNAIL,
618 | $this->cacheThumbs,
619 | $width,
620 | $height,
621 | $demand->getMethod(),
622 | self::IMAGE_NAME
623 | );
624 |
625 | $this->cacheDisk
626 | ->exists($cacheFile)
627 | ->shouldNotBeCalled();
628 | $this->cacheDisk
629 | ->path($cacheFile)
630 | ->shouldNotBeCalled();
631 | $this->cacheDisk
632 | ->get($cacheFile)
633 | ->shouldNotBeCalled();
634 |
635 | $this->imageManager
636 | ->read($cacheFile)
637 | ->shouldNotBeCalled();
638 | $this->imageManager
639 | ->read(self::IMAGE_PATH)
640 | ->shouldNotBeCalled();
641 |
642 | $this->logger
643 | ->warning(
644 | Argument::containingString('Invalid'),
645 | [
646 | 'method' => $demand->getMethod(),
647 | ]
648 | )
649 | ->shouldBeCalledTimes(1);
650 |
651 | $this->expectException(NotFoundHttpException::class);
652 |
653 | $this->subject->getImageThumbnail($demand);
654 | }
655 |
656 | /**
657 | * @throws Exception
658 | */
659 | public function testGetImageThumbnailWhenImageRetrievalFails(): void
660 | {
661 | $width = self::IMAGE_WIDTH;
662 | $height = self::IMAGE_HEIGHT;
663 | $demand = new Thumbnail(
664 | $this->request->reveal(),
665 | $this->guidedImage->reveal(),
666 | self::THUMBNAIL_METHOD_COVER,
667 | $width,
668 | $height
669 | );
670 | $cacheFile = sprintf(
671 | self::CACHE_FILE_FORMAT_THUMBNAIL,
672 | $this->cacheThumbs,
673 | $width,
674 | $height,
675 | $demand->getMethod(),
676 | self::IMAGE_NAME
677 | );
678 |
679 | $this->cacheDisk
680 | ->exists($cacheFile)
681 | ->shouldBeCalledTimes(1)
682 | ->willReturn(false);
683 | $this->cacheDisk
684 | ->path($cacheFile)
685 | ->shouldNotBeCalled();
686 | $this->cacheDisk
687 | ->get($cacheFile)
688 | ->shouldNotBeCalled();
689 |
690 | $this->imageManager
691 | ->read($cacheFile)
692 | ->shouldNotBeCalled();
693 | $this->imageManager
694 | ->read(self::IMAGE_PATH)
695 | ->shouldBeCalledTimes(1)
696 | ->willThrow(RuntimeException::class);
697 |
698 | $this->logger
699 | ->error(
700 | Argument::containingString('Exception'),
701 | Argument::type('array')
702 | )
703 | ->shouldBeCalledTimes(1);
704 |
705 | $this->expectException(NotFoundHttpException::class);
706 |
707 | $this->subject->getImageThumbnail($demand);
708 | }
709 |
710 | private function getImageMock(): ImageInterface|MockInterface
711 | {
712 | /** @var Origin|MockInterface $imageOrigin */
713 | $imageOrigin = Mockery::mock(Origin::class);
714 | $imageOrigin->shouldReceive('mediaType')
715 | ->andReturn(self::IMAGE_MEDIA_TYPE);
716 | $imageOrigin->shouldReceive('filePath')
717 | ->andReturn(self::IMAGE_FILE_PATH);
718 |
719 | $imageMethods = [
720 | 'fill',
721 | 'save',
722 | 'resize',
723 | 'resizeDown',
724 | 'scale',
725 | 'scaleDown',
726 | self::THUMBNAIL_METHOD_CROP,
727 | self::THUMBNAIL_METHOD_COVER,
728 | ];
729 | /** @var ImageInterface|MockInterface $image */
730 | $image = Mockery::mock(ImageInterface::class);
731 | $image->dirname = 'directory';
732 | $image->basename = 'basename';
733 | $image->shouldReceive(...$imageMethods)
734 | ->andReturn($image);
735 | $image->shouldReceive('origin')
736 | ->andReturn($imageOrigin);
737 |
738 | /** @var EncodedImageInterface|MockInterface $encodedImage */
739 | $encodedImage = Mockery::mock(EncodedImageInterface::class);
740 | $encodedImage->shouldReceive('toFilePointer')
741 | ->andReturn(self::FOO_RESOURCE);
742 | $image->shouldReceive('encode')
743 | ->andReturn($encodedImage);
744 |
745 | return $image;
746 | }
747 |
748 | private function getCacheFilename(int $width, int $height): string
749 | {
750 | return sprintf(
751 | self::CACHE_FILE_NAME_FORMAT_RESIZED,
752 | $this->cacheResized,
753 | $width,
754 | $height,
755 | 1,
756 | 0,
757 | self::IMAGE_NAME
758 | );
759 | }
760 | }
761 |
--------------------------------------------------------------------------------
/tests/Unit/Service/ImageUploaderTest.php:
--------------------------------------------------------------------------------
1 | configProvider = $this->prophesize(ConfigProvider::class);
81 | $filesystemManager = $this->prophesize(FilesystemManager::class);
82 | $this->fileHelper = $this->prophesize(FileHelper::class);
83 | $this->validationFactory = $this->prophesize(ValidationFactory::class);
84 | $this->validator = $this->prophesize(Validator::class);
85 | $this->guidedImage = $this->prophesize(GuidedImage::class);
86 | $this->logger = $this->prophesize(Logger::class);
87 | $this->builder = $this->prophesize(Builder::class);
88 | $this->uploadedFile = $this->getUploadedFileMock();
89 | $this->uploadDisk = $this->prophesize(FilesystemAdapter::class);
90 |
91 | $this->configProvider
92 | ->getAllowedExtensions()
93 | ->shouldBeCalledTimes(1)
94 | ->willReturn(self::ALLOWED_EXTENSIONS);
95 | $this->configProvider
96 | ->getImageRules()
97 | ->shouldBeCalledTimes(1)
98 | ->willReturn(self::IMAGE_RULES);
99 | $this->configProvider
100 | ->getUploadDirectory()
101 | ->shouldBeCalledTimes(1)
102 | ->willReturn(self::UPLOAD_DIRECTORY);
103 | $this->configProvider
104 | ->getUploadDiskName()
105 | ->shouldBeCalledTimes(1)
106 | ->willReturn(self::UPLOAD_DISK_NAME);
107 | $this->configProvider
108 | ->generateUploadDateSubDirectories()
109 | ->shouldBeCalledTimes(1)
110 | ->willReturn(1);
111 |
112 | $filesystemManager
113 | ->disk(self::UPLOAD_DISK_NAME)
114 | ->shouldBeCalledTimes(1)
115 | ->willReturn($this->uploadDisk);
116 |
117 | $this->validationFactory
118 | ->make(Argument::cetera())
119 | ->shouldBeCalledTimes(1)
120 | ->willReturn($this->validator);
121 | $this->validator
122 | ->fails()
123 | ->shouldBeCalledTimes(1)
124 | ->willReturn(false);
125 |
126 | $this->guidedImage
127 | ->where(Argument::cetera())
128 | ->shouldBeCalledTimes(1)
129 | ->willReturn($this->builder);
130 | $this->guidedImage
131 | ->unguard()
132 | ->shouldBeCalledTimes(1);
133 | $this->guidedImage
134 | ->reguard()
135 | ->shouldBeCalledTimes(1);
136 |
137 | $this->builder
138 | ->where(Argument::cetera())
139 | ->shouldBeCalledTimes(1)
140 | ->willReturn($this->builder);
141 | $this->builder
142 | ->first()
143 | ->shouldBeCalledTimes(1)
144 | ->willReturn(null);
145 |
146 | $this->logger
147 | ->error(Argument::cetera())
148 | ->shouldNotBeCalled();
149 |
150 | $this->fileHelper
151 | ->getImageSize(Argument::type('string'))
152 | ->willReturn(self::UPLOADED_IMAGE_SIZE);
153 |
154 | $this->subject = new ImageUploader(
155 | $this->configProvider->reveal(),
156 | $filesystemManager->reveal(),
157 | $this->fileHelper->reveal(),
158 | $this->validationFactory->reveal(),
159 | $this->guidedImage->reveal(),
160 | $this->logger->reveal()
161 | );
162 | }
163 |
164 | /**
165 | * @throws Exception
166 | */
167 | public function testUpload(): void
168 | {
169 | $this->guidedImage
170 | ->create(
171 | Argument::that(
172 | function ($argument) {
173 | return in_array($this->uploadedFile->getFilename(), $argument, true);
174 | }
175 | )
176 | )
177 | ->shouldBeCalledTimes(1);
178 |
179 | $this->uploadDisk
180 | ->putFileAs(Argument::cetera())
181 | ->shouldBeCalled();
182 |
183 | $result = $this->subject->upload($this->uploadedFile);
184 |
185 | self::assertInstanceOf(Result::class, $result);
186 | self::assertTrue($result->isSuccess());
187 | }
188 |
189 | /**
190 | * @throws Exception
191 | */
192 | public function testUploadWhenFileShouldBeReused(): void
193 | {
194 | $existingGuidedImage = $this->prophesize(GuidedImage::class)
195 | ->reveal();
196 |
197 | $this->guidedImage
198 | ->unguard()
199 | ->shouldNotBeCalled();
200 | $this->guidedImage
201 | ->create(Argument::cetera())
202 | ->shouldNotBeCalled();
203 | $this->guidedImage
204 | ->reguard()
205 | ->shouldNotBeCalled();
206 |
207 | $this->builder
208 | ->first()
209 | ->shouldBeCalledTimes(1)
210 | ->willReturn($existingGuidedImage);
211 |
212 | $this->uploadDisk
213 | ->putFileAs(Argument::cetera())
214 | ->shouldNotBeCalled();
215 |
216 | $this->logger
217 | ->error(Argument::cetera())
218 | ->shouldNotBeCalled();
219 |
220 | $result = $this->subject->upload($this->uploadedFile);
221 |
222 | self::assertInstanceOf(Result::class, $result);
223 | self::assertSame($existingGuidedImage, $result->getExtra());
224 | self::assertStringContainsStringIgnoringCase('reused', $result->getMessage());
225 | self::assertTrue($result->isSuccess());
226 | }
227 |
228 | /**
229 | * @throws Exception
230 | */
231 | public function testUploadWhenValidationFails(): void
232 | {
233 | $this->configProvider
234 | ->getUploadDirectory()
235 | ->shouldNotBeCalled();
236 | $this->configProvider
237 | ->generateUploadDateSubDirectories()
238 | ->shouldNotBeCalled();
239 |
240 | $this->validator
241 | ->fails()
242 | ->shouldBeCalledTimes(1)
243 | ->willReturn(true);
244 |
245 | $this->guidedImage
246 | ->where(Argument::cetera())
247 | ->shouldNotBeCalled();
248 | $this->guidedImage
249 | ->unguard()
250 | ->shouldNotBeCalled();
251 | $this->guidedImage
252 | ->create(Argument::cetera())
253 | ->shouldNotBeCalled();
254 | $this->guidedImage
255 | ->reguard()
256 | ->shouldNotBeCalled();
257 |
258 | $this->builder
259 | ->where(Argument::cetera())
260 | ->shouldNotBeCalled();
261 | $this->builder
262 | ->first()
263 | ->shouldNotBeCalled();
264 |
265 | $this->uploadDisk
266 | ->putFileAs(Argument::cetera())
267 | ->shouldNotBeCalled();
268 |
269 | $this->logger
270 | ->error(Argument::cetera())
271 | ->shouldNotBeCalled();
272 |
273 | $result = $this->subject->upload($this->uploadedFile);
274 |
275 | self::assertInstanceOf(Result::class, $result);
276 | self::assertFalse($result->isSuccess());
277 | self::assertStringContainsStringIgnoringCase('invalid', $result->getError());
278 | }
279 |
280 | /**
281 | * @throws Exception
282 | */
283 | public function testUploadWhenFileUploadFails(): void
284 | {
285 | $this->guidedImage
286 | ->unguard()
287 | ->shouldNotBeCalled();
288 | $this->guidedImage
289 | ->create(Argument::cetera())
290 | ->shouldNotBeCalled();
291 | $this->guidedImage
292 | ->reguard()
293 | ->shouldNotBeCalled();
294 |
295 | $this->uploadDisk
296 | ->putFileAs(Argument::cetera())
297 | ->shouldBeCalled()
298 | ->willThrow(Exception::class);
299 |
300 | $this->logger
301 | ->error(Argument::cetera())
302 | ->shouldBeCalledTimes(1);
303 |
304 | $result = $this->subject->upload($this->uploadedFile);
305 |
306 | self::assertInstanceOf(Result::class, $result);
307 | self::assertFalse($result->isSuccess());
308 | }
309 |
310 | /**
311 | * @throws Exception
312 | */
313 | public function testUploadFromUrl(): void
314 | {
315 | $url = '//url';
316 | $imageContent = 'foo';
317 | $tempName = 'tmp.name';
318 | $systemTempDir = 'sys.temp';
319 | $mimeType = 'img/jpeg';
320 |
321 | $this->fileHelper
322 | ->getSystemTempDirectory()
323 | ->shouldBeCalledTimes(1)
324 | ->willReturn($systemTempDir);
325 | $this->fileHelper
326 | ->tempName($systemTempDir, self::TEMP_FILE_PREFIX)
327 | ->shouldBeCalledTimes(1)
328 | ->willReturn($tempName);
329 | $this->fileHelper
330 | ->getContents($url)
331 | ->shouldBeCalledTimes(1)
332 | ->willReturn($imageContent);
333 | $this->fileHelper
334 | ->putContents($tempName, $imageContent)
335 | ->shouldBeCalledTimes(1)
336 | ->willReturn(1234);
337 | $this->fileHelper
338 | ->getMimeType($tempName)
339 | ->shouldBeCalledTimes(1)
340 | ->willReturn($mimeType);
341 | $this->fileHelper
342 | ->mime2Ext($mimeType)
343 | ->shouldBeCalledTimes(1)
344 | ->willReturn('jpeg');
345 | $this->fileHelper
346 | ->createUploadedFile($tempName, Argument::type('string'), $mimeType)
347 | ->shouldBeCalledTimes(1)
348 | ->willReturn($this->uploadedFile);
349 | $this->fileHelper
350 | ->unlink($tempName)
351 | ->shouldBeCalledTimes(1)
352 | ->willReturn(true);
353 |
354 | $this->configProvider
355 | ->getImageRules()
356 | ->shouldNotBeCalled();
357 |
358 | $this->validationFactory
359 | ->make(Argument::cetera())
360 | ->shouldNotBeCalled();
361 |
362 | $this->validator
363 | ->fails()
364 | ->shouldNotBeCalled();
365 |
366 | $this->guidedImage
367 | ->create(
368 | Argument::that(
369 | function ($argument) {
370 | return in_array($this->uploadedFile->getFilename(), $argument, true);
371 | }
372 | )
373 | )
374 | ->shouldBeCalledTimes(1);
375 |
376 | $this->uploadDisk
377 | ->putFileAs(Argument::cetera())
378 | ->shouldBeCalled();
379 |
380 | $result = $this->subject->uploadFromUrl($url);
381 |
382 | self::assertInstanceOf(Result::class, $result);
383 | self::assertTrue($result->isSuccess());
384 | }
385 |
386 | private function getUploadedFileMock(): UploadedFile
387 | {
388 | $filename = 'my-image';
389 |
390 | return Mockery::mock(
391 | UploadedFile::class,
392 | [
393 | 'getFilename' => $filename,
394 | 'getClientOriginalName' => $filename,
395 | 'getClientOriginalExtension' => 'jpg',
396 | 'getMimeType' => 'image/jpeg',
397 | 'getSize' => 80000,
398 | 'getRealPath' => $filename,
399 | 'move' => null,
400 | ]
401 | );
402 | }
403 | }
404 |
--------------------------------------------------------------------------------
/tests/Unit/TestCase.php:
--------------------------------------------------------------------------------
1 | setGroups(array_merge($this->groups(), [self::GROUP]));
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 |