Passed
Push — master ( 6143b8...b69905 )
by Stefan
03:43
created

PNDataProviderMySQL::setLogger()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
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
 * #### History
18
 * - *2020-04-02*   initial version
19
 * - *2020-08-03*   PHP 7.4 type hint
20
 * 
21
 * @package SKien/PNServer
22
 * @version 1.1.0
23
 * @author Stefanius <[email protected]>
24
 * @copyright MIT License - see the LICENSE file for details
25
*/
26
class PNDataProviderMySQL implements PNDataProvider 
27
{
28
    /** @var string tablename            */
29
    protected string $strTableName = '';
30
    /** @var string DB host  */
31
    protected string $strDBHost = '';
32
    /** @var string DB user  */
33
    protected string $strDBUser = '';
34
    /** @var string Password for DB  */
35
    protected string $strDBPwd = '';
36
    /** @var string DB name  */
37
    protected string $strDBName = '';
38
    /** @var \mysqli|bool internal MySQL DB  (No type hint, as \mysqli or bool is possible type)  */
39
    protected $db = false;
40
    /** @var \mysqli_result|bool result of DB queries  (No type hint, as \mysqli_result or bool is possible type) */
41
    protected $dbres = false;
42
    /** @var array last fetched row or null      */
43
    protected ?array $row = null;
44
    /** @var string last error                   */
45
    protected string $strLastError = '';
46
    /** @var bool does table exist               */
47
    protected bool $bTableExist = false;
48
    /** @var LoggerInterface $logger     */
49
    protected LoggerInterface $logger;
50
    
51
    /**
52
     * @param string $strDBHost     DB Host
53
     * @param string $strDBUser     DB User
54
     * @param string $strDBPwd      DB Password
55
     * @param string $strDBName     DB Name
56
     * @param string $strTableName  tablename for the subscriptions - if null, self::TABLE_NAME is used and created if not exist
57
     * @param LoggerInterface $logger
58
     */
59
    public function __construct(string $strDBHost, string $strDBUser, string $strDBPwd, string $strDBName, ?string $strTableName = null, ?LoggerInterface $logger = null)
60
    {
61
        $this->logger = isset($logger) ? $logger : new NullLogger();
62
        $this->strDBHost = $strDBHost; 
63
        $this->strDBUser = $strDBUser; 
64
        $this->strDBPwd = $strDBPwd; 
65
        $this->strDBName = $strDBName; 
66
        $this->strTableName = isset($strTableName) ? $strTableName : self::TABLE_NAME;
67
        
68
        $this->db = @mysqli_connect($strDBHost, $strDBUser, $strDBPwd, $strDBName);
69
        if ($this->db !== false) {
70
            if (!$this->tableExist()) {
71
                $this->createTable();
72
            }
73
        } else {
74
            $this->strLastError = 'MySQL: Connect Error ' . mysqli_connect_errno();
75
            $this->logger->error(__CLASS__ . ': ' . $this->strLastError);
76
        }
77
    }
78
79
    /**
80
     * {@inheritDoc}
81
     * @see PNDataProvider::isConnected()
82
     */
83
    public function isConnected() : bool 
84
    {
85
        if (!$this->db) {
86
            if (strlen($this->strLastError) == 0) {
87
                $this->strLastError = 'no database connected!';
88
            }
89
            $this->logger->error(__CLASS__ . ': ' . $this->strLastError);
90
        } else if (!$this->tableExist()) {
91
            // Condition cannot be forced to test
92
            // - can only occur during development using invalid SQL-statement for creation!
93
            // @codeCoverageIgnoreStart
94
            if (strlen($this->strLastError) == 0) {
95
                $this->strLastError = 'database table ' . $this->strTableName . ' not exist!';
96
            }
97
            $this->logger->error(__CLASS__ . ': ' . $this->strLastError);
98
            // @codeCoverageIgnoreEnd
99
        }
100
        return ($this->db && $this->bTableExist);
101
    }
102
    
103
    /**
104
     * {@inheritDoc}
105
     * @see PNDataProvider::saveSubscription()
106
     */
107
    public function saveSubscription(string $strJSON) : bool 
108
    {
109
        $bSucceeded = false;
110
        if ($this->db) {
111
            $oSubscription = json_decode($strJSON, true);
112
            if ($oSubscription) {
113
                $iExpires = isset($oSubscription['expirationTime']) ? intval(bcdiv($oSubscription['expirationTime'], '1000')) : 0;
114
                $tsExpires = $iExpires > 0 ? date("'Y-m-d H:i:s'", $iExpires) : 'NULL';
115
                $strUserAgent = isset($oSubscription['userAgent']) ? $oSubscription['userAgent'] : 'unknown UserAgent';
116
                                
117
                $strSQL  = "INSERT INTO " . $this->strTableName . " (";
118
                $strSQL .= self::COL_ENDPOINT;
119
                $strSQL .= "," . self::COL_EXPIRES;
120
                $strSQL .= "," . self::COL_SUBSCRIPTION;
121
                $strSQL .= "," . self::COL_USERAGENT;
122
                $strSQL .= ") VALUES(";
123
                $strSQL .= "'" . $oSubscription['endpoint'] . "'";
124
                $strSQL .= "," . $tsExpires;
125
                $strSQL .= ",'" . $strJSON . "'";
126
                $strSQL .= ",'" . $strUserAgent . "'";
127
                $strSQL .= ") ";
128
                $strSQL .= "ON DUPLICATE KEY UPDATE "; // in case of UPDATE UA couldn't have been changed and endpoint is the UNIQUE key!
129
                $strSQL .= " expires = " . $tsExpires;
130
                $strSQL .= ",subscription = '" . $strJSON . "'";
131
                $strSQL .= ";";
132
                
133
                $bSucceeded = $this->db->query($strSQL) !== false;
134
                $this->strLastError = $this->db->error;
135
                $this->logger->info(__CLASS__ . ': ' . 'Subscription saved', strlen($this->strLastError) > 0 ? ['error' => $this->strLastError] : []);
136
            } else {
137
                $this->strLastError = 'Error json_decode: ' . json_last_error_msg();
138
                $this->logger->error(__CLASS__ . ': ' . $this->strLastError);
139
            }
140
        }
141
        return $bSucceeded;
142
    }
143
    
144
    /**
145
     * {@inheritDoc}
146
     * @see PNDataProvider::removeSubscription()
147
     */
148
    public function removeSubscription(string $strEndpoint) : bool 
149
    {
150
        $bSucceeded = false;
151
        if ($this->db) {
152
            $strSQL  = "DELETE FROM " . $this->strTableName . " WHERE endpoint LIKE ";
153
            $strSQL .= "'" . $strEndpoint . "'";
154
        
155
            $bSucceeded = $this->db->query($strSQL) !== false;
156
            $this->strLastError = $this->db->error;
157
            $this->logger->info(__CLASS__ . ': ' . 'Subscription removed', strlen($this->strLastError) > 0 ? ['error' => $this->strLastError] : []);
158
        }
159
        return $bSucceeded;
160
    }
161
    
162
    /**
163
     * Select all subscriptions not expired so far.
164
     * columns expired and lastupdated are timestamp for better handling and visualization 
165
     * e.g. in phpMyAdmin. For compatibility reasons with other dataproviders the query 
166
     * selects the unis_timestamp values  
167
     * 
168
     * {@inheritDoc}
169
     * @see PNDataProvider::init()
170
     */
171
    public function init(bool $bAutoRemove = true) : bool 
172
    {
173
        $bSucceeded = false;
174
        $this->dbres = false;
175
        $this->row = null;
176
        if ($this->db) {
177
            $strWhere = '';
178
            if ($bAutoRemove) {
179
                // remove expired subscriptions from DB
180
                $strSQL = "DELETE FROM " . $this->strTableName . " WHERE ";
181
                $strSQL .= self::COL_EXPIRES . " IS NOT NULL AND ";
182
                $strSQL .= self::COL_EXPIRES . " < NOW()";
183
            
184
                $bSucceeded = $this->db->query($strSQL) !== false;
185
                if (!$bSucceeded) {
186
                    $this->strLastError = 'MySQL: ' . $this->db->error;
187
                    $this->logger->error(__CLASS__ . ': ' . $this->strLastError);
188
                }
189
            } else {
190
                // or just exclude them from query
191
                $strWhere  = " WHERE ";
192
                $strWhere .= self::COL_EXPIRES . " IS NULL OR ";
193
                $strWhere .= self::COL_EXPIRES . " >= NOW()";
194
                $bSucceeded = true;
195
            }
196
            if ($bSucceeded) {
197
                $strSQL  = "SELECT ";
198
                $strSQL .= self::COL_ID;
199
                $strSQL .= "," . self::COL_ENDPOINT;
200
                $strSQL .= ",UNIX_TIMESTAMP(" . self::COL_EXPIRES . ") AS " . self::COL_EXPIRES;
201
                $strSQL .= "," . self::COL_SUBSCRIPTION;
202
                $strSQL .= "," . self::COL_USERAGENT;
203
                $strSQL .= ",UNIX_TIMESTAMP(" . self::COL_LASTUPDATED . ") AS " . self::COL_LASTUPDATED;
204
                $strSQL .= " FROM " . $this->strTableName . $strWhere;
205
    
206
                $this->dbres = $this->db->query($strSQL);
207
                if ($this->dbres === false) {
208
                    // @codeCoverageIgnoreStart
209
                    // can only occur during development!
210
                    $this->strLastError = 'MySQL: ' . $this->db->error;
211
                    $this->logger->error(__CLASS__ . ': ' . $this->strLastError);
212
                    $bSucceeded = false;
213
                    // @codeCoverageIgnoreEnd
214
                }
215
            }
216
        }
217
        return $bSucceeded;
218
    }
219
220
    /**
221
     * {@inheritDoc}
222
     * @see PNDataProvider::count()
223
     */
224
    public function count() : int 
225
    {
226
        $iCount = 0;
227
        if ($this->db) {
228
            $dbres = $this->db->query("SELECT count(*) AS iCount FROM " . $this->strTableName);
229
            $row = $dbres->fetch_array(MYSQLI_ASSOC);
230
            $iCount = intval($row['iCount']);
231
        }
232
        return $iCount;
233
    }
234
    
235
    /**
236
     * {@inheritDoc}
237
     * @see PNDataProvider::fetch()
238
     */
239
    public function fetch() 
240
    {
241
        $strSubJSON = false;
242
        if ($this->dbres !== false) {
243
            $this->row = $this->dbres->fetch_array(MYSQLI_ASSOC);
244
            if ($this->row) {
245
                $strSubJSON = $this->row[self::COL_SUBSCRIPTION];
246
            }
247
        }
248
        return $strSubJSON;
249
    }
250
    
251
    /**
252
     * {@inheritDoc}
253
     * @see PNDataProvider::truncate()
254
     */
255
    public function truncate() : bool
256
    {
257
        $bSucceeded = false;
258
        if ($this->isConnected()) {
259
            $bSucceeded = $this->db->query("TRUNCATE TABLE " . $this->strTableName);
260
            $this->logger->info(__CLASS__ . ': ' . 'Subscription table truncated');
261
        }
262
        return $bSucceeded;
263
    }
264
    
265
    /**
266
     * {@inheritDoc}
267
     * @see PNDataProvider::getColumn()
268
     */
269
    public function getColumn(string $strName) : ?string 
270
    {
271
        $value = null;
272
        if ($this->row !== false && isset($this->row[$strName])) {
273
            $value = $this->row[$strName];
274
            if ($strName == self::COL_EXPIRES || $strName == self::COL_LASTUPDATED) {
275
                
276
            }
277
        }
278
        return $value;          
279
    }
280
281
    /**
282
     * get last error
283
     * @return string
284
     */
285
    public function getError() : string
286
    {
287
        return $this->strLastError;
288
    }
289
    
290
    /**
291
     * check, if table exist
292
     * @return bool
293
     */
294
    private function tableExist() : bool 
295
    {
296
        if (!$this->bTableExist) {
297
            if ($this->db) {
298
                $dbres = $this->db->query("SHOW TABLES LIKE '" . $this->strTableName . "'");
299
                $this->bTableExist = $dbres->num_rows > 0;
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 ($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