Issues (260)

src/seoelements/SeoEntry.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\db\ElementQueryInterface;
18
use craft\elements\Entry;
19
use craft\events\DefineHtmlEvent;
20
use craft\events\SectionEvent;
21
use craft\gql\interfaces\elements\Entry as EntryInterface;
22
use craft\models\Section;
23
use craft\models\Site;
24
use craft\services\Entries;
25
use nystudio107\seomatic\assetbundles\seomatic\SeomaticAsset;
26
use nystudio107\seomatic\base\GqlSeoElementInterface;
27
use nystudio107\seomatic\base\SeoElementInterface;
28
use nystudio107\seomatic\helpers\ArrayHelper;
29
use nystudio107\seomatic\helpers\Config as ConfigHelper;
30
use nystudio107\seomatic\helpers\PluginTemplate;
31
use nystudio107\seomatic\models\MetaBundle;
32
use nystudio107\seomatic\Seomatic;
33
use yii\base\Event;
34
use yii\base\InvalidConfigException;
35
36
/**
37
 * @author    nystudio107
38
 * @package   Seomatic
39
 * @since     3.2.0
40
 */
41
class SeoEntry implements SeoElementInterface, GqlSeoElementInterface
42
{
43
    // Constants
44
    // =========================================================================
45
46
    public const META_BUNDLE_TYPE = 'section';
47
    public const ELEMENT_CLASSES = [
48
        Entry::class,
49
    ];
50
    public const REQUIRED_PLUGIN_HANDLE = null;
51
    public const CONFIG_FILE_PATH = 'entrymeta/Bundle';
52
53
    // Public Static Methods
54
    // =========================================================================
55
56
    /**
57
     * Return the sourceBundleType for that this SeoElement handles
58
     *
59
     * @return string
60
     */
61
    public static function getMetaBundleType(): string
62
    {
63
        return self::META_BUNDLE_TYPE;
64
    }
65
66
    /**
67
     * Returns an array of the element classes that are handled by this SeoElement
68
     *
69
     * @return array
70
     */
71
    public static function getElementClasses(): array
72
    {
73
        return self::ELEMENT_CLASSES;
74
    }
75
76
    /**
77
     * Return the refHandle (e.g.: `entry` or `category`) for the SeoElement
78
     *
79
     * @return string
80
     */
81
    public static function getElementRefHandle(): string
82
    {
83
        return Entry::refHandle() ?? 'entry';
84
    }
85
86
    /**
87
     * Return the handle to a required plugin for this SeoElement type
88
     *
89
     * @return null|string
90
     */
91
    public static function getRequiredPluginHandle()
92
    {
93
        return self::REQUIRED_PLUGIN_HANDLE;
94
    }
95
96
    /**
97
     * Install any event handlers for this SeoElement type
98
     */
99
    public static function installEventHandlers()
100
    {
101
        $request = Craft::$app->getRequest();
102
103
        // Install for all requests
104
        Event::on(
105
            Entries::class,
106
            Entries::EVENT_AFTER_SAVE_SECTION,
107
            function(SectionEvent $event) {
108
                Craft::debug(
109
                    'Entries::EVENT_AFTER_SAVE_SECTION',
110
                    __METHOD__
111
                );
112
                Seomatic::$plugin->metaBundles->resaveMetaBundles(self::META_BUNDLE_TYPE);
113
            }
114
        );
115
        Event::on(
116
            Entries::class,
117
            Entries::EVENT_AFTER_DELETE_SECTION,
118
            function(SectionEvent $event) {
119
                Craft::debug(
120
                    'Entries::EVENT_AFTER_DELETE_SECTION',
121
                    __METHOD__
122
                );
123
                Seomatic::$plugin->metaBundles->resaveMetaBundles(self::META_BUNDLE_TYPE);
124
            }
125
        );
126
127
        // Install for all non-console requests
128
        if (!$request->getIsConsoleRequest()) {
129
            // Handler: Entries::EVENT_AFTER_SAVE_SECTION
130
            Event::on(
131
                Entries::class,
132
                Entries::EVENT_AFTER_SAVE_SECTION,
133
                function(SectionEvent $event) {
134
                    Craft::debug(
135
                        'Entries::EVENT_AFTER_SAVE_SECTION',
136
                        __METHOD__
137
                    );
138
                    if ($event->section !== null && $event->section->id !== null) {
139
                        Seomatic::$plugin->metaBundles->invalidateMetaBundleById(
140
                            SeoEntry::getMetaBundleType(),
141
                            $event->section->id,
142
                            $event->isNew
143
                        );
144
                        // Create the meta bundles for this section if it's new
145
                        if ($event->isNew) {
146
                            SeoEntry::createContentMetaBundle($event->section);
147
                            Seomatic::$plugin->sitemaps->submitSitemapIndex();
148
                        }
149
                    }
150
                }
151
            );
152
            // Handler: Entries::EVENT_AFTER_DELETE_SECTION
153
            Event::on(
154
                Entries::class,
155
                Entries::EVENT_AFTER_DELETE_SECTION,
156
                function(SectionEvent $event) {
157
                    Craft::debug(
158
                        'Entries::EVENT_AFTER_DELETE_SECTION',
159
                        __METHOD__
160
                    );
161
                    if ($event->section !== null && $event->section->id !== null) {
162
                        Seomatic::$plugin->metaBundles->invalidateMetaBundleById(
163
                            SeoEntry::getMetaBundleType(),
164
                            $event->section->id,
165
                            false
166
                        );
167
                        // Delete the meta bundles for this section
168
                        Seomatic::$plugin->metaBundles->deleteMetaBundleBySourceId(
169
                            SeoEntry::getMetaBundleType(),
170
                            $event->section->id
171
                        );
172
                    }
173
                }
174
            );
175
        }
176
177
        // Install only for non-console site requests
178
        if ($request->getIsSiteRequest() && !$request->getIsConsoleRequest()) {
179
        }
180
181
        // Handler: Entry::EVENT_DEFINE_SIDEBAR_HTML
182
        Event::on(
183
            Entry::class,
184
            Entry::EVENT_DEFINE_SIDEBAR_HTML,
185
            static function(DefineHtmlEvent $event) {
186
                Craft::debug(
187
                    'Entry::EVENT_DEFINE_SIDEBAR_HTML',
188
                    __METHOD__
189
                );
190
                $html = '';
191
                Seomatic::$view->registerAssetBundle(SeomaticAsset::class);
192
                /** @var Entry $entry */
193
                $entry = $event->sender ?? null;
194
                if ($entry !== null && $entry->uri !== null) {
195
                    Seomatic::$plugin->metaContainers->previewMetaContainers($entry->uri, $entry->siteId, true);
196
                    // Render our preview sidebar template
197
                    if (Seomatic::$settings->displayPreviewSidebar && Seomatic::$matchedElement) {
198
                        $html .= PluginTemplate::renderPluginTemplate('_sidebars/entry-preview.twig');
199
                    }
200
                    // Render our analysis sidebar template
201
// @TODO: This will be added an upcoming 'pro' edition
202
//                if (Seomatic::$settings->displayAnalysisSidebar && Seomatic::$matchedElement) {
203
//                    $html .= PluginTemplate::renderPluginTemplate('_sidebars/entry-analysis.twig');
204
//                }
205
                }
206
                $event->html .= $html;
207
            }
208
        );
209
    }
210
211
    /**
212
     * Return an ElementQuery for the sitemap elements for the given MetaBundle
213
     *
214
     * @param MetaBundle $metaBundle
215
     *
216
     * @return ElementQueryInterface
217
     */
218
    public static function sitemapElementsQuery(MetaBundle $metaBundle): ElementQueryInterface
219
    {
220
        $query = Entry::find()
221
            ->section($metaBundle->sourceHandle)
222
            ->siteId($metaBundle->sourceSiteId)
223
            ->limit($metaBundle->metaSitemapVars->sitemapLimit);
224
        if ($metaBundle->sourceType === 'structure'
225
            && !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 Entry::find()
248
            ->section($metaBundle->sourceHandle)
249
            ->id($elementId)
250
            ->siteId($siteId)
251
            ->limit(1)
252
            ->one();
253
    }
254
255
    /**
256
     * Return a preview URI for a given $sourceHandle and $siteId
257
     * This just returns the first element
258
     *
259
     * @param string $sourceHandle
260
     * @param int|null $siteId
261
     *
262
     * @return string|null
263
     */
264
    public static function previewUri(string $sourceHandle, $siteId)
265
    {
266
        $uri = null;
267
        $element = Entry::find()
268
            ->section($sourceHandle)
269
            ->siteId($siteId)
270
            ->one();
271
        if ($element) {
272
            $uri = $element->uri;
273
        }
274
275
        return $uri;
276
    }
277
278
    /**
279
     * Return an array of FieldLayouts from the $sourceHandle
280
     *
281
     * @param string $sourceHandle
282
     *
283
     * @return array
284
     */
285
    public static function fieldLayouts(string $sourceHandle): array
286
    {
287
        $layouts = [];
288
        $section = Craft::$app->getEntries()->getSectionByHandle($sourceHandle);
289
        if ($section) {
290
            $entryTypes = $section->getEntryTypes();
291
            foreach ($entryTypes as $entryType) {
292
                if ($entryType->fieldLayoutId) {
293
                    $layouts[] = Craft::$app->getFields()->getLayoutById($entryType->fieldLayoutId);
294
                }
295
            }
296
        }
297
298
        return $layouts;
299
    }
300
301
    /**
302
     * Return the (entry) type menu as a $id => $name associative array
303
     *
304
     * @param string $sourceHandle
305
     *
306
     * @return array
307
     */
308
    public static function typeMenuFromHandle(string $sourceHandle): array
309
    {
310
        $typeMenu = [];
311
312
        $section = self::sourceModelFromHandle($sourceHandle);
313
        if ($section !== null) {
314
            $entryTypes = $section->getEntryTypes();
315
            // Only create a menu if there's more than 1 entry type
316
            if (count($entryTypes) > 1) {
317
                foreach ($entryTypes as $entryType) {
318
                    $typeMenu[$entryType->id] = $entryType->name;
319
                }
320
            }
321
        }
322
323
        return $typeMenu;
324
    }
325
326
    /**
327
     * Return the source model of the given $sourceId
328
     *
329
     * @param int $sourceId
330
     *
331
     * @return Section|null
332
     */
333
    public static function sourceModelFromId(int $sourceId)
334
    {
335
        return Craft::$app->getEntries()->getSectionById($sourceId);
336
    }
337
338
    /**
339
     * Return the source model of the given $sourceId
340
     *
341
     * @param string $sourceHandle
342
     *
343
     * @return Section|null
344
     */
345
    public static function sourceModelFromHandle(string $sourceHandle)
346
    {
347
        return Craft::$app->getEntries()->getSectionByHandle($sourceHandle);
348
    }
349
350
    /**
351
     * Return the most recently updated Element from a given source model
352
     *
353
     * @param Model $sourceModel
354
     * @param int $sourceSiteId
355
     *
356
     * @return null|ElementInterface
357
     */
358
    public static function mostRecentElement(Model $sourceModel, int $sourceSiteId)
359
    {
360
        /** @var Section $sourceModel */
361
        return Entry::find()
362
            ->section($sourceModel->handle)
363
            ->siteId($sourceSiteId)
364
            ->limit(1)
365
            ->orderBy(['elements.dateUpdated' => SORT_DESC])
366
            ->one();
367
    }
368
369
    /**
370
     * Return the path to the config file directory
371
     *
372
     * @return string
373
     */
374
    public static function configFilePath(): string
375
    {
376
        return self::CONFIG_FILE_PATH;
377
    }
378
379
    /**
380
     * Return a meta bundle config array for the given $sourceModel
381
     *
382
     * @param Model $sourceModel
383
     *
384
     * @return array
385
     */
386
    public static function metaBundleConfig(Model $sourceModel): array
387
    {
388
        /** @var Section $sourceModel */
389
        return ArrayHelper::merge(
390
            ConfigHelper::getConfigFromFile(self::configFilePath()),
391
            [
392
                'sourceId' => $sourceModel->id,
393
                'sourceName' => (string)$sourceModel->name,
394
                'sourceHandle' => $sourceModel->handle,
395
                'sourceType' => $sourceModel->type,
396
            ]
397
        );
398
    }
399
400
    /**
401
     * Return the source id from the $element
402
     *
403
     * @param ElementInterface $element
404
     *
405
     * @return int|null
406
     */
407
    public static function sourceIdFromElement(ElementInterface $element)
408
    {
409
        /** @var Entry $element */
410
        return $element->sectionId;
411
    }
412
413
    /**
414
     * Return the (entry) type id from the $element
415
     *
416
     * @param ElementInterface $element
417
     *
418
     * @return int|null
419
     */
420
    public static function typeIdFromElement(ElementInterface $element)
421
    {
422
        /** @var Entry $element */
423
        return $element->typeId;
424
    }
425
426
    /**
427
     * Return the source handle from the $element
428
     *
429
     * @param ElementInterface $element
430
     *
431
     * @return string|null
432
     */
433
    public static function sourceHandleFromElement(ElementInterface $element)
434
    {
435
        $sourceHandle = '';
436
        /** @var Entry $element */
437
        try {
438
            $sourceHandle = $element->getSection()?->handle;
439
        } catch (InvalidConfigException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
440
        }
441
442
        return $sourceHandle;
443
    }
444
445
    /**
446
     * Create a MetaBundle in the db for each site, from the passed in $sourceModel
447
     *
448
     * @param Model $sourceModel
449
     */
450
    public static function createContentMetaBundle(Model $sourceModel)
451
    {
452
        /** @var Section $sourceModel */
453
        $sites = Craft::$app->getSites()->getAllSites();
454
        /** @var Site $site */
455
        foreach ($sites as $site) {
456
            $seoElement = self::class;
457
            /** @var SeoElementInterface $seoElement */
458
            Seomatic::$plugin->metaBundles->createMetaBundleFromSeoElement($seoElement, $sourceModel, $site->id);
459
        }
460
    }
461
462
    /**
463
     * Create all the MetaBundles in the db for this Seo Element
464
     */
465
    public static function createAllContentMetaBundles()
466
    {
467
        // Get all of the sections with URLs
468
        $sections = Craft::$app->getEntries()->getAllSections();
469
        foreach ($sections as $section) {
470
            self::createContentMetaBundle($section);
471
        }
472
    }
473
474
    /**
475
     * @inheritdoc
476
     */
477
    public static function getGqlInterfaceTypeName()
478
    {
479
        return EntryInterface::getName();
480
    }
481
}
482