PNDataProviderMySQL   B
last analyzed

Complexity

Total Complexity 50

Size/Duplication

Total Lines 316
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 153
dl 0
loc 316
rs 8.4
c 1
b 0
f 0
wmc 50

13 Methods

Rating   Name   Duplication   Size   Complexity  
A createTable() 0 21 3
A setLogger() 0 3 1
A count() 0 13 4
A isConnected() 0 15 5
A fetch() 0 10 3
B saveSubscription() 0 35 7
A tableExist() 0 11 4
B init() 0 49 7
A removeSubscription() 0 12 3
A __construct() 0 17 5
A getError() 0 3 1
A getColumn() 0 10 5
A truncate() 0 8 2

How to fix   Complexity   

Complex Class

Complex classes like PNDataProviderMySQL often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use PNDataProviderMySQL, and based on these observations, apply Extract Interface, too.

1
<?php
2
declare(strict_types=1);
3
4
namespace SKien\PNServer;
5
6
use Psr\Log\LoggerInterface;
7
use Psr\Log\NullLogger;
8
9
/**
10
 * Dataprovider for MySQL database.
11
 * uses given Table in specified MySQL database
12
 *
13
 * if not specified in constructor, default table self::TABLE_NAME in
14
 * MySQL database is used.
15
 * table will be created if not exist so far.
16
 *
17
 * @package PNServer
18
 * @author Stefanius <[email protected]>
19
 * @copyright MIT License - see the LICENSE file for details
20
*/
21
class PNDataProviderMySQL implements PNDataProvider
22
{
23
    /** @var string tablename            */
24
    protected string $strTableName = '';
25
    /** @var string DB host  */
26
    protected string $strDBHost = '';
27
    /** @var string DB user  */
28
    protected string $strDBUser = '';
29
    /** @var string Password for DB  */
30
    protected string $strDBPwd = '';
31
    /** @var string DB name  */
32
    protected string $strDBName = '';
33
    /** @var \mysqli|false|null internal MySQL DB */
34
    protected $db = false;
35
    /** @var \mysqli_result<string,mixed>|false result of DB queries */
36
    protected $dbres = false;
37
    /** @var array<string,mixed> last fetched row or null      */
38
    protected ?array $row = null;
39
    /** @var string last error                   */
40
    protected string $strLastError = '';
41
    /** @var bool does table exist               */
42
    protected bool $bTableExist = false;
43
    /** @var LoggerInterface $logger     */
44
    protected LoggerInterface $logger;
45
46
    /**
47
     * @param string $strDBHost     DB Host
48
     * @param string $strDBUser     DB User
49
     * @param string $strDBPwd      DB Password
50
     * @param string $strDBName     DB Name
51
     * @param string $strTableName  tablename for the subscriptions - if null, self::TABLE_NAME is used and created if not exist
52
     * @param LoggerInterface $logger
53
     */
54
    public function __construct(string $strDBHost, string $strDBUser, string $strDBPwd, string $strDBName, ?string $strTableName = null, ?LoggerInterface $logger = null)
55
    {
56
        $this->logger = isset($logger) ? $logger : new NullLogger();
57
        $this->strDBHost = $strDBHost;
58
        $this->strDBUser = $strDBUser;
59
        $this->strDBPwd = $strDBPwd;
60
        $this->strDBName = $strDBName;
61
        $this->strTableName = isset($strTableName) ? $strTableName : self::TABLE_NAME;
62
63
        $this->db = @mysqli_connect($strDBHost, $strDBUser, $strDBPwd, $strDBName);
0 ignored issues
show
Bug introduced by
The call to mysqli_connect() has too few arguments starting with port. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

63
        $this->db = @/** @scrutinizer ignore-call */ mysqli_connect($strDBHost, $strDBUser, $strDBPwd, $strDBName);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
64
        if ($this->db !== false) {
65
            if (!$this->tableExist()) {
66
                $this->createTable();
67
            }
68
        } else {
69
            $this->strLastError = 'MySQL: Connect Error ' . mysqli_connect_errno();
70
            $this->logger->error(__CLASS__ . ': ' . $this->strLastError);
71
        }
72
    }
73
74
    /**
75
     * {@inheritDoc}
76
     * @see PNDataProvider::isConnected()
77
     */
78
    public function isConnected() : bool
79
    {
80
        if (!$this->db) {
81
            $this->logger->error(__CLASS__ . ': ' . $this->strLastError);
82
        } else if (!$this->tableExist()) {
83
            // Condition cannot be forced to test
84
            // - can only occur during development using invalid SQL-statement for creation!
85
            // @codeCoverageIgnoreStart
86
            if (strlen($this->strLastError) == 0) {
87
                $this->strLastError = 'database table ' . $this->strTableName . ' not exist!';
88
            }
89
            $this->logger->error(__CLASS__ . ': ' . $this->strLastError);
90
            // @codeCoverageIgnoreEnd
91
        }
92
        return ($this->db && $this->bTableExist);
93
    }
94
95
    /**
96
     * {@inheritDoc}
97
     * @see PNDataProvider::saveSubscription()
98
     */
99
    public function saveSubscription(string $strJSON) : bool
100
    {
101
        $bSucceeded = false;
102
        if ($this->db) {
103
            $oSubscription = json_decode($strJSON, true);
104
            if ($oSubscription) {
105
                $iExpires = isset($oSubscription['expirationTime']) ? intval(bcdiv($oSubscription['expirationTime'], '1000')) : 0;
106
                $tsExpires = $iExpires > 0 ? date("'Y-m-d H:i:s'", $iExpires) : 'NULL';
107
                $strUserAgent = isset($oSubscription['userAgent']) ? $oSubscription['userAgent'] : 'unknown UserAgent';
108
109
                $strSQL  = "INSERT INTO " . $this->strTableName . " (";
110
                $strSQL .= self::COL_ENDPOINT;
111
                $strSQL .= "," . self::COL_EXPIRES;
112
                $strSQL .= "," . self::COL_SUBSCRIPTION;
113
                $strSQL .= "," . self::COL_USERAGENT;
114
                $strSQL .= ") VALUES(";
115
                $strSQL .= "'" . $oSubscription['endpoint'] . "'";
116
                $strSQL .= "," . $tsExpires;
117
                $strSQL .= ",'" . $strJSON . "'";
118
                $strSQL .= ",'" . $strUserAgent . "'";
119
                $strSQL .= ") ";
120
                $strSQL .= "ON DUPLICATE KEY UPDATE "; // in case of UPDATE UA couldn't have been changed and endpoint is the UNIQUE key!
121
                $strSQL .= " expires = " . $tsExpires;
122
                $strSQL .= ",subscription = '" . $strJSON . "'";
123
                $strSQL .= ";";
124
125
                $bSucceeded = $this->db->query($strSQL) !== false;
126
                $this->strLastError = $this->db->error;
127
                $this->logger->info(__CLASS__ . ': ' . 'Subscription saved', strlen($this->strLastError) > 0 ? ['error' => $this->strLastError] : []);
128
            } else {
129
                $this->strLastError = 'Error json_decode: ' . json_last_error_msg();
130
                $this->logger->error(__CLASS__ . ': ' . $this->strLastError);
131
            }
132
        }
133
        return $bSucceeded;
134
    }
135
136
    /**
137
     * {@inheritDoc}
138
     * @see PNDataProvider::removeSubscription()
139
     */
140
    public function removeSubscription(string $strEndpoint) : bool
141
    {
142
        $bSucceeded = false;
143
        if ($this->db) {
144
            $strSQL  = "DELETE FROM " . $this->strTableName . " WHERE endpoint LIKE ";
145
            $strSQL .= "'" . $strEndpoint . "'";
146
147
            $bSucceeded = $this->db->query($strSQL) !== false;
148
            $this->strLastError = $this->db->error;
149
            $this->logger->info(__CLASS__ . ': ' . 'Subscription removed', strlen($this->strLastError) > 0 ? ['error' => $this->strLastError] : []);
150
        }
151
        return $bSucceeded;
152
    }
153
154
    /**
155
     * Select all subscriptions not expired so far.
156
     * columns expired and lastupdated are timestamp for better handling and visualization
157
     * e.g. in phpMyAdmin. For compatibility reasons with other dataproviders the query
158
     * selects the unis_timestamp values
159
     *
160
     * {@inheritDoc}
161
     * @see PNDataProvider::init()
162
     */
163
    public function init(bool $bAutoRemove = true) : bool
164
    {
165
        $bSucceeded = false;
166
        $this->dbres = false;
0 ignored issues
show
Documentation Bug introduced by
It seems like false of type false is incompatible with the declared type mysqli_result of property $dbres.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
167
        $this->row = null;
168
        if ($this->db) {
169
            $strWhere = '';
170
            if ($bAutoRemove) {
171
                // remove expired subscriptions from DB
172
                $strSQL = "DELETE FROM " . $this->strTableName . " WHERE ";
173
                $strSQL .= self::COL_EXPIRES . " IS NOT NULL AND ";
174
                $strSQL .= self::COL_EXPIRES . " < NOW()";
175
176
                $bSucceeded = $this->db->query($strSQL) !== false;
177
                if (!$bSucceeded) {
178
                    $this->strLastError = 'MySQL: ' . $this->db->error;
179
                    $this->logger->error(__CLASS__ . ': ' . $this->strLastError);
180
                }
181
            } else {
182
                // or just exclude them from query
183
                $strWhere  = " WHERE ";
184
                $strWhere .= self::COL_EXPIRES . " IS NULL OR ";
185
                $strWhere .= self::COL_EXPIRES . " >= NOW()";
186
                $bSucceeded = true;
187
            }
188
            if ($bSucceeded) {
189
                $strSQL  = "SELECT ";
190
                $strSQL .= self::COL_ID;
191
                $strSQL .= "," . self::COL_ENDPOINT;
192
                $strSQL .= ",UNIX_TIMESTAMP(" . self::COL_EXPIRES . ") AS " . self::COL_EXPIRES;
193
                $strSQL .= "," . self::COL_SUBSCRIPTION;
194
                $strSQL .= "," . self::COL_USERAGENT;
195
                $strSQL .= ",UNIX_TIMESTAMP(" . self::COL_LASTUPDATED . ") AS " . self::COL_LASTUPDATED;
196
                $strSQL .= " FROM " . $this->strTableName . $strWhere;
197
198
                $dbres = $this->db->query($strSQL);
199
                if ($dbres === false) {
200
                    // @codeCoverageIgnoreStart
201
                    // can only occur during development!
202
                    $this->strLastError = 'MySQL: ' . $this->db->error;
203
                    $this->logger->error(__CLASS__ . ': ' . $this->strLastError);
204
                    $bSucceeded = false;
205
                    // @codeCoverageIgnoreEnd
206
                } elseif (is_object($dbres)) {
207
                    $this->dbres = $dbres;
208
                }
209
            }
210
        }
211
        return $bSucceeded;
212
    }
213
214
    /**
215
     * {@inheritDoc}
216
     * @see PNDataProvider::count()
217
     */
218
    public function count() : int
219
    {
220
        $iCount = 0;
221
        if ($this->db) {
222
            $dbres = $this->db->query("SELECT count(*) AS iCount FROM " . $this->strTableName);
223
            if (is_object($dbres)) {
224
                $row = $dbres->fetch_array(MYSQLI_ASSOC);
225
                if ($row !== null) {
226
                    $iCount = intval($row['iCount']);
227
                }
228
            }
229
        }
230
        return $iCount;
231
    }
232
233
    /**
234
     * {@inheritDoc}
235
     * @see PNDataProvider::fetch()
236
     */
237
    public function fetch()
238
    {
239
        $strSubJSON = false;
240
        if ($this->dbres !== false) {
0 ignored issues
show
introduced by
The condition $this->dbres !== false is always true.
Loading history...
241
            $this->row = $this->dbres->fetch_array(MYSQLI_ASSOC);
242
            if ($this->row) {
243
                $strSubJSON = $this->row[self::COL_SUBSCRIPTION];
244
            }
245
        }
246
        return $strSubJSON;
247
    }
248
249
    /**
250
     * {@inheritDoc}
251
     * @see PNDataProvider::truncate()
252
     */
253
    public function truncate() : bool
254
    {
255
        $bSucceeded = false;
256
        if ($this->isConnected()) {
257
            $bSucceeded = $this->db->query("TRUNCATE TABLE " . $this->strTableName); // @phpstan-ignore-line
258
            $this->logger->info(__CLASS__ . ': ' . 'Subscription table truncated');
259
        }
260
        return $bSucceeded !== false;
261
    }
262
263
    /**
264
     * {@inheritDoc}
265
     * @see PNDataProvider::getColumn()
266
     */
267
    public function getColumn(string $strName) : ?string
268
    {
269
        $value = null;
270
        if ($this->row !== false && isset($this->row[$strName])) {
271
            $value = $this->row[$strName];
272
            if ($strName == self::COL_EXPIRES || $strName == self::COL_LASTUPDATED) {
273
274
            }
275
        }
276
        return $value;
277
    }
278
279
    /**
280
     * get last error
281
     * @return string
282
     */
283
    public function getError() : string
284
    {
285
        return $this->strLastError;
286
    }
287
288
    /**
289
     * check, if table exist
290
     * @return bool
291
     */
292
    private function tableExist() : bool
293
    {
294
        if (!$this->bTableExist) {
295
            if ($this->db) {
296
                $dbres = $this->db->query("SHOW TABLES LIKE '" . $this->strTableName . "'");
297
                if (is_object($dbres)) {
298
                    $this->bTableExist = $dbres->num_rows > 0;
299
                }
300
            }
301
        }
302
        return $this->bTableExist;
303
    }
304
305
    /**
306
     * create table if not exist
307
     */
308
    private function createTable() : bool
309
    {
310
        $bSucceeded = false;
311
        if (is_object($this->db)) {
312
            $strSQL  = "CREATE TABLE IF NOT EXISTS " . $this->strTableName . " (";
313
            $strSQL .= " " . self::COL_ID . " int NOT NULL AUTO_INCREMENT";
314
            $strSQL .= "," . self::COL_ENDPOINT . " text NOT NULL";
315
            $strSQL .= "," . self::COL_EXPIRES . " timestamp NULL DEFAULT NULL";
316
            $strSQL .= "," . self::COL_SUBSCRIPTION . " text NOT NULL";
317
            $strSQL .= "," . self::COL_USERAGENT . " varchar(255) NOT NULL";
318
            $strSQL .= "," . self::COL_LASTUPDATED . " timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP";
319
            $strSQL .= ",PRIMARY KEY (id)";
320
            $strSQL .= ",UNIQUE (endpoint(500))";
321
            $strSQL .= ") ENGINE=InnoDB;";
322
323
            $bSucceeded = $this->db->query($strSQL) !== false;
324
            $this->strLastError = $this->db->error;
325
            $this->logger->info(__CLASS__ . ': ' . 'Subscription table created', strlen($this->strLastError) > 0 ? ['error' => $this->strLastError] : []);
326
        }
327
        $this->bTableExist = $bSucceeded;
328
        return $bSucceeded;
329
    }
330
331
    /**
332
     * @param LoggerInterface $logger
333
     */
334
    public function setLogger(LoggerInterface $logger) : void
335
    {
336
        $this->logger = $logger;
337
    }
338
}
339