Issues (1497)

src/fields/OptimizedImages.php (88 issues)

1
<?php
2
/**
3
 * Image Optimize plugin for Craft CMS
4
 *
5
 * Automatically optimize images after they've been transformed
6
 *
7
 * @link      https://nystudio107.com
0 ignored issues
show
The tag in position 1 should be the @copyright tag
Loading history...
8
 * @copyright Copyright (c) 2017 nystudio107
0 ignored issues
show
@copyright tag must contain a year and the name of the copyright holder
Loading history...
9
 */
0 ignored issues
show
PHP version not specified
Loading history...
Missing @category tag in file comment
Loading history...
Missing @package tag in file comment
Loading history...
Missing @author tag in file comment
Loading history...
Missing @license tag in file comment
Loading history...
10
11
namespace nystudio107\imageoptimize\fields;
12
13
use Craft;
14
use craft\base\ElementInterface;
0 ignored issues
show
The type craft\base\ElementInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
15
use craft\base\Field;
16
use craft\elements\Asset;
17
use craft\fields\Matrix;
18
use craft\helpers\Html;
19
use craft\helpers\Json;
20
use craft\models\FieldLayout;
21
use craft\models\Volume;
22
use craft\validators\ArrayValidator;
23
use GraphQL\Type\Definition\Type;
24
use nystudio107\imageoptimize\assetbundles\imageoptimize\ImageOptimizeAsset;
25
use nystudio107\imageoptimize\fields\OptimizedImages as OptimizedImagesField;
26
use nystudio107\imageoptimize\gql\types\generators\OptimizedImagesGenerator;
27
use nystudio107\imageoptimize\ImageOptimize;
28
use nystudio107\imageoptimize\models\OptimizedImage;
29
use nystudio107\imageoptimize\models\Settings;
30
use ReflectionClass;
31
use ReflectionException;
32
use yii\base\InvalidConfigException;
33
use yii\db\Exception;
34
use yii\db\Schema;
35
use function is_array;
36
use function is_string;
37
38
/** @noinspection MissingPropertyAnnotationsInspection */
0 ignored issues
show
The open comment tag must be the only content on the line
Loading history...
The close comment tag must be the only content on the line
Loading history...
Missing short description in doc comment
Loading history...
39
40
/**
0 ignored issues
show
Missing short description in doc comment
Loading history...
41
 * @author    nystudio107
0 ignored issues
show
The tag in position 1 should be the @package tag
Loading history...
Content of the @author tag must be in the form "Display Name <[email protected]>"
Loading history...
Tag value for @author tag indented incorrectly; expected 2 spaces but found 4
Loading history...
42
 * @package   ImageOptimize
0 ignored issues
show
Tag value for @package tag indented incorrectly; expected 1 spaces but found 3
Loading history...
43
 * @since     1.2.0
0 ignored issues
show
The tag in position 3 should be the @author tag
Loading history...
Tag value for @since tag indented incorrectly; expected 3 spaces but found 5
Loading history...
44
 */
