Total Complexity | 52 |
Total Lines | 509 |
Duplicated Lines | 0 % |
Changes | 2 | ||
Bugs | 0 | Features | 0 |
Complex classes like DB 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 DB, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
24 | class DB extends PDO implements ArrayAccess { |
||
25 | |||
26 | /** |
||
27 | * @var string |
||
28 | */ |
||
29 | private $driver; |
||
30 | |||
31 | /** |
||
32 | * @var Junction[] |
||
33 | */ |
||
34 | protected $junctions = []; |
||
35 | |||
36 | /** |
||
37 | * Notified whenever a query is executed or a statement is prepared. |
||
38 | * This is a stub closure by default. |
||
39 | * |
||
40 | * `fn($sql):void` |
||
41 | * |
||
42 | * @var Closure |
||
43 | */ |
||
44 | protected $logger; |
||
45 | |||
46 | /** |
||
47 | * @var Record[] |
||
48 | */ |
||
49 | protected $records = []; |
||
50 | |||
51 | /** |
||
52 | * @var Table[] |
||
53 | */ |
||
54 | protected $tables = []; |
||
55 | |||
56 | /** |
||
57 | * The count of open transactions/savepoints. |
||
58 | * |
||
59 | * @var int |
||
60 | */ |
||
61 | protected $transactions = 0; |
||
62 | |||
63 | /** |
||
64 | * Sets various attributes to streamline operations. |
||
65 | * |
||
66 | * Registers missing SQLite functions. |
||
67 | * |
||
68 | * @param string $dsn |
||
69 | * @param string $username |
||
70 | * @param string $password |
||
71 | * @param array $options |
||
72 | */ |
||
73 | public function __construct ($dsn, $username = null, $password = null, array $options = []) { |
||
74 | $options += [ |
||
75 | self::ATTR_STATEMENT_CLASS => [Statement::class, [$this]] |
||
76 | ]; |
||
77 | parent::__construct($dsn, $username, $password, $options); |
||
78 | $this->setAttribute(self::ATTR_DEFAULT_FETCH_MODE, self::FETCH_ASSOC); |
||
79 | $this->setAttribute(self::ATTR_EMULATE_PREPARES, false); |
||
80 | $this->setAttribute(self::ATTR_ERRMODE, self::ERRMODE_EXCEPTION); |
||
81 | $this->setAttribute(self::ATTR_STRINGIFY_FETCHES, false); |
||
82 | $this->logger ??= fn() => null; |
||
83 | $this->driver = $this->getAttribute(self::ATTR_DRIVER_NAME); |
||
84 | |||
85 | if ($this->isSQLite()) { |
||
86 | // polyfill sqlite functions |
||
87 | $this->sqliteCreateFunctions([ // deterministic functions |
||
88 | // https://www.sqlite.org/lang_mathfunc.html |
||
89 | 'ACOS' => 'acos', |
||
90 | 'ASIN' => 'asin', |
||
91 | 'ATAN' => 'atan', |
||
92 | 'CEIL' => 'ceil', |
||
93 | 'COS' => 'cos', |
||
94 | 'DEGREES' => 'rad2deg', |
||
95 | 'EXP' => 'exp', |
||
96 | 'FLOOR' => 'floor', |
||
97 | 'LN' => 'log', |
||
98 | 'LOG' => fn($b, $x) => log($x, $b), |
||
99 | 'LOG10' => 'log10', |
||
100 | 'LOG2' => fn($x) => log($x, 2), |
||
101 | 'PI' => 'pi', |
||
102 | 'POW' => 'pow', |
||
103 | 'RADIANS' => 'deg2rad', |
||
104 | 'SIN' => 'sin', |
||
105 | 'SQRT' => 'sqrt', |
||
106 | 'TAN' => 'tan', |
||
107 | |||
108 | // these are not in sqlite at all but are in other dbms |
||
109 | 'CONV' => 'base_convert', |
||
110 | 'SIGN' => fn($x) => ($x > 0) - ($x < 0), |
||
111 | ]); |
||
112 | |||
113 | $this->sqliteCreateFunctions([ // non-deterministic |
||
114 | 'RAND' => fn() => mt_rand(0, 1), |
||
115 | ], false); |
||
116 | } |
||
117 | } |
||
118 | |||
119 | /** |
||
120 | * Returns the driver. |
||
121 | * |
||
122 | * @return string |
||
123 | */ |
||
124 | final public function __toString () { |
||
125 | return $this->driver; |
||
126 | } |
||
127 | |||
128 | /** |
||
129 | * Allows nested transactions by using `SAVEPOINT` |
||
130 | * |
||
131 | * Use {@link DB::newTransaction()} to work with {@link Transaction} instead. |
||
132 | * |
||
133 | * @return true |
||
134 | */ |
||
135 | public function beginTransaction () { |
||
136 | assert($this->transactions >= 0); |
||
137 | if ($this->transactions === 0) { |
||
138 | $this->logger->__invoke("BEGIN TRANSACTION"); |
||
139 | parent::beginTransaction(); |
||
140 | } |
||
141 | else { |
||
142 | $this->exec("SAVEPOINT SAVEPOINT_{$this->transactions}"); |
||
143 | } |
||
144 | $this->transactions++; |
||
145 | return true; |
||
146 | } |
||
147 | |||
148 | /** |
||
149 | * Allows nested transactions by using `RELEASE SAVEPOINT` |
||
150 | * |
||
151 | * Use {@link DB::newTransaction()} to work with {@link Transaction} instead. |
||
152 | * |
||
153 | * @return true |
||
154 | */ |
||
155 | public function commit () { |
||
156 | assert($this->transactions > 0); |
||
157 | if ($this->transactions === 1) { |
||
158 | $this->logger->__invoke("COMMIT TRANSACTION"); |
||
159 | parent::commit(); |
||
160 | } |
||
161 | else { |
||
162 | $savepoint = $this->transactions - 1; |
||
163 | $this->exec("RELEASE SAVEPOINT SAVEPOINT_{$savepoint}"); |
||
164 | } |
||
165 | $this->transactions--; |
||
166 | return true; |
||
167 | } |
||
168 | |||
169 | /** |
||
170 | * Notifies the logger. |
||
171 | * |
||
172 | * @param string $sql |
||
173 | * @return int |
||
174 | */ |
||
175 | public function exec ($sql): int { |
||
176 | $this->logger->__invoke($sql); |
||
177 | return parent::exec($sql); |
||
178 | } |
||
179 | |||
180 | /** |
||
181 | * Central point of object creation. |
||
182 | * |
||
183 | * Override this to override classes. |
||
184 | * |
||
185 | * The only thing that calls this should be {@link \Helix\DB\FactoryTrait} |
||
186 | * |
||
187 | * @param string $class |
||
188 | * @param mixed ...$args |
||
189 | * @return mixed |
||
190 | */ |
||
191 | public function factory (string $class, ...$args) { |
||
192 | return new $class($this, ...$args); |
||
193 | } |
||
194 | |||
195 | /** |
||
196 | * @return string |
||
197 | */ |
||
198 | final public function getDriver (): string { |
||
199 | return $this->driver; |
||
200 | } |
||
201 | |||
202 | /** |
||
203 | * Returns a {@link Junction} access object based on an annotated interface. |
||
204 | * |
||
205 | * @param string $interface |
||
206 | * @return Junction |
||
207 | */ |
||
208 | public function getJunction ($interface) { |
||
209 | return $this->junctions[$interface] ??= Junction::fromInterface($this, $interface); |
||
210 | } |
||
211 | |||
212 | /** |
||
213 | * @return Closure |
||
214 | */ |
||
215 | public function getLogger () { |
||
216 | return $this->logger; |
||
217 | } |
||
218 | |||
219 | /** |
||
220 | * Returns a {@link Record} access object based on an annotated class. |
||
221 | * |
||
222 | * @param string|EntityInterface $class |
||
223 | * @return Record |
||
224 | */ |
||
225 | public function getRecord ($class) { |
||
226 | if (is_object($class)) { |
||
227 | $class = get_class($class); |
||
228 | } |
||
229 | return $this->records[$class] ??= Record::fromClass($this, $class); |
||
230 | } |
||
231 | |||
232 | /** |
||
233 | * @param string $name |
||
234 | * @return null|Table |
||
235 | */ |
||
236 | public function getTable (string $name) { |
||
237 | if (!isset($this->tables[$name])) { |
||
238 | if ($this->isSQLite()) { |
||
239 | $info = $this->query("PRAGMA table_info({$this->quote($name)})")->fetchAll(); |
||
240 | $cols = array_column($info, 'name'); |
||
241 | } |
||
242 | else { |
||
243 | $cols = $this->query( |
||
244 | "SELECT column_name FROM information_schema.tables WHERE table_name = {$this->quote($name)}" |
||
245 | )->fetchAll(self::FETCH_COLUMN); |
||
246 | } |
||
247 | if (!$cols) { |
||
|
|||
248 | return null; |
||
249 | } |
||
250 | $this->tables[$name] = Table::factory($this, $name, $cols); |
||
251 | } |
||
252 | return $this->tables[$name]; |
||
253 | } |
||
254 | |||
255 | /** |
||
256 | * @return bool |
||
257 | */ |
||
258 | final public function isMySQL (): bool { |
||
259 | return $this->driver === 'mysql'; |
||
260 | } |
||
261 | |||
262 | /** |
||
263 | * @return bool |
||
264 | */ |
||
265 | final public function isPostgreSQL (): bool { |
||
266 | return $this->driver === 'pgsql'; |
||
267 | } |
||
268 | |||
269 | /** |
||
270 | * @return bool |
||
271 | */ |
||
272 | final public function isSQLite (): bool { |
||
273 | return $this->driver === 'sqlite'; |
||
274 | } |
||
275 | |||
276 | /** |
||
277 | * Generates an equality {@link Predicate} from mixed arguments. |
||
278 | * |
||
279 | * If `$b` is a closure, returns from `$b($a, DB $this)` |
||
280 | * |
||
281 | * If `$a` is an integer (enumerated item), returns `$b` as a {@link Predicate} |
||
282 | * |
||
283 | * If `$b` is an array, returns `$a IN (...quoted $b)` |
||
284 | * |
||
285 | * If `$b` is a {@link Select}, returns `$a IN ($b->toSql())` |
||
286 | * |
||
287 | * Otherwise predicates `$a = quoted $b` |
||
288 | * |
||
289 | * @param mixed $a |
||
290 | * @param mixed $b |
||
291 | * @return Predicate |
||
292 | */ |
||
293 | public function match ($a, $b) { |
||
294 | if ($b instanceof Closure) { |
||
295 | return $b->__invoke($a, $this); |
||
296 | } |
||
297 | if (is_int($a)) { |
||
298 | return Predicate::factory($this, $b); |
||
299 | } |
||
300 | if (is_array($b)) { |
||
301 | return Predicate::factory($this, "{$a} IN ({$this->quoteList($b)})"); |
||
302 | } |
||
303 | if ($b instanceof Select) { |
||
304 | return Predicate::factory($this, "{$a} IN ({$b->toSql()})"); |
||
305 | } |
||
306 | return Predicate::factory($this, "{$a} = {$this->quote($b)}"); |
||
307 | } |
||
308 | |||
309 | /** |
||
310 | * Returns a scoped transaction. |
||
311 | * |
||
312 | * @return Transaction |
||
313 | */ |
||
314 | public function newTransaction () { |
||
315 | return Transaction::factory($this); |
||
316 | } |
||
317 | |||
318 | /** |
||
319 | * Whether a table exists. |
||
320 | * |
||
321 | * @param string $table |
||
322 | * @return bool |
||
323 | */ |
||
324 | final public function offsetExists ($table): bool { |
||
325 | return (bool)$this->getTable($table); |
||
326 | } |
||
327 | |||
328 | /** |
||
329 | * Returns a table by name. |
||
330 | * |
||
331 | * @param string $table |
||
332 | * @return null|Table |
||
333 | */ |
||
334 | final public function offsetGet ($table) { |
||
335 | return $this->getTable($table); |
||
336 | } |
||
337 | |||
338 | /** |
||
339 | * @param $offset |
||
340 | * @param $value |
||
341 | * @throws LogicException |
||
342 | */ |
||
343 | final public function offsetSet ($offset, $value) { |
||
344 | throw new LogicException('Raw table access is immutable.'); |
||
345 | } |
||
346 | |||
347 | /** |
||
348 | * @param $offset |
||
349 | * @throws LogicException |
||
350 | */ |
||
351 | final public function offsetUnset ($offset) { |
||
352 | throw new LogicException('Raw table access is immutable.'); |
||
353 | } |
||
354 | |||
355 | /** |
||
356 | * `PI()` |
||
357 | * |
||
358 | * @return Num |
||
359 | */ |
||
360 | public function pi () { |
||
361 | return Num::factory($this, "PI()"); |
||
362 | } |
||
363 | |||
364 | /** |
||
365 | * Notifies the logger. |
||
366 | * |
||
367 | * @param string $sql |
||
368 | * @param array $options |
||
369 | * @return Statement |
||
370 | */ |
||
371 | public function prepare ($sql, $options = []) { |
||
372 | $this->logger->__invoke($sql); |
||
373 | /** @var Statement $statement */ |
||
374 | $statement = parent::prepare($sql, $options); |
||
375 | return $statement; |
||
376 | } |
||
377 | |||
378 | /** |
||
379 | * Notifies the logger and executes. |
||
380 | * |
||
381 | * @param string $sql |
||
382 | * @param int $mode |
||
383 | * @param mixed $arg3 Optional. |
||
384 | * @param array $ctorargs Optional. |
||
385 | * @return Statement |
||
386 | */ |
||
387 | public function query ($sql, $mode = PDO::ATTR_DEFAULT_FETCH_MODE, $arg3 = null, array $ctorargs = []) { |
||
388 | $this->logger->__invoke($sql); |
||
389 | /** @var Statement $statement */ |
||
390 | $statement = parent::query(...func_get_args()); |
||
391 | return $statement; |
||
392 | } |
||
393 | |||
394 | /** |
||
395 | * Quotes a value, with special considerations. |
||
396 | * |
||
397 | * - {@link ExpressionInterface} instances are returned as-is. |
||
398 | * - Booleans and integers are returned as unquoted integer-string. |
||
399 | * - Everything else is returned as a quoted string. |
||
400 | * |
||
401 | * @param bool|number|string|object $value |
||
402 | * @param int $type Ignored. |
||
403 | * @return string|ExpressionInterface |
||
404 | */ |
||
405 | public function quote ($value, $type = self::PARAM_STR) { |
||
406 | if ($value instanceof ExpressionInterface) { |
||
407 | return $value; |
||
408 | } |
||
409 | switch (gettype($value)) { |
||
410 | case 'integer' : |
||
411 | case 'boolean' : |
||
412 | case 'resource' : |
||
413 | return (string)(int)$value; |
||
414 | default: |
||
415 | return parent::quote((string)$value); |
||
416 | } |
||
417 | } |
||
418 | |||
419 | /** |
||
420 | * Quotes an array of values. Keys are preserved. |
||
421 | * |
||
422 | * @param array $values |
||
423 | * @return string[] |
||
424 | */ |
||
425 | public function quoteArray (array $values) { |
||
427 | } |
||
428 | |||
429 | /** |
||
430 | * Returns a quoted, comma-separated list. |
||
431 | * |
||
432 | * @param array $values |
||
433 | * @return string |
||
434 | */ |
||
435 | public function quoteList (array $values): string { |
||
436 | return implode(',', $this->quoteArray($values)); |
||
437 | } |
||
438 | |||
439 | /** |
||
440 | * `RAND()` float between `0` and `1` |
||
441 | * |
||
442 | * @return Num |
||
443 | */ |
||
444 | public function rand () { |
||
446 | } |
||
447 | |||
448 | /** |
||
449 | * Allows nested transactions by using `ROLLBACK TO SAVEPOINT` |
||
450 | * |
||
451 | * Use {@link DB::newTransaction()} to work with {@link Transaction} instead. |
||
452 | * |
||
453 | * @return true |
||
454 | */ |
||
455 | public function rollBack () { |
||
456 | assert($this->transactions > 0); |
||
457 | if ($this->transactions === 1) { |
||
458 | $this->logger->__invoke("ROLLBACK TRANSACTION"); |
||
459 | parent::rollBack(); |
||
460 | } |
||
461 | else { |
||
462 | $savepoint = $this->transactions - 1; |
||
463 | $this->exec("ROLLBACK TO SAVEPOINT SAVEPOINT_{$savepoint}"); |
||
464 | } |
||
465 | $this->transactions--; |
||
466 | return true; |
||
467 | } |
||
468 | |||
469 | /** |
||
470 | * Forwards to the entity's {@link Record} |
||
471 | * |
||
472 | * @param EntityInterface $entity |
||
473 | * @return int ID |
||
474 | */ |
||
475 | public function save (EntityInterface $entity): int { |
||
476 | return $this->getRecord($entity)->save($entity); |
||
477 | } |
||
478 | |||
479 | /** |
||
480 | * @param string $interface |
||
481 | * @param Junction $junction |
||
482 | * @return $this |
||
483 | */ |
||
484 | public function setJunction (string $interface, Junction $junction) { |
||
485 | $this->junctions[$interface] = $junction; |
||
486 | return $this; |
||
487 | } |
||
488 | |||
489 | /** |
||
490 | * @param Closure $logger |
||
491 | * @return $this |
||
492 | */ |
||
493 | public function setLogger (Closure $logger) { |
||
494 | $this->logger = $logger; |
||
495 | return $this; |
||
496 | } |
||
497 | |||
498 | /** |
||
499 | * @param string $class |
||
500 | * @param Record $record |
||
501 | * @return $this |
||
502 | */ |
||
503 | public function setRecord (string $class, Record $record) { |
||
506 | } |
||
507 | |||
508 | /** |
||
509 | * @param callable[] $callbacks Keyed by function name. |
||
510 | * @param bool $deterministic Whether the callbacks aren't random / are without side-effects. |
||
511 | */ |
||
512 | public function sqliteCreateFunctions (array $callbacks, bool $deterministic = true): void { |
||
513 | $deterministic = $deterministic ? self::SQLITE_DETERMINISTIC : 0; |
||
514 | foreach ($callbacks as $name => $callback) { |
||
515 | $argc = (new ReflectionFunction($callback))->getNumberOfRequiredParameters(); |
||
516 | $this->sqliteCreateFunction($name, $callback, $argc, $deterministic); |
||
517 | } |
||
518 | } |
||
519 | |||
520 | /** |
||
521 | * Performs work within a scoped transaction. |
||
522 | * |
||
523 | * The work is rolled back if an exception is thrown. |
||
524 | * |
||
525 | * @param callable $work |
||
526 | * @return mixed The return value of `$work` |
||
527 | */ |
||
528 | public function transact (callable $work) { |
||
529 | $transaction = $this->newTransaction(); |
||
533 | } |
||
534 | } |
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.