SeoProduct   B
last analyzed

Complexity

Total Complexity 45

Size/Duplication

Total Lines 437
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 45
eloc 121
dl 0
loc 437
c 1
b 0
f 0
ccs 0
cts 137
cp 0
rs 8.8

21 Methods

Rating   Name   Duplication   Size   Complexity  
A getMetaBundleType() 0 3 1
A getRequiredPluginHandle() 0 3 1
A getElementClasses() 0 3 1
A getElementRefHandle() 0 3 1
A sitemapAltElement() 0 10 1
A sitemapElementsQuery() 0 8 1
A mostRecentElement() 0 9 1
A typeIdFromElement() 0 4 1
A sourceHandleFromElement() 0 10 2
A sourceIdFromElement() 0 4 1
A typeMenuFromHandle() 0 3 1
A previewUri() 0 12 2
A createAllContentMetaBundles() 0 8 3
A getGqlInterfaceTypeName() 0 7 2
A configFilePath() 0 3 1
A sourceModelFromHandle() 0 9 2
A fieldLayouts() 0 20 5
A sourceModelFromId() 0 9 2
A createContentMetaBundle() 0 8 2
A metaBundleConfig() 0 9 1
C installEventHandlers() 0 98 13

How to fix   Complexity   

Complex Class

Complex classes like SeoProduct often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SeoProduct, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * SEOmatic plugin for Craft CMS 3.x
4
 *
5
 * A turnkey SEO implementation for Craft CMS that is comprehensive, powerful,
6
 * and flexible
7
 *
8
 * @link      https://nystudio107.com
9
 * @copyright Copyright (c) 2019 nystudio107
10
 */
11
12
namespace nystudio107\seomatic\seoelements;
13
14
use Craft;
15
use craft\base\ElementInterface;
16
use craft\base\Model;
17
use craft\commerce\elements\Product;
18
use craft\commerce\events\ProductTypeEvent;
19
use craft\commerce\gql\interfaces\elements\Product as ProductInterface;
20
use craft\commerce\models\ProductType;
21
use craft\commerce\Plugin as CommercePlugin;
22
use craft\commerce\services\ProductTypes;
23
use craft\elements\db\ElementQueryInterface;
24
use craft\models\Site;
25
use Exception;
26
use nystudio107\seomatic\assetbundles\seomatic\SeomaticAsset;
27
use nystudio107\seomatic\base\GqlSeoElementInterface;
28
use nystudio107\seomatic\base\SeoElementInterface;
29
use nystudio107\seomatic\helpers\ArrayHelper;
30
use nystudio107\seomatic\helpers\Config as ConfigHelper;
31
use nystudio107\seomatic\helpers\PluginTemplate;
32
use nystudio107\seomatic\models\MetaBundle;
33
use nystudio107\seomatic\Seomatic;
34
use yii\base\Event;
35
use yii\base\InvalidConfigException;
36
37
/**
38
 * @author    nystudio107
39
 * @package   Seomatic
40
 * @since     3.2.0
41
 */
