Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 199
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
PageViewsController
0.00% covered (danger)
0.00%
0 / 199
0.00% covered (danger)
0.00%
0 / 9
552
0.00% covered (danger)
0.00%
0 / 1
 initialize
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 pageViewStats
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
6
 viewRecords
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
12
 filterStats
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
30
 dashboard
0.00% covered (danger)
0.00%
0 / 60
0.00% covered (danger)
0.00%
0 / 1
12
 getBrowserStats
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
6
 extractBrowser
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 getHourlyDistribution
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
6
 getTopReferrers
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2declare(strict_types=1);
3
4namespace App\Controller\Admin;
5
6use App\Controller\AppController;
7use App\Model\Table\ArticlesTable;
8use Cake\Core\Configure;
9use Cake\Http\Exception\NotFoundException;
10use Cake\Http\Response;
11use Cake\Log\LogTrait;
12use DateTime;
13use Exception;
14
15/**
16 * PageViews Controller
17 *
18 * Manages page view statistics for articles.
19 *
20 * @property \App\Model\Table\PageViewsTable $PageViews
21 */
22class PageViewsController extends AppController
23{
24    use LogTrait;
25
26    /**
27     * Articles Table
28     *
29     * @var \App\Model\Table\ArticlesTable
30     * This property holds an instance of the ArticlesTable class.
31     * It is used to interact with the articles table in the database.
32     * The ArticlesTable class provides methods for querying and manipulating
33     * article data, such as finding, saving, and deleting articles.
34     */
35    protected ArticlesTable $Articles;
36
37    /**
38     * Initialize method
39     *
40     * @return void
41     */
42    public function initialize(): void
43    {
44        parent::initialize();
45        $this->Articles = $this->fetchTable('Articles');
46    }
47
48    /**
49     * Retrieves page view statistics for a specific article.
50     *
51     * This method fetches an article by its ID and retrieves the number of page views
52     * grouped by date. It then sets the data to be used in the view.
53     *
54     * @param string $articleId The ID of the article to retrieve statistics for
55     * @return void
56     * @throws \Cake\Http\Exception\NotFoundException If the article is not found
57     */
58    public function pageViewStats(string $articleId): void
59    {
60        $article = $this->Articles->find()
61            ->select(['id', 'title', 'slug'])
62            ->where(['id' => $articleId])
63            ->first();
64
65        if (!$article) {
66            throw new NotFoundException(__('Article not found'));
67        }
68
69        $viewsOverTime = $this->PageViews->find()
70            ->where(['article_id' => $articleId])
71            ->select([
72                'date' => 'DATE(created)',
73                'count' => $this->PageViews->find()->func()->count('*'),
74            ])
75            ->groupBy('DATE(created)')
76            ->orderBy(['DATE(created)' => 'DESC'])
77            ->all();
78
79        $this->set(compact('viewsOverTime', 'article'));
80    }
81
82    /**
83     * Retrieves view records for a specific article.
84     *
85     * This method fetches an article by its ID and retrieves all associated page view records.
86     * If a date query parameter is provided, it filters the page views by that date.
87     * The results are then set to be available in the view.
88     *
89     * @param string $articleId The ID of the article to retrieve view records for
90     * @return void
91     * @throws \Cake\Http\Exception\NotFoundException If the article is not found
92     */
93    public function viewRecords(string $articleId): void
94    {
95        $article = $this->Articles->find()
96            ->select(['id', 'title', 'slug'])
97            ->where(['id' => $articleId])
98            ->first();
99
100        if (!$article) {
101            throw new NotFoundException(__('Article not found'));
102        }
103
104        $query = $this->PageViews->find()
105            ->where(['article_id' => $articleId])
106            ->orderBy(['created' => 'DESC']);
107
108        if ($this->request->getQuery('date')) {
109            $date = new DateTime($this->request->getQuery('date'));
110            $query->where([
111                'DATE(created)' => $date->format('Y-m-d'),
112            ]);
113        }
114
115        $viewRecords = $query->all();
116
117        $this->set(compact('viewRecords', 'article'));
118    }
119
120    /**
121     * Filters page view statistics for a specific article based on date range.
122     *
123     * @param string $articleId The ID of the article to retrieve statistics for
124     * @return \Cake\Http\Response|null JSON response with filtered data or error message
125     */
126    public function filterStats(string $articleId): ?Response
127    {
128        if (Configure::read('debug')) {
129            $this->log('Filter request received for article ID: ' . $articleId, 'debug');
130            $this->log('Start date: ' . $this->request->getQuery('start'), 'debug');
131            $this->log('End date: ' . $this->request->getQuery('end'), 'debug');
132        }
133
134        try {
135            $article = $this->Articles->find()
136                ->select(['id', 'title', 'slug'])
137                ->where(['id' => $articleId])
138                ->first();
139
140            if (!$article) {
141                throw new NotFoundException(__('Article not found'));
142            }
143
144            $startDate = new DateTime($this->request->getQuery('start'));
145            $endDate = new DateTime($this->request->getQuery('end'));
146
147            $viewsOverTime = $this->PageViews->find()
148                ->where([
149                    'article_id' => $articleId,
150                    'created >=' => $startDate->format('Y-m-d'),
151                    'created <=' => $endDate->format('Y-m-d 23:59:59'),
152                ])
153                ->select([
154                    'date' => 'DATE(created)',
155                    'count' => $this->PageViews->find()->func()->count('*'),
156                ])
157                ->groupBy('DATE(created)')
158                ->orderBy(['DATE(created)' => 'ASC'])
159                ->all();
160
161            $totalViews = array_sum(array_column($viewsOverTime->toArray(), 'count'));
162
163            $filteredData = [
164                'viewsOverTime' => $viewsOverTime,
165                'totalViews' => $totalViews,
166            ];
167
168            if (Configure::read('debug')) {
169                $this->log('Filtered data: ' . json_encode($filteredData), 'debug');
170            }
171
172            return $this->response->withType('application/json')->withStringBody(json_encode($filteredData));
173        } catch (Exception $e) {
174            $this->log('Error in filterStats: ' . $e->getMessage(), 'error');
175
176            $errorMsg = __('An error occurred while processing your request.');
177
178            return $this->response->withStatus(500)
179                ->withType('application/json')
180                ->withStringBody(json_encode(['error' => $errorMsg]));
181        }
182    }
183
184    /**
185     * Enhanced analytics dashboard with comprehensive metrics
186     *
187     * @return void
188     */
189    public function dashboard(): void
190    {
191        // Get date range from request or default to last 30 days
192        $endDate = new DateTime();
193        $startDate = (clone $endDate)->modify('-30 days');
194
195        if ($this->request->getQuery('start')) {
196            $startDate = new DateTime($this->request->getQuery('start'));
197        }
198        if ($this->request->getQuery('end')) {
199            $endDate = new DateTime($this->request->getQuery('end'));
200        }
201
202        // Overall statistics
203        $totalViews = $this->PageViews->find()
204            ->where([
205                'created >=' => $startDate->format('Y-m-d'),
206                'created <=' => $endDate->format('Y-m-d 23:59:59'),
207            ])
208            ->count();
209
210        $uniqueVisitors = $this->PageViews->find()
211            ->where([
212                'created >=' => $startDate->format('Y-m-d'),
213                'created <=' => $endDate->format('Y-m-d 23:59:59'),
214            ])
215            ->select(['ip_address'])
216            ->distinct(['ip_address'])
217            ->count();
218
219        // Views over time
220        $viewsOverTime = $this->PageViews->find()
221            ->where([
222                'created >=' => $startDate->format('Y-m-d'),
223                'created <=' => $endDate->format('Y-m-d 23:59:59'),
224            ])
225            ->select([
226                'date' => 'DATE(created)',
227                'count' => $this->PageViews->find()->func()->count('*'),
228            ])
229            ->groupBy('DATE(created)')
230            ->orderBy(['DATE(created)' => 'ASC'])
231            ->all();
232
233        // Top articles
234        $topArticles = $this->PageViews->find()
235            ->contain(['Articles' => ['fields' => ['id', 'title', 'slug']]])
236            ->where([
237                'PageViews.created >=' => $startDate->format('Y-m-d'),
238                'PageViews.created <=' => $endDate->format('Y-m-d 23:59:59'),
239            ])
240            ->select([
241                'article_id',
242                'count' => $this->PageViews->find()->func()->count('*'),
243            ])
244            ->groupBy(['article_id'])
245            ->orderBy(['count' => 'DESC'])
246            ->limit(10)
247            ->all();
248
249        // Get additional analytics data
250        $browserStats = $this->getBrowserStats($startDate, $endDate);
251        $hourlyDistribution = $this->getHourlyDistribution($startDate, $endDate);
252        $topReferrers = $this->getTopReferrers($startDate, $endDate);
253
254        $this->set(compact(
255            'totalViews',
256            'uniqueVisitors',
257            'viewsOverTime',
258            'topArticles',
259            'browserStats',
260            'hourlyDistribution',
261            'topReferrers',
262            'startDate',
263            'endDate',
264        ));
265    }
266
267    /**
268     * Get browser statistics
269     *
270     * @param \DateTime $startDate Start date
271     * @param \DateTime $endDate End date
272     * @return array Browser statistics
273     */
274    private function getBrowserStats(DateTime $startDate, DateTime $endDate): array
275    {
276        $results = $this->PageViews->find()
277            ->where([
278                'created >=' => $startDate->format('Y-m-d'),
279                'created <=' => $endDate->format('Y-m-d 23:59:59'),
280            ])
281            ->select(['user_agent'])
282            ->all();
283
284        $browserCounts = [];
285        foreach ($results as $result) {
286            $userAgent = $result->user_agent ?? '';
287            $browser = $this->extractBrowser($userAgent);
288            $browserCounts[$browser] = ($browserCounts[$browser] ?? 0) + 1;
289        }
290
291        arsort($browserCounts);
292
293        return array_slice($browserCounts, 0, 10, true);
294    }
295
296    /**
297     * Extract browser name from user agent string
298     *
299     * @param string $userAgent User agent string
300     * @return string Browser name
301     */
302    private function extractBrowser(string $userAgent): string
303    {
304        $browsers = [
305            'Chrome' => '/Chrome\/[\d.]+/',
306            'Firefox' => '/Firefox\/[\d.]+/',
307            'Safari' => '/Safari\/[\d.]+/',
308            'Edge' => '/Edg\/[\d.]+/',
309            'Opera' => '/OPR\/[\d.]+/',
310            'Internet Explorer' => '/MSIE [\d.]+/',
311        ];
312
313        foreach ($browsers as $browser => $pattern) {
314            if (preg_match($pattern, $userAgent)) {
315                return $browser;
316            }
317        }
318
319        return 'Other';
320    }
321
322    /**
323     * Get hourly distribution of views
324     *
325     * @param \DateTime $startDate Start date
326     * @param \DateTime $endDate End date
327     * @return array Hourly distribution
328     */
329    private function getHourlyDistribution(DateTime $startDate, DateTime $endDate): array
330    {
331        $results = $this->PageViews->find()
332            ->where([
333                'created >=' => $startDate->format('Y-m-d'),
334                'created <=' => $endDate->format('Y-m-d 23:59:59'),
335            ])
336            ->select([
337                'hour' => 'HOUR(created)',
338                'count' => $this->PageViews->find()->func()->count('*'),
339            ])
340            ->groupBy('HOUR(created)')
341            ->orderBy(['hour' => 'ASC'])
342            ->all();
343
344        $hourlyData = array_fill(0, 24, 0);
345        foreach ($results as $result) {
346            $hourlyData[(int)$result->hour] = $result->count;
347        }
348
349        return $hourlyData;
350    }
351
352    /**
353     * Get top referrers
354     *
355     * @param \DateTime $startDate Start date
356     * @param \DateTime $endDate End date
357     * @return array Top referrers
358     */
359    private function getTopReferrers(DateTime $startDate, DateTime $endDate): array
360    {
361        $results = $this->PageViews->find()
362            ->where([
363                'created >=' => $startDate->format('Y-m-d'),
364                'created <=' => $endDate->format('Y-m-d 23:59:59'),
365                'referer IS NOT' => null,
366                'referer !=' => '',
367            ])
368            ->select([
369                'referer',
370                'count' => $this->PageViews->find()->func()->count('*'),
371            ])
372            ->groupBy(['referer'])
373            ->orderBy(['count' => 'DESC'])
374            ->limit(10)
375            ->all();
376
377        $referrers = [];
378        foreach ($results as $result) {
379            $domain = parse_url($result->referer, PHP_URL_HOST) ?? $result->referer;
380            $referrers[] = [
381                'domain' => $domain,
382                'count' => $result->count,
383                'url' => $result->referer,
384            ];
385        }
386
387        return $referrers;
388    }
389}