Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
0.00% |
0 / 1 |
|
0.00% |
0 / 16 |
CRAP | |
0.00% |
0 / 111 |
Storage | |
0.00% |
0 / 1 |
|
0.00% |
0 / 16 |
812 | |
0.00% |
0 / 111 |
__construct | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
store | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 4 |
|||
tableHitsPerHour | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 8 |
|||
tableHitsPerPage | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 8 |
|||
tableVisitorsPerCountry | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 11 |
|||
tableSystemsPerMonth | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 11 |
|||
getHitsPerDay | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 7 |
|||
getHitsPerHourPerWeekday | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 6 |
|||
getHitsPerPage | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 6 |
|||
getVisitorsPerCountry | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 6 |
|||
getVisitorsPerPlatform | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
getVisitorsPerBrowser | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
getVisitorsPerBrowserVersion | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
getVisitorsPerDeviceType | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
getVisitorsPerSystem | |
0.00% |
0 / 1 |
20 | |
0.00% |
0 / 13 |
|||
buildWhere | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 25 |
1 | <?php |
2 | |
3 | namespace Miniframe\Statistics\Service; |
4 | |
5 | use Miniframe\Statistics\Model\PageHit; |
6 | |
7 | class 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 | } |