Passed
Push — dependabot/npm_and_yarn/docs/w... ( a770a9...3a5b31 )
by
unknown
07:47
created

DashboardController::search()   A

Complexity

Conditions 3
Paths 1

Size

Total Lines 31
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 3.0105

Importance

Changes 0
Metric Value
cc 3
eloc 22
nc 1
nop 1
dl 0
loc 31
ccs 17
cts 19
cp 0.8947
crap 3.0105
rs 9.568
c 0
b 0
f 0
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
        return $modules->filter(function ($module) {
112 1
            return ($module['search'] ?? false);
113
        })->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
            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
                return [
126 1
                    'id' => $item->id,
127 1
                    'href' => moduleRoute($module['name'], $module['routePrefix'] ?? null, 'edit', $item->id),
128 1
                    'thumbnail' => method_exists($item, 'defaultCmsImage') ? $item->defaultCmsImage(['w' => 100, 'h' => 100]) : null,
129 1
                    'published' => $item->published,
130 1
                    'activity' => 'Last edited',
131 1
                    'date' => $item->updated_at->toIso8601String(),
132 1
                    'title' => $item->titleInDashboard ?? $item->title,
133 1
                    'author' => $author,
134 1
                    'type' => ucfirst($module['label_singular'] ?? Str::singular($module['name'])),
135
                ];
136 1
            });
137 1
        })->collapse()->values();
138
    }
139
140
    /**
141
     * @return array
142
     */
143 50
    private function getAllActivities()
144
    {
145
        return Activity::take(20)->latest()->get()->map(function ($activity) {
146 2
            return $this->formatActivity($activity);
147 50
        })->filter()->values();
148
    }
149
150
    /**
151
     * @return array
152
     */
153 50
    private function getLoggedInUserActivities()
154
    {
155
        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...
156
            return $this->formatActivity($activity);
157 50
        })->filter()->values();
158
    }
159
160
    /**
161
     * @param \Spatie\Activitylog\Models\Activity $activity
162
     * @return array|null
163
     */
164 2
    private function formatActivity($activity)
165
    {
166 2
        $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...
167
168 2
        if (!$dashboardModule || !$dashboardModule['activity'] ?? false) {
169 2
            return null;
170
        }
171
172
        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...
173
            return null;
174
        }
175
176
        return [
177
            'id' => $activity->id,
178
            'type' => ucfirst($dashboardModule['label_singular'] ?? Str::singular($dashboardModule['name'])),
179
            '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 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

179
            'date' => $activity->created_at->/** @scrutinizer ignore-call */ toIso8601String(),
Loading history...
180
            'author' => $activity->causer->name ?? 'Unknown',
181
            'name' => $activity->subject->titleInDashboard ?? $activity->subject->title,
182
            'activity' => ucfirst($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...
183
        ] + (classHasTrait($activity->subject, HasMedias::class) ? [
184
            'thumbnail' => $activity->subject->defaultCmsImage(['w' => 100, 'h' => 100]),
185
        ] : []) + (!$activity->subject->trashed() ? [
186
            '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...
187
        ] : []) + (!is_null($activity->subject->published) ? [
188
            'published' => $activity->description === 'published' ? true : ($activity->description === 'unpublished' ? false : $activity->subject->published),
189
        ] : []);
190
    }
191
192
    /**
193
     * @return array|\Illuminate\Support\Collection
194
     */
195
    private function getFacts()
