Issues (260)

src/seoelements/SeoProduct.php (1 issue)

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;
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) {
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);
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
    {
273
        return Product::find()
274
            ->id($elementId)
275
            ->siteId($siteId)
276
            ->limit(1)
277
            ->one();
278
    }
279
280
    /**
281
     * Return a preview URI for a given $sourceHandle and $siteId
282
     * This just returns the first element
283
     *
284
     * @param string $sourceHandle
285
     * @param int|null $siteId
286
     *
287
     * @return string|null
288
     */
289
    public static function previewUri(string $sourceHandle, $siteId)
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
     *
308
     * @return array
309
     */
310
    public static function fieldLayouts(string $sourceHandle): array
311
    {
312
        $layouts = [];
313
        $commerce = CommercePlugin::getInstance();
314
        if ($commerce !== null) {
315
            $layoutId = null;
316
            try {
317
                $productType = $commerce->getProductTypes()->getProductTypeByHandle($sourceHandle);
318
                if ($productType) {
319
                    $layoutId = $productType->getFieldLayoutId();
320
                }
321
            } catch (Exception $e) {
322
                $layoutId = null;
323
            }
324
            if ($layoutId) {
325
                $layouts[] = Craft::$app->getFields()->getLayoutById($layoutId);
326
            }
327
        }
328
329
        return $layouts;
330
    }
331
332
    /**
333
     * Return the (entry) type menu as a $id => $name associative array
334
     *
335
     * @param string $sourceHandle
336
     *
337
     * @return array
338
     */
339
    public static function typeMenuFromHandle(string $sourceHandle): array
340
    {
341
        return [];
342
    }
343
344
    /**
345
     * Return the source model of the given $sourceId
346
     *
347
     * @param int $sourceId
348
     *
349
     * @return ProductType|null
350
     */
351
    public static function sourceModelFromId(int $sourceId)
352
    {
353
        $productType = null;
354
        $commerce = CommercePlugin::getInstance();
355
        if ($commerce !== null) {
356
            $productType = $commerce->getProductTypes()->getProductTypeById($sourceId);
357
        }
358
359
        return $productType;
360
    }
361
362
    /**
363
     * Return the source model of the given $sourceHandle
364
     *
365
     * @param string $sourceHandle
366
     *
367
     * @return ProductType|null
368
     */
369
    public static function sourceModelFromHandle(string $sourceHandle)
370
    {
371
        $productType = null;
372
        $commerce = CommercePlugin::getInstance();
373
        if ($commerce !== null) {
374
            $productType = $commerce->getProductTypes()->getProductTypeByHandle($sourceHandle);
375
        }
376
377
        return $productType;
378
    }
379
380
    /**
381
     * Return the most recently updated Element from a given source model
382
     *
383
     * @param Model $sourceModel
384
     * @param int $sourceSiteId
385
     *
386
     * @return null|ElementInterface
387
     */
388
    public static function mostRecentElement(Model $sourceModel, int $sourceSiteId)
389
    {
390
        /** @var ProductType $sourceModel */
391
        return Product::find()
392
            ->type($sourceModel->handle)
393
            ->siteId($sourceSiteId)
394
            ->limit(1)
395
            ->orderBy(['elements.dateUpdated' => SORT_DESC])
396
            ->one();
397
    }
398
399
    /**
400
     * Return the path to the config file directory
401
     *
402
     * @return string
403
     */
404
    public static function configFilePath(): string
405
    {
406
        return self::CONFIG_FILE_PATH;
407
    }
408
409
    /**
410
     * Return a meta bundle config array for the given $sourceModel
411
     *
412
     * @param Model $sourceModel
413
     *
414
     * @return array
415
     */
416
    public static function metaBundleConfig(Model $sourceModel): array
417
    {
418
        /** @var ProductType $sourceModel */
419
        return ArrayHelper::merge(
420
            ConfigHelper::getConfigFromFile(self::configFilePath()),
421
            [
422
                'sourceId' => $sourceModel->id,
423
                'sourceName' => (string)$sourceModel->name,
424
                'sourceHandle' => $sourceModel->handle,
425
            ]
426
        );
427
    }
428
429
    /**
430
     * Return the source id from the $element
431
     *
432
     * @param ElementInterface $element
433
     *
434
     * @return int|null
435
     */
436
    public static function sourceIdFromElement(ElementInterface $element)
437
    {
438
        /** @var Product $element */
439
        return $element->typeId;
440
    }
441
442
    /**
443
     * Return the (product) type id from the $element
444
     *
445
     * @param ElementInterface $element
446
     *
447
     * @return int|null
448
     */
449
    public static function typeIdFromElement(ElementInterface $element)
450
    {
451
        /** @var Product $element */
452
        return null;
453
    }
454
455
    /**
456
     * Return the source handle from the $element
457
     *
458
     * @param ElementInterface $element
459
     *
460
     * @return string|null
461
     */
462
    public static function sourceHandleFromElement(ElementInterface $element)
463
    {
464
        $sourceHandle = '';
465
        /** @var Product $element */
466
        try {
467
            $sourceHandle = $element->getType()->handle;
468
        } catch (InvalidConfigException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
469
        }
470
471
        return $sourceHandle;
472
    }
473
474
    /**
475
     * Create a MetaBundle in the db for each site, from the passed in $sourceModel
476
     *
477
     * @param Model $sourceModel
478
     */
479
    public static function createContentMetaBundle(Model $sourceModel)
480
    {
481
        /** @var ProductType $sourceModel */
482
        $sites = Craft::$app->getSites()->getAllSites();
483
        /** @var Site $site */
484
        foreach ($sites as $site) {
485
            $seoElement = self::class;
486
            /** @var SeoElementInterface $seoElement */
487
            Seomatic::$plugin->metaBundles->createMetaBundleFromSeoElement($seoElement, $sourceModel, $site->id);
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