Issues (260)

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