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;
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
                ]);
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...
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