| Total Complexity | 50 |
| Total Lines | 316 |
| Duplicated Lines | 0 % |
| Changes | 1 | ||
| Bugs | 0 | Features | 0 |
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 |
||
| 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); |
||
|
|
|||
| 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; |
||
| 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) { |
||
| 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 |
||
| 329 | } |
||
| 330 | |||
| 331 | /** |
||
| 332 | * @param LoggerInterface $logger |
||
| 333 | */ |
||
| 334 | public function setLogger(LoggerInterface $logger) : void |
||
| 337 | } |
||
| 338 | } |
||
| 339 |
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.