Issues (1497)

src/ImageOptimize.php (1 issue)

1
<?php
2
/**
3
 * ImageOptimize plugin for Craft CMS
4
 *
5
 * Automatically optimize images after they've been transformed
6
 *
7
 * @link      https://nystudio107.com
8
 * @copyright Copyright (c) 2017 nystudio107
9
 */
10
11
namespace nystudio107\imageoptimize;
12
13
use Craft;
14
use craft\base\Field;
15
use craft\base\Model;
16
use craft\base\Plugin;
17
use craft\elements\Asset;
18
use craft\events\DefineAssetThumbUrlEvent;
19
use craft\events\DefineAssetUrlEvent;
20
use craft\events\ElementEvent;
21
use craft\events\FieldEvent;
22
use craft\events\ImageTransformerOperationEvent;
23
use craft\events\PluginEvent;
24
use craft\events\RegisterComponentTypesEvent;
25
use craft\events\RegisterTemplateRootsEvent;
26
use craft\events\RegisterUrlRulesEvent;
27
use craft\events\ReplaceAssetEvent;
28
use craft\events\VolumeEvent;
29
use craft\helpers\ArrayHelper;
30
use craft\helpers\UrlHelper;
31
use craft\imagetransforms\ImageTransformer;
32
use craft\models\FieldLayout;
33
use craft\services\Assets;
34
use craft\services\Elements;
35
use craft\services\Fields;
36
use craft\services\Plugins;
37
use craft\services\Utilities;
38
use craft\services\Volumes;
39
use craft\web\Controller;
40
use craft\web\TemplateResponseBehavior;
41
use craft\web\twig\variables\CraftVariable;
42
use craft\web\UrlManager;
43
use craft\web\View;
44
use nystudio107\imageoptimize\fields\OptimizedImages;
45
use nystudio107\imageoptimize\imagetransforms\CraftImageTransform;
46
use nystudio107\imageoptimize\imagetransforms\ImageTransformInterface;
47
use nystudio107\imageoptimize\models\Settings;
48
use nystudio107\imageoptimize\services\ServicesTrait;
49
use nystudio107\imageoptimize\utilities\ImageOptimizeUtility;
50
use nystudio107\imageoptimize\variables\ImageOptimizeVariable;
51
use ReflectionClassConstant;
52
use ReflectionException;
53
use yii\base\Event;
54
use yii\base\Exception;
55
use yii\base\InvalidConfigException;
56
use yii\web\Response;
57
use function function_exists;
58
59
/** @noinspection MissingPropertyAnnotationsInspection */
60
61
/**
62
 * Class ImageOptimize
63
 *
64
 * @author    nystudio107
65
 * @package   ImageOptimize
66
 * @since     1.0.0
67
 *
68
 * @property ImageTransformInterface $transformMethod
69
 */
