Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 111
Storage
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 16
812
0.00% covered (danger)
0.00%
0 / 111
 __construct
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 store
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 4
 tableHitsPerHour
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 8
 tableHitsPerPage
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 8
 tableVisitorsPerCountry
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 11
 tableSystemsPerMonth
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 11
 getHitsPerDay
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 7
 getHitsPerHourPerWeekday
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 6
 getHitsPerPage
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 6
 getVisitorsPerCountry
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 6
 getVisitorsPerPlatform
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getVisitorsPerBrowser
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getVisitorsPerBrowserVersion
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getVisitorsPerDeviceType
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getVisitorsPerSystem
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 13
 buildWhere
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 25
1<?php
2
3namespace Miniframe\Statistics\Service;
4
5use Miniframe\Statistics\Model\PageHit;
6
7class Storage
8{
9    /**
10     * Precision constants for Storage->buildWhere()
11     */
12    protected const WHERE_PRECISION_MONTH = 1, WHERE_PRECISION_DAY = 2, WHERE_PRECISION_HOUR = 3;
13
14    /**
15     * Reference to the SQLite3 database.
16     *
17     * @var \SQLite3
18     */
19    protected $db;
20
21    /**
22     * Initializes the statistics storage
23     *
24     * @param string $storagePath Path to the storage folder.
25     */
26    public function __construct(string $storagePath)
27    {
28        $dbFile = rtrim($storagePath, '/') . '/statistics.db';
29        $this->db = new \SQLite3($dbFile);
30    }
31
32    /**
33     * Stores the hit in several tables
34     *
35     * @param PageHit $pageHit The page hit.
36     *
37     * @return void
38     */
39    public function store(PageHit $pageHit): void
40    {
41        $this->tableHitsPerHour($pageHit);
42        $this->tableHitsPerPage($pageHit);
43        $this->tableVisitorsPerCountry($pageHit);
44        $this->tableSystemsPerMonth($pageHit);
45    }
46
47    /**
48     * Populate the hits_per_hour table; that's 8.760 to 8.784 records per year
49     *
50     * @param PageHit $pageHit The page hit.
51     *
52     * @return void
53     */
54    protected function tableHitsPerHour(PageHit $pageHit): void
55    {
56        $year = $pageHit->getDateTime()->format("Y");
57        $month = $pageHit->getDateTime()->format("n");
58        $day = $pageHit->getDateTime()->format("j");
59        $hour = $pageHit->getDateTime()->format("G");
60        $weekday = $pageHit->getDateTime()->format("N");
61
62        // Create table
63        $this->db->query('CREATE TABLE IF NOT EXISTS "hits_per_hour" (
64             "year"     INTEGER NOT NULL,
65             "month"    INTEGER NOT NULL,
66             "day"      INTEGER NOT NULL,
67             "hour"     INTEGER NOT NULL,
68             "weekday"  INTEGER NOT NULL,
69             "hits"     INTEGER NOT NULL,
70             "visitors" INTEGER NOT NULL,
71             CONSTRAINT "date" PRIMARY KEY("year","month","day","hour")
72        )');
73
74        // Create record for the date
75        $this->db->query('INSERT OR IGNORE INTO "hits_per_hour" VALUES (
76            ' . $year . ', ' . $month . ', ' . $day . ', ' . $hour . ', ' . $weekday . ', 0, 0
77        )');
78
79        // Increment correct values
80        $this->db->query('UPDATE `hits_per_hour` SET 
81            `hits` = `hits` + 1
82            ' . ($pageHit->isNewVisitor() ? ', `visitors` = `visitors` + 1' : '') . '
83        WHERE 
84            `year` = ' . $year . ' AND `month` = ' . $month . ' AND `day` = ' . $day . ' AND `hour` = ' . $hour . '
85        ');
86    }
87
88    /**
89     * Populate the hits_per_page table
90     *
91     * @param PageHit $pageHit The page hit.
92     *
93     * @return void
94     */
95    protected function tableHitsPerPage(PageHit $pageHit): void
96    {
97        $year = $pageHit->getDateTime()->format("Y");
98        $month = $pageHit->getDateTime()->format("n");
99        $day = $pageHit->getDateTime()->format("j");
100        $weekday = $pageHit->getDateTime()->format("N");
101        $page = addslashes(parse_url($pageHit->getRequestUri(), PHP_URL_PATH)); // Remove query part
102
103        // Create table
104        $this->db->query('CREATE TABLE IF NOT EXISTS "hits_per_page" (
105             "year"    INTEGER NOT NULL,
106             "month"   INTEGER NOT NULL,
107             "day"     INTEGER NOT NULL,
108             "weekday" INTEGER NOT NULL,
109             "page"    TEXT NOT NULL,
110             "hits"    INTEGER NOT NULL,
111             CONSTRAINT "date" PRIMARY KEY("year","month","day","page")
112        )');
113
114        // Create record for the date
115        $this->db->query('INSERT OR IGNORE INTO "hits_per_page" VALUES (
116            ' . $year . ', ' . $month . ', ' . $day . ', ' . $weekday . ', "' . $page . '", 0
117        )');
118
119        // Increment correct value
120        $this->db->query('UPDATE `hits_per_page` SET 
121            `hits` = `hits` + 1
122        WHERE 
123            `year` = ' . $year . ' AND `month` = ' . $month . ' AND `day` = ' . $day . ' AND `page` = "' . $page . '"
124        ');
125    }
126
127    /**
128     * Populate the visitors_per_country table
129     *
130     * @param PageHit $pageHit The page hit.
131     *
132     * @return void
133     */
134    protected function tableVisitorsPerCountry(PageHit $pageHit): void
135    {
136        // Only log unique visitors
137        if (!$pageHit->isNewVisitor()) {
138            return;
139        }
140
141        $year = $pageHit->getDateTime()->format("Y");
142        $month = $pageHit->getDateTime()->format("n");
143        $day = $pageHit->getDateTime()->format("j");
144        $hour = $pageHit->getDateTime()->format("G");
145        $weekday = $pageHit->getDateTime()->format("N");
146        $country = addslashes($pageHit->getCountry() ?? '--');
147
148        // Create table
149        $this->db->query('CREATE TABLE IF NOT EXISTS "visitors_per_country" (
150            "year"     INTEGER NOT NULL,
151            "month"    INTEGER NOT NULL,
152            "day"      INTEGER NOT NULL,
153            "hour"     INTEGER NOT NULL,
154            "weekday"  INTEGER NOT NULL,
155            "country"  TEXT NOT NULL,
156            "visitors" INTEGER NOT NULL,
157            PRIMARY KEY("year","month","day","hour","country")
158        )');
159
160        // Create record for the date + country
161        $this->db->query('INSERT OR IGNORE INTO "visitors_per_country" VALUES (
162            ' . $year . ', ' . $month . ', ' . $day . ', ' . $hour . ', ' . $weekday . ', "' . $country . '", 0
163        )');
164
165        // Increment correct value
166        $this->db->query('UPDATE `visitors_per_country` SET 
167            `visitors` = `visitors` + 1
168        WHERE 
169            `year` = ' . $year . ' AND `month` = ' . $month . ' AND `day` = ' . $day . ' AND `hour` = ' . $hour
170            . ' AND `country` = "' . $country . '"
171        ');
172    }
173
174    /**
175     * Populate the systems_per_month table
176     *
177     * @param PageHit $pageHit The page hit.
178     *
179     * @return void
180     */
181    protected function tableSystemsPerMonth(PageHit $pageHit): void
182    {
183        // Only log unique visitors
184        if (!$pageHit->isNewVisitor()) {
185            return;
186        }
187
188        $year = $pageHit->getDateTime()->format("Y");
189        $month = $pageHit->getDateTime()->format("n");
190        $browser = addslashes($pageHit->getBrowserBrand() ?? '--');
191        $version = addslashes($pageHit->getBrowserVersion() ?? '--');
192        $platform = addslashes($pageHit->getPlatform() ?? '--');
193        $deviceType = addslashes($pageHit->getDeviceType() ?? '--');
194
195        // Create table
196        $this->db->query('CREATE TABLE IF NOT EXISTS "systems_per_month" (
197            "year"        INTEGER NOT NULL,
198            "month"       INTEGER NOT NULL,
199            "browser"     TEXT NOT NULL,
200            "version"     TEXT NOT NULL,
201            "platform"    TEXT NOT NULL,
202            "device_type" TEXT NOT NULL,
203            "visitors"    INTEGER NOT NULL,
204            PRIMARY KEY("year","month","browser","version","platform","device_type")
205        )');
206
207        // Create record for the date + system
208        $this->db->query('INSERT OR IGNORE INTO "systems_per_month" VALUES (
209            ' . $year . ', ' . $month
210            . ', "' . $browser . '", "' . $version . '", "' . $platform . '", "' . $deviceType . '", 0
211        )');
212
213        // Increment correct value
214        $this->db->query('UPDATE `systems_per_month` SET 
215            `visitors` = `visitors` + 1
216        WHERE 
217            `year` = ' . $year . ' AND `month` = ' . $month . ' AND `browser` = "' . $browser . '" AND `version` = "'
218            . $version . '" AND `platform` = "' . $platform . '" AND `device_type` = "' . $deviceType . '"
219        ');
220    }
221
222    /**
223     * Returns the amount of hits and unique visitors per day between two dates.
224     *
225     * @param \DateTime $from Start date.
226     * @param \DateTime $till End date.
227     *
228     * @return array
229     */
230    public function getHitsPerDay(\DateTime $from, \DateTime $till): array
231    {
232        // Build and execute query
233        $query = 'SELECT `year`, `month`, `day`, SUM(`hits`), SUM(`visitors`) FROM `hits_per_hour`
234            WHERE ' . $this->buildWhere($from, $till, static::WHERE_PRECISION_DAY) . '
235            GROUP BY `year`, `month`, `day`
236        ;';
237        $result = $this->db->query($query);
238
239        // Convert result in a return array
240        $return = array();
241        while ($row = $result->fetchArray()) {
242            $date = date('Y-m-d', mktime(0, 0, 0, $row[1], $row[2], $row[0]));
243            $return[] = ['date' => $date, 'hits' => $row[3], 'visitors' => $row[4]];
244        }
245
246        return $return;
247    }
248
249    /**
250     * Returns the amount of hits per hour and weekday between two dates.
251     *
252     * @param \DateTime $from Start date.
253     * @param \DateTime $till End date.
254     *
255     * @return array
256     */
257    public function getHitsPerHourPerWeekday(\DateTime $from, \DateTime $till): array
258    {
259        // Build and execute query
260        $query = 'SELECT `weekday`, `hour`, SUM(`hits`) FROM `hits_per_hour`
261            WHERE ' . $this->buildWhere($from, $till, static::WHERE_PRECISION_DAY) . '
262            GROUP BY `weekday`, `hour`
263        ;';
264        $result = $this->db->query($query);
265
266        // Convert result in a return array
267        $return = array();
268        while ($row = $result->fetchArray()) {
269            $return[$row[0]][$row[1]] = $row[2];
270        }
271
272        return $return;
273    }
274
275    /**
276     * Returns the amount of hits per page
277     *
278     * @param \DateTime $from  Start date.
279     * @param \DateTime $till  End date.
280     * @param integer   $limit The max. rows to return.
281     *
282     * @return array
283     */
284    public function getHitsPerPage(\DateTime $from, \DateTime $till, int $limit = 100): array
285    {
286        // Build and execute query
287        $query = 'SELECT `page`, SUM(`hits`) AS `total` FROM `hits_per_page`
288            WHERE ' . $this->buildWhere($from, $till, static::WHERE_PRECISION_DAY) . '
289            GROUP BY `page`
290            ORDER BY `total` DESC
291            LIMIT ' . $limit . '
292        ;';
293        $result = $this->db->query($query);
294
295        // Convert result in a return array
296        $return = array();
297        while ($row = $result->fetchArray()) {
298            $return[] = ['page' => $row[0], 'hits' => $row[1]];
299        }
300
301        return $return;
302    }
303
304    /**
305     * Returns the amount of visitors per country
306     *
307     * @param \DateTime $from  Start date.
308     * @param \DateTime $till  End date.
309     * @param integer   $limit The max. rows to return.
310     *
311     * @return array
312     */
313    public function getVisitorsPerCountry(\DateTime $from, \DateTime $till, int $limit = 100): array
314    {
315        // Build and execute query
316        $query = 'SELECT `country`, SUM(`visitors`) AS `total` FROM `visitors_per_country`
317            WHERE ' . $this->buildWhere($from, $till, static::WHERE_PRECISION_DAY) . '
318            GROUP BY `country`
319            ORDER BY `total` DESC
320            LIMIT ' . $limit . '
321        ;';
322        $result = $this->db->query($query);
323
324        // Convert result in a return array
325        $return = array();
326        while ($row = $result->fetchArray()) {
327            $return[] = ['country' => $row[0], 'visitors' => $row[1]];
328        }
329
330        return $return;
331    }
332
333    /**
334     * Returns the amount of visitors per platform
335     *
336     * @param \DateTime $from  Start date.
337     * @param \DateTime $till  End date.
338     * @param integer   $limit The max. rows to return.
339     *
340     * @return array
341     */
342    public function getVisitorsPerPlatform(\DateTime $from, \DateTime $till, int $limit = 100): array
343    {
344        return $this->getVisitorsPerSystem('platform', $from, $till, $limit);
345    }
346
347    /**
348     * Returns the amount of visitors per browser
349     *
350     * @param \DateTime $from  Start date.
351     * @param \DateTime $till  End date.
352     * @param integer   $limit The max. rows to return.
353     *
354     * @return array
355     */
356    public function getVisitorsPerBrowser(\DateTime $from, \DateTime $till, int $limit = 100): array
357    {
358        return $this->getVisitorsPerSystem('browser', $from, $till, $limit);
359    }
360
361    /**
362     * Returns the amount of visitors per browser version
363     *
364     * @param \DateTime $from  Start date.
365     * @param \DateTime $till  End date.
366     * @param integer   $limit The max. rows to return.
367     *
368     * @return array
369     */
370    public function getVisitorsPerBrowserVersion(\DateTime $from, \DateTime $till, int $limit = 100): array
371    {
372        return $this->getVisitorsPerSystem('version', $from, $till, $limit);
373    }
374
375    /**
376     * Returns the amount of visitors per device type
377     *
378     * @param \DateTime $from  Start date.
379     * @param \DateTime $till  End date.
380     * @param integer   $limit The max. rows to return.
381     *
382     * @return array
383     */
384    public function getVisitorsPerDeviceType(\DateTime $from, \DateTime $till, int $limit = 100): array
385    {
386        return $this->getVisitorsPerSystem('device_type', $from, $till, $limit);
387    }
388
389    /**
390     * Returns the amount of visitors per system
391     *
392     * @param string    $column Column to zoom in on.
393     * @param \DateTime $from   Start date.
394     * @param \DateTime $till   End date.
395     * @param integer   $limit  The max. rows to return.
396     *
397     * @return array
398     */
399    protected function getVisitorsPerSystem(string $column, \DateTime $from, \DateTime $till, int $limit): array
400    {
401        if ($column == 'version') {
402            $columnString = "`browser`, `version`";
403        } else {
404            $columnString = "`" . $column . "`";
405        }
406
407        // Build and execute query
408        $query = 'SELECT ' . $columnString . ', SUM(`visitors`) AS `total` FROM `systems_per_month`
409            WHERE ' . $this->buildWhere($from, $till, static::WHERE_PRECISION_MONTH) . '
410            GROUP BY ' . $columnString . '
411            ORDER BY `total` DESC
412            LIMIT ' . $limit . '
413        ;';
414        $result = $this->db->query($query);
415
416        // Convert result in a return array
417        $return = array();
418        while ($row = $result->fetchArray()) {
419            if ($column == 'version') {
420                $return[] = ['browser_version' => $row[0] . ' ' . $row[1], 'visitors' => $row[2]];
421            } else {
422                $return[] = [$column => $row[0], 'visitors' => $row[1]];
423            }
424        }
425
426        return $return;
427    }
428
429    /**
430     * Builds a WHERE statement between two dates.
431     *
432     * @param \DateTime $from      The start date.
433     * @param \DateTime $till      The end date.
434     * @param integer   $precision The precision (WHERE_PRECISION_MONTH, WHERE_PRECISION_DAY, WHERE_PRECISION_HOUR).
435     *
436     * @return string
437     */
438    protected function buildWhere(\DateTime $from, \DateTime $till, int $precision): string
439    {
440        $fromYear = $from->format("Y");
441        $fromMonth = $from->format("n");
442        $fromDay = $from->format("j");
443        $fromHour = $from->format("G");
444        $tillYear = $till->format("Y");
445        $tillMonth = $till->format("n");
446        $tillDay = $till->format("j");
447        $tillHour = $till->format("G");
448
449        $fromWhere  = '"year" > ' . $fromYear;
450        $tillWhere  = '"year" < ' . $tillYear;
451
452        if ($precision == static::WHERE_PRECISION_MONTH) {
453            $fromWhere .= ' OR ("year" = ' . $fromYear . ' AND "month" >= ' . $fromMonth . ')';
454            $tillWhere .= ' OR ("year" = ' . $tillYear . ' AND "month" <= ' . $tillMonth . ')';
455        } else {
456            $fromWhere .= ' OR ("year" = ' . $fromYear . ' AND "month" > ' . $fromMonth . ')';
457            $tillWhere .= ' OR ("year" = ' . $tillYear . ' AND "month" < ' . $tillMonth . ')';
458            if ($precision == static::WHERE_PRECISION_DAY) {
459                $fromWhere .= ' OR ("year" = ' . $fromYear . ' AND "month" = ' . $fromMonth . ' AND "day" >= '
460                    . $fromDay . ')';
461                $tillWhere .= ' OR ("year" = ' . $tillYear . ' AND "month" = ' . $tillMonth . ' AND "day" <= '
462                    . $tillDay . ')';
463            } else {
464                $fromWhere .= ' OR ("year" = ' . $fromYear . ' AND "month" = ' . $fromMonth . ' AND "day" > '
465                    . $fromDay . ')';
466                $fromWhere .= ' OR ("year" = ' . $fromYear . ' AND "month" = ' . $fromMonth . ' AND "day" = '
467                    . $fromDay . ' AND "hour" >= ' . $fromHour . ')';
468                $tillWhere .= ' OR ("year" = ' . $tillYear . ' AND "month" = ' . $tillMonth . ' AND "day" < '
469                    . $tillDay . ')';
470                $tillWhere .= ' OR ("year" = ' . $tillYear . ' AND "month" = ' . $tillMonth . ' AND "day" = '
471                    . $tillDay . ' AND "hour" <= ' . $tillHour . ')';
472            }
473        }
474
475        return '(' . $fromWhere . ') AND (' . $tillWhere . ')';
476    }
477}