42
class SeoProduct implements SeoElementInterface, GqlSeoElementInterface
43
{
44
    // Constants
45
    // =========================================================================
46
47
    const META_BUNDLE_TYPE = 'product';
48
    const ELEMENT_CLASSES = [
49
        Product::class,
50
    ];
51
    const REQUIRED_PLUGIN_HANDLE = 'commerce';
52
    const CONFIG_FILE_PATH = 'productmeta/Bundle';
53
54
    // Public Static Methods
55
    // =========================================================================
56
57
    /**
58
     * Return the sourceBundleType for that this SeoElement handles
59
     *
60
     * @return string
61
     */
62
    public static function getMetaBundleType(): string
63
    {
64
        return self::META_BUNDLE_TYPE;
65
    }
66
67
    /**
68
     * Returns an array of the element classes that are handled by this SeoElement
69
     *
70
     * @return array
71
     */
72
    public static function getElementClasses(): array
73
    {
74
        return self::ELEMENT_CLASSES;
75
    }
76
77
    /**
78
     * Return the refHandle (e.g.: `entry` or `category`) for the SeoElement
79
     *
80
     * @return string
81
     */
82
    public static function getElementRefHandle(): string
83
    {
84
        return Product::refHandle() ?? 'product';
85
    }
86
87
    /**
88
     * Return the handle to a required plugin for this SeoElement type
89
     *
90
     * @return null|string
91
     */
92
    public static function getRequiredPluginHandle()
93
    {
94
        return self::REQUIRED_PLUGIN_HANDLE;
95
    }
96
97
    /**
98
     * Install any event handlers for this SeoElement type
99
     */
100
    public static function installEventHandlers()
101
    {
102
        $request = Craft::$app->getRequest();
103
104
        // Install for all requests
105
        Event::on(
106
            ProductTypes::class,
107
            ProductTypes::EVENT_AFTER_SAVE_PRODUCTTYPE,
108
            function(ProductTypeEvent $event) {
0 ignored issues
show
Unused Code introduced by
The parameter $event is not used and could be removed. ( Ignorable by Annotation )

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

108
            function(/** @scrutinizer ignore-unused */ ProductTypeEvent $event) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
109
                Craft::debug(
110
                    'ProductTypes::EVENT_AFTER_SAVE_PRODUCTTYPE',
111
                    __METHOD__
112
                );
113
                Seomatic::$plugin->metaBundles->resaveMetaBundles(self::META_BUNDLE_TYPE);
114
            }
115
        );
116
117
        // Install for all non-console requests
118
        if (!$request->getIsConsoleRequest()) {
119
            // Handler: ProductTypes::EVENT_AFTER_SAVE_PRODUCTTYPE
120
            Event::on(
121
                ProductTypes::class,
122
                ProductTypes::EVENT_AFTER_SAVE_PRODUCTTYPE,
123
                static function(ProductTypeEvent $event) {
124
                    Craft::debug(
125
                        'ProductTypes::EVENT_AFTER_SAVE_PRODUCTTYPE',
126
                        __METHOD__
127
                    );
128
                    if ($event->productType !== null && $event->productType->id !== null) {
129
                        Seomatic::$plugin->metaBundles->invalidateMetaBundleById(
130
                            SeoProduct::getMetaBundleType(),
131
                            $event->productType->id,
132
                            $event->isNew
133
                        );
134
                        // Create the meta bundles for this Product Type if it's new
135
                        if ($event->isNew) {
136
                            SeoProduct::createContentMetaBundle($event->productType);
137
                            Seomatic::$plugin->sitemaps->submitSitemapIndex();
138
                        }
139
                    }
140
                }
141
            );
142
            /*
143
             * @TODO Sadly this event doesn't exist yet
144
            // Handler: ProductTypes::EVENT_AFTER_DELETE_PRODUCTTYPE
145
            Event::on(
146
                ProductTypes::class,
147
                ProductTypes::EVENT_AFTER_DELETE_PRODUCTTYPE,
148
                function (ProductTypeEvent $event) {
149
                    Craft::debug(
150
                        'ProductTypes::EVENT_AFTER_DELETE_PRODUCTTYPE',
151
                        __METHOD__
152
                    );
153
                    if ($event->productType !== null && $event->productType->id !== null) {
154
                        Seomatic::$plugin->metaBundles->invalidateMetaBundleById(
155
                            SeoProduct::getMetaBundleType(),
156
                            $event->productType->id,
157
                            false
158
                        );
159
                        // Delete the meta bundles for this Product Type
160
                        Seomatic::$plugin->metaBundles->deleteMetaBundleBySourceId(
161
                            SeoProduct::getMetaBundleType(),
162
                            $event->productType->id
163
                        );
164
                    }
165
                }
166
            );
167
            */
168
        }
169
170
        // Install only for non-console site requests
171
        if ($request->getIsSiteRequest() && !$request->getIsConsoleRequest()) {
172
        }
173
174
        // Install only for non-console Control Panel requests
175
        if ($request->getIsCpRequest() && !$request->getIsConsoleRequest()) {
176
            // Commerce Product Types sidebar
177
            $commerce = CommercePlugin::getInstance();
178
            if ($commerce !== null) {
179
                Seomatic::$view->hook('cp.commerce.product.edit.details', static function(&$context) {
180
                    $html = '';
181
                    Seomatic::$view->registerAssetBundle(SeomaticAsset::class);
182
                    /** @var Product $product */
183
                    $product = $context[self::getElementRefHandle()] ?? null;
184
                    if ($product !== null && $product->uri !== null) {
185
                        Seomatic::$plugin->metaContainers->previewMetaContainers($product->uri, $product->siteId, true);
186
                        // Render our preview sidebar template
187
                        if (Seomatic::$settings->displayPreviewSidebar) {
188
                            $html .= PluginTemplate::renderPluginTemplate('_sidebars/product-preview.twig');
189
                        }
190
                        // Render our analysis sidebar template
191
// @TODO: This will be added an upcoming 'pro' edition
192
//                if (Seomatic::$settings->displayAnalysisSidebar) {
193
//                    $html .= PluginTemplate::renderPluginTemplate('_sidebars/product-analysis.twig');
194
//                }
195
                    }
196
197
                    return $html;
198
                });
199
            }
200
        }
201
    }
202
203
    /**
204
     * Return an ElementQuery for the sitemap elements for the given MetaBundle
205
     *
206
     * @param MetaBundle $metaBundle
207
     *
208
     * @return ElementQueryInterface
209
     */
210
    public static function sitemapElementsQuery(MetaBundle $metaBundle): ElementQueryInterface
211
    {
212
        $query = Product::find()
213
            ->type($metaBundle->sourceHandle)
214
            ->siteId($metaBundle->sourceSiteId)
215
            ->limit($metaBundle->metaSitemapVars->sitemapLimit);
216
217
        return $query;
218
    }
219
220
    /**
221
     * Return an ElementInterface for the sitemap alt element for the given MetaBundle
222
     * and Element ID
223
     *
224
     * @param MetaBundle $metaBundle
225
     * @param int $elementId
226
     * @param int $siteId
227
     *
228
     * @return null|ElementInterface
229
     */
230
    public static function sitemapAltElement(
231
        MetaBundle $metaBundle,
232
        int        $elementId,
233
        int        $siteId
234
    ) {
235
        return Product::find()
0 ignored issues
show
Bug Best Practice introduced by
The expression return craft\commerce\el...iteId)->limit(1)->one() also could return the type array which is incompatible with the documented return type craft\base\ElementInterface|null.
Loading history...
236
            ->id($elementId)
237
            ->siteId($siteId)
238
            ->limit(1)
239
            ->one();
240
    }
