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) 2017 nystudio107 |
||
10 | */ |
||
11 | |||
12 | namespace nystudio107\seomatic; |
||
13 | |||
14 | use nystudio107\seomatic\assetbundles\seomatic\SeomaticAsset; |
||
15 | use nystudio107\seomatic\fields\SeoSettings as SeoSettingsField; |
||
16 | use nystudio107\seomatic\fields\Seomatic_Meta as Seomatic_MetaField; |
||
17 | use nystudio107\seomatic\gql\arguments\SeomaticArguments; |
||
18 | use nystudio107\seomatic\gql\interfaces\SeomaticInterface; |
||
19 | use nystudio107\seomatic\gql\resolvers\SeomaticResolver; |
||
20 | use nystudio107\seomatic\gql\queries\SeomaticQuery; |
||
21 | use nystudio107\seomatic\helpers\Environment as EnvironmentHelper; |
||
22 | use nystudio107\seomatic\helpers\Gql as GqlHelper; |
||
23 | use nystudio107\seomatic\helpers\MetaValue as MetaValueHelper; |
||
24 | use nystudio107\seomatic\integrations\feedme\SeoSettings as SeoSettingsFeedMe; |
||
25 | use nystudio107\seomatic\listeners\GetCraftQLSchema; |
||
26 | use nystudio107\seomatic\models\MetaScriptContainer; |
||
27 | use nystudio107\seomatic\models\Settings; |
||
28 | use nystudio107\seomatic\services\FrontendTemplates as FrontendTemplatesService; |
||
29 | use nystudio107\seomatic\services\Helper as HelperService; |
||
30 | use nystudio107\seomatic\services\JsonLd as JsonLdService; |
||
31 | use nystudio107\seomatic\services\Link as LinkService; |
||
32 | use nystudio107\seomatic\services\MetaBundles as MetaBundlesService; |
||
33 | use nystudio107\seomatic\services\MetaContainers as MetaContainersService; |
||
34 | use nystudio107\seomatic\services\Script as ScriptService; |
||
35 | use nystudio107\seomatic\services\SeoElements as SeoElementsService; |
||
36 | use nystudio107\seomatic\services\Sitemaps as SitemapsService; |
||
37 | use nystudio107\seomatic\services\Tag as TagService; |
||
38 | use nystudio107\seomatic\services\Title as TitleService; |
||
39 | use nystudio107\seomatic\twigextensions\SeomaticTwigExtension; |
||
40 | use nystudio107\seomatic\variables\SeomaticVariable; |
||
41 | |||
42 | use nystudio107\pluginmanifest\services\ManifestService; |
||
43 | |||
44 | use nystudio107\fastcgicachebust\FastcgiCacheBust; |
||
45 | |||
46 | use Craft; |
||
47 | use craft\base\Element; |
||
48 | use craft\base\ElementInterface; |
||
49 | use craft\base\Plugin; |
||
50 | use craft\elements\User; |
||
51 | use craft\elements\Entry; |
||
52 | use craft\errors\SiteNotFoundException; |
||
53 | use craft\events\ElementEvent; |
||
54 | use craft\events\PluginEvent; |
||
55 | use craft\events\DefineGqlTypeFieldsEvent; |
||
56 | use craft\events\RegisterCacheOptionsEvent; |
||
57 | use craft\events\RegisterComponentTypesEvent; |
||
58 | use craft\events\RegisterGqlQueriesEvent; |
||
59 | use craft\events\RegisterGqlSchemaComponentsEvent; |
||
60 | use craft\events\RegisterGqlTypesEvent; |
||
61 | use craft\events\RegisterPreviewTargetsEvent; |
||
62 | use craft\events\RegisterUrlRulesEvent; |
||
63 | use craft\events\RegisterUserPermissionsEvent; |
||
64 | use craft\gql\TypeManager; |
||
65 | use craft\helpers\StringHelper; |
||
66 | use craft\services\Elements; |
||
67 | use craft\services\Fields; |
||
68 | use craft\services\Gql; |
||
69 | use craft\services\Plugins; |
||
70 | use craft\services\UserPermissions; |
||
71 | use craft\helpers\UrlHelper; |
||
72 | use craft\utilities\ClearCaches; |
||
73 | use craft\web\UrlManager; |
||
74 | use craft\web\View; |
||
75 | |||
76 | use craft\feedme\Plugin as FeedMe; |
||
77 | use craft\feedme\events\RegisterFeedMeFieldsEvent; |
||
78 | use craft\feedme\services\Fields as FeedMeFields; |
||
79 | |||
80 | use markhuot\CraftQL\Builders\Schema; |
||
81 | use markhuot\CraftQL\CraftQL; |
||
82 | use markhuot\CraftQL\Events\AlterSchemaFields; |
||
83 | |||
84 | use yii\base\Event; |
||
85 | |||
86 | /** @noinspection MissingPropertyAnnotationsInspection */ |
||
87 | |||
88 | /** |
||
89 | * Class Seomatic |
||
90 | * |
||
91 | * @author nystudio107 |
||
92 | * @package Seomatic |
||
93 | * @since 3.0.0 |
||
94 | * |
||
95 | * @property FrontendTemplatesService $frontendTemplates |
||
96 | * @property HelperService $helper |
||
97 | * @property JsonLdService $jsonLd |
||
98 | * @property LinkService $link |
||
99 | * @property MetaBundlesService $metaBundles |
||
100 | * @property MetaContainersService $metaContainers |
||
101 | * @property ScriptService $script |
||
102 | * @property SeoElementsService $seoElements |
||
103 | * @property SitemapsService $sitemaps |
||
104 | * @property TagService $tag |
||
105 | * @property TitleService $title |
||
106 | * @property ManifestService $manifest |
||
107 | */ |
||
108 | class Seomatic extends Plugin |
||
109 | { |
||
110 | // Constants |
||
111 | // ========================================================================= |
||
112 | |||
113 | const SEOMATIC_HANDLE = 'Seomatic'; |
||
114 | |||
115 | const DEVMODE_CACHE_DURATION = 30; |
||
116 | |||
117 | const FRONTEND_SEO_FILE_LINK = 'seomatic/seo-file-link/<url:[^\/]+>/<robots:[^\/]+>/<canonical:[^\/]+>/<inline:\d+>/<fileName:[-\w\.*]+>'; |
||
118 | |||
119 | const FRONTEND_PREVIEW_PATH = 'seomatic/preview-social-media'; |
||
120 | |||
121 | const SEOMATIC_PREVIEW_AUTHORIZATION_KEY = 'seomaticPreviewAuthorizationKey'; |
||
122 | |||
123 | const GQL_ELEMENT_INTERFACES = [ |
||
124 | 'EntryInterface', |
||
125 | 'CategoryInterface', |
||
126 | 'ProductInterface', |
||
127 | ]; |
||
128 | |||
129 | // Static Properties |
||
130 | // ========================================================================= |
||
131 | |||
132 | /** |
||
133 | * @var Seomatic |
||
134 | */ |
||
135 | public static $plugin; |
||
136 | |||
137 | /** |
||
138 | * @var SeomaticVariable |
||
139 | */ |
||
140 | public static $seomaticVariable; |
||
141 | |||
142 | /** |
||
143 | * @var Settings |
||
144 | */ |
||
145 | public static $settings; |
||
146 | |||
147 | /** |
||
148 | * @var ElementInterface |
||
149 | */ |
||
150 | public static $matchedElement; |
||
151 | |||
152 | /** |
||
153 | * @var bool |
||
154 | */ |
||
155 | public static $devMode; |
||
156 | |||
157 | /** |
||
158 | * @var View |
||
159 | */ |
||
160 | public static $view; |
||
161 | |||
162 | /** |
||
163 | * @var string |
||
164 | */ |
||
165 | public static $language; |
||
166 | |||
167 | /** |
||
168 | * @var string |
||
169 | */ |
||
170 | public static $environment; |
||
171 | |||
172 | /** |
||
173 | * @var int |
||
174 | */ |
||
175 | public static $cacheDuration; |
||
176 | |||
177 | /** |
||
178 | * @var bool |
||
179 | */ |
||
180 | public static $previewingMetaContainers = false; |
||
181 | |||
182 | /** |
||
183 | * @var bool |
||
184 | */ |
||
185 | public static $loadingMetaContainers = false; |
||
186 | |||
187 | /** |
||
188 | * @var bool |
||
189 | */ |
||
190 | public static $savingSettings = false; |
||
191 | |||
192 | /** |
||
193 | * @var bool |
||
194 | */ |
||
195 | public static $headlessRequest = false; |
||
196 | |||
197 | /** |
||
198 | * @var bool |
||
199 | */ |
||
200 | public static $craft31 = false; |
||
201 | |||
202 | /** |
||
203 | * @var bool |
||
204 | */ |
||
205 | public static $craft32 = false; |
||
206 | |||
207 | /** |
||
208 | * @var bool |
||
209 | */ |
||
210 | public static $craft33 = false; |
||
211 | |||
212 | /** |
||
213 | * @var bool |
||
214 | */ |
||
215 | public static $craft34 = false; |
||
216 | |||
217 | /** |
||
218 | * @var bool |
||
219 | */ |
||
220 | public static $craft35 = false; |
||
221 | |||
222 | // Static Methods |
||
223 | // ========================================================================= |
||
224 | |||
225 | /** |
||
226 | * @inheritdoc |
||
227 | */ |
||
228 | public function __construct($id, $parent = null, array $config = []) |
||
229 | { |
||
230 | $config['components'] = [ |
||
231 | 'frontendTemplates' => FrontendTemplatesService::class, |
||
232 | 'helper' => HelperService::class, |
||
233 | 'jsonLd' => JsonLdService::class, |
||
234 | 'link' => LinkService::class, |
||
235 | 'metaBundles' => MetaBundlesService::class, |
||
236 | 'metaContainers' => MetaContainersService::class, |
||
237 | 'script' => ScriptService::class, |
||
238 | 'seoElements' => SeoElementsService::class, |
||
239 | 'sitemaps' => SitemapsService::class, |
||
240 | 'tag' => TagService::class, |
||
241 | 'title' => TitleService::class, |
||
242 | // Register the manifest service |
||
243 | 'manifest' => [ |
||
244 | 'class' => ManifestService::class, |
||
245 | 'assetClass' => SeomaticAsset::class, |
||
246 | 'devServerManifestPath' => 'http://craft-seomatic-buildchain:8080/', |
||
247 | 'devServerPublicPath' => 'http://craft-seomatic-buildchain:8080/', |
||
248 | ], |
||
249 | ]; |
||
250 | |||
251 | parent::__construct($id, $parent, $config); |
||
252 | } |
||
253 | |||
254 | /** |
||
255 | * Set the matched element |
||
256 | * |
||
257 | * @param $element null|ElementInterface |
||
258 | */ |
||
259 | public static function setMatchedElement($element) |
||
260 | { |
||
261 | self::$matchedElement = $element; |
||
262 | /** @var $element Element */ |
||
263 | if ($element) { |
||
264 | self::$language = MetaValueHelper::getSiteLanguage($element->siteId); |
||
265 | } else { |
||
266 | self::$language = MetaValueHelper::getSiteLanguage(null); |
||
267 | } |
||
268 | MetaValueHelper::cache(); |
||
269 | } |
||
270 | |||
271 | // Public Properties |
||
272 | // ========================================================================= |
||
273 | |||
274 | /** |
||
275 | * @var string |
||
276 | */ |
||
277 | public $schemaVersion = '3.0.10'; |
||
278 | |||
279 | /** |
||
280 | * @var bool |
||
281 | */ |
||
282 | public $hasCpSection = true; |
||
283 | |||
284 | /** |
||
285 | * @var bool |
||
286 | */ |
||
287 | public $hasCpSettings = true; |
||
288 | |||
289 | // Public Methods |
||
290 | // ========================================================================= |
||
291 | |||
292 | /** |
||
293 | * @inheritdoc |
||
294 | */ |
||
295 | public function init() |
||
296 | { |
||
297 | parent::init(); |
||
298 | self::$plugin = $this; |
||
299 | // Handle any console commands |
||
300 | $request = Craft::$app->getRequest(); |
||
301 | if ($request->getIsConsoleRequest()) { |
||
302 | $this->controllerNamespace = 'nystudio107\seomatic\console\controllers'; |
||
303 | } |
||
304 | // Initialize properties |
||
305 | self::$settings = self::$plugin->getSettings(); |
||
306 | self::$devMode = Craft::$app->getConfig()->getGeneral()->devMode; |
||
307 | self::$view = Craft::$app->getView(); |
||
308 | self::$cacheDuration = self::$devMode |
||
309 | ? self::DEVMODE_CACHE_DURATION |
||
310 | : self::$settings->metaCacheDuration ?? null; |
||
311 | self::$cacheDuration = self::$cacheDuration === null ? null : (int)self::$cacheDuration; |
||
312 | self::$environment = EnvironmentHelper::determineEnvironment(); |
||
313 | MetaValueHelper::cache(); |
||
314 | // Version helpers |
||
315 | self::$craft31 = version_compare(Craft::$app->getVersion(), '3.1', '>='); |
||
316 | self::$craft32 = version_compare(Craft::$app->getVersion(), '3.2', '>='); |
||
317 | self::$craft33 = version_compare(Craft::$app->getVersion(), '3.3', '>='); |
||
318 | self::$craft34 = version_compare(Craft::$app->getVersion(), '3.4', '>='); |
||
319 | self::$craft35 = version_compare(Craft::$app->getVersion(), '3.5', '>='); |
||
0 ignored issues
–
show
|
|||
320 | $this->name = self::$settings->pluginName; |
||
321 | // Install our event listeners |
||
322 | $this->installEventListeners(); |
||
323 | // We're loaded |
||
324 | Craft::info( |
||
325 | Craft::t( |
||
326 | 'seomatic', |
||
327 | '{name} plugin loaded', |
||
328 | ['name' => $this->name] |
||
329 | ), |
||
330 | __METHOD__ |
||
331 | ); |
||
332 | } |
||
333 | |||
334 | /** |
||
335 | * @inheritdoc |
||
336 | */ |
||
337 | public function getSettings() |
||
338 | { |
||
339 | // For all the emojis |
||
340 | $settingsModel = parent::getSettings(); |
||
341 | if ($settingsModel !== null && !self::$savingSettings) { |
||
342 | $attributes = $settingsModel->attributes(); |
||
343 | if ($attributes !== null) { |
||
344 | foreach ($attributes as $attribute) { |
||
345 | if (is_string($settingsModel->$attribute)) { |
||
346 | $settingsModel->$attribute = html_entity_decode( |
||
347 | $settingsModel->$attribute, |
||
348 | ENT_NOQUOTES, |
||
349 | 'UTF-8' |
||
350 | ); |
||
351 | } |
||
352 | } |
||
353 | } |
||
354 | self::$savingSettings = false; |
||
355 | } |
||
356 | |||
357 | return $settingsModel; |
||
358 | } |
||
359 | |||
360 | /** |
||
361 | * @inheritdoc |
||
362 | */ |
||
363 | public function getSettingsResponse() |
||
364 | { |
||
365 | // Just redirect to the plugin settings page |
||
366 | Craft::$app->getResponse()->redirect(UrlHelper::cpUrl('seomatic/plugin')); |
||
367 | } |
||
368 | |||
369 | /** |
||
370 | * @inheritdoc |
||
371 | */ |
||
372 | public function getCpNavItem() |
||
373 | { |
||
374 | $subNavs = []; |
||
375 | $navItem = parent::getCpNavItem(); |
||
376 | /** @var User $currentUser */ |
||
377 | $request = Craft::$app->getRequest(); |
||
378 | $siteSuffix = ''; |
||
379 | if ($request->getSegment(1) === 'seomatic') { |
||
380 | $segments = $request->getSegments(); |
||
381 | $lastSegment = end($segments); |
||
382 | $site = Craft::$app->getSites()->getSiteByHandle($lastSegment); |
||
383 | if ($site !== null) { |
||
384 | $siteSuffix = '/'.$lastSegment; |
||
385 | } |
||
386 | } |
||
387 | $currentUser = Craft::$app->getUser()->getIdentity(); |
||
388 | // Only show sub-navs the user has permission to view |
||
389 | if ($currentUser->can('seomatic:dashboard')) { |
||
390 | $subNavs['dashboard'] = [ |
||
391 | 'label' => Craft::t('seomatic', 'Dashboard'), |
||
392 | 'url' => 'seomatic/dashboard'.$siteSuffix, |
||
393 | ]; |
||
394 | } |
||
395 | if ($currentUser->can('seomatic:global-meta')) { |
||
396 | $subNavs['global'] = [ |
||
397 | 'label' => Craft::t('seomatic', 'Global SEO'), |
||
398 | 'url' => 'seomatic/global/general'.$siteSuffix, |
||
399 | ]; |
||
400 | } |
||
401 | if ($currentUser->can('seomatic:content-meta')) { |
||
402 | $subNavs['content'] = [ |
||
403 | 'label' => Craft::t('seomatic', 'Content SEO'), |
||
404 | 'url' => 'seomatic/content'.$siteSuffix, |
||
405 | ]; |
||
406 | } |
||
407 | if ($currentUser->can('seomatic:site-settings')) { |
||
408 | $subNavs['site'] = [ |
||
409 | 'label' => Craft::t('seomatic', 'Site Settings'), |
||
410 | 'url' => 'seomatic/site/identity'.$siteSuffix, |
||
411 | ]; |
||
412 | } |
||
413 | if ($currentUser->can('seomatic:tracking-scripts')) { |
||
414 | $subNavs['tracking'] = [ |
||
415 | 'label' => Craft::t('seomatic', 'Tracking Scripts'), |
||
416 | 'url' => 'seomatic/tracking/gtag'.$siteSuffix, |
||
417 | ]; |
||
418 | } |
||
419 | $editableSettings = true; |
||
420 | $general = Craft::$app->getConfig()->getGeneral(); |
||
421 | if (self::$craft31 && !$general->allowAdminChanges) { |
||
422 | $editableSettings = false; |
||
423 | } |
||
424 | if ($currentUser->can('seomatic:plugin-settings') && $editableSettings) { |
||
425 | $subNavs['plugin'] = [ |
||
426 | 'label' => Craft::t('seomatic', 'Plugin Settings'), |
||
427 | 'url' => 'seomatic/plugin', |
||
428 | ]; |
||
429 | } |
||
430 | $navItem = array_merge($navItem, [ |
||
431 | 'subnav' => $subNavs, |
||
432 | ]); |
||
433 | |||
434 | return $navItem; |
||
435 | } |
||
436 | |||
437 | /** |
||
438 | * Clear all the caches! |
||
439 | */ |
||
440 | public function clearAllCaches() |
||
441 | { |
||
442 | // Clear all of SEOmatic's caches |
||
443 | self::$plugin->frontendTemplates->invalidateCaches(); |
||
444 | self::$plugin->metaContainers->invalidateCaches(); |
||
445 | self::$plugin->sitemaps->invalidateCaches(); |
||
446 | // If they are using Craft 3.3 or later, clear the GraphQL caches too |
||
447 | if (self::$craft33) { |
||
448 | $gql = Craft::$app->getGql(); |
||
449 | if (method_exists($gql, 'invalidateCaches')) { |
||
450 | $gql->invalidateCaches(); |
||
451 | } |
||
452 | } |
||
453 | // If the FastCGI Cache Bust plugin is installed, clear its caches too |
||
454 | $plugin = Craft::$app->getPlugins()->getPlugin('fastcgi-cache-bust'); |
||
455 | if ($plugin !== null) { |
||
456 | FastcgiCacheBust::$plugin->cache->clearAll(); |
||
457 | } |
||
458 | } |
||
459 | |||
460 | /** |
||
461 | * Determine whether our table schema exists or not; this is needed because |
||
462 | * migrations such as the install migration and base_install migration may |
||
463 | * not have been run by the time our init() method has been called |
||
464 | * |
||
465 | * @return bool |
||
466 | */ |
||
467 | public function migrationsAndSchemaReady(): bool |
||
468 | { |
||
469 | $pluginsService = Craft::$app->getPlugins(); |
||
470 | if ($pluginsService->doesPluginRequireDatabaseUpdate(self::$plugin)) { |
||
471 | return false; |
||
472 | } |
||
473 | if (Craft::$app->db->schema->getTableSchema('{{%seomatic_metabundles}}') === null) { |
||
474 | return false; |
||
475 | } |
||
476 | |||
477 | return true; |
||
478 | } |
||
479 | |||
480 | // Protected Methods |
||
481 | // ========================================================================= |
||
482 | |||
483 | /** |
||
484 | * Install our event listeners. |
||
485 | */ |
||
486 | protected function installEventListeners() |
||
487 | { |
||
488 | // Install our event listeners only if our table schema exists |
||
489 | if ($this->migrationsAndSchemaReady()) { |
||
490 | // Add in our Twig extensions |
||
491 | self::$view->registerTwigExtension(new SeomaticTwigExtension); |
||
492 | $request = Craft::$app->getRequest(); |
||
493 | // Add in our event listeners that are needed for every request |
||
494 | $this->installGlobalEventListeners(); |
||
495 | // Install only for non-console site requests |
||
496 | if ($request->getIsSiteRequest() && !$request->getIsConsoleRequest()) { |
||
497 | $this->installSiteEventListeners(); |
||
498 | } |
||
499 | // Install only for non-console Control Panel requests |
||
500 | if ($request->getIsCpRequest() && !$request->getIsConsoleRequest()) { |
||
501 | $this->installCpEventListeners(); |
||
502 | } |
||
503 | } |
||
504 | // Handler: EVENT_AFTER_INSTALL_PLUGIN |
||
505 | Event::on( |
||
506 | Plugins::class, |
||
507 | Plugins::EVENT_AFTER_INSTALL_PLUGIN, |
||
508 | function (PluginEvent $event) { |
||
509 | if ($event->plugin === $this) { |
||
510 | // Invalidate our caches after we've been installed |
||
511 | $this->clearAllCaches(); |
||
512 | // Send them to our welcome screen |
||
513 | $request = Craft::$app->getRequest(); |
||
514 | if ($request->isCpRequest) { |
||
515 | Craft::$app->getResponse()->redirect(UrlHelper::cpUrl( |
||
516 | 'seomatic/dashboard', |
||
517 | [ |
||
518 | 'showWelcome' => true, |
||
519 | ] |
||
520 | ))->send(); |
||
521 | } |
||
522 | } |
||
523 | } |
||
524 | ); |
||
525 | // Handler: ClearCaches::EVENT_REGISTER_CACHE_OPTIONS |
||
526 | Event::on( |
||
527 | ClearCaches::class, |
||
528 | ClearCaches::EVENT_REGISTER_CACHE_OPTIONS, |
||
529 | function (RegisterCacheOptionsEvent $event) { |
||
530 | Craft::debug( |
||
531 | 'ClearCaches::EVENT_REGISTER_CACHE_OPTIONS', |
||
532 | __METHOD__ |
||
533 | ); |
||
534 | // Register our Cache Options |
||
535 | $event->options = array_merge( |
||
536 | $event->options, |
||
537 | $this->customAdminCpCacheOptions() |
||
538 | ); |
||
539 | } |
||
540 | ); |
||
541 | // Handler: EVENT_BEFORE_SAVE_PLUGIN_SETTINGS |
||
542 | Event::on( |
||
543 | Plugins::class, |
||
544 | Plugins::EVENT_BEFORE_SAVE_PLUGIN_SETTINGS, |
||
545 | function (PluginEvent $event) { |
||
546 | if ($event->plugin === $this && !Craft::$app->getDb()->getSupportsMb4()) { |
||
547 | // For all the emojis |
||
548 | $settingsModel = $this->getSettings(); |
||
549 | self::$savingSettings = true; |
||
550 | if ($settingsModel !== null) { |
||
551 | $attributes = $settingsModel->attributes(); |
||
552 | if ($attributes !== null) { |
||
553 | foreach ($attributes as $attribute) { |
||
554 | if (is_string($settingsModel->$attribute)) { |
||
555 | $settingsModel->$attribute = |
||
556 | StringHelper::encodeMb4($settingsModel->$attribute); |
||
557 | } |
||
558 | } |
||
559 | } |
||
560 | } |
||
561 | } |
||
562 | } |
||
563 | ); |
||
564 | } |
||
565 | |||
566 | /** |
||
567 | * Install global event listeners for all request types |
||
568 | */ |
||
569 | protected function installGlobalEventListeners() |
||
570 | { |
||
571 | // Handler: Plugins::EVENT_AFTER_LOAD_PLUGINS |
||
572 | Event::on( |
||
573 | Plugins::class, |
||
574 | Plugins::EVENT_AFTER_LOAD_PLUGINS, |
||
575 | function () { |
||
576 | // Install these only after all other plugins have loaded |
||
577 | $request = Craft::$app->getRequest(); |
||
578 | // Allow the SeoElements to register their own event handlers |
||
579 | self::$plugin->seoElements->getAllSeoElementTypes(); |
||
580 | // Only respond to non-console site requests |
||
581 | if ($request->getIsSiteRequest() && !$request->getIsConsoleRequest()) { |
||
582 | $this->handleSiteRequest(); |
||
583 | } |
||
584 | // Respond to Control Panel requests |
||
585 | if ($request->getIsCpRequest() && !$request->getIsConsoleRequest()) { |
||
586 | $this->handleAdminCpRequest(); |
||
587 | } |
||
588 | } |
||
589 | ); |
||
590 | // Handler: Fields::EVENT_REGISTER_FIELD_TYPES |
||
591 | Event::on( |
||
592 | Fields::class, |
||
593 | Fields::EVENT_REGISTER_FIELD_TYPES, |
||
594 | function (RegisterComponentTypesEvent $event) { |
||
595 | $event->types[] = SeoSettingsField::class; |
||
596 | $event->types[] = Seomatic_MetaField::class; |
||
597 | } |
||
598 | ); |
||
599 | // Handler: Elements::EVENT_AFTER_SAVE_ELEMENT |
||
600 | Event::on( |
||
601 | Elements::class, |
||
602 | Elements::EVENT_AFTER_SAVE_ELEMENT, |
||
603 | function (ElementEvent $event) { |
||
604 | Craft::debug( |
||
605 | 'Elements::EVENT_AFTER_SAVE_ELEMENT', |
||
606 | __METHOD__ |
||
607 | ); |
||
608 | /** @var $element Element */ |
||
609 | $element = $event->element; |
||
610 | self::$plugin->metaBundles->invalidateMetaBundleByElement( |
||
611 | $element, |
||
612 | $event->isNew |
||
613 | ); |
||
614 | if ($event->isNew) { |
||
615 | self::$plugin->sitemaps->submitSitemapForElement($element); |
||
616 | } |
||
617 | } |
||
618 | ); |
||
619 | // Handler: Elements::EVENT_AFTER_DELETE_ELEMENT |
||
620 | Event::on( |
||
621 | Elements::class, |
||
622 | Elements::EVENT_AFTER_DELETE_ELEMENT, |
||
623 | function (ElementEvent $event) { |
||
624 | Craft::debug( |
||
625 | 'Elements::EVENT_AFTER_DELETE_ELEMENT', |
||
626 | __METHOD__ |
||
627 | ); |
||
628 | /** @var $element Element */ |
||
629 | $element = $event->element; |
||
630 | self::$plugin->metaBundles->invalidateMetaBundleByElement( |
||
631 | $element, |
||
632 | false |
||
633 | ); |
||
634 | } |
||
635 | ); |
||
636 | // Add social media preview targets on Craft 3.2 or later |
||
637 | if (self::$craft32 && Seomatic::$settings->socialMediaPreviewTarget) { |
||
638 | // Handler: Entry::EVENT_REGISTER_PREVIEW_TARGETS |
||
639 | Event::on( |
||
640 | Entry::class, |
||
641 | Entry::EVENT_REGISTER_PREVIEW_TARGETS, |
||
642 | function (RegisterPreviewTargetsEvent $e) { |
||
643 | /** @var Element $element */ |
||
644 | $element = $e->sender; |
||
645 | if ($element->uri !== null) { |
||
646 | $e->previewTargets[] = [ |
||
647 | 'label' => '📣 '.Craft::t('seomatic', 'Social Media Preview'), |
||
648 | 'url' => UrlHelper::siteUrl(self::FRONTEND_PREVIEW_PATH, [ |
||
649 | 'elementId' => $element->id, |
||
650 | 'siteId' => $element->siteId, |
||
651 | ]), |
||
652 | ]; |
||
653 | // Don't allow the preview to be accessed publicly |
||
654 | Craft::$app->getSession()->authorize(self::SEOMATIC_PREVIEW_AUTHORIZATION_KEY.$element->id); |
||
655 | } |
||
656 | } |
||
657 | ); |
||
658 | } |
||
659 | // Add native GraphQL support on Craft 3.3 or later |
||
660 | if (self::$craft33) { |
||
661 | // Handler: Gql::EVENT_REGISTER_GQL_TYPES |
||
662 | Event::on( |
||
663 | Gql::class, |
||
664 | Gql::EVENT_REGISTER_GQL_TYPES, |
||
665 | function (RegisterGqlTypesEvent $event) { |
||
666 | Craft::debug( |
||
667 | 'Gql::EVENT_REGISTER_GQL_TYPES', |
||
668 | __METHOD__ |
||
669 | ); |
||
670 | $event->types[] = SeomaticInterface::class; |
||
671 | } |
||
672 | ); |
||
673 | // Handler: Gql::EVENT_REGISTER_GQL_QUERIES |
||
674 | Event::on( |
||
675 | Gql::class, |
||
676 | Gql::EVENT_REGISTER_GQL_QUERIES, |
||
677 | function (RegisterGqlQueriesEvent $event) { |
||
678 | Craft::debug( |
||
679 | 'Gql::EVENT_REGISTER_GQL_QUERIES', |
||
680 | __METHOD__ |
||
681 | ); |
||
682 | $queries = SeomaticQuery::getQueries(); |
||
683 | foreach ($queries as $key => $value) { |
||
684 | $event->queries[$key] = $value; |
||
685 | } |
||
686 | } |
||
687 | ); |
||
688 | if (self::$craft35) { |
||
689 | // Handler: Gql::EVENT_REGISTER_GQL_SCHEMA_COMPONENTS |
||
690 | Event::on( |
||
691 | Gql::class, |
||
692 | Gql::EVENT_REGISTER_GQL_SCHEMA_COMPONENTS, |
||
693 | function (RegisterGqlSchemaComponentsEvent $event) { |
||
694 | Craft::debug( |
||
695 | 'Gql::EVENT_REGISTER_GQL_SCHEMA_COMPONENTS', |
||
696 | __METHOD__ |
||
697 | ); |
||
698 | $label = Craft::t('seomatic', 'Seomatic'); |
||
699 | $event->queries[$label]['seomatic.all:read'] = ['label' => Craft::t('seomatic', 'Query Seomatic data')]; |
||
700 | } |
||
701 | ); |
||
702 | } |
||
703 | } |
||
704 | // Add support for querying for SEOmatic metadata inside of element queries |
||
705 | if (self::$craft34) { |
||
706 | // Handler: TypeManager::EVENT_DEFINE_GQL_TYPE_FIELDS |
||
707 | Event::on( |
||
708 | TypeManager::class, |
||
709 | TypeManager::EVENT_DEFINE_GQL_TYPE_FIELDS, |
||
710 | function (DefineGqlTypeFieldsEvent $event) { |
||
711 | if (in_array($event->typeName, self::GQL_ELEMENT_INTERFACES, true)) { |
||
712 | Craft::debug( |
||
713 | 'TypeManager::EVENT_DEFINE_GQL_TYPE_FIELDS', |
||
714 | __METHOD__ |
||
715 | ); |
||
716 | |||
717 | if (GqlHelper::canQuerySeo()) { |
||
718 | // Make Seomatic tags available to all entries. |
||
719 | $event->fields['seomatic'] = [ |
||
720 | 'name' => 'seomatic', |
||
721 | 'type' => SeomaticInterface::getType(), |
||
722 | 'args' => SeomaticArguments::getArguments(), |
||
723 | 'resolve' => SeomaticResolver::class . '::resolve', |
||
724 | 'description' => Craft::t('seomatic', 'This query is used to query for SEOmatic meta data.') |
||
725 | ]; |
||
726 | } |
||
727 | } |
||
728 | }); |
||
729 | } |
||
730 | // CraftQL Support |
||
731 | if (class_exists(CraftQL::class)) { |
||
732 | Event::on( |
||
733 | Schema::class, |
||
734 | AlterSchemaFields::EVENT, |
||
735 | [GetCraftQLSchema::class, 'handle'] |
||
736 | ); |
||
737 | } |
||
738 | // FeedMe Support |
||
739 | if (class_exists(FeedMe::class)) { |
||
740 | Event::on( |
||
741 | FeedMeFields::class, |
||
742 | FeedMeFields::EVENT_REGISTER_FEED_ME_FIELDS, |
||
743 | function(RegisterFeedMeFieldsEvent $e) { |
||
744 | Craft::debug( |
||
745 | 'FeedMeFields::EVENT_REGISTER_FEED_ME_FIELDS', |
||
746 | __METHOD__ |
||
747 | ); |
||
748 | $e->fields[] = SeoSettingsFeedMe::class; |
||
749 | } |
||
750 | ); |
||
751 | } |
||
752 | } |
||
753 | |||
754 | /** |
||
755 | * Install site event listeners for site requests only |
||
756 | */ |
||
757 | protected function installSiteEventListeners() |
||
758 | { |
||
759 | // Load the sitemap containers |
||
760 | self::$plugin->sitemaps->loadSitemapContainers(); |
||
761 | // Load the frontend template containers |
||
762 | self::$plugin->frontendTemplates->loadFrontendTemplateContainers(); |
||
763 | // Handler: UrlManager::EVENT_REGISTER_SITE_URL_RULES |
||
764 | Event::on( |
||
765 | UrlManager::class, |
||
766 | UrlManager::EVENT_REGISTER_SITE_URL_RULES, |
||
767 | function (RegisterUrlRulesEvent $event) { |
||
768 | Craft::debug( |
||
769 | 'UrlManager::EVENT_REGISTER_SITE_URL_RULES', |
||
770 | __METHOD__ |
||
771 | ); |
||
772 | // FileController |
||
773 | $route = self::$plugin->handle.'/file/seo-file-link'; |
||
774 | $event->rules[self::FRONTEND_SEO_FILE_LINK] = ['route' => $route]; |
||
775 | // PreviewController |
||
776 | $route = self::$plugin->handle.'/preview/social-media'; |
||
777 | $event->rules[self::FRONTEND_PREVIEW_PATH] = ['route' => $route]; |
||
778 | // Register our Control Panel routes |
||
779 | $event->rules = array_merge( |
||
780 | $event->rules, |
||
781 | $this->customFrontendRoutes() |
||
782 | ); |
||
783 | } |
||
784 | ); |
||
785 | } |
||
786 | |||
787 | /** |
||
788 | * Install site event listeners for Control Panel requests only |
||
789 | */ |
||
790 | protected function installCpEventListeners() |
||
791 | { |
||
792 | // Load the frontend template containers |
||
793 | self::$plugin->frontendTemplates->loadFrontendTemplateContainers(); |
||
794 | // Handler: UrlManager::EVENT_REGISTER_CP_URL_RULES |
||
795 | Event::on( |
||
796 | UrlManager::class, |
||
797 | UrlManager::EVENT_REGISTER_CP_URL_RULES, |
||
798 | function (RegisterUrlRulesEvent $event) { |
||
799 | Craft::debug( |
||
800 | 'UrlManager::EVENT_REGISTER_CP_URL_RULES', |
||
801 | __METHOD__ |
||
802 | ); |
||
803 | // Register our Control Panel routes |
||
804 | $event->rules = array_merge( |
||
805 | $event->rules, |
||
806 | $this->customAdminCpRoutes() |
||
807 | ); |
||
808 | } |
||
809 | ); |
||
810 | // Handler: UserPermissions::EVENT_REGISTER_PERMISSIONS |
||
811 | Event::on( |
||
812 | UserPermissions::class, |
||
813 | UserPermissions::EVENT_REGISTER_PERMISSIONS, |
||
814 | function (RegisterUserPermissionsEvent $event) { |
||
815 | Craft::debug( |
||
816 | 'UserPermissions::EVENT_REGISTER_PERMISSIONS', |
||
817 | __METHOD__ |
||
818 | ); |
||
819 | // Register our custom permissions |
||
820 | $event->permissions[Craft::t('seomatic', 'SEOmatic')] = $this->customAdminCpPermissions(); |
||
821 | } |
||
822 | ); |
||
823 | } |
||
824 | |||
825 | /** |
||
826 | * Handle site requests. We do it only after we receive the event |
||
827 | * EVENT_AFTER_LOAD_PLUGINS so that any pending db migrations can be run |
||
828 | * before our event listeners kick in |
||
829 | */ |
||
830 | protected function handleSiteRequest() |
||
831 | { |
||
832 | // Handler: View::EVENT_BEGIN_BODY |
||
833 | Event::on( |
||
834 | View::class, |
||
835 | View::EVENT_BEGIN_BODY, |
||
836 | function () { |
||
837 | Craft::debug( |
||
838 | 'View::EVENT_BEGIN_BODY', |
||
839 | __METHOD__ |
||
840 | ); |
||
841 | // The <body> placeholder tag has just rendered, include any script HTML |
||
842 | if (self::$settings->renderEnabled && self::$seomaticVariable) { |
||
843 | self::$plugin->metaContainers->includeScriptBodyHtml(View::POS_BEGIN); |
||
844 | } |
||
845 | } |
||
846 | ); |
||
847 | // Handler: View::EVENT_END_BODY |
||
848 | Event::on( |
||
849 | View::class, |
||
850 | View::EVENT_END_BODY, |
||
851 | function () { |
||
852 | Craft::debug( |
||
853 | 'View::EVENT_END_BODY', |
||
854 | __METHOD__ |
||
855 | ); |
||
856 | // The </body> placeholder tag is about to be rendered, include any script HTML |
||
857 | if (self::$settings->renderEnabled && self::$seomaticVariable) { |
||
858 | self::$plugin->metaContainers->includeScriptBodyHtml(View::POS_END); |
||
859 | } |
||
860 | } |
||
861 | ); |
||
862 | // Handler: View::EVENT_END_PAGE |
||
863 | Event::on( |
||
864 | View::class, |
||
865 | View::EVENT_END_PAGE, |
||
866 | function () { |
||
867 | Craft::debug( |
||
868 | 'View::EVENT_END_PAGE', |
||
869 | __METHOD__ |
||
870 | ); |
||
871 | // The page is done rendering, include our meta containers |
||
872 | if (self::$settings->renderEnabled && self::$seomaticVariable) { |
||
873 | self::$plugin->metaContainers->includeMetaContainers(); |
||
874 | } |
||
875 | } |
||
876 | ); |
||
877 | } |
||
878 | |||
879 | /** |
||
880 | * Handle Control Panel requests. We do it only after we receive the event |
||
881 | * EVENT_AFTER_LOAD_PLUGINS so that any pending db migrations can be run |
||
882 | * before our event listeners kick in |
||
883 | */ |
||
884 | protected function handleAdminCpRequest() |
||
885 | { |
||
886 | // Don't cache Control Panel requests |
||
887 | self::$cacheDuration = 1; |
||
888 | // Prefix the Control Panel title |
||
889 | self::$view->hook('cp.layouts.base', function (&$context) { |
||
890 | if (self::$devMode) { |
||
891 | $context['docTitle'] = self::$settings->devModeCpTitlePrefix.$context['docTitle']; |
||
892 | } else { |
||
893 | $context['docTitle'] = self::$settings->cpTitlePrefix.$context['docTitle']; |
||
894 | } |
||
895 | }); |
||
896 | } |
||
897 | |||
898 | /** |
||
899 | * @inheritdoc |
||
900 | */ |
||
901 | protected function createSettingsModel() |
||
902 | { |
||
903 | return new Settings(); |
||
904 | } |
||
905 | |||
906 | /** |
||
907 | * Return the custom Control Panel routes |
||
908 | * |
||
909 | * @return array |
||
910 | */ |
||
911 | protected function customAdminCpRoutes(): array |
||
912 | { |
||
913 | return [ |
||
914 | 'seomatic' => |
||
915 | 'seomatic/settings/dashboard', |
||
916 | 'seomatic/dashboard' => |
||
917 | 'seomatic/settings/dashboard', |
||
918 | 'seomatic/dashboard/<siteHandle:{handle}>' => |
||
919 | 'seomatic/settings/dashboard', |
||
920 | |||
921 | 'seomatic/global' => [ |
||
922 | 'route' => 'seomatic/settings/global', |
||
923 | 'defaults' => ['subSection' => 'general'], |
||
924 | ], |
||
925 | 'seomatic/global/<subSection:{handle}>' => |
||
926 | 'seomatic/settings/global', |
||
927 | 'seomatic/global/<subSection:{handle}>/<siteHandle:{handle}>' => |
||
928 | 'seomatic/settings/global', |
||
929 | |||
930 | 'seomatic/content' => |
||
931 | 'seomatic/settings/content', |
||
932 | 'seomatic/content/<siteHandle:{handle}>' => |
||
933 | 'seomatic/settings/content', |
||
934 | |||
935 | 'seomatic/edit-content/<subSection:{handle}>/<sourceBundleType:{handle}>/<sourceHandle:{handle}>' => |
||
936 | 'seomatic/settings/edit-content', |
||
937 | 'seomatic/edit-content/<subSection:{handle}>/<sourceBundleType:{handle}>/<sourceHandle:{handle}>/<siteHandle:{handle}>' => |
||
938 | 'seomatic/settings/edit-content', |
||
939 | |||
940 | 'seomatic/site' => [ |
||
941 | 'route' => 'seomatic/settings/site', |
||
942 | 'defaults' => ['subSection' => 'identity'], |
||
943 | ], |
||
944 | 'seomatic/site/<subSection:{handle}>' => |
||
945 | 'seomatic/settings/site', |
||
946 | 'seomatic/site/<subSection:{handle}>/<siteHandle:{handle}>' => |
||
947 | 'seomatic/settings/site', |
||
948 | |||
949 | 'seomatic/tracking' => [ |
||
950 | 'route' => 'seomatic/settings/tracking', |
||
951 | 'defaults' => ['subSection' => 'googleAnalytics'], |
||
952 | ], |
||
953 | 'seomatic/tracking/<subSection:{handle}>' => |
||
954 | 'seomatic/settings/tracking', |
||
955 | 'seomatic/tracking/<subSection:{handle}>/<siteHandle:{handle}>' => |
||
956 | 'seomatic/settings/tracking', |
||
957 | |||
958 | 'seomatic/plugin' => |
||
959 | 'seomatic/settings/plugin', |
||
960 | ]; |
||
961 | } |
||
962 | |||
963 | /** |
||
964 | * Return the custom frontend routes |
||
965 | * |
||
966 | * @return array |
||
967 | */ |
||
968 | protected function customFrontendRoutes(): array |
||
969 | { |
||
970 | return [ |
||
971 | ]; |
||
972 | } |
||
973 | |||
974 | /** |
||
975 | * Returns the custom Control Panel cache options. |
||
976 | * |
||
977 | * @return array |
||
978 | */ |
||
979 | protected function customAdminCpCacheOptions(): array |
||
980 | { |
||
981 | return [ |
||
982 | // Frontend template caches |
||
983 | [ |
||
984 | 'key' => 'seomatic-frontendtemplate-caches', |
||
985 | 'label' => Craft::t('seomatic', 'SEOmatic frontend template caches'), |
||
986 | 'action' => [self::$plugin->frontendTemplates, 'invalidateCaches'], |
||
987 | ], |
||
988 | // Meta bundle caches |
||
989 | [ |
||
990 | 'key' => 'seomatic-metabundle-caches', |
||
991 | 'label' => Craft::t('seomatic', 'SEOmatic metadata caches'), |
||
992 | 'action' => [self::$plugin->metaContainers, 'invalidateCaches'], |
||
993 | ], |
||
994 | // Sitemap caches |
||
995 | [ |
||
996 | 'key' => 'seomatic-sitemap-caches', |
||
997 | 'label' => Craft::t('seomatic', 'SEOmatic sitemap caches'), |
||
998 | 'action' => [self::$plugin->sitemaps, 'invalidateCaches'], |
||
999 | ], |
||
1000 | ]; |
||
1001 | } |
||
1002 | |||
1003 | /** |
||
1004 | * Returns the custom Control Panel user permissions. |
||
1005 | * |
||
1006 | * @return array |
||
1007 | */ |
||
1008 | protected function customAdminCpPermissions(): array |
||
1009 | { |
||
1010 | // The script meta containers for the global meta bundle |
||
1011 | try { |
||
1012 | $currentSiteId = Craft::$app->getSites()->getCurrentSite()->id ?? 1; |
||
1013 | } catch (SiteNotFoundException $e) { |
||
1014 | $currentSiteId = 1; |
||
1015 | } |
||
1016 | // Dynamic permissions for the scripts |
||
1017 | $metaBundle = self::$plugin->metaBundles->getGlobalMetaBundle($currentSiteId); |
||
1018 | $scriptsPerms = []; |
||
1019 | if ($metaBundle !== null) { |
||
1020 | $scripts = self::$plugin->metaBundles->getContainerDataFromBundle( |
||
1021 | $metaBundle, |
||
1022 | MetaScriptContainer::CONTAINER_TYPE |
||
1023 | ); |
||
1024 | foreach ($scripts as $scriptHandle => $scriptData) { |
||
1025 | $scriptsPerms["seomatic:tracking-scripts:${scriptHandle}"] = [ |
||
1026 | 'label' => Craft::t('seomatic', $scriptData->name), |
||
1027 | ]; |
||
1028 | } |
||
1029 | } |
||
1030 | |||
1031 | return [ |
||
1032 | 'seomatic:dashboard' => [ |
||
1033 | 'label' => Craft::t('seomatic', 'Dashboard'), |
||
1034 | ], |
||
1035 | 'seomatic:global-meta' => [ |
||
1036 | 'label' => Craft::t('seomatic', 'Edit Global Meta'), |
||
1037 | 'nested' => [ |
||
1038 | 'seomatic:global-meta:general' => [ |
||
1039 | 'label' => Craft::t('seomatic', 'General'), |
||
1040 | ], |
||
1041 | 'seomatic:global-meta:twitter' => [ |
||
1042 | 'label' => Craft::t('seomatic', 'Twitter'), |
||
1043 | ], |
||
1044 | 'seomatic:global-meta:facebook' => [ |
||
1045 | 'label' => Craft::t('seomatic', 'Facebook'), |
||
1046 | ], |
||
1047 | 'seomatic:global-meta:robots' => [ |
||
1048 | 'label' => Craft::t('seomatic', 'Robots'), |
||
1049 | ], |
||
1050 | 'seomatic:global-meta:humans' => [ |
||
1051 | 'label' => Craft::t('seomatic', 'Humans'), |
||
1052 | ], |
||
1053 | 'seomatic:global-meta:ads' => [ |
||
1054 | 'label' => Craft::t('seomatic', 'Ads'), |
||
1055 | ], |
||
1056 | ], |
||
1057 | ], |
||
1058 | 'seomatic:content-meta' => [ |
||
1059 | 'label' => Craft::t('seomatic', 'Edit Content SEO'), |
||
1060 | 'nested' => [ |
||
1061 | 'seomatic:content-meta:general' => [ |
||
1062 | 'label' => Craft::t('seomatic', 'General'), |
||
1063 | ], |
||
1064 | 'seomatic:content-meta:twitter' => [ |
||
1065 | 'label' => Craft::t('seomatic', 'Twitter'), |
||
1066 | ], |
||
1067 | 'seomatic:content-meta:facebook' => [ |
||
1068 | 'label' => Craft::t('seomatic', 'Facebook'), |
||
1069 | ], |
||
1070 | 'seomatic:content-meta:sitemap' => [ |
||
1071 | 'label' => Craft::t('seomatic', 'Sitemap'), |
||
1072 | ], |
||
1073 | ], |
||
1074 | ], |
||
1075 | 'seomatic:site-settings' => [ |
||
1076 | 'label' => Craft::t('seomatic', 'Edit Site Settings'), |
||
1077 | 'nested' => [ |
||
1078 | 'seomatic:site-settings:identity' => [ |
||
1079 | 'label' => Craft::t('seomatic', 'Identity'), |
||
1080 | ], |
||
1081 | 'seomatic:site-settings:creator' => [ |
||
1082 | 'label' => Craft::t('seomatic', 'Creator'), |
||
1083 | ], |
||
1084 | 'seomatic:site-settings:social' => [ |
||
1085 | 'label' => Craft::t('seomatic', 'Social Media'), |
||
1086 | ], |
||
1087 | 'seomatic:site-settings:sitemap' => [ |
||
1088 | 'label' => Craft::t('seomatic', 'Sitemap'), |
||
1089 | ], |
||
1090 | 'seomatic:site-settings:miscellaneous' => [ |
||
1091 | 'label' => Craft::t('seomatic', 'Miscellaneous'), |
||
1092 | ], |
||
1093 | ], |
||
1094 | ], |
||
1095 | 'seomatic:tracking-scripts' => [ |
||
1096 | 'label' => Craft::t('seomatic', 'Edit Tracking Scripts'), |
||
1097 | 'nested' => $scriptsPerms, |
||
1098 | ], |
||
1099 | 'seomatic:plugin-settings' => [ |
||
1100 | 'label' => Craft::t('seomatic', 'Edit Plugin Settings'), |
||
1101 | ], |
||
1102 | ]; |
||
1103 | } |
||
1104 | } |
||
1105 |
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.