SeoProduct::getElementClasses()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 0
cts 2
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * SEOmatic plugin for Craft CMS
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;
0 ignored issues
show
Bug introduced by
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...
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\events\DefineHtmlEvent;
25
use craft\models\Site;
26
use Exception;
27
use nystudio107\seomatic\assetbundles\seomatic\SeomaticAsset;
28
use nystudio107\seomatic\base\GqlSeoElementInterface;
29
use nystudio107\seomatic\base\SeoElementInterface;
30
use nystudio107\seomatic\helpers\ArrayHelper;
31
use nystudio107\seomatic\helpers\Config as ConfigHelper;
32
use nystudio107\seomatic\helpers\PluginTemplate;
33
use nystudio107\seomatic\models\MetaBundle;
34
use nystudio107\seomatic\Seomatic;
35
use yii\base\Event;
36
use yii\base\InvalidConfigException;
37
38
/**
39
 * @author    nystudio107
40
 * @package   Seomatic
41
 * @since     3.2.0
42
 */
43
class SeoProduct implements SeoElementInterface, GqlSeoElementInterface
44
{
45
    // Constants
46
    // =========================================================================
47
48
    public const META_BUNDLE_TYPE = 'product';
49
    public const ELEMENT_CLASSES = [
50
        Product::class,
51
    ];
52
    public const REQUIRED_PLUGIN_HANDLE = 'commerce';
53
    public const CONFIG_FILE_PATH = 'productmeta/Bundle';
54
55
    // Public Static Methods
56
    // =========================================================================
57
58
    /**
59
     * Return the sourceBundleType for that this SeoElement handles
60
     *
61
     * @return string
62
     */
63
    public static function getMetaBundleType(): string
64
    {
65
        return self::META_BUNDLE_TYPE;
66
    }
67
68
    /**
69
     * Returns an array of the element classes that are handled by this SeoElement
70
     *
71
     * @return array
72
     */
73
    public static function getElementClasses(): array
74
    {
75
        return self::ELEMENT_CLASSES;
76
    }
77
78
    /**
79
     * Return the refHandle (e.g.: `entry` or `category`) for the SeoElement
80
     *
81
     * @return string
82
     */
83
    public static function getElementRefHandle(): string
84
    {
85
        return Product::refHandle() ?? 'product';
86
    }
87
88
    /**
89
     * Return the handle to a required plugin for this SeoElement type
90
     *
91
     * @return null|string
92
     */
93
    public static function getRequiredPluginHandle()
94
    {
95
        return self::REQUIRED_PLUGIN_HANDLE;
96
    }
97
98
    /**
99
     * Install any event handlers for this SeoElement type
100
     */
101
    public static function installEventHandlers()
102
    {
103
        $request = Craft::$app->getRequest();
104
105
        // Install for all requests
106
        Event::on(
107
            ProductTypes::class,
108
            ProductTypes::EVENT_AFTER_SAVE_PRODUCTTYPE,
109
            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

109
            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...
110
                Craft::debug(
111
                    'ProductTypes::EVENT_AFTER_SAVE_PRODUCTTYPE',
112
                    __METHOD__
113
                );
114
                Seomatic::$plugin->metaBundles->resaveMetaBundles(self::META_BUNDLE_TYPE);
115
            }
116
        );
117
118
        // Install for all non-console requests
119
        if (!$request->getIsConsoleRequest()) {
120
            // Handler: ProductTypes::EVENT_AFTER_SAVE_PRODUCTTYPE
121
            Event::on(
122
                ProductTypes::class,
123
                ProductTypes::EVENT_AFTER_SAVE_PRODUCTTYPE,
124
                static function(ProductTypeEvent $event) {
125
                    Craft::debug(
126
                        'ProductTypes::EVENT_AFTER_SAVE_PRODUCTTYPE',
127
                        __METHOD__
128
                    );
129
                    if ($event->productType !== null && $event->productType->id !== null) {
130
                        Seomatic::$plugin->metaBundles->invalidateMetaBundleById(
131
                            SeoProduct::getMetaBundleType(),
132
                            $event->productType->id,
133
                            $event->isNew
134
                        );
135
                        // Create the meta bundles for this Product Type if it's new
136
                        if ($event->isNew) {
137
                            SeoProduct::createContentMetaBundle($event->productType);
138
                            Seomatic::$plugin->sitemaps->submitSitemapIndex();
139
                        }
140
                    }
141
                }
142
            );
143
            /*
144
             * @TODO Sadly this event doesn't exist yet
145
            // Handler: ProductTypes::EVENT_AFTER_DELETE_PRODUCTTYPE
146
            Event::on(
147
                ProductTypes::class,
148
                ProductTypes::EVENT_AFTER_DELETE_PRODUCTTYPE,
149
                function (ProductTypeEvent $event) {
150
                    Craft::debug(
151
                        'ProductTypes::EVENT_AFTER_DELETE_PRODUCTTYPE',
152
                        __METHOD__
153
                    );
154
                    if ($event->productType !== null && $event->productType->id !== null) {
155
                        Seomatic::$plugin->metaBundles->invalidateMetaBundleById(
156
                            SeoProduct::getMetaBundleType(),
157
                            $event->productType->id,
158
                            false
159
                        );
160
                        // Delete the meta bundles for this Product Type
161
                        Seomatic::$plugin->metaBundles->deleteMetaBundleBySourceId(
162
                            SeoProduct::getMetaBundleType(),
163
                            $event->productType->id
164
                        );
165
                    }
166
                }
167
            );
168
            */
169
        }
170
171
        // Install only for non-console site requests
172
        if ($request->getIsSiteRequest() && !$request->getIsConsoleRequest()) {
173
        }
174
175
        /**
176
         * N.B. This is in here seemingly twice (see the Product::EVENT_DEFINE_SIDEBAR_HTML below)
177
         * because Commerce apparently doesn't render the templates in a way that causes the
178
         * Product::EVENT_DEFINE_SIDEBAR_HTML to be thrown, despite there being code that implies it does:
179
         * https://github.com/craftcms/commerce/blob/develop/src/elements/Product.php#L796
180
         **/
181
182
        // Install only for non-console Control Panel requests
183
        if ($request->getIsCpRequest() && !$request->getIsConsoleRequest()) {
184
            // Commerce Product Types sidebar
185
            $commerce = CommercePlugin::getInstance();
186
            if ($commerce !== null) {
187
                Seomatic::$view->hook('cp.commerce.product.edit.details', static function(&$context) {
188
                    $html = '';
189
                    Seomatic::$view->registerAssetBundle(SeomaticAsset::class);
0 ignored issues
show
Bug introduced by
The method registerAssetBundle() 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

189
                    Seomatic::$view->/** @scrutinizer ignore-call */ 
190
                                     registerAssetBundle(SeomaticAsset::class);

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...
190
                    /** @var Product $product */
191
                    $product = $context[self::getElementRefHandle()] ?? null;
192
                    if ($product !== null && $product->uri !== null) {
193
                        Seomatic::$plugin->metaContainers->previewMetaContainers($product->uri, $product->siteId, true);
194
                        // Render our preview sidebar template
195
                        if (Seomatic::$settings->displayPreviewSidebar) {
196
                            $html .= PluginTemplate::renderPluginTemplate('_sidebars/product-preview.twig');
197
                        }
198
                        // Render our analysis sidebar template
199
// @TODO: This will be added an upcoming 'pro' edition
200
//                if (Seomatic::$settings->displayAnalysisSidebar) {
201
//                    $html .= PluginTemplate::renderPluginTemplate('_sidebars/product-analysis.twig');
202
//                }
203
                    }
204
205
                    return $html;
206
                });
207
            }
208
        }
209
210
        // Handler: Product::EVENT_DEFINE_SIDEBAR_HTML
211
        Event::on(
212
            Product::class,
213
            Product::EVENT_DEFINE_SIDEBAR_HTML,
214
            static function(DefineHtmlEvent $event) {
215
                Craft::debug(
216
                    'Product::EVENT_DEFINE_SIDEBAR_HTML',
217
                    __METHOD__
218
                );
219
                $html = '';
220
                Seomatic::$view->registerAssetBundle(SeomaticAsset::class);
221
                /** @var Product $product */
222
                $product = $event->sender ?? null;
223
                if ($product !== null && $product->uri !== null) {
224
                    Seomatic::$plugin->metaContainers->previewMetaContainers($product->uri, $product->siteId, true);
225
                    // Render our preview sidebar template
226
                    if (Seomatic::$settings->displayPreviewSidebar) {
227
                        $html .= PluginTemplate::renderPluginTemplate('_sidebars/product-preview.twig');
228
                    }
229
                    // Render our analysis sidebar template
230
// @TODO: This will be added an upcoming 'pro' edition
231
//                if (Seomatic::$settings->displayAnalysisSidebar) {
232
//                    $html .= PluginTemplate::renderPluginTemplate('_sidebars/product-analysis.twig');
233
//                }
234
                }
235
                $event->html .= $html;
236
            }
237
        );
238
    }