196
    {
197
        try {
198
            $response = Analytics::performQuery(
199
                Period::days(60),
200
                'ga:users,ga:pageviews,ga:bouncerate,ga:pageviewsPerSession',
201
                ['dimensions' => 'ga:date']
202
            );
203
        } catch (InvalidConfiguration $exception) {
204
            $this->logger->error($exception);
205
            return [];
206
        }
207
208
        $statsByDate = Collection::make($response['rows'] ?? [])->map(function (array $dateRow) {
209
            return [
210
                'date' => $dateRow[0],
211
                'users' => (int) $dateRow[1],
212
                'pageViews' => (int) $dateRow[2],
213
                'bounceRate' => $dateRow[3],
214
                'pageviewsPerSession' => $dateRow[4],
215
            ];
216
        })->reverse()->values();
217
218
        return Collection::make([
219
            'today',
220
            'yesterday',
221
            'week',
222
            'month',
223
        ])->mapWithKeys(function ($period) use ($statsByDate) {
224
            $stats = $this->getPeriodStats($period, $statsByDate);
0 ignored issues
show
Bug introduced by
$statsByDate of type Illuminate\Support\Collection is incompatible with the type Illuminate\Database\Query\Builder expected by parameter $statsByDate of A17\Twill\Http\Controlle...oller::getPeriodStats(). ( Ignorable by Annotation )

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

224
            $stats = $this->getPeriodStats($period, /** @scrutinizer ignore-type */ $statsByDate);
Loading history...
225
            return [
226
                $period => [
227
                    [
228
                        'label' => 'Users',
229
                        'figure' => $this->formatStat($stats['stats']['users']),
230
                        'insight' => round($stats['stats']['bounceRate']) . '% Bounce rate',
231
                        'trend' => $stats['moreUsers'] ? 'up' : 'down',
232
                        'data' => $stats['usersData']->reverse()->values()->toArray(),
233
                        'url' => 'https://analytics.google.com/analytics/web',
234
                    ],
235
                    [
236
                        'label' => 'Pageviews',
237
                        'figure' => $this->formatStat($stats['stats']['pageViews']),
238
                        'insight' => round($stats['stats']['pageviewsPerSession'], 1) . ' Pages / Session',
239
                        'trend' => $stats['morePageViews'] ? 'up' : 'down',
240
                        'data' => $stats['pageViewsData']->reverse()->values()->toArray(),
241
                        'url' => 'https://analytics.google.com/analytics/web',
242
                    ],
243
                ],
244
            ];
245
        });
246
    }
247
248
    /**
249
     * @param string $period
250
     * @param \Illuminate\Database\Query\Builder $statsByDate
251
     * @return array
252
     */
253
    private function getPeriodStats($period, $statsByDate)
254
    {
255
        if ($period === 'today') {
256
            return [
257
                'stats' => $stats = $statsByDate->first(),
258
                'moreUsers' => $stats['users'] > $statsByDate->get(1)['users'],
259
                'morePageViews' => $stats['pageViews'] > $statsByDate->get(1)['pageViews'],
260
                'usersData' => $statsByDate->take(7)->map(function ($stat) {
261
                    return $stat['users'];
262
                }),
263
                'pageViewsData' => $statsByDate->take(7)->map(function ($stat) {
264
                    return $stat['pageViews'];
265
                }),
266
            ];
267
        } elseif ($period === 'yesterday') {
268
            return [
269
                'stats' => $stats = $statsByDate->get(1),
270
                'moreUsers' => $stats['users'] > $statsByDate->get(2)['users'],
271
                'morePageViews' => $stats['pageViews'] > $statsByDate->get(2)['pageViews'],
272
                'usersData' => $statsByDate->slice(1)->take(7)->map(function ($stat) {
273
                    return $stat['users'];
274
                }),
275
                'pageViewsData' => $statsByDate->slice(1)->take(7)->map(function ($stat) {
276
                    return $stat['pageViews'];
277
                }),
278
            ];
279
        } elseif ($period === 'week') {
280
            $first7stats = $statsByDate->take(7)->all();
281
282
            $stats = [
283
                'users' => array_sum(array_column($first7stats, 'users')),
284
                'pageViews' => array_sum(array_column($first7stats, 'pageViews')),
285
                'bounceRate' => array_sum(array_column($first7stats, 'bounceRate')) / 7,
286
                'pageviewsPerSession' => array_sum(array_column($first7stats, 'pageviewsPerSession')) / 7,
287
            ];
288
289
            $compareStats = [
290
                'users' => array_sum(array_column($statsByDate->slice(7)->take(7)->all(), 'users')),
291
                'pageViews' => array_sum(array_column($statsByDate->slice(7)->take(7)->all(), 'pageViews')),
292
            ];
293
294
            return [
295
                'stats' => $stats,
296
                'moreUsers' => $stats['users'] > $compareStats['users'],
297
                'morePageViews' => $stats['pageViews'] > $compareStats['pageViews'],
298
                'usersData' => $statsByDate->slice(1)->take(29)->map(function ($stat) {
299
                    return $stat['users'];
300
                }),
301
                'pageViewsData' => $statsByDate->slice(1)->take(29)->map(function ($stat) {
302
                    return $stat['pageViews'];
303
                }),
304
            ];
305
        } elseif ($period === 'month') {
306
            $first30stats = $statsByDate->take(30)->all();
307
308
            $stats = [
309
                'users' => array_sum(array_column($first30stats, 'users')),
310
                'pageViews' => array_sum(array_column($first30stats, 'pageViews')),
311
                'bounceRate' => array_sum(array_column($first30stats, 'bounceRate')) / 30,
312
                'pageviewsPerSession' => array_sum(array_column($first30stats, 'pageviewsPerSession')) / 30,
313
            ];
314
315
            $compareStats = [
316
                'users' => array_sum(array_column($statsByDate->slice(30)->take(30)->all(), 'users')),
317
                'pageViews' => array_sum(array_column($statsByDate->slice(30)->take(30)->all(), 'pageViews')),
318
            ];
319
320
            return [
321
                'stats' => $stats,
322
                'moreUsers' => $stats['users'] > $compareStats['users'],
323
                'morePageViews' => $stats['pageViews'] > $compareStats['pageViews'],
324
                'usersData' => $statsByDate->slice(1)->take(29)->map(function ($stat) {
325
                    return $stat['users'];
326
                }),
327
                'pageViewsData' => $statsByDate->slice(1)->take(29)->map(function ($stat) {
328
                    return $stat['pageViews'];
329
                }),
330
            ];
331
        }
332
    }
333
334
    /**
335
     * @param int $count
336
     * @return string
337
     */
338
    private function formatStat($count)
339
    {
340
        if ($count >= 1000) {
341
            return round($count / 1000, 1) . "k";
342
        }
343
344
        return $count;
345
    }
346
347
    /**
348
     * @param array $modules
349
     * @return array
350
     */
351 50
    private function getShortcuts($modules)
352
    {
353
        return $modules->filter(function ($module) {
354 32
            return ($module['count'] ?? false) || ($module['create'] ?? false);
355
        })->map(function ($module) {
356 32
            $repository = $this->getRepository($module['name'], $module['repository'] ?? null);
357
358
            $moduleOptions = [
359 32
                'count' => $module['count'] ?? false,
360 32
                'create' => $module['create'] ?? false,
361 32
                'label' => $module['label'] ?? $module['name'],
362 32
                'singular' => $module['label_singular'] ?? Str::singular($module['name']),
363
            ];
364
365
            return [
366 32
                'label' => ucfirst($moduleOptions['label']),
367 32
                'singular' => ucfirst($moduleOptions['singular']),
368 32
                'number' => $moduleOptions['count'] ? $repository->getCountByStatusSlug(
369 32
                    'all', $module['countScope'] ?? []
370
                ) : null,
371 32
                'url' => moduleRoute(
372 32
                    $module['name'],
373 32
                    $module['routePrefix'] ?? null,
374 32
                    'index'
375
                ),
376 31
                'createUrl' => $moduleOptions['create'] ? moduleRoute(
377 31
                    $module['name'],
378 31
                    $module['routePrefix'] ?? null,
379 31
                    'index',
380 31
                    ['openCreate' => true]
381
                ) : null
382
            ];
383 50
        })->values();
384
    }
385
386
    /**
387
     * @param array $modules
388
     * @return array
389
     */
390 49
    private function getDrafts($modules)
391
    {
392
        return $modules->filter(function ($module) {
393 31
            return ($module['draft'] ?? false);
394
        })->map(function ($module) {
395 31
            $repository = $this->getRepository($module['name'], $module['repository'] ?? null);
396
397 31
            $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

397
            $query = $repository->/** @scrutinizer ignore-call */ draft()->limit(3)->latest();
Loading history...
398
399 31
            if ($repository->hasBehavior('revisions')) {
400 31
                $drafts = $query->mine();
0 ignored issues
show
Unused Code introduced by
The assignment to $drafts is dead and can be removed.
Loading history...
401
            }
402
403 31
            $drafts = $query->get();
404
405
            return $drafts->map(function ($draft) use ($module) {
406
                return [
407
                    'type' => ucfirst($module['label_singular'] ?? Str::singular($module['name'])),
408
                    'name' => $draft->titleInDashboard ?? $draft->title,
409
                    'url' => moduleRoute($module['name'], $module['routePrefix'] ?? null, 'edit', $draft->id),
410
                ];
411 31
            });
412 49
        })->collapse()->values();
413
    }
414
415
    /**
416
     * @param string $module
417
     * @return \A17\Twill\Repositories\ModuleRepository
418
     */
419 32
    private function getRepository($module, $forModule = null)
420
    {
421 32
        return $this->app->make($forModule ?: $this->config->get('twill.namespace') . "\Repositories\\" . ucfirst(Str::singular($module)) . "Repository");
422
    }
423
}
424