0 ignored issues
show
Missing @category tag in class comment
Loading history...
Missing @license tag in class comment
Loading history...
Missing @link tag in class comment
Loading history...
45
class OptimizedImages extends Field
46
{
47
    // Constants
48
    // =========================================================================
49
50
    public const DEFAULT_ASPECT_RATIOS = [
51
        ['x' => 16, 'y' => 9],
52
    ];
53
    public const DEFAULT_IMAGE_VARIANTS = [
54
        [
55
            'width' => 1200,
56
            'useAspectRatio' => true,
57
            'aspectRatioX' => 16.0,
58
            'aspectRatioY' => 9.0,
59
            'retinaSizes' => ['1'],
60
            'quality' => 82,
61
            'format' => 'jpg',
62
        ],
63
    ];
64
65
    public const MAX_VOLUME_SUBFOLDERS = 30;
66
67
    // Public Properties
68
    // =========================================================================
69
70
    /**
0 ignored issues
show
Missing short description in doc comment
Loading history...
71
     * @var array
72
     */
73
    public array $fieldVolumeSettings = [];
74
75
    /**
0 ignored issues
show
Missing short description in doc comment
Loading history...
76
     * @var array
77
     */
78
    public array $ignoreFilesOfType = [];
79
80
    /**
0 ignored issues
show
Missing short description in doc comment
Loading history...
81
     * @var bool
82
     */
83
    public bool $displayOptimizedImageVariants = true;
84
85
    /**
0 ignored issues
show
Missing short description in doc comment
Loading history...
86
     * @var bool
87
     */
88
    public bool $displayDominantColorPalette = true;
89
90
    /**
0 ignored issues
show
Missing short description in doc comment
Loading history...
91
     * @var bool
92
     */
93
    public bool $displayLazyLoadPlaceholderImages = true;
94
95
    /**
0 ignored issues
show
Missing short description in doc comment
Loading history...
96
     * @var array
97
     */
98
    public array $variants = [];
99
100
    // Private Properties
101
    // =========================================================================
102
103
    /**
0 ignored issues
show
Missing short description in doc comment
Loading history...
104
     * @var array
105
     */
106
    private array $aspectRatios = [];
0 ignored issues
show
Private member variable "aspectRatios" must be prefixed with an underscore
Loading history...
107
108
    // Static Methods
109
    // =========================================================================
110
111
    /**
0 ignored issues
show
Parameter $config should have a doc-comment as per coding-style.
Loading history...
Missing short description in doc comment
Loading history...
112
     * @inheritdoc
113
     */
114
    public function __construct(array $config = [])
115
    {
116
        // Unset any deprecated properties
117
        if (!empty($config)) {
118
            unset($config['transformMethod'], $config['imgixDomain']);
119
        }
120
        parent::__construct($config);
121
    }
122
123
    /**
0 ignored issues
show
Missing short description in doc comment
Loading history...
124
     * @inheritdoc
125
     */
0 ignored issues
show
Missing @return tag in function comment
Loading history...
126
    public static function displayName(): string
127
    {
128
        return 'OptimizedImages';
129
    }
130
131
    /**
0 ignored issues
show
Missing short description in doc comment
Loading history...
132
     * @inheritdoc
133
     */
0 ignored issues
show
Missing @return tag in function comment
Loading history...
134
    public static function icon(): string
135
    {
136
        return '@nystudio107/imageoptimize/icon-mask.svg';
137
    }
138
139
    /**
0 ignored issues
show
Missing short description in doc comment
Loading history...
140
     * @inheritdoc
141
     */
0 ignored issues
show
Missing @return tag in function comment
Loading history...
142
    public static function phpType(): string
143
    {
144
        return sprintf('\\%s', OptimizedImage::class);
145
    }
146
147
    /**
0 ignored issues
show
Missing short description in doc comment
Loading history...
148
     * @inheritdoc
149
     */
0 ignored issues
show
Missing @return tag in function comment
Loading history...
150
    public static function dbType(): array|string|null
151
    {
152
        return Schema::TYPE_TEXT;
153
    }
154
155
    // Public Methods
156
    // =========================================================================
157
158
    /**
0 ignored issues
show
Missing short description in doc comment
Loading history...
159
     * @inheritdoc
160
     */
0 ignored issues
show
Missing @return tag in function comment
Loading history...
161
    public function init(): void
162
    {
163
        parent::init();
164
165
        // Handle cases where the plugin has been uninstalled
166
        if (ImageOptimize::$plugin !== null) {
167
            /** @var ?Settings $settings */
0 ignored issues
show
The open comment tag must be the only content on the line
Loading history...
Missing short description in doc comment
Loading history...
The close comment tag must be the only content on the line
Loading history...
168
            $settings = ImageOptimize::$plugin->getSettings();
169
            if ($settings) {
0 ignored issues
show
$settings is of type nystudio107\imageoptimize\models\Settings, thus it always evaluated to true.
Loading history...
170
                if (empty($this->variants)) {
171
                    $this->variants = $settings->defaultVariants;
172
                }
173
                $this->aspectRatios = $settings->defaultAspectRatios;
174
            }
175
        }
176
        // If the user has deleted all default aspect ratios, provide a fallback
177
        if (empty($this->aspectRatios)) {
178
            $this->aspectRatios = self::DEFAULT_ASPECT_RATIOS;
179
        }
180
        // If the user has deleted all default variants, provide a fallback
181
        if (empty($this->variants)) {
182
            $this->variants = self::DEFAULT_IMAGE_VARIANTS;
183
        }
184
    }
185
186
    /**
0 ignored issues
show
Missing short description in doc comment
Loading history...
187
     * @inheritdoc
188
     */
0 ignored issues
show
Missing @return tag in function comment
Loading history...
189
    public function rules(): array
190
    {
191
        $rules = parent::rules();
192
        return array_merge($rules, [
0 ignored issues
show
The opening parenthesis of a multi-line function call should be the last content on the line.
Loading history...
193
            [
194
                [
195
                    'displayOptimizedImageVariants',
196
                    'displayDominantColorPalette',
197
                    'displayLazyLoadPlaceholderImages',
198
                ],
199
                'boolean',
200
            ],
201
            [
202
                [
203
                    'ignoreFilesOfType',
204
                    'variants',
205
                ],
206
                ArrayValidator::class,
207
            ],
208
        ]);
0 ignored issues
show
For multi-line function calls, the closing parenthesis should be on a new line.

If a function call spawns multiple lines, the coding standard suggests to move the closing parenthesis to a new line:

someFunctionCall(
    $firstArgument,
    $secondArgument,
    $thirdArgument
); // Closing parenthesis on a new line.
Loading history...
209
    }
210
211
    /**
0 ignored issues
show
Missing short description in doc comment
Loading history...
212
     * @inheritdoc
213
     * @since 1.6.2
0 ignored issues
show
Tag value for @since tag indented incorrectly; expected 6 spaces but found 1
Loading history...
214
     */
0 ignored issues
show
Missing @return tag in function comment
Loading history...
215
    public function getContentGqlType(): Type|array
216
    {
217
        $typeArray = OptimizedImagesGenerator::generateTypes($this);
218
219
        return [
220
            'name' => $this->handle,
221
            'description' => 'Optimized Images field',
222
            'type' => array_shift($typeArray),
223
        ];
224
    }
225
226
    /**
0 ignored issues
show
Missing short description in doc comment
Loading history...
227
     * @inheritdoc
228
     * @since 1.7.0
0 ignored issues
show
Tag value for @since tag indented incorrectly; expected 6 spaces but found 1
Loading history...
229
     */
0 ignored issues
show
Missing @return tag in function comment
Loading history...
230
    public function useFieldset(): bool
231
    {
232
        return true;
233
    }
234
235
    /**
0 ignored issues
show
Missing short description in doc comment
Loading history...
Parameter $element should have a doc-comment as per coding-style.
Loading history...
Parameter $isNew should have a doc-comment as per coding-style.
Loading history...
236
     * @inheritdoc
237
     */
0 ignored issues
show
Missing @return tag in function comment
Loading history...
238
    public function afterElementSave(ElementInterface $element, bool $isNew): void
239
    {
240
        parent::afterElementSave($element, $isNew);
241
        // Update our OptimizedImages Field data now that the Asset has been saved
242
        // If this element is propagating, we don't need to redo the image saving for each site
243
        if ($element instanceof Asset && $element->id !== null && !$element->propagating) {
244
            // If the scenario is Asset::SCENARIO_FILEOPS or Asset::SCENARIO_MOVE (if using Craft > v3.7.1) treat it as a new asset
245
            $scenario = $element->getScenario();
246
            $request = Craft::$app->getRequest();
247
            if ($isNew || $scenario === Asset::SCENARIO_FILEOPS || $scenario === Asset::SCENARIO_MOVE) {
248
                /**
249
                 * If this is a newly uploaded/created Asset, we can save the variants
250
                 * via a queue job to prevent it from blocking
251
                 */
252
                ImageOptimize::$plugin->optimizedImages->resaveAsset($element->id);
253
            } elseif (!$request->isConsoleRequest && $request->getPathInfo() === 'assets/save-image') {
254
                /**
255
                 * If it's not a newly uploaded/created Asset, check to see if the image
256
                 * itself is being updated (via the ImageEditor). If so, update the
257
                 * variants immediately so the AssetSelectorHud displays the new images
258
                 */
259
                try {
260
                    ImageOptimize::$plugin->optimizedImages->updateOptimizedImageFieldData($this, $element);
261
                } catch (Exception $e) {
262
                    Craft::error($e->getMessage(), __METHOD__);
263
                }
264
            } else {
265
                ImageOptimize::$plugin->optimizedImages->resaveAsset($element->id);
266
            }
267
        }
268
    }
269
270
    /**
0 ignored issues
show
Parameter $value should have a doc-comment as per coding-style.
Loading history...
Parameter $asset should have a doc-comment as per coding-style.
Loading history...
Missing short description in doc comment
Loading history...
271
     * @inheritdoc
272
     */
0 ignored issues
show
Missing @return tag in function comment
Loading history...
273
    public function normalizeValue($value, ElementInterface $asset = null): mixed
274
    {
275
        // If we're passed in a string, assume it's JSON-encoded, and decode it
276
        if (is_string($value) && !empty($value)) {
277
            $value = Json::decodeIfJson($value);
278
        }
279
        // If we're passed in an array, make a model from it
280
        if (is_array($value)) {
281
            // Create a new OptimizedImage model and populate it
282
            $model = new OptimizedImage($value);
283
        } elseif ($value instanceof OptimizedImage) {
284
            $model = $value;
285
        } else {
286
            // Just create a new empty model
287
            $model = new OptimizedImage([]);
288
        }
289
290
        return $model;
291
    }
292
293
    /**
0 ignored issues
show
Missing short description in doc comment
Loading history...
294
     * @inheritdoc
295
     */
0 ignored issues
show
Missing @return tag in function comment
Loading history...
296
    public function getSettingsHtml(): null|string
297
    {
298
        $namespace = Craft::$app->getView()->getNamespace();
299
        if (str_contains($namespace, Matrix::class)) {
300
            // Render an error template, since the field only works when attached to an Asset
301
            try {
302
                return Craft::$app->getView()->renderTemplate(
303
                    'image-optimize/_components/fields/OptimizedImages_error',
304
                    [
305
                    ]
306
                );
307
            } catch (Exception $e) {
308
                Craft::error($e->getMessage(), __METHOD__);
309
            }
310
        }
311
        // Register our asset bundle
312
        try {
313
            Craft::$app->getView()->registerAssetBundle(ImageOptimizeAsset::class);
314
        } catch (InvalidConfigException $e) {
315
            Craft::error($e->getMessage(), __METHOD__);
316
        }
317
318
        try {
319
            $reflect = new ReflectionClass($this);
320
            $thisId = $reflect->getShortName();
321
        } catch (ReflectionException $e) {
322
            Craft::error($e->getMessage(), __METHOD__);
323
            $thisId = 0;
324
        }
325
        // Get our id and namespace
326
        $id = Html::id($thisId);
327
        $namespacedId = Craft::$app->getView()->namespaceInputId($id);
328
        $namespacePrefix = Craft::$app->getView()->namespaceInputName($thisId);
329
        $sizesWrapperId = Craft::$app->getView()->namespaceInputId('sizes-wrapper');
330
        $view = Craft::$app->getView();
331
        $view->registerJs(
332
            'document.addEventListener("vite-script-loaded", function (e) {' .
333
            'if (e.detail.path === "../src/web/assets/src/js/OptimizedImagesField.js") {' .
334
            'new Craft.OptimizedImagesInput(' .
335
            '"' . $namespacedId . '", ' .
336
            '"' . $namespacePrefix . '",' .
337
            '"' . $sizesWrapperId . '"' .
338
            ');' .
339
            '}' .
340
            '});'
341
        );
342
343
        // Prep our aspect ratios
344
        $aspectRatios = [];
345
        $index = 1;
346
        foreach ($this->aspectRatios as $aspectRatio) {
347
            if ($index % 6 === 0) {
348
                $aspectRatio['break'] = true;
349
            }
350
            $aspectRatios[] = $aspectRatio;
351
            $index++;
352
        }
353
        $aspectRatio = ['x' => 2, 'y' => 2, 'custom' => true];
354
        $aspectRatios[] = $aspectRatio;
355
        // Get only the user-editable settings
356
        /** @var Settings $settings */
0 ignored issues
show
The open comment tag must be the only content on the line
Loading history...
Missing short description in doc comment
Loading history...
The close comment tag must be the only content on the line
Loading history...
357
        $settings = ImageOptimize::$plugin->getSettings();
0 ignored issues
show
The method getSettings() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

357
        /** @scrutinizer ignore-call */ 
358
        $settings = ImageOptimize::$plugin->getSettings();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
358
359
        // Render the settings template
360
        try {
361
            return Craft::$app->getView()->renderTemplate(
362
                'image-optimize/_components/fields/OptimizedImages_settings',
363
                [
364
                    'field' => $this,
365
                    'settings' => $settings,
366
                    'aspectRatios' => $aspectRatios,
367
                    'id' => $id,
368
                    'name' => $this->handle,
369
                    'namespace' => $namespacedId,
370
                    'fieldVolumes' => $this->getFieldVolumeInfo($this->handle),
371
                ]
372
            );
373
        } catch (Exception $e) {
374
            Craft::error($e->getMessage(), __METHOD__);
375
        }
376
377
        return '';
378
    }
379
380
    /**
0 ignored issues
show
Parameter $value should have a doc-comment as per coding-style.
Loading history...
Parameter $element should have a doc-comment as per coding-style.
Loading history...
Missing short description in doc comment
Loading history...
381
     * @inheritdoc
382
     */
0 ignored issues
show
Missing @return tag in function comment
Loading history...
383
    public function getInputHtml($value, ElementInterface $element = null): string
384
    {
385
        if ($element instanceof Asset && $this->handle !== null) {
386
            /** @var Asset $element */
0 ignored issues
show
The open comment tag must be the only content on the line
Loading history...
Missing short description in doc comment
Loading history...
The close comment tag must be the only content on the line
Loading history...
387
            // Register our asset bundle
388
            try {
389
                Craft::$app->getView()->registerAssetBundle(ImageOptimizeAsset::class);
390
            } catch (InvalidConfigException $e) {
391
                Craft::error($e->getMessage(), __METHOD__);
392
            }
393
394
            // Get our id and namespace
395
            $id = Html::id($this->handle);
396
            $nameSpaceId = Craft::$app->getView()->namespaceInputId($id);
397
398
            // Variables to pass down to our field JavaScript to let it namespace properly
399
            $jsonVars = [
400
                'id' => $id,
401
                'name' => $this->handle,
402
                'namespace' => $nameSpaceId,
403
                'prefix' => Craft::$app->getView()->namespaceInputId(''),
404
            ];
405
            $jsonVars = Json::encode($jsonVars);
406
            $view = Craft::$app->getView();
407
            $view->registerJs(
408
                'document.addEventListener("vite-script-loaded", function (e) {' .
409
                'if (e.detail.path === "../src/web/assets/src/js/OptimizedImagesField.js") {' .
410
                "$('#{$nameSpaceId}-field').ImageOptimizeOptimizedImages(" .
411
                $jsonVars .
412
                ");" .
413
                '}' .
414
                '});'
415
            );
416
417
            /** @var Settings $settings */
0 ignored issues
show
The open comment tag must be the only content on the line
Loading history...
Missing short description in doc comment
Loading history...
The close comment tag must be the only content on the line
Loading history...
418
            $settings = ImageOptimize::$plugin->getSettings();
419
            $createVariants = ImageOptimize::$plugin->optimizedImages->shouldCreateVariants($this, $element);
420
421
            // Render the input template
422
            try {
423
                return Craft::$app->getView()->renderTemplate(
424
                    'image-optimize/_components/fields/OptimizedImages_input',
425
                    [
426
                        'name' => $this->handle,
427
                        'value' => $value,
428
                        'variants' => $this->variants,
429
                        'field' => $this,
430
                        'settings' => $settings,
431
                        'elementId' => $element->id,
432
                        'format' => $element->getExtension(),
433
                        'id' => $id,
434
                        'nameSpaceId' => $nameSpaceId,
435
                        'createVariants' => $createVariants,
436
                    ]
437
                );
438
            } catch (Exception $e) {
439
                Craft::error($e->getMessage(), __METHOD__);
440
            }
441
        }
442
443
        // Render an error template, since the field only works when attached to an Asset
444
        try {
445
            return Craft::$app->getView()->renderTemplate(
446
                'image-optimize/_components/fields/OptimizedImages_error',
447
                [
448
                ]
449
            );
450
        } catch (Exception $e) {
451
            Craft::error($e->getMessage(), __METHOD__);
452
        }
453
454
        return '';
455
    }
456
457
    // Protected Methods
458
    // =========================================================================
459
460
    /**
461
     * Returns an array of asset volumes and their sub-folders
462
     *
463
     * @param string|null $fieldHandle
0 ignored issues
show
Missing parameter comment
Loading history...
464
     *
465
     * @return array
466
     * @throws InvalidConfigException
467
     */
468
    protected function getFieldVolumeInfo(?string $fieldHandle): array
469
    {
470
        $result = [];
471
        if ($fieldHandle !== null) {
472
            $volumes = Craft::$app->getVolumes()->getAllVolumes();
473
            $assets = Craft::$app->getAssets();
474
            foreach ($volumes as $volume) {
475
                if (($volume instanceof Volume) && $this->volumeHasField($volume, $fieldHandle)) {
476
                    $tree = $assets->getFolderTreeByVolumeIds([$volume->id]);
477
                    $result[] = [
478
                        'name' => $volume->name,
479
                        'handle' => $volume->handle,
480
                        'subfolders' => $this->assembleSourceList($tree),
481
                    ];
482
                }
483
            }
484
        }
485
        // If there are too many sub-folders in an Asset volume, don't display them, return an empty array
486
        if (count($result) > self::MAX_VOLUME_SUBFOLDERS) {
487
            $result = [];
488
        }
489
490
        return $result;
491
    }
492
493
    /**
494
     * See if the passed $volume has an OptimizedImagesField with the handle $fieldHandle
495
     *
496
     * @param Volume $volume
0 ignored issues
show
Missing parameter comment
Loading history...
497
     * @param string $fieldHandle
0 ignored issues
show
Missing parameter comment
Loading history...
498
     *
499
     * @return bool
500
     */
501
    protected function volumeHasField(Volume $volume, string $fieldHandle): bool
502
    {
503
        $result = false;
504
        /** @var ?FieldLayout $fieldLayout */
0 ignored issues
show
The open comment tag must be the only content on the line
Loading history...
Missing short description in doc comment
Loading history...
The close comment tag must be the only content on the line
Loading history...
505
        $fieldLayout = $volume->getFieldLayout();
506
        // Loop through the fields in the layout to see if there is an OptimizedImages field
507
        if ($fieldLayout) {
508
            $fields = $fieldLayout->getCustomFields();
509
            foreach ($fields as $field) {
510
                if ($field instanceof OptimizedImagesField && $field->handle === $fieldHandle) {
511
                    $result = true;
512
                }
513
            }
514
        }
515
516
        return $result;
517
    }
518
519
    /**
520
     * Transforms an asset folder tree into a source list.
521
     *
522
     * @param array $folders
0 ignored issues
show
Missing parameter comment
Loading history...
523
     * @param bool $includeNestedFolders
0 ignored issues
show
Missing parameter comment
Loading history...
Expected 2 spaces after parameter type; 1 found
Loading history...
524
     *
525
     * @return array
526
     */
527
    protected function assembleSourceList(array $folders, bool $includeNestedFolders = true): array
528
    {
529
        $sources = [];
530
531
        foreach ($folders as $folder) {
532
            $children = $folder->getChildren();
533
            foreach ($children as $child) {
534
                $sources[$child->name] = $child->name;
535
            }
536
        }
537
538
        return $sources;
539
    }
540
}
541