239
240
    /**
241
     * Return an ElementQuery for the sitemap elements for the given MetaBundle
242
     *
243
     * @param MetaBundle $metaBundle
244
     *
245
     * @return ElementQueryInterface
246
     */
247
    public static function sitemapElementsQuery(MetaBundle $metaBundle): ElementQueryInterface
248
    {
249
        $query = Product::find()
250
            ->type($metaBundle->sourceHandle)
251
            ->siteId($metaBundle->sourceSiteId)
252
            ->limit($metaBundle->metaSitemapVars->sitemapLimit);
253
254
        return $query;
255
    }
256
257
    /**
258
     * Return an ElementInterface for the sitemap alt element for the given MetaBundle
259
     * and Element ID
260
     *
261
     * @param MetaBundle $metaBundle
262
     * @param int $elementId
263
     * @param int $siteId
264
     *
265
     * @return null|ElementInterface
266
     */
267
    public static function sitemapAltElement(
268
        MetaBundle $metaBundle,
269
        int        $elementId,
270
        int        $siteId,
271
    ) {
272
        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...
273
            ->id($elementId)
274
            ->siteId($siteId)
275
            ->limit(1)
276
            ->one();
277
    }
278
279
    /**
280
     * Return a preview URI for a given $sourceHandle and $siteId
281
     * This just returns the first element
282
     *
283
     * @param string $sourceHandle
284
     * @param int|null $siteId
285
     * @param int|string|null $typeId
286
     *
287
     * @return ?string
288
     */
