Passed
Push — v1 ( 44bb2f...f1a3ae )
by Andrew
11:42 queued 08:28
created

src/Webperf.php (22 issues)

1
<?php
2
/**
3
 * Webperf plugin for Craft CMS 3.x
4
 *
5
 * Monitor the performance of your webpages through real-world user timing data
6
 *
7
 * @link      https://nystudio107.com
8
 * @copyright Copyright (c) 2019 nystudio107
9
 */
10
11
namespace nystudio107\webperf;
12
13
use nystudio107\webperf\assetbundles\webperf\WebperfAsset;
14
use nystudio107\webperf\base\CraftDataSample;
15
use nystudio107\webperf\helpers\PluginTemplate;
16
use nystudio107\webperf\log\ErrorsTarget;
17
use nystudio107\webperf\log\ProfileTarget;
18
use nystudio107\webperf\models\RecommendationDataSample;
19
use nystudio107\webperf\models\Settings;
20
use nystudio107\webperf\services\DataSamples as DataSamplesService;
21
use nystudio107\webperf\services\ErrorSamples as ErrorSamplesService;
22
use nystudio107\webperf\services\Beacons as BeaconsService;
23
use nystudio107\webperf\services\Recommendations as RecommendationsService;
24
use nystudio107\webperf\variables\WebperfVariable;
25
use nystudio107\webperf\widgets\Metrics as MetricsWidget;
26
27
use Craft;
28
use craft\base\Element;
0 ignored issues
show
The type craft\base\Element was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
29
use craft\base\Plugin;
30
use craft\services\Plugins;
31
use craft\events\PluginEvent;
32
use craft\events\RegisterComponentTypesEvent;
33
use craft\events\RegisterUserPermissionsEvent;
34
use craft\events\RegisterUrlRulesEvent;
35
use craft\helpers\UrlHelper;
36
use craft\services\Dashboard;
37
use craft\services\UserPermissions;
38
use craft\web\Application;
39
use craft\web\twig\variables\CraftVariable;
40
use craft\web\UrlManager;
41
use craft\web\View;
42
43
use yii\base\Event;
44
use yii\base\InvalidConfigException;
45
46
/**
47
 * Class Webperf
48
 *
49
 * @author    nystudio107
50
 * @package   Webperf
51
 * @since     1.0.0
52
 *
53
 * @property  RecommendationsService  $recommendations
54
 * @property  DataSamplesService      $dataSamples
55
 * @property  ErrorSamplesService     $errorSamples
56
 * @property  BeaconsService          $beacons
57
 * @property  ErrorsTarget            $errorsTarget
58
 * @property  ProfileTarget           $profileTarget
59
 */