241
242
    /**
243
     * Return a preview URI for a given $sourceHandle and $siteId
244
     * This just returns the first element
245
     *
246
     * @param string $sourceHandle
247
     * @param int|null $siteId
248
     * @param int|string|null $typeId
249
     *
250
     * @return ?string
251
     */
252
    public static function previewUri(string $sourceHandle, $siteId, $typeId = null)
253
    {
254
        $uri = null;
255
        $element = Product::find()
256
            ->type($sourceHandle)
257
            ->siteId($siteId)
258
            ->one();
259
        if ($element) {
260
            $uri = $element->uri;
261
        }
262
263
        return $uri;
264
    }
265
266
    /**
267
     * Return an array of FieldLayouts from the $sourceHandle
268
     *
269
     * @param string $sourceHandle
270
     * @param int|string|null $typeId
271
     *
272
     * @return array
273
     */
274
    public static function fieldLayouts(string $sourceHandle, $typeId = null): array
275
    {
276
        $layouts = [];
277
        $commerce = CommercePlugin::getInstance();
278
        if ($commerce !== null) {
279
            $layoutId = null;
280
            try {
281
                $productType = $commerce->getProductTypes()->getProductTypeByHandle($sourceHandle);
282
                if ($productType) {
283
                    $layoutId = $productType->getFieldLayoutId();
284
                }
285
            } catch (Exception $e) {
286
                $layoutId = null;
287
            }
288
            if ($layoutId) {
289
                $layouts[] = Craft::$app->getFields()->getLayoutById($layoutId);
290
            }
291
        }
292
293
        return $layouts;
294
    }
295
296
    /**
297
     * Return the (entry) type menu as a $id => $name associative array
298
     *
299
     * @param string $sourceHandle
300
     *
301
     * @return array
302
     */
303
    public static function typeMenuFromHandle(string $sourceHandle): array
304
    {
305
        return [];
306
    }
307
308
    /**
309
     * Return the source model of the given $sourceId
310
     *
311
     * @param int $sourceId
312
     *
313
     * @return ProductType|null
314
     */
315
    public static function sourceModelFromId(int $sourceId)
316
    {
317
        $productType = null;
318
        $commerce = CommercePlugin::getInstance();
319
        if ($commerce !== null) {
320
            $productType = $commerce->getProductTypes()->getProductTypeById($sourceId);
321
        }
322
323
        return $productType;
324
    }
325
326
    /**
327
     * Return the source model of the given $sourceHandle
328
     *
329
     * @param string $sourceHandle
330
     *
331
     * @return ProductType|null
332
     */
333
    public static function sourceModelFromHandle(string $sourceHandle)