70
class ImageOptimize extends Plugin
71
{
72
    // Traits
73
    // =========================================================================
74
75
    use ServicesTrait;
76
77
    // Static Properties
78
    // =========================================================================
79
80
    /**
81
     * @var ?ImageOptimize
82
     */
83
    public static ?ImageOptimize $plugin = null;
84
85
    /**
86
     * @var bool
87
     */
88
    public static bool $generatePlaceholders = true;
89
90
    // Public Properties
91
    // =========================================================================
92
    /**
93
     * @var string
94
     */
95
    public string $schemaVersion = '1.0.0';
96
97
    /**
98
     * @var bool
99
     */
100
    public bool $hasCpSection = false;
101
102
    /**
103
     * @var bool
104
     */
105
    public bool $hasCpSettings = true;
106
107
    // Public Methods
108
    // =========================================================================
109
110
    /**
111
     * @inheritdoc
112
     */
113
    public function init(): void
114
    {
115
        parent::init();
116
        self::$plugin = $this;
117
        // Handle any console commands
118
        $request = Craft::$app->getRequest();
119
        if ($request->getIsConsoleRequest()) {
120
            $this->controllerNamespace = 'nystudio107\imageoptimize\console\controllers';
121
        }
122
        // Set the image transform component
123
        $this->setImageTransformComponent();
124
        // Add in our Craft components
125
        $this->addComponents();
126
        // Install our global event handlers
127
        $this->installEventHandlers();
128
        // Log that the plugin has loaded
129
        Craft::info(
130
            Craft::t(
131
                'image-optimize',
132
                '{name} plugin loaded',
133
                ['name' => $this->name]
134
            ),
135
            __METHOD__
136
        );
137
    }
138
139
    /**
140
     * @inheritdoc
141
     */
142
    public function getSettingsResponse(): TemplateResponseBehavior|Response
143
    {
144
        $view = Craft::$app->getView();
145
        $namespace = $view->getNamespace();
146
        $view->setNamespace('settings');
147
        $settingsHtml = $this->settingsHtml();
148
        $view->setNamespace($namespace);
149
        /** @var Controller $controller */
150
        $controller = Craft::$app->controller;
0 ignored issues
show
Documentation Bug introduced by
It seems like Craft::app->controller can also be of type yii\web\Controller. However, the property $controller is declared as type yii\console\Controller. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
151
152
        return $controller->renderTemplate('image-optimize/settings/index.twig', [
153
            'plugin' => $this,
154
            'settingsHtml' => $settingsHtml,
155
        ]);
156
    }
157
158
    /**
159
     * @inheritdoc
160
     */
161
    public function settingsHtml(): ?string
162
    {
163
        // Get only the user-editable settings
164
        /** @var Settings $settings */
165
        $settings = $this->getSettings();
166
167
        // Get the image transform types
168
        $allImageTransformTypes = self::$plugin->optimize->getAllImageTransformTypes();
169
        $imageTransformTypeOptions = [];
170
        /** @var ImageTransformInterface $class */
171
        foreach ($allImageTransformTypes as $class) {
172
            if ($class::isSelectable()) {
173
                $imageTransformTypeOptions[] = [
174
                    'value' => $class,
175
                    'label' => $class::displayName(),
176
                ];
177
            }
178
        }
179
        // Sort them by name
180
        ArrayHelper::multisort($imageTransformTypeOptions, 'label');
181
182
        // Render the settings template
183
        try {
184
            return Craft::$app->getView()->renderTemplate(
185
                'image-optimize/settings/_settings.twig',
186
                [
187
                    'settings' => $settings,
188
                    'gdInstalled' => function_exists('imagecreatefromjpeg'),
189
                    'imageTransformTypeOptions' => $imageTransformTypeOptions,
190
                    'allImageTransformTypes' => $allImageTransformTypes,
191
                    'imageTransform' => self::$plugin->transformMethod,
192
                ]
193
            );
194
        } catch (Exception $e) {
195
            Craft::error($e->getMessage(), __METHOD__);
196
        }
197
198
        return '';
199
    }
200
201
    // Protected Methods
202
    // =========================================================================
203
204
    /**
205
     * @inheritdoc
206
     */
207
    protected function createSettingsModel(): ?Model
208
    {
209
        return new Settings();
210
    }
211
212
    /**
213
     * Set the transformMethod component
214
     */
215
    protected function setImageTransformComponent(): void
216
    {
217
        /** @var Settings $settings */
218
        $settings = $this->getSettings();
219
        $definition = array_merge(
220
            $settings->imageTransformTypeSettings[$settings->transformClass] ?? [],
221
            ['class' => $settings->transformClass]
222
        );
223
        try {
224
            $this->set('transformMethod', $definition);
225
        } catch (InvalidConfigException $e) {
226
            Craft::error($e->getMessage(), __METHOD__);
227
        }
228
    }
229
230
    /**
231
     * Add in our Craft components
232
     */
233
    protected function addComponents(): void
234
    {
235
        // Register our variables
236
        Event::on(
237
            CraftVariable::class,
238
            CraftVariable::EVENT_INIT,
239
            function(Event $event) {
240
                /** @var CraftVariable $variable */
241
                $variable = $event->sender;
242
                $variable->set('imageOptimize', [
243
                    'class' => ImageOptimizeVariable::class,
244
                    'viteService' => $this->vite,
245
                ]);
246
            }
247
        );
248
249
        // Register our Field
250
        Event::on(
251
            Fields::class,
252
            Fields::EVENT_REGISTER_FIELD_TYPES,
253
            static function(RegisterComponentTypesEvent $event) {
254
                Craft::debug(
255
                    'Fields::EVENT_REGISTER_FIELD_TYPES',
256
                    __METHOD__
257
                );
258
                $event->types[] = OptimizedImages::class;
259
            }
260
        );
261
262
        // Register our Utility only if they are using the CraftImageTransform method
263
        if (self::$plugin->transformMethod instanceof CraftImageTransform) {
264
            Event::on(
265
                Utilities::class,
266
                Utilities::EVENT_REGISTER_UTILITIES,
267
                static function(RegisterComponentTypesEvent $event) {
268
                    $event->types[] = ImageOptimizeUtility::class;
269
                }
270
            );
271
        }
272
    }
273
274
    /**
275
     * Install our event handlers
276
     */
277
    protected function installEventHandlers(): void
278
    {
279
        $this->installAssetEventHandlers();
280
        $this->installElementEventHandlers();
281
        $this->installMiscEventHandlers();
282
        $request = Craft::$app->getRequest();
283
        // Install only for non-console site requests
284
        if ($request->getIsSiteRequest() && !$request->getIsConsoleRequest()) {
285
            $this->installSiteEventListeners();
286
        }
287
        // Install only for non-console cp requests
288
        if ($request->getIsCpRequest() && !$request->getIsConsoleRequest()) {
289
            $this->installCpEventListeners();
290
        }
291
    }
292
293
    /**
294
     * Install our Asset event handlers
295
     */
296
    protected function installAssetEventHandlers(): void
297
    {
298
        // Use Asset::EVENT_BEFORE_DEFINE_URL if it's available
299
        // ref: https://github.com/craftcms/cms/issues/13018
300
        try {
301
            $ref = new ReflectionClassConstant(Asset::class, 'EVENT_BEFORE_DEFINE_URL');
302
        } /** @noinspection PhpRedundantCatchClauseInspection */ catch (ReflectionException) {
303
            $ref = null;
304
        }
305
        $eventName = $ref?->getDeclaringClass()->name === Asset::class
306
            ? Asset::EVENT_BEFORE_DEFINE_URL
307
            : Asset::EVENT_DEFINE_URL;
308
        // Handler: Assets::EVENT_DEFINE_URL
309
        Event::on(
310
            Asset::class,
311
            $eventName,
312
            static function(DefineAssetUrlEvent $event): void {
313
                Craft::debug(
314
                    'Asset::EVENT_DEFINE_URL',
315
                    __METHOD__
316
                );
317
                // Return the URL to the asset URL or null to let Craft handle it
318
                $event->url = ImageOptimize::$plugin->optimize->handleGetAssetUrlEvent(
319
                    $event
320
                );
321
            }
322
        );
323
324
        // Handler: Assets::EVENT_GET_ASSET_THUMB_URL
325
        Event::on(
326
            Assets::class,
327
            Assets::EVENT_DEFINE_THUMB_URL,
328
            static function(DefineAssetThumbUrlEvent $event): void {
329
                Craft::debug(
330
                    'Assets::EVENT_DEFINE_THUMB_URL',
331
                    __METHOD__
332
                );
333
                // Return the URL to the asset URL or null to let Craft handle it
334
                $event->url = ImageOptimize::$plugin->optimize->handleGetAssetThumbUrlEvent(
335
                    $event
336
                );
337
            }
338
        );
339
340
        // Handler: ImageTransformer::EVENT_TRANSFORM_IMAGE
341
        Event::on(
342
            ImageTransformer::class,
343
            ImageTransformer::EVENT_TRANSFORM_IMAGE,
344
            static function(ImageTransformerOperationEvent $event): void {
345
                Craft::debug(
346
                    'ImageTransformer::EVENT_TRANSFORM_IMAGE',
347
                    __METHOD__
348
                );
349
                // Return the path to the optimized image to _createTransformForAsset()
350
                $tempPath = ImageOptimize::$plugin->optimize->handleGenerateTransformEvent(
351
                    $event
352
                );
353
                if ($tempPath) {
354
                    // Remove the old Craft generated transform that's still sitting in the temp directory.
355
                    @unlink($event->tempPath);
356
                    $event->tempPath = $tempPath;
357
                }
358
            }
359
        );
360
361
        // Handler: ImageTransformer::EVENT_DELETE_TRANSFORMED_IMAGE
362
        Event::on(
363
            ImageTransformer::class,
364
            ImageTransformer::EVENT_DELETE_TRANSFORMED_IMAGE,
365
            static function(ImageTransformerOperationEvent $event): void {
366
                Craft::debug(
367
                    'ImageTransformer::EVENT_DELETE_TRANSFORMED_IMAGE',
368
                    __METHOD__
369
                );
370
                // Clean up any stray variant files
371
                ImageOptimize::$plugin->optimize->handleAfterDeleteTransformsEvent(
372
                    $event
373
                );
374
            }
375
        );
376
377
        // Handler: Assets::EVENT_BEFORE_REPLACE_ASSET
378
        Event::on(
379
            Assets::class,
380
            Assets::EVENT_BEFORE_REPLACE_ASSET,
381
            static function(ReplaceAssetEvent $event) {
382
                Craft::debug(
383
                    'Assets::EVENT_BEFORE_REPLACE_ASSET',
384
                    __METHOD__
385
                );
386
                $element = $event->asset;
387
                // Purge the URL
388
                $purgeUrl = ImageOptimize::$plugin->transformMethod->getPurgeUrl($element);
389
                if ($purgeUrl) {
390
                    ImageOptimize::$plugin->transformMethod->purgeUrl($purgeUrl);
391
                }
392
            }
393
        );
394
395
        // Handler: Assets::EVENT_AFTER_REPLACE_ASSET
396
        Event::on(
397
            Assets::class,
398
            Assets::EVENT_AFTER_REPLACE_ASSET,
399
            static function(ReplaceAssetEvent $event) {
400
                Craft::debug(
401
                    'Assets::EVENT_AFTER_REPLACE_ASSET',
402
                    __METHOD__
403
                );
404
                $element = $event->asset;
405
                if ($element->id !== null) {
406
                    ImageOptimize::$plugin->optimizedImages->resaveAsset($element->id, true);
407
                }
408
            }
409
        );
410
    }
411
412
    /**
413
     * Install our Element event handlers
414
     */
415
    protected function installElementEventHandlers(): void
416
    {
417
        // Handler: Elements::EVENT_BEFORE_SAVE_ELEMENT
418
        Event::on(
419
            Assets::class,
420
            Elements::EVENT_BEFORE_SAVE_ELEMENT,
421
            static function(ElementEvent $event) {
422
                Craft::debug(
423
                    'Elements::EVENT_BEFORE_SAVE_ELEMENT',
424
                    __METHOD__
425
                );
426
                /** @var Asset $asset */
427
                $asset = $event->element;
428
                if (!$event->isNew) {
429
                    // Purge the URL
430
                    $purgeUrl = ImageOptimize::$plugin->transformMethod->getPurgeUrl($asset);
431
                    if ($purgeUrl) {
432
                        ImageOptimize::$plugin->transformMethod->purgeUrl($purgeUrl);
433
                    }
434
                }
435
            }
436
        );
437
438
        // Handler: Elements::EVENT_BEFORE_DELETE_ELEMENT
439
        Event::on(
440
            Asset::class,
441
            Elements::EVENT_BEFORE_DELETE_ELEMENT,
442
            static function(ElementEvent $event) {
443
                Craft::debug(
444
                    'Elements::EVENT_BEFORE_DELETE_ELEMENT',
445
                    __METHOD__
446
                );
447
                /** @var Asset $asset */
448
                $asset = $event->element;
449
                // Purge the URL
450
                $purgeUrl = ImageOptimize::$plugin->transformMethod->getPurgeUrl($asset);
451
                if ($purgeUrl) {
452
                    ImageOptimize::$plugin->transformMethod->purgeUrl($purgeUrl);
453
                }
454
            }
455
        );
456
    }
457
458
    /**
459
     * Install our miscellaneous event handlers
460
     */
461
    protected function installMiscEventHandlers(): void
462
    {
463
        // Handler: Fields::EVENT_AFTER_SAVE_FIELD
464
        Event::on(
465
            Fields::class,
466
            Fields::EVENT_AFTER_SAVE_FIELD,
467
            function(FieldEvent $event) {
468
                Craft::debug(
469
                    'Fields::EVENT_AFTER_SAVE_FIELD',
470
                    __METHOD__
471
                );
472
                /** @var Settings $settings */
473
                $settings = $this->getSettings();
474
                if (!$event->isNew && $settings->automaticallyResaveImageVariants) {
475
                    $this->checkForOptimizedImagesField($event);
476
                }
477
            }
478
        );
479
480
        // Handler: Plugins::EVENT_AFTER_SAVE_PLUGIN_SETTINGS
481
        Event::on(
482
            Plugins::class,
483
            Plugins::EVENT_AFTER_SAVE_PLUGIN_SETTINGS,
484
            function(PluginEvent $event) {
485
                if ($event->plugin === $this) {
486
                    Craft::debug(
487
                        'Plugins::EVENT_AFTER_SAVE_PLUGIN_SETTINGS',
488
                        __METHOD__
489
                    );
490
                    /** @var ?Settings $settings */
491
                    $settings = $this->getSettings();
492
                    if (($settings !== null) && $settings->automaticallyResaveImageVariants) {
493
                        // After they have changed the settings, resave all the assets
494
                        ImageOptimize::$plugin->optimizedImages->resaveAllVolumesAssets();
495
                    }
496
                }
497
            }
498
        );
499
500
        // Handler: Volumes::EVENT_AFTER_SAVE_VOLUME
501
        Event::on(
502
            Volumes::class,
503
            Volumes::EVENT_AFTER_SAVE_VOLUME,
504
            function(VolumeEvent $event) {
505
                Craft::debug(
506
                    'Volumes::EVENT_AFTER_SAVE_VOLUME',
507
                    __METHOD__
508
                );
509
                /** @var ?Settings $settings */
510
                $settings = $this->getSettings();
511
                // Only worry about this volume if it's not new
512
                if (($settings !== null) && !$event->isNew && $settings->automaticallyResaveImageVariants) {
513
                    $volume = $event->volume;
514
                    ImageOptimize::$plugin->optimizedImages->resaveVolumeAssets($volume);
515
                }
516
            }
517
        );
518
519
        // Handler: Plugins::EVENT_AFTER_INSTALL_PLUGIN
520
        Event::on(
521
            Plugins::class,
522
            Plugins::EVENT_AFTER_INSTALL_PLUGIN,
523
            function(PluginEvent $event) {
524
                if ($event->plugin === $this) {
525
                    $request = Craft::$app->getRequest();
526
                    if ($request->isCpRequest) {
527
                        Craft::$app->getResponse()->redirect(UrlHelper::cpUrl('image-optimize/welcome'))->send();
528
                    }
529
                }
530
            }
531
        );
532
    }
533
534
    /**
535
     * Install site event listeners for site requests only
536
     */
537
    protected function installSiteEventListeners(): void
538
    {
539
        // Handler: UrlManager::EVENT_REGISTER_SITE_URL_RULES
540
        Event::on(
541
            UrlManager::class,
542
            UrlManager::EVENT_REGISTER_SITE_URL_RULES,
543
            function(RegisterUrlRulesEvent $event) {
544
                Craft::debug(
545
                    'UrlManager::EVENT_REGISTER_SITE_URL_RULES',
546
                    __METHOD__
547
                );
548
                // Register our Control Panel routes
549
                $event->rules = array_merge(
550
                    $event->rules,
551
                    $this->customFrontendRoutes()
552
                );
553
            }
554
        );
555
    }
556
557
    /**
558
     * Install site event listeners for cp requests only
559
     */
560
    protected function installCpEventListeners(): void
561
    {
562
        // Handler: Plugins::EVENT_AFTER_LOAD_PLUGINS
563
        Event::on(
564
            Plugins::class,
565
            Plugins::EVENT_AFTER_LOAD_PLUGINS,
566
            static function() {
567
                // Install these only after all other plugins have loaded
568
                Event::on(
569
                    View::class,
570
                    View::EVENT_REGISTER_CP_TEMPLATE_ROOTS,
571
                    static function(RegisterTemplateRootsEvent $e) {
572
                        // Register the root directodies
573
                        $allImageTransformTypes = ImageOptimize::$plugin->optimize->getAllImageTransformTypes();
574
                        /** @var ImageTransformInterface $imageTransformType */
575
                        foreach ($allImageTransformTypes as $imageTransformType) {
576
                            [$id, $baseDir] = $imageTransformType::getTemplatesRoot();
577
                            if (is_dir($baseDir)) {
578
                                $e->roots[$id] = $baseDir;
579
                            }
580
                        }
581
                    }
582
                );
583
            }
584
        );
585
    }
586
587
    /**
588
     * Return the custom frontend routes
589
     *
590
     * @return array
591
     */
592
    protected function customFrontendRoutes(): array
593
    {
594
        return [
595
        ];
596
    }
597
598
    /**
599
     * If the Field being saved is an OptimizedImages field, re-save the
600
     * responsive image variants automatically
601
     *
602
     * @param FieldEvent $event
603
     */
604
    protected function checkForOptimizedImagesField(FieldEvent $event): void
605
    {
606
        $thisField = $event->field;
607
        if ($thisField instanceof OptimizedImages) {
608
            $volumes = Craft::$app->getVolumes()->getAllVolumes();
609
            foreach ($volumes as $volume) {
610
                $needToReSave = false;
611
                /** @var ?FieldLayout $fieldLayout */
612
                $fieldLayout = $volume->getFieldLayout();
613
                // Loop through the fields in the layout to see if it contains our field
614
                if ($fieldLayout) {
615
                    $fields = $fieldLayout->getCustomFields();
616
                    foreach ($fields as $field) {
617
                        /** @var Field $field */
618
                        if ($thisField->handle === $field->handle) {
619
                            $needToReSave = true;
620
                        }
621
                    }
622
                    if ($needToReSave) {
623
                        self::$plugin->optimizedImages->resaveVolumeAssets($volume, $thisField->id);
624
                    }
625
                }
626
            }
627
        }
628
    }
629
}
630