60
class Webperf extends Plugin
61
{
62
    // Constants
63
    // =========================================================================
64
65
    const RECOMMENDATIONS_CACHE_KEY = 'webperf-recommendations';
66
    const RECOMMENDATIONS_CACHE_DURATION = 60;
67
68
    const ERRORS_CACHE_KEY = 'webperf-errors';
69
    const ERRORS_CACHE_DURATION = 60;
70
71
    // Static Properties
72
    // =========================================================================
73
74
    /**
75
     * @var Webperf
76
     */
77
    public static $plugin;
78
79
    /**
80
     * @var Settings
81
     */
82
    public static $settings;
83
84
    /**
85
     * @var int|null
86
     */
87
    public static $requestUuid;
88
89
    /**
90
     * @var int|null
91
     */
92
    public static $requestUrl;
93
94
    /**
95
     * @var bool
96
     */
97
    public static $beaconIncluded = false;
98
99
    /**
100
     * @var string
101
     */
102
    public static $renderType = 'html';
103
104
    // Public Properties
105
    // =========================================================================
106
107
    /**
108
     * @var string
109
     */
110
    public $schemaVersion = '1.0.0';
111
112
    // Public Methods
113
    // =========================================================================
114
115
    /**
116
     * @inheritdoc
117
     */
118
    public function init()
119
    {
120
        parent::init();
121
        // Initialize properties
122
        self::$plugin = $this;
123
        self::$settings = $this->getSettings();
124
        try {
125
            self::$requestUuid = random_int(0, PHP_INT_MAX);
126
        } catch (\Exception $e) {
127
            self::$requestUuid = null;
128
        }
129
        $this->name = self::$settings->pluginName;
130
        // Handle any console commands
131
        $request = Craft::$app->getRequest();
132
        if ($request->getIsConsoleRequest()) {
133
            $this->controllerNamespace = 'nystudio107\webperf\console\controllers';
134
        }
135
        // Add in our components
136
        $this->addComponents();
137
        // Install event listeners
138
        $this->installEventListeners();
139
        // Load that we've loaded
140
        Craft::info(
141
            Craft::t(
142
                'webperf',
143
                '{name} plugin loaded',
144
                ['name' => $this->name]
145
            ),
146
            __METHOD__
147
        );
148
    }
149
150
    /**
151
     * @inheritdoc
152
     */
153
    public function getSettingsResponse()
154
    {
155
        // Just redirect to the plugin settings page
156
        Craft::$app->getResponse()->redirect(UrlHelper::cpUrl('webperf/settings'));
157
    }
158
159
    /**
160
     * @inheritdoc
161
     */
162
    public function getCpNavItem()
163
    {
164
        $subNavs = [];
165
        $navItem = parent::getCpNavItem();
166
        $recommendations = $this->getRecommendationsCount();
167
        $errors = $this->getErrorsCount();
168
        $navItem['badgeCount'] = $errors.$recommendations;
169
        $currentUser = Craft::$app->getUser()->getIdentity();
170
        // Only show sub-navs the user has permission to view
171
        if ($currentUser->can('webperf:dashboard')) {
172
            $subNavs['dashboard'] = [
173
                'label' => Craft::t('webperf', 'Dashboard'),
174
                'url' => 'webperf/dashboard',
175
            ];
176
        }
177
        if ($currentUser->can('webperf:performance')) {
178
            $subNavs['performance'] = [
179
                'label' => Craft::t('webperf', 'Performance'),
180
                'url' => 'webperf/performance',
181
            ];
182
        }
183
        if ($currentUser->can('webperf:errors')) {
184
            $subNavs['errors'] = [
185
                'label' => Craft::t('webperf', 'Errors').' '.$errors,
186
                'url' => 'webperf/errors',
187
                'badge' => $errors,
188
            ];
189
        }
190
        if ($currentUser->can('webperf:alerts')) {
191
            $subNavs['alerts'] = [
192
                'label' => 'Alerts',
193
                'url' => 'webperf/alerts',
194
            ];
195
        }
196
        if ($currentUser->can('webperf:settings')) {
197
            $subNavs['settings'] = [
198
                'label' => Craft::t('webperf', 'Settings'),
199
                'url' => 'webperf/settings',
200
            ];
201
        }
202
        $navItem = array_merge($navItem, [
203
            'subnav' => $subNavs,
204
        ]);
205
206
        return $navItem;
207
    }
208
209
    // Protected Methods
210
    // =========================================================================
211
212
    /**
213
     * Add in our components
214
     */
215
    protected function addComponents()
216
    {
217
        $request = Craft::$app->getRequest();
218
        if ($request->getIsSiteRequest() && !$request->getIsConsoleRequest()) {
219
            $this->setRequestUrl();
220
            try {
221
                $uri = $request->getPathInfo();
222
            } catch (InvalidConfigException $e) {
223
                $uri = '';
224
            }
225
            // Ignore our own controllers
226
            if (self::$settings->includeCraftProfiling && strpos($uri, 'webperf/') === false) {
227
                // Add in the ProfileTarget component
228
                try {
229
                    $this->set('profileTarget', [
230
                        'class' => ProfileTarget::class,
231
                        'levels' => ['profile'],
232
                        'categories' => [],
233
                        'logVars' => [],
234
                        'except' => [],
235
                    ]);
236
                } catch (InvalidConfigException $e) {
237
                    Craft::error($e->getMessage(), __METHOD__);
238
                }
239
                // Attach our log target
240
                Craft::$app->getLog()->targets['webperf-profile'] = $this->profileTarget;
241
                // Add in the ErrorsTarget component
242
                $except = [];
243
                // If devMode is on, exclude errors/warnings from `seomatic`
244
                if (Craft::$app->getConfig()->getGeneral()->devMode) {
245
                    $except = ['nystudio107\seomatic\*'];
246
                }
247
                $levels = ['error'];
248
                if (self::$settings->includeCraftWarnings) {
249
                    $levels[] = 'warning';
250
                }
251
                try {
252
                    $this->set('errorsTarget', [
253
                        'class' => ErrorsTarget::class,
254
                        'levels' => $levels,
255
                        'categories' => [],
256
                        'logVars' => [],
257
                        'except' => $except,
258
                    ]);
259
                } catch (InvalidConfigException $e) {
260
                    Craft::error($e->getMessage(), __METHOD__);
261
                }
262
                // Attach our log target
263
                Craft::$app->getLog()->targets['webperf-errors'] = $this->errorsTarget;
264
            }
265
        }
266
    }
267
268
    /**
269
     * Set the request URL
270
     *
271
     * @param bool $force
272
     */
273
    protected function setRequestUrl(bool $force = false)
274
    {
275
        self::$requestUrl = CraftDataSample::PLACEHOLDER_URL;
276
        if (!self::$settings->includeBeacon || $force || self::$settings->staticCachedSite) {
277
            $request = Craft::$app->getRequest();
278
            self::$requestUrl = UrlHelper::stripQueryString(
279
                urldecode($request->getAbsoluteUrl())
280
            );
281
        }
282
    }
283
284
    /**
285
     * Install our event listeners.
286
     */
287
    protected function installEventListeners()
288
    {
289
        $request = Craft::$app->getRequest();
290
        // Add in our event listeners that are needed for every request
291
        $this->installGlobalEventListeners();
292
        // Install only for non-console site requests
293
        if ($request->getIsSiteRequest() && !$request->getIsConsoleRequest()) {
294
            $this->installSiteEventListeners();
295
        }
296
        // Install only for non-console Control Panel requests
297
        if ($request->getIsCpRequest() && !$request->getIsConsoleRequest()) {
298
            $this->installCpEventListeners();
299
        }
300
        // Handler: EVENT_AFTER_INSTALL_PLUGIN
301
        Event::on(
302
            Plugins::class,
303
            Plugins::EVENT_AFTER_INSTALL_PLUGIN,
304
            function (PluginEvent $event) {
305
                if ($event->plugin === $this) {
306
                    // Invalidate our caches after we've been installed
307
                    $this->clearAllCaches();
308
                    // Send them to our welcome screen
309
                    $request = Craft::$app->getRequest();
310
                    if ($request->isCpRequest) {
311
                        Craft::$app->getResponse()->redirect(UrlHelper::cpUrl(
312
                            'webperf/dashboard',
313
                            [
314
                                'showWelcome' => true,
315
                            ]
316
                        ))->send();
317
                    }
318
                }
319
            }
320
        );
321
    }
322
323
    /**
324
     * Install global event listeners for all request types
325
     */
326
    protected function installGlobalEventListeners()
327
    {
328
        // Handler: CraftVariable::EVENT_INIT
329
        Event::on(
330
            CraftVariable::class,
331
            CraftVariable::EVENT_INIT,
332
            function (Event $event) {
333
                /** @var CraftVariable $variable */
334
                $variable = $event->sender;
335
                $variable->set('webperf', WebperfVariable::class);
336
            }
337
        );
338
        // Handler: Plugins::EVENT_AFTER_LOAD_PLUGINS
339
        Event::on(
340
            Plugins::class,
341
            Plugins::EVENT_AFTER_LOAD_PLUGINS,
342
            function () {
343
                // Install these only after all other plugins have loaded
344
                $request = Craft::$app->getRequest();
345
                // Only respond to non-console site requests
346
                if ($request->getIsSiteRequest() && !$request->getIsConsoleRequest()) {
347
                    $this->handleSiteRequest();
348
                }
349
                // Respond to Control Panel requests
350
                if ($request->getIsCpRequest() && !$request->getIsConsoleRequest()) {
351
                    $this->handleAdminCpRequest();
352
                }
353
            }
354
        );
355
    }
356
357
    /**
358
     * Install site event listeners for site requests only
359
     */
360
    protected function installSiteEventListeners()
361
    {
362
        // Handler: UrlManager::EVENT_REGISTER_SITE_URL_RULES
363
        Event::on(
364
            UrlManager::class,
365
            UrlManager::EVENT_REGISTER_SITE_URL_RULES,
366
            function (RegisterUrlRulesEvent $event) {
367
                Craft::debug(
368
                    'UrlManager::EVENT_REGISTER_SITE_URL_RULES',
369
                    __METHOD__
370
                );
371
                // Register our Control Panel routes
372
                $event->rules = array_merge(
373
                    $event->rules,
374
                    $this->customFrontendRoutes()
375
                );
376
            }
377
        );
378
    }
379
380
    /**
381
     * Install site event listeners for Control Panel requests only
382
     */
383
    protected function installCpEventListeners()
384
    {
385
        // Handler: UrlManager::EVENT_REGISTER_CP_URL_RULES
386
        Event::on(
387
            UrlManager::class,
388
            UrlManager::EVENT_REGISTER_CP_URL_RULES,
389
            function (RegisterUrlRulesEvent $event) {
390
                Craft::debug(
391
                    'UrlManager::EVENT_REGISTER_CP_URL_RULES',
392
                    __METHOD__
393
                );
394
                // Register our Control Panel routes
395
                $event->rules = array_merge(
396
                    $event->rules,
397
                    $this->customAdminCpRoutes()
398
                );
399
            }
400
        );
401
        // Handler: Dashboard::EVENT_REGISTER_WIDGET_TYPES
402
        Event::on(
403
            Dashboard::class,
404
            Dashboard::EVENT_REGISTER_WIDGET_TYPES,
405
            function (RegisterComponentTypesEvent $event) {
406
                $event->types[] = MetricsWidget::class;
407
            }
408
        );
409
        // Handler: UserPermissions::EVENT_REGISTER_PERMISSIONS
410
        Event::on(
411
            UserPermissions::class,
412
            UserPermissions::EVENT_REGISTER_PERMISSIONS,
413
            function (RegisterUserPermissionsEvent $event) {
414
                Craft::debug(
415
                    'UserPermissions::EVENT_REGISTER_PERMISSIONS',
416
                    __METHOD__
417
                );
418
                // Register our custom permissions
419
                $event->permissions[Craft::t('webperf', 'Webperf')] = $this->customAdminCpPermissions();
420
            }
421
        );
422
    }
423
424
    /**
425
     * Handle site requests.  We do it only after we receive the event
426
     * EVENT_AFTER_LOAD_PLUGINS so that any pending db migrations can be run
427
     * before our event listeners kick in
428
     */
429
    protected function handleSiteRequest()
430
    {
431
        // Don't include the beacon for response codes >= 400
432
        $response = Craft::$app->getResponse();
433
        if ($response->statusCode < 400) {
434
            // Handler: View::EVENT_END_PAGE
435
            Event::on(
436
                View::class,
437
                View::EVENT_END_PAGE,
438
                function () {
439
                    Craft::debug(
440
                        'View::EVENT_END_PAGE',
441
                        __METHOD__
442
                    );
443
                    $view = Craft::$app->getView();
444
                    // The page is done rendering, include our beacon
445
                    if (Webperf::$settings->includeBeacon && $view->getIsRenderingPageTemplate()) {
446
                        switch (self::$renderType) {
447
                            case 'html':
448
                                Webperf::$plugin->beacons->includeHtmlBeacon();
449
                                self::$beaconIncluded = true;
450
                                break;
451
                            case 'amp-html':
452
                                Webperf::$plugin->beacons->includeAmpHtmlScript();
453
                                break;
454
                        }
455
                    }
456
                }
457
            );
458
            // Handler: View::EVENT_END_BODY
459
            Event::on(
460
                View::class,
461
                View::EVENT_END_BODY,
462
                function () {
463
                    Craft::debug(
464
                        'View::EVENT_END_BODY',
465
                        __METHOD__
466
                    );
467
                    $view = Craft::$app->getView();
468
                    // The page is done rendering, include our beacon
469
                    if (Webperf::$settings->includeBeacon && $view->getIsRenderingPageTemplate()) {
470
                        switch (self::$renderType) {
471
                            case 'html':
472
                                break;
473
                            case 'amp-html':
474
                                Webperf::$plugin->beacons->includeAmpHtmlBeacon();
475
                                self::$beaconIncluded = true;
476
                                break;
477
                        }
478
                    }
479
                }
480
            );
481
            // Handler: Application::EVENT_AFTER_REQUEST
482
            Event::on(
483
                Application::class,
484
                Application::EVENT_AFTER_REQUEST,
485
                function () {
486
                    Craft::debug(
487
                        'Application::EVENT_AFTER_REQUEST',
488
                        __METHOD__
489
                    );
490
                    // If the beacon wasn't included, allow for the Craft timings
491
                    if (!self::$beaconIncluded) {
492
                        $this->setRequestUrl(true);
493
                    }
494
                }
495
            );
496
        }
497
    }
498
499
    /**
500
     * Handle Control Panel requests. We do it only after we receive the event
501
     * EVENT_AFTER_LOAD_PLUGINS so that any pending db migrations can be run
502
     * before our event listeners kick in
503
     */
504
    protected function handleAdminCpRequest()
505
    {
506
        $currentUser = Craft::$app->getUser()->getIdentity();
507
        // Only show sub-navs the user has permission to view
508
        if (self::$settings->displaySidebar && $currentUser->can('webperf:sidebar')) {
509
            $view = Craft::$app->getView();
510
            // Entries sidebar
511
            $view->hook('cp.entries.edit.details', function (&$context) {
0 ignored issues
show
The opening parenthesis of a multi-line function call should be the last content on the line.
Loading history...
512
                /** @var  Element $element */
0 ignored issues
show
The open comment tag must be the only content on the line
Loading history...
Missing short description in doc comment
Loading history...
Tag value indented incorrectly; expected 1 spaces but found 2
Loading history...
The close comment tag must be the only content on the line
Loading history...
513
                $element = $context['entry'] ?? null;
514
515
                return $this->renderSidebar($element);
516
            });
0 ignored issues
show
For multi-line function calls, the closing parenthesis should be on a new line.

If a function call spawns multiple lines, the coding standard suggests to move the closing parenthesis to a new line:

someFunctionCall(
    $firstArgument,
    $secondArgument,
    $thirdArgument
); // Closing parenthesis on a new line.
Loading history...
517
            // Category Groups sidebar
518
            $view->hook('cp.categories.edit.details', function (&$context) {
0 ignored issues
show
The opening parenthesis of a multi-line function call should be the last content on the line.
Loading history...
519
                /** @var  Element $element */
0 ignored issues
show
The open comment tag must be the only content on the line
Loading history...
Missing short description in doc comment
Loading history...
Tag value indented incorrectly; expected 1 spaces but found 2
Loading history...
The close comment tag must be the only content on the line
Loading history...
520
                $element = $context['category'] ?? null;
521
522
                return $this->renderSidebar($element);
523
            });
0 ignored issues
show
For multi-line function calls, the closing parenthesis should be on a new line.

If a function call spawns multiple lines, the coding standard suggests to move the closing parenthesis to a new line:

someFunctionCall(
    $firstArgument,
    $secondArgument,
    $thirdArgument
); // Closing parenthesis on a new line.
Loading history...
524
            // Commerce Product Types sidebar
525
            $view->hook('cp.commerce.product.edit.details', function (&$context) {
0 ignored issues
show
The opening parenthesis of a multi-line function call should be the last content on the line.
Loading history...
526
                /** @var  Element $element */
0 ignored issues
show
The open comment tag must be the only content on the line
Loading history...
Missing short description in doc comment
Loading history...
Tag value indented incorrectly; expected 1 spaces but found 2
Loading history...
The close comment tag must be the only content on the line
Loading history...
527
                $element = $context['product'] ?? null;
528
529
                return $this->renderSidebar($element);
530
            });
0 ignored issues
show
For multi-line function calls, the closing parenthesis should be on a new line.

If a function call spawns multiple lines, the coding standard suggests to move the closing parenthesis to a new line:

someFunctionCall(
    $firstArgument,
    $secondArgument,
    $thirdArgument
); // Closing parenthesis on a new line.
Loading history...
531
        }
532
    }
533
534
    /**
0 ignored issues
show
Missing short description in doc comment
Loading history...
535
     * @param Element $element
0 ignored issues
show
Missing parameter comment
Loading history...
536
     *
537
     * @return string
538
     */
539
    protected function renderSidebar(Element $element): string
540
    {
541
        $html = '';
542
        if ($element !== null && $element->url !== null) {
543
            $view = Craft::$app->getView();
544
            try {
545
                $view->registerAssetBundle(WebperfAsset::class);
546
            } catch (InvalidConfigException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
547
            }
548
            try {
549
                $now = new \DateTime();
550
            } catch (\Exception $e) {
551
                return $html;
552
            }
553
            $end = $now->format('Y-m-d');
554
            $start = $now->modify('-30 days')->format('Y-m-d');
555
            $html .= PluginTemplate::renderPluginTemplate(
556
                '_sidebars/webperf-sidebar.twig',
557
                [
558
                    'settings' => self::$settings,
559
                    'pageUrl' => $element->url,
560
                    'start' => $start,
561
                    'end' => $end,
562
                    'currentSiteId' => $element->siteId ?? 0,
563
                ]
564
            );
565
        }
566
567
        return $html;
568
    }
569
570
    /**
571
     * Clear all the caches!
572
     */
573
    public function clearAllCaches()
574
    {
575
    }
576
577
    /**
578
     * @inheritdoc
579
     */
580
    protected function createSettingsModel()
581
    {
582
        return new Settings();
583
    }
584
585
    /**
586
     * @inheritdoc
587
     */
588
    protected function settingsHtml(): string
589
    {
590
        return Craft::$app->view->renderTemplate(
591
            'webperf/settings',
592
            [
593
                'settings' => $this->getSettings()
594
            ]
595
        );
596
    }
597
598
    /**
599
     * Return the custom frontend routes
600
     *
601
     * @return array
602
     */
603
    protected function customFrontendRoutes(): array
604
    {
605
        return [
606
            // Beacon
607
            '/webperf/metrics/beacon' => 'webperf/metrics/beacon',
608
            // Render
609
            '/webperf/render/amp-iframe' => 'webperf/render/amp-iframe',
610
            // Tables
611
            '/webperf/tables/pages-index' => 'webperf/tables/pages-index',
612
            '/webperf/tables/page-detail' => 'webperf/tables/page-detail',
613
            '/webperf/tables/errors-index' => 'webperf/tables/errors-index',
614
            '/webperf/tables/errors-detail' => 'webperf/tables/errors-detail',
615
            // Charts
616
            '/webperf/charts/dashboard-stats-average/<column:{handle}>'
617
            => 'webperf/charts/dashboard-stats-average',
618
            '/webperf/charts/dashboard-stats-average/<column:{handle}>/<siteId:\d+>'
619
            => 'webperf/charts/dashboard-stats-average',
620
621
            '/webperf/charts/dashboard-slowest-pages/<column:{handle}>/<limit:\d+>'
622
            => 'webperf/charts/dashboard-slowest-pages',
623
            '/webperf/charts/dashboard-slowest-pages/<column:{handle}>/<limit:\d+>/<siteId:\d+>'
624
            => 'webperf/charts/dashboard-slowest-pages',
625
626
            '/webperf/charts/pages-area-chart'
627
            => 'webperf/charts/pages-area-chart',
628
            '/webperf/charts/pages-area-chart/<siteId:\d+>'
629
            => 'webperf/charts/pages-area-chart',
630
631
            '/webperf/charts/errors-area-chart'
632
            => 'webperf/charts/errors-area-chart',
633
            '/webperf/charts/errors-area-chart/<siteId:\d+>'
634
            => 'webperf/charts/errors-area-chart',
635
636
            '/webperf/recommendations/list'
637
            => 'webperf/recommendations/list',
638
            '/webperf/recommendations/list/<siteId:\d+>'
639
            => 'webperf/recommendations/list',
640
641
            '/webperf/charts/widget/<days>' => 'webperf/charts/widget',
642
        ];
643
    }
644
    /**
645
     * Return the custom Control Panel routes
646
     *
647
     * @return array
648
     */
649
    protected function customAdminCpRoutes(): array
650
    {
651
        return [
652
            'webperf' => 'webperf/sections/dashboard',
653
            'webperf/dashboard' => 'webperf/sections/dashboard',
654
            'webperf/dashboard/<siteHandle:{handle}>' => 'webperf/sections/dashboard',
655
656
            'webperf/performance' => 'webperf/sections/pages-index',
657
            'webperf/performance/<siteHandle:{handle}>' => 'webperf/sections/pages-index',
658
659
            'webperf/performance/page-detail' => 'webperf/sections/page-detail',
660
            'webperf/performance/page-detail/<siteHandle:{handle}>' => 'webperf/sections/page-detail',
661
662
            'webperf/errors' => 'webperf/sections/errors-index',
663
            'webperf/errors/<siteHandle:{handle}>' => 'webperf/sections/errors-index',
664
665
            'webperf/errors/page-detail' => 'webperf/sections/errors-detail',
666
            'webperf/errors/page-detail/<siteHandle:{handle}>' => 'webperf/errors/page-detail',
667
668
            'webperf/alerts' => 'webperf/sections/alerts',
669
            'webperf/alerts/<siteHandle:{handle}>' => 'webperf/sections/alerts',
670
671
            'webperf/settings' => 'webperf/settings/plugin-settings',
672
        ];
673
    }
674
675
    /**
676
     * Returns the custom Control Panel user permissions.
677
     *
678
     * @return array
679
     */
680
    protected function customAdminCpPermissions(): array
681
    {
682
        return [
683
            'webperf:dashboard' => [
684
                'label' => Craft::t('webperf', 'Dashboard'),
685
            ],
686
            'webperf:performance' => [
687
                'label' => Craft::t('webperf', 'Performance'),
688
                'nested' => [
689
                    'webperf:performance-detail' => [
690
                        'label' => Craft::t('webperf', 'Performance Detail'),
691
                    ],
692
                    'webperf:delete-data-samples' => [
693
                        'label' => Craft::t('webperf', 'Delete Data Samples'),
694
                    ],
695
                ],
696
            ],
697
            'webperf:errors' => [
698
                'label' => Craft::t('webperf', 'Errors'),
699
                'nested' => [
700
                    'webperf:errors-detail' => [
701
                        'label' => Craft::t('webperf', 'Errors Detail'),
702
                    ],
703
                    'webperf:delete-error-samples' => [
704
                        'label' => Craft::t('webperf', 'Delete Error Samples'),
705
                    ],
706
                ],
707
            ],
708
            'webperf:alerts' => [
709
                'label' => Craft::t('webperf', 'Alerts'),
710
            ],
711
            'webperf:recommendations' => [
712
                'label' => Craft::t('webperf', 'Recommendations'),
713
            ],
714
            'webperf:sidebar' => [
715
                'label' => Craft::t('webperf', 'Performance Sidebar'),
716
            ],
717
            'webperf:settings' => [
718
                'label' => Craft::t('webperf', 'Settings'),
719
            ],
720
        ];
721
    }
722
723
    /**
724
     * Get a string value with the number of recommendations
725
     *
726
     * @return string
727
     */
728
    protected function getRecommendationsCount(): string
729
    {
730
        $cache = Craft::$app->getCache();
731
        // See if there are any recommendations to add as a badge
732
        $recommendations = $cache->getOrSet(self::RECOMMENDATIONS_CACHE_KEY, function () {
733
            $data = [];
734
            $now = new \DateTime();
735
            $end = $now->format('Y-m-d');
736
            $start = $now->modify('-30 days')->format('Y-m-d');
737
            $stats = Webperf::$plugin->recommendations->data('', $start, $end);
738
            if (!empty($stats)) {
739
                $recSample = new RecommendationDataSample($stats);
740
                $data = Webperf::$plugin->recommendations->list($recSample);
741
            }
742
743
            return count($data);
744
        }, self::RECOMMENDATIONS_CACHE_DURATION);
745
746
        if (!$recommendations) {
747
            $recommendations = '';
748
        }
749
750
        return (string)$recommendations;
751
    }
752
753
    /**
754
     * Get a string value with the number of errors
755
     *
756
     * @return string
757
     */
758
    protected function getErrorsCount(): string
759
    {
760
        $cache = Craft::$app->getCache();
761
        // See if there are any recommendations to add as a badge
762
        $errors = $cache->getOrSet(self::ERRORS_CACHE_KEY, function () {
763
            $now = new \DateTime();
764
            $end = $now->format('Y-m-d');
765
            $start = $now->modify('-30 days')->format('Y-m-d');
766
767
            return Webperf::$plugin->errorSamples->totalErrorSamplesRange(0, $start, $end);
768
        }, self::ERRORS_CACHE_DURATION);
769
770
        if (!$errors) {
771
            $errors = '';
772
        } else {
773
            $errors = '⚠';
774
        }
775
776
        return (string)$errors;
777
    }
778
}
779