334
    {
335
        $productType = null;
336
        $commerce = CommercePlugin::getInstance();
337
        if ($commerce !== null) {
338
            $productType = $commerce->getProductTypes()->getProductTypeByHandle($sourceHandle);
339
        }
340
341
        return $productType;
342
    }
343
344
    /**
345
     * Return the most recently updated Element from a given source model
346
     *
347
     * @param Model $sourceModel
348
     * @param int $sourceSiteId
349
     *
350
     * @return null|ElementInterface
351
     */
352
    public static function mostRecentElement(Model $sourceModel, int $sourceSiteId)
353
    {
354
        /** @var ProductType $sourceModel */
355
        return Product::find()
0 ignored issues
show
Bug Best Practice introduced by
The expression return craft\commerce\el...ents\SORT_DESC))->one() also could return the type array which is incompatible with the documented return type craft\base\ElementInterface|null.
Loading history...
356
            ->type($sourceModel->handle)
357
            ->siteId($sourceSiteId)
358
            ->limit(1)
359
            ->orderBy(['elements.dateUpdated' => SORT_DESC])
360
            ->one();
361
    }
362
363
    /**
364
     * Return the path to the config file directory
365
     *
366
     * @return string
367
     */
368
    public static function configFilePath(): string
369
    {
370
        return self::CONFIG_FILE_PATH;
371
    }
372
373
    /**
374
     * Return a meta bundle config array for the given $sourceModel
375
     *
376
     * @param Model $sourceModel
377
     *
378
     * @return array
379
     */
380
    public static function metaBundleConfig(Model $sourceModel): array
381
    {
382
        /** @var ProductType $sourceModel */
383
        return ArrayHelper::merge(
384
            ConfigHelper::getConfigFromFile(self::configFilePath()),
385
            [
386
                'sourceId' => $sourceModel->id,
387
                'sourceName' => (string)$sourceModel->name,
388
                'sourceHandle' => $sourceModel->handle,
389
            ]
390
        );
391
    }
392
393
    /**
394
     * Return the source id from the $element
395
     *
396
     * @param ElementInterface $element
397
     *
398
     * @return int|null
399
     */
400
    public static function sourceIdFromElement(ElementInterface $element)
401
    {
402
        /** @var Product $element */
403
        return $element->typeId;
404
    }
405
406
    /**
407
     * Return the (product) type id from the $element
408
     *
409
     * @param ElementInterface $element
410
     *
411
     * @return int|null
412
     */
413
    public static function typeIdFromElement(ElementInterface $element)
414
    {
415
        /** @var Product $element */
416
        return null;
417
    }
418
419
    /**
420
     * Return the source handle from the $element
421
     *
422
     * @param ElementInterface $element
423
     *
424
     * @return string|null
425
     */
426
    public static function sourceHandleFromElement(ElementInterface $element)
427
    {
428
        $sourceHandle = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $sourceHandle is dead and can be removed.
Loading history...
429
        /** @var Product $element */
430
        try {
431
            $sourceHandle = $element->getType()->handle;
432
        } catch (InvalidConfigException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
433
        }
434
435
        return $sourceHandle;
436
    }
437
438
    /**
439
     * Create a MetaBundle in the db for each site, from the passed in $sourceModel
440
     *
441
     * @param Model $sourceModel
442
     */
443
    public static function createContentMetaBundle(Model $sourceModel)
444
    {
445
        /** @var ProductType $sourceModel */
446
        $sites = Craft::$app->getSites()->getAllSites();
447
        /** @var Site $site */
448
        foreach ($sites as $site) {
449
            $seoElement = self::class;
450
            Seomatic::$plugin->metaBundles->createMetaBundleFromSeoElement($seoElement, $sourceModel, $site->id, null, true);
451
        }
452
    }
453
454
    /**
455
     * Create all the MetaBundles in the db for this Seo Element
456
     */
457
    public static function createAllContentMetaBundles()
458
    {
459
        $commerce = CommercePlugin::getInstance();
460
        if ($commerce !== null) {
461
            // Get all of the calendars with URLs
462
            $productTypes = $commerce->getProductTypes()->getAllProductTypes();
463
            foreach ($productTypes as $productType) {
464
                self::createContentMetaBundle($productType);
465
            }
466
        }
467
    }
468
469
    /**
470
     * @inheritdoc
471
     */
472
    public static function getGqlInterfaceTypeName()
473
    {
474
        if (class_exists(ProductInterface::class)) {
475
            return ProductInterface::getName();
476
        }
477
478
        return '';
479
    }
480
}
481