Passed
Push — master ( 60e75a...8e7e6d )
by Dispositif
02:35
created

Stats::open()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
eloc 3
c 1
b 1
f 0
dl 0
loc 6
rs 10
cc 3
nc 2
nop 0
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 Stats
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
        $this->createTableIfNotExists();
29
    }
30
31
    public function increment(string $tag): bool
32
    {
33
        $tag = $this->formatTag($tag);
34
35
        $this->upsertTag(sprintf('%s.%s', date('Ymd'), $tag), 'tagnum_daily');
36
        $this->upsertTag(sprintf('%s.%s', date('Ym'), $tag), 'tagnum_monthly');
37
38
        return $this->upsertTag($tag);
39
    }
40
41
    private function formatTag(string $tag)
42
    {
43
        return mb_substr($tag, 0, self::MAX_TAG_LENGTH);
44
    }
45
46
    protected function upsertTag(string $tag, string $table = 'tagnum'): bool
47
    {
48
        // `num` default value is 1 so insert is enough to set num=1
49
        try {
50
            return $this->sqliteExecWriteOrWait(
51
                'INSERT INTO ' . $table . ' (tag) VALUES("' . $tag . '") ON CONFLICT(tag) DO UPDATE SET num=num+1'
52
            );
53
        } catch (Exception $e) {
54
            return false;
55
        }
56
    }
57
58
    /**
59
     * Sqlite do not allow concurrent write from different process.
60
     * So wait a little and retry.
61
     */
62
    protected function sqliteExecWriteOrWait(string $query, int $maxRetry = 10): bool
63
    {
64
        $this->open();
65
        $retry = 0;
66
        while ($retry < $maxRetry) {
67
            $success = @$this->db->exec($query);
68
            if ($success) {
69
                $this->close();
70
                return true;
71
            }
72
            $retry++;
73
            usleep(100000); // 100000 µs = 0.1 s
74
        }
75
        echo "DEBUG: Sqlite retry : max retry reached\n"; // todo remove
76
        $this->close();
77
78
        return false;
79
    }
80
81
    public function set(string $tag, int $num): bool
82
    {
83
        $tag = $this->formatTag($tag);
84
        try {
85
            return $this->sqliteExecWriteOrWait(
86
                'INSERT OR REPLACE INTO tagnum (tag,num) VALUES("' . $tag . '", ' . $num . ')'
87
            );
88
        } catch (Exception $e) {
89
            return false;
90
        }
91
    }
92
93
    public function decrement(string $tag): bool
94
    {
95
        $tag = $this->formatTag($tag);
96
        try {
97
            return $this->sqliteExecWriteOrWait(
98
                'INSERT INTO tagnum (tag) VALUES("' . $tag . '") ON CONFLICT(tag) DO UPDATE SET num=num-1'
99
            );
100
        } catch (Exception $e) {
101
            return false;
102
        }
103
    }
104
105
    public function select(string $tag): ?int
106
    {
107
        $tag = $this->formatTag($tag);
108
        $this->open();
109
        try {
110
            $stmt = $this->db->prepare('SELECT tag,num FROM tagnum WHERE tag LIKE :tag');
111
            $stmt->bindValue(':tag', $tag, SQLITE3_TEXT);
112
            $result = $stmt->execute();
113
            $this->close();
114
115
            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...
116
        } catch (Exception $e) {
117
            $this->close();
118
            return null;
119
        }
120
    }
121
122
    protected function open(): void
123
    {
124
        if (!class_exists('SQLite3')) {
125
            throw new Exception('Stats ERROR : SQLite 3 NOT supported.');
126
        }
127
        $this->db = new SQLite3(getenv('STATS_FILEPATH') ?: self::DEFAULT_FILEPATH);
128
    }
129
130
    protected function close()
131
    {
132
        $this->db->close();
133
    }
134
135
    protected function createTableIfNotExists(): bool
136
    {
137
        $this->open();
138
        // note : Sqlite VARCHAR(X) do not truncate to the supposed X max chars. VARCHAR is TEXT.
139
        try {
140
            $this->db->exec('CREATE TABLE if not exists tagnum_monthly (
141
                tag TEXT NOT NULL PRIMARY KEY,
142
                num int(11) NOT NULL DEFAULT 1
143
            )');
144
145
            $this->db->exec('CREATE TABLE if not exists tagnum_daily (
146
                tag TEXT NOT NULL PRIMARY KEY,
147
                num int(11) NOT NULL DEFAULT 1
148
            )');
149
150
            $res = $this->db->exec('CREATE TABLE if not exists tagnum (
151
                tag TEXT NOT NULL PRIMARY KEY,
152
                num int(11) NOT NULL DEFAULT 1
153
            )');
154
155
            $this->close();
156
157
            return $res;
158
        } catch (Exception $e) {
159
            $this->close();
160
            return false;
161
        }
162
    }
163
}