Passed
Push — master ( 8e7e6d...9f9e2a )
by Dispositif
03:39
created

StatsSqlite3   A

Complexity

Total Complexity 19

Size/Duplication

Total Lines 121
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
wmc 19
eloc 53
c 1
b 0
f 1
dl 0
loc 121
rs 10

9 Methods

Rating   Name   Duplication   Size   Complexity  
A sqliteExecWriteOrWait() 0 14 3
A increment() 0 8 1
A __construct() 0 7 3
A set() 0 9 2
A upsertTag() 0 9 2
A createTableIfNotExists() 0 20 2
A formatTag() 0 3 1
A select() 0 11 3
A decrement() 0 9 2
1
<?php
2
/*
3
 * This file is part of dispositif/wikibot application (@github)
4
 * 2019-2023 © Philippe M./Irønie  <[email protected]>
5
 * For the full copyright and MIT license information, view the license file.
6
 */
7
8
declare(strict_types=1);
9
10
namespace App\Infrastructure\Monitor;
11
12
use Exception;
13
use SQLite3;
14
15
/**
16
 * SQLite3 base: stats.db
17
 * tables: tagnum, tagnum_daily, tagnum_monthly
18
 */
19
class StatsSqlite3 implements StatsInterface
20
{
21
    protected const DEFAULT_FILEPATH = __DIR__ . '/../../../log/stats.db';
22
    protected const MAX_TAG_LENGTH = 100;
23
24
    protected SQLite3 $db;
25
26
    public function __construct()
27
    {
28
        if (!class_exists('SQLite3')) {
29
            throw new Exception('Stats ERROR : SQLite 3 NOT supported.');
30
        }
31
        $this->db = new SQLite3(getenv('STATS_FILEPATH') ?: self::DEFAULT_FILEPATH);
32
        $this->createTableIfNotExists();
33
    }
34
35
    protected function createTableIfNotExists(): bool
36
    {
37
        // note : Sqlite VARCHAR(X) do not truncate to the supposed X max chars. VARCHAR is TEXT.
38
        try {
39
            $this->db->exec('CREATE TABLE if not exists tagnum_monthly (
40
                tag TEXT NOT NULL PRIMARY KEY,
41
                num int(11) NOT NULL DEFAULT 1
42
            )');
43
44
            $this->db->exec('CREATE TABLE if not exists tagnum_daily (
45
                tag TEXT NOT NULL PRIMARY KEY,
46
                num int(11) NOT NULL DEFAULT 1
47
            )');
48
49
            return $this->db->exec('CREATE TABLE if not exists tagnum (
50
                tag TEXT NOT NULL PRIMARY KEY,
51
                num int(11) NOT NULL DEFAULT 1
52
            )');
53
        } catch (Exception $e) {
54
            return false;
55
        }
56
    }
57
58
    public function increment(string $tag): bool
59
    {
60
        $tag = $this->formatTag($tag);
61
62
        $this->upsertTag(sprintf('%s.%s', date('Ymd'), $tag), 'tagnum_daily');
63
        $this->upsertTag(sprintf('%s.%s', date('Ym'), $tag), 'tagnum_monthly');
64
65
        return $this->upsertTag($tag);
66
    }
67
68
    private function formatTag(string $tag): string
69
    {
70
        return mb_substr($tag, 0, self::MAX_TAG_LENGTH);
71
    }
72
73
    protected function upsertTag(string $tag, string $table = 'tagnum'): bool
74
    {
75
        // `num` default value is 1 so insert is enough to set num=1
76
        try {
77
            return $this->sqliteExecWriteOrWait(
78
                'INSERT INTO ' . $table . ' (tag) VALUES("' . $tag . '") ON CONFLICT(tag) DO UPDATE SET num=num+1'
79
            );
80
        } catch (Exception $e) {
81
            return false;
82
        }
83
    }
84
85
    /**
86
     * Sqlite do not allow concurrent write from different process.
87
     * So wait a little and retry.
88
     */
89
    protected function sqliteExecWriteOrWait(string $query, int $maxRetry = 10): bool
90
    {
91
        $retry = 0;
92
        while ($retry < $maxRetry) {
93
            $success = @$this->db->exec($query);
94
            if ($success) {
95
                return true;
96
            }
97
            $retry++;
98
            usleep(100000); // 100000 µs = 0.1 s
99
        }
100
        echo "DEBUG: Sqlite retry : max retry reached\n"; // todo remove
101
102
        return false;
103
    }
104
105
    public function set(string $tag, int $num): bool
106
    {
107
        $tag = $this->formatTag($tag);
108
        try {
109
            return $this->sqliteExecWriteOrWait(
110
                'INSERT OR REPLACE INTO tagnum (tag,num) VALUES("' . $tag . '", ' . $num . ')'
111
            );
112
        } catch (Exception $e) {
113
            return false;
114
        }
115
    }
116
117
    public function decrement(string $tag): bool
118
    {
119
        $tag = $this->formatTag($tag);
120
        try {
121
            return $this->sqliteExecWriteOrWait(
122
                'INSERT INTO tagnum (tag) VALUES("' . $tag . '") ON CONFLICT(tag) DO UPDATE SET num=num-1'
123
            );
124
        } catch (Exception $e) {
125
            return false;
126
        }
127
    }
128
129
    public function select(string $tag): ?int
130
    {
131
        $tag = $this->formatTag($tag);
132
        try {
133
            $stmt = $this->db->prepare('SELECT tag,num FROM tagnum WHERE tag LIKE :tag');
134
            $stmt->bindValue(':tag', $tag, SQLITE3_TEXT);
135
            $result = $stmt->execute();
136
137
            return $result ? $result->fetchArray(SQLITE3_ASSOC)['num'] : null;
0 ignored issues
show
introduced by
$result is of type SQLite3Result, thus it always evaluated to true.
Loading history...
138
        } catch (Exception $e) {
139
            return null;
140
        }
141
    }
142
}