DashboardController   A
last analyzed

Complexity

Total Complexity 40

Size/Duplication

Total Lines 438
Duplicated Lines 0 %

Test Coverage

Coverage 42.13%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 239
c 3
b 0
f 0
dl 0
loc 438
ccs 83
cts 197
cp 0.4213
rs 9.2
wmc 40

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 14 1
A index() 0 33 2
A getLoggedInUserActivities() 0 5 1
B getFacts() 0 74 6
A formatStat() 0 7 2
A getAllActivities() 0 5 1
B getPeriodStats() 0 76 5
A search() 0 38 5
B formatActivity() 0 26 9
A getShortcuts() 0 34 4
A getRepository() 0 3 2
A getDrafts() 0 23 2

How to fix   Complexity   

Complex Class

Complex classes like DashboardController often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DashboardController, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace A17\Twill\Http\Controllers\Admin;
4
5
use A17\Twill\Models\Behaviors\HasMedias;
6
use Analytics;
0 ignored issues
show
Bug introduced by
The type Analytics 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...
7
use Illuminate\Config\Repository as Config;
8
use Illuminate\Contracts\Auth\Factory as AuthFactory;
9
use Illuminate\Contracts\Foundation\Application;
10
use Illuminate\Http\Request;
11
use Illuminate\Support\Collection;
12
use Illuminate\Support\Str;
13
use Illuminate\View\Factory as ViewFactory;
14
use Psr\Log\LoggerInterface as Logger;
15
use Spatie\Activitylog\Models\Activity;
16
use Spatie\Analytics\Exceptions\InvalidConfiguration;
17
use Spatie\Analytics\Period;
18
19
class DashboardController extends Controller
20
{
21
    /**
22
     * @var Application
23
     */
24
    protected $app;
25
26
    /**
27
     * @var Config
28
     */
29
    protected $config;
30
31
    /**
32
     * @var Logger
33
     */
34
    protected $logger;
35
36
    /**
37
     * @var ViewFactory
38
     */
39
    protected $viewFactory;
40
41
    /**
42
     * @var AuthFactory
43
     */
44
    protected $authFactory;
45
46 51
    public function __construct(
47
        Application $app,
48
        Config $config,
49
        Logger $logger,
50
        ViewFactory $viewFactory,
51
        AuthFactory $authFactory
52
    ) {
53 51
        parent::__construct();
54
55 51
        $this->app = $app;
56 51
        $this->config = $config;
57 51
        $this->logger = $logger;
58 51
        $this->viewFactory = $viewFactory;
59 51
        $this->authFactory = $authFactory;
60 51
    }
61
62
    /**
63
     * Displays the Twill dashboard.
64
     *
65
     * @return \Illuminate\View\View
66
     */
67 50
    public function index()
68
    {
69 50
        $modules = Collection::make($this->config->get('twill.dashboard.modules'));
70
71 50
        return $this->viewFactory->make('twill::layouts.dashboard', [
72 50
            'allActivityData' => $this->getAllActivities(),
73 50
            'myActivityData' => $this->getLoggedInUserActivities(),
74
            'tableColumns' => [
75
                [
76
                    'name' => 'thumbnail',
77
                    'label' => 'Thumbnail',
78
                    'visible' => true,
79
                    'optional' => false,
80
                    'sortable' => false,
81
                ],
82
                [
83
                    'name' => 'published',
84
                    'label' => 'Published',
85
                    'visible' => true,
86
                    'optional' => false,
87
                    'sortable' => false,
88
                ],
89
                [
90
                    'name' => 'name',
91
                    'label' => 'Name',
92
                    'visible' => true,
93
                    'optional' => false,
94
                    'sortable' => true,
95
                ],
96
            ],
97 50
            'shortcuts' => $this->getShortcuts($modules),
0 ignored issues
show
Bug introduced by
$modules of type Illuminate\Support\Collection is incompatible with the type array expected by parameter $modules of A17\Twill\Http\Controlle...troller::getShortcuts(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

97
            'shortcuts' => $this->getShortcuts(/** @scrutinizer ignore-type */ $modules),
Loading history...
98 49
            'facts' => $this->config->get('twill.dashboard.analytics.enabled', false) ? $this->getFacts() : null,
99 49
            'drafts' => $this->getDrafts($modules),
0 ignored issues
show
Bug introduced by
$modules of type Illuminate\Support\Collection is incompatible with the type array expected by parameter $modules of A17\Twill\Http\Controlle...Controller::getDrafts(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

99
            'drafts' => $this->getDrafts(/** @scrutinizer ignore-type */ $modules),
Loading history...
100
        ]);
101
    }
102
103
    /**
104
     * @param Request $request
105
     * @return Collection
106
     */
107 1
    public function search(Request $request)
108
    {
109 1
        $modules = Collection::make($this->config->get('twill.dashboard.modules'));
110
111 1
        return $modules->filter(function ($module) {
112 1
            return ($module['search'] ?? false);
113 1
        })->map(function ($module) use ($request) {
114 1
            $repository = $this->getRepository($module['name'], $module['repository'] ?? null);
115
116 1
            $found = $repository->cmsSearch($request->get('search'), $module['search_fields'] ?? ['title'])->take(10);
117
118 1
            return $found->map(function ($item) use ($module) {
119
                try {
120 1
                    $author = $item->revisions()->latest()->first()->user->name ?? 'Admin';
121
                } catch (\Exception $e) {
122
                    $author = 'Admin';
123
                }
124
125
                $date = null;
126 1
                if ($item->updated_at) {
127 1
                    $date = $item->updated_at->toIso8601String();
128 1
                } elseif ($item->created_at) {
129 1
                    $date = $item->created_at->toIso8601String();
130 1
                }
131 1
132 1
                return [
133 1
                    'id' => $item->id,
134 1
                    'href' => moduleRoute($module['name'], $module['routePrefix'] ?? null, 'edit', $item->id),
135
                    'thumbnail' => method_exists($item, 'defaultCmsImage') ? $item->defaultCmsImage(['w' => 100, 'h' => 100]) : null,
136 1
                    'published' => $item->published,
137 1
                    'activity' => twillTrans('twill::lang.dashboard.search.last-edit'),
138
                    'date' => $date,
139
                    'title' => $item->titleInDashboard ?? $item->title,
140
                    'author' => $author,
141
                    'type' => ucfirst($module['label_singular'] ?? Str::singular($module['name'])),
142
                ];
143 50
            });
144
        })->collapse()->values();
145 50
    }
146 2
147 50
    /**
148
     * @return array
149
     */
150
    private function getAllActivities()
151
    {
152
        return Activity::take(20)->latest()->get()->map(function ($activity) {
153 50
            return $this->formatActivity($activity);
154
        })->filter()->values();
155 50
    }
156
157 50
    /**
158
     * @return array
159
     */
160
    private function getLoggedInUserActivities()
161
    {
162
        return Activity::where('causer_id', $this->authFactory->guard('twill_users')->user()->id)->take(20)->latest()->get()->map(function ($activity) {
0 ignored issues
show
Bug introduced by
Accessing id on the interface Illuminate\Contracts\Auth\Authenticatable suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
163
            return $this->formatActivity($activity);
164 2
        })->filter()->values();
165
    }
166 2
167
    /**
168 2
     * @param \Spatie\Activitylog\Models\Activity $activity
169 2
     * @return array|null
170
     */
171
    private function formatActivity($activity)
172
    {
173
        $dashboardModule = $this->config->get('twill.dashboard.modules.' . $activity->subject_type);
0 ignored issues
show
Bug introduced by
The property subject_type does not seem to exist on Spatie\Activitylog\Models\Activity. Are you sure there is no database migration missing?

Checks if undeclared accessed properties appear in database migrations and if the creating migration is correct.

Loading history...
174
175
        if (!$dashboardModule || !$dashboardModule['activity'] ?? false) {
176
            return null;
177
        }
178
179
        if (is_null($activity->subject)) {
0 ignored issues
show
Bug introduced by
The property subject does not seem to exist on Spatie\Activitylog\Models\Activity. Are you sure there is no database migration missing?

Checks if undeclared accessed properties appear in database migrations and if the creating migration is correct.

Loading history...
180
            return null;
181
        }
182
183
        return [
184
            'id' => $activity->id,
185
            'type' => ucfirst($dashboardModule['label_singular'] ?? Str::singular($dashboardModule['name'])),
186
            'date' => $activity->created_at->toIso8601String(),
0 ignored issues
show
Bug introduced by
The method toIso8601String() does not exist on DateTime. It seems like you code against a sub-type of DateTime such as Nette\Utils\DateTime or Carbon\Carbon. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

186
            'date' => $activity->created_at->/** @scrutinizer ignore-call */ toIso8601String(),
Loading history...
187
            'author' => $activity->causer->name ?? twillTrans('twill::lang.dashboard.unknown-author'),
188
            'name' => $activity->subject->titleInDashboard ?? $activity->subject->title,
189
            'activity' => twillTrans('twill::lang.dashboard.activities.' . $activity->description),
0 ignored issues
show
Bug introduced by
The property description does not seem to exist on Spatie\Activitylog\Models\Activity. Are you sure there is no database migration missing?

Checks if undeclared accessed properties appear in database migrations and if the creating migration is correct.

Loading history...
190
        ] + (classHasTrait($activity->subject, HasMedias::class) ? [
191
            'thumbnail' => $activity->subject->defaultCmsImage(['w' => 100, 'h' => 100]),
192
        ] : []) + (!$activity->subject->trashed() ? [
193
            'edit' => moduleRoute($dashboardModule['name'], $dashboardModule['routePrefix'] ?? null, 'edit', $activity->subject_id),
0 ignored issues
show
Bug introduced by
The property subject_id does not seem to exist on Spatie\Activitylog\Models\Activity. Are you sure there is no database migration missing?

Checks if undeclared accessed properties appear in database migrations and if the creating migration is correct.

Loading history...
194
        ] : []) + (!is_null($activity->subject->published) ? [
195
            'published' => $activity->description === 'published' ? true : ($activity->description === 'unpublished' ? false : $activity->subject->published),
196
        ] : []);
197
    }
198
199
    /**
200
     * @return array|\Illuminate\Support\Collection
201
     */
202
    private function getFacts()
203
    {
204
        try {
205
            $response = Analytics::performQuery(
206
                Period::days(60),
207
                'ga:users,ga:pageviews,ga:bouncerate,ga:pageviewsPerSession',
208
                ['dimensions' => 'ga:date']
209
            );
210
        } catch (InvalidConfiguration $exception) {
211
            $this->logger->error($exception);
212
            return [];
213
        }
214
215
        $statsByDate = Collection::make($response['rows'] ?? [])->map(function (array $dateRow) {
216
            return [
217
                'date' => $dateRow[0],
218
                'users' => (int) $dateRow[1],
219
                'pageViews' => (int) $dateRow[2],
220
                'bounceRate' => $dateRow[3],
221
                'pageviewsPerSession' => $dateRow[4],
222
            ];
223
        })->reverse()->values();
224
225
        $dummyData = null;
226
        if ($statsByDate->isEmpty()) {
227
            $dummyData = [
228
                [
229
                    'label' => 'Users',
230
                    'figure' => 0,
231
                    'insight' => '0% Bounce rate',
232
                    'trend' => __('None'),
233
                    'data' => [0 => 0],
234
                    'url' => 'https://analytics.google.com/analytics/web',
235
                ],
236
                [
237
                    'label' => 'Pageviews',
238
                    'figure' => 0,
239
                    'insight' => '0 Pages / Session',
240
                    'trend' => __('None'),
241
                    'data' => [0 => 0],
242
                    'url' => 'https://analytics.google.com/analytics/web',
243
                ],
244
            ];
245
        }
246
247
        return Collection::make([
248
            'today',
249
            'yesterday',
250
            'week',
251
            'month',
252
        ])->mapWithKeys(function ($period) use ($statsByDate, $dummyData) {
253
254
            if ($dummyData) {
255
                return [$period => $dummyData];
256
            }
257
258
            $stats = $this->getPeriodStats($period, $statsByDate);
259
            return [
260
                $period => [
261
                    [
262
                        'label' => 'Users',
263
                        'figure' => $this->formatStat($stats['stats']['users']),
264
                        'insight' => round($stats['stats']['bounceRate']) . '% Bounce rate',
265
                        'trend' => $stats['moreUsers'] ? 'up' : 'down',
266
                        'data' => $stats['usersData']->reverse()->values()->toArray(),
267
                        'url' => 'https://analytics.google.com/analytics/web',
268
                    ],
269
                    [
270
                        'label' => 'Pageviews',
271
                        'figure' => $this->formatStat($stats['stats']['pageViews']),
272
                        'insight' => round($stats['stats']['pageviewsPerSession'], 1) . ' Pages / Session',
273
                        'trend' => $stats['morePageViews'] ? 'up' : 'down',
274
                        'data' => $stats['pageViewsData']->reverse()->values()->toArray(),
275
                        'url' => 'https://analytics.google.com/analytics/web',
276
                    ],
277
                ],
278
            ];
279
        });
280
    }
281
282
    /**
283
     * @param string $period
284
     * @param \Illuminate\Support\Collection $statsByDate
285
     * @return array
286
     */
287
    private function getPeriodStats($period, $statsByDate)
288
    {
289
        if ($period === 'today') {
290
            return [
291
                'stats' => $stats = $statsByDate->first(),
292
                'moreUsers' => $stats['users'] > $statsByDate->get(1)['users'],
293
                'morePageViews' => $stats['pageViews'] > $statsByDate->get(1)['pageViews'],
294
                'usersData' => $statsByDate->take(7)->map(function ($stat) {
295
                    return $stat['users'];
296
                }),
297
                'pageViewsData' => $statsByDate->take(7)->map(function ($stat) {
298
                    return $stat['pageViews'];
299
                }),
300
            ];
301
        } elseif ($period === 'yesterday') {
302
            return [
303
                'stats' => $stats = $statsByDate->get(1),
304
                'moreUsers' => $stats['users'] > $statsByDate->get(2)['users'],
305
                'morePageViews' => $stats['pageViews'] > $statsByDate->get(2)['pageViews'],
306
                'usersData' => $statsByDate->slice(1)->take(7)->map(function ($stat) {
307
                    return $stat['users'];
308
                }),
309
                'pageViewsData' => $statsByDate->slice(1)->take(7)->map(function ($stat) {
310
                    return $stat['pageViews'];
311
                }),
312
            ];
313
        } elseif ($period === 'week') {
314
            $first7stats = $statsByDate->take(7)->all();
315
316
            $stats = [
317
                'users' => array_sum(array_column($first7stats, 'users')),
318
                'pageViews' => array_sum(array_column($first7stats, 'pageViews')),
319
                'bounceRate' => array_sum(array_column($first7stats, 'bounceRate')) / 7,
320
                'pageviewsPerSession' => array_sum(array_column($first7stats, 'pageviewsPerSession')) / 7,
321
            ];
322
323
            $compareStats = [
324
                'users' => array_sum(array_column($statsByDate->slice(7)->take(7)->all(), 'users')),
325
                'pageViews' => array_sum(array_column($statsByDate->slice(7)->take(7)->all(), 'pageViews')),
326
            ];
327
328
            return [
329
                'stats' => $stats,
330
                'moreUsers' => $stats['users'] > $compareStats['users'],
331
                'morePageViews' => $stats['pageViews'] > $compareStats['pageViews'],
332
                'usersData' => $statsByDate->slice(1)->take(29)->map(function ($stat) {
333
                    return $stat['users'];
334
                }),
335
                'pageViewsData' => $statsByDate->slice(1)->take(29)->map(function ($stat) {
336
                    return $stat['pageViews'];
337
                }),
338
            ];
339
        } elseif ($period === 'month') {
340
            $first30stats = $statsByDate->take(30)->all();
341
342
            $stats = [
343
                'users' => array_sum(array_column($first30stats, 'users')),
344
                'pageViews' => array_sum(array_column($first30stats, 'pageViews')),
345
                'bounceRate' => array_sum(array_column($first30stats, 'bounceRate')) / 30,
346
                'pageviewsPerSession' => array_sum(array_column($first30stats, 'pageviewsPerSession')) / 30,
347
            ];
348
349
            $compareStats = [
350
                'users' => array_sum(array_column($statsByDate->slice(30)->take(30)->all(), 'users')),
351 50
                'pageViews' => array_sum(array_column($statsByDate->slice(30)->take(30)->all(), 'pageViews')),
352
            ];
353 50
354 32
            return [
355 50
                'stats' => $stats,
356 32
                'moreUsers' => $stats['users'] > $compareStats['users'],
357
                'morePageViews' => $stats['pageViews'] > $compareStats['pageViews'],
358
                'usersData' => $statsByDate->slice(1)->take(29)->map(function ($stat) {
359 32
                    return $stat['users'];
360 32
                }),
361 32
                'pageViewsData' => $statsByDate->slice(1)->take(29)->map(function ($stat) {
362 32
                    return $stat['pageViews'];
363
                }),
364
            ];
365
        }
366 32
    }
367 32
368 32
    /**
369 32
     * @param int $count
370
     * @return string
371
     */
372 32
    private function formatStat($count)
373 32
    {
374 32
        if ($count >= 1000) {
375
            return round($count / 1000, 1) . "k";
376 31
        }
377 31
378 31
        return $count;
379 31
    }
380 31
381
    /**
382
     * @param array $modules
383 50
     * @return array
384
     */
385
    private function getShortcuts($modules)
386
    {
387
        return $modules->filter(function ($module) {
388
            return ($module['count'] ?? false) || ($module['create'] ?? false);
389
        })->map(function ($module) {
390 49
            $repository = $this->getRepository($module['name'], $module['repository'] ?? null);
391
392 49
            $moduleOptions = [
393 31
                'count' => $module['count'] ?? false,
394 49
                'create' => $module['create'] ?? false,
395 31
                'label' => $module['label'] ?? $module['name'],
396
                'singular' => $module['label_singular'] ?? Str::singular($module['name']),
397 31
            ];
398
399 31
            return [
400 31
                'label' => ucfirst($moduleOptions['label']),
401
                'singular' => ucfirst($moduleOptions['singular']),
402
                'number' => $moduleOptions['count'] ? $repository->getCountByStatusSlug(
403 31
                    'all',
404
                    $module['countScope'] ?? []
405 31
                ) : null,
406
                'url' => moduleRoute(
407
                    $module['name'],
408
                    $module['routePrefix'] ?? null,
409
                    'index'
410
                ),
411 31
                'createUrl' => $moduleOptions['create'] ? moduleRoute(
412 49
                    $module['name'],
413
                    $module['routePrefix'] ?? null,
414
                    'index',
415
                    ['openCreate' => true]
416
                ) : null
417
            ];
418
        })->values();
419 32
    }
420
421 32
    /**
422
     * @param array $modules
423
     * @return array
424
     */
425
    private function getDrafts($modules)
426
    {
427
        return $modules->filter(function ($module) {
428
            return ($module['draft'] ?? false);
429
        })->map(function ($module) {
430
            $repository = $this->getRepository($module['name'], $module['repository'] ?? null);
431
432
            $query = $repository->draft()->limit(3)->latest();
0 ignored issues
show
Bug introduced by
The method draft() does not exist on A17\Twill\Repositories\ModuleRepository. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

432
            $query = $repository->/** @scrutinizer ignore-call */ draft()->limit(3)->latest();
Loading history...
433
434
            if ($repository->hasBehavior('revisions')) {
435
                $drafts = $query->mine();
0 ignored issues
show
Unused Code introduced by
The assignment to $drafts is dead and can be removed.
Loading history...
436
            }
437
438
            $drafts = $query->get();
439
440
            return $drafts->map(function ($draft) use ($module) {
441
                return [
442
                    'type' => ucfirst($module['label_singular'] ?? Str::singular($module['name'])),
443
                    'name' => $draft->titleInDashboard ?? $draft->title,
444
                    'url' => moduleRoute($module['name'], $module['routePrefix'] ?? null, 'edit', $draft->id),
445
                ];
446
            });
447
        })->collapse()->values();
448
    }
449
450
    /**
451
     * @param string $module
452
     * @return \A17\Twill\Repositories\ModuleRepository
453
     */
454
    private function getRepository($module, $forModule = null)
455
    {
456
        return $this->app->make($forModule ?: $this->config->get('twill.namespace') . "\Repositories\\" . ucfirst(Str::singular($module)) . "Repository");
457
    }
458
}
459