289
    public static function previewUri(string $sourceHandle, $siteId, $typeId = null): ?string
290
    {
291
        $uri = null;
292
        $element = Product::find()
293
            ->type($sourceHandle)
294
            ->siteId($siteId)
295
            ->one();
296
        if ($element) {
297
            $uri = $element->uri;
298
        }
299
300
        return $uri;
301
    }
302
303
    /**
304
     * Return an array of FieldLayouts from the $sourceHandle
305
     *
306
     * @param string $sourceHandle
307
     * @param int|string|null $typeId
308
     *
309
     * @return array
310
     */
311
    public static function fieldLayouts(string $sourceHandle, $typeId = null): array
312
    {
313
        $layouts = [];
314
        $commerce = CommercePlugin::getInstance();
315
        if ($commerce !== null) {
316
            $layoutId = null;
317
            try {
318
                $productType = $commerce->getProductTypes()->getProductTypeByHandle($sourceHandle);
319
                if ($productType) {
320
                    $layoutId = $productType->getFieldLayoutId();
321
                }
322
            } catch (Exception $e) {
323
                $layoutId = null;
324
            }
325
            if ($layoutId) {
326
                $layouts[] = Craft::$app->getFields()->getLayoutById($layoutId);
327
            }
328
        }
329
330
        return $layouts;
331
    }
332
333
    /**
334
     * Return the (entry) type menu as a $id => $name associative array
335
     *
336
     * @param string $sourceHandle
337
     *
338
     * @return array
339
     */
340
    public static function typeMenuFromHandle(string $sourceHandle): array
341
    {
342
        return [];
343
    }
344
345
    /**
346
     * Return the source model of the given $sourceId
347
     *
348
     * @param int $sourceId
349
     *
350
     * @return ProductType|null
351
     */
352
    public static function sourceModelFromId(int $sourceId)
353
    {
354
        $productType = null;
355
        $commerce = CommercePlugin::getInstance();
356
        if ($commerce !== null) {
357
            $productType = $commerce->getProductTypes()->getProductTypeById($sourceId);
358
        }
359
360
        return $productType;
361
    }
362
363
    /**
364
     * Return the source model of the given $sourceHandle
365
     *
366
     * @param string $sourceHandle
367
     *
368
     * @return ProductType|null
369
     */
370
    public static function sourceModelFromHandle(string $sourceHandle)
371
    {
372
        $productType = null;
373
        $commerce = CommercePlugin::getInstance();
374
        if ($commerce !== null) {
375
            $productType = $commerce->getProductTypes()->getProductTypeByHandle($sourceHandle);
376
        }
377
378
        return $productType;
379
    }
380
381
    /**
382
     * Return the most recently updated Element from a given source model
383
     *
384
     * @param Model $sourceModel
385
     * @param int $sourceSiteId
386
     *
387
     * @return null|ElementInterface
388
     */
389
    public static function mostRecentElement(Model $sourceModel, int $sourceSiteId)
390
    {
391
        /** @var ProductType $sourceModel */
392
        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...
393
            ->type($sourceModel->handle)
394
            ->siteId($sourceSiteId)
395
            ->limit(1)
396
            ->orderBy(['elements.dateUpdated' => SORT_DESC])
397
            ->one();
398
    }
399
400
    /**
401
     * Return the path to the config file directory
402
     *
403
     * @return string
404
     */
405
    public static function configFilePath(): string
406
    {
407
        return self::CONFIG_FILE_PATH;
408
    }
409
410
    /**
411
     * Return a meta bundle config array for the given $sourceModel
412
     *
413
     * @param Model $sourceModel
414
     *
415
     * @return array
416
     */
417
    public static function metaBundleConfig(Model $sourceModel): array
418
    {
419
        /** @var ProductType $sourceModel */
420
        return ArrayHelper::merge(
421
            ConfigHelper::getConfigFromFile(self::configFilePath()),
422
            [
423
                'sourceId' => $sourceModel->id,
424
                'sourceName' => (string)$sourceModel->name,
425
                'sourceHandle' => $sourceModel->handle,
426
            ]
427
        );
428
    }
429
430
    /**
431
     * Return the source id from the $element
432
     *
433
     * @param ElementInterface $element
434
     *
435
     * @return int|null
436
     */
437
    public static function sourceIdFromElement(ElementInterface $element)
438
    {
439
        /** @var Product $element */
440
        return $element->typeId;
441
    }
442
443
    /**
444
     * Return the (product) type id from the $element
445
     *
446
     * @param ElementInterface $element
447
     *
448
     * @return int|null
449
     */
450
    public static function typeIdFromElement(ElementInterface $element)
451
    {
452
        /** @var Product $element */
453
        return null;
454
    }
455
456
    /**
457
     * Return the source handle from the $element
458
     *
459
     * @param ElementInterface $element
460
     *
461
     * @return string|null
462
     */
463
    public static function sourceHandleFromElement(ElementInterface $element)
464
    {
465
        $sourceHandle = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $sourceHandle is dead and can be removed.
Loading history...
466
        /** @var Product $element */
467
        try {
468
            $sourceHandle = $element->getType()->handle;
469
        } catch (InvalidConfigException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
470
        }
471
472
        return $sourceHandle;
473
    }
474
475
    /**
476
     * Create a MetaBundle in the db for each site, from the passed in $sourceModel
477
     *
478
     * @param Model $sourceModel
479
     */
480
    public static function createContentMetaBundle(Model $sourceModel)
481
    {
482
        /** @var ProductType $sourceModel */
483
        $sites = Craft::$app->getSites()->getAllSites();
484
        /** @var Site $site */
485
        foreach ($sites as $site) {
486
            $seoElement = self::class;
487
            Seomatic::$plugin->metaBundles->createMetaBundleFromSeoElement($seoElement, $sourceModel, $site->id, null, true);
488
        }
489
    }
490
491
    /**
492
     * Create all the MetaBundles in the db for this Seo Element
493
     */
494
    public static function createAllContentMetaBundles()
495
    {
496
        $commerce = CommercePlugin::getInstance();
497
        if ($commerce !== null) {
498
            // Get all of the calendars with URLs
499
            $productTypes = $commerce->getProductTypes()->getAllProductTypes();
500
            foreach ($productTypes as $productType) {
501
                self::createContentMetaBundle($productType);
502
            }
503
        }
504
    }
505
506
    /**
507
     * @inheritdoc
508
     */
509
    public static function getGqlInterfaceTypeName()
510
    {
511
        return ProductInterface::getName();
512
    }
513
}
514