Total Complexity | 43 |
Total Lines | 367 |
Duplicated Lines | 0 % |
Changes | 7 | ||
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 |
||
21 | class DB extends PDO implements ArrayAccess { |
||
22 | |||
23 | /** |
||
24 | * @var string |
||
25 | */ |
||
26 | private $driver; |
||
27 | |||
28 | /** |
||
29 | * @var Junction[] |
||
30 | */ |
||
31 | protected $junctions = []; |
||
32 | |||
33 | /** |
||
34 | * Notified whenever a query is executed or a statement is prepared. |
||
35 | * Takes only one argument: the SQL being executed or prepared. |
||
36 | * This is a stub closure by default. |
||
37 | * |
||
38 | * @var Closure |
||
39 | */ |
||
40 | protected $logger; |
||
41 | |||
42 | /** |
||
43 | * @var Record[] |
||
44 | */ |
||
45 | protected $records = []; |
||
46 | |||
47 | /** |
||
48 | * @var Table[] |
||
49 | */ |
||
50 | protected $tables = []; |
||
51 | |||
52 | /** |
||
53 | * Sets various attributes to streamline operations. |
||
54 | * |
||
55 | * Registers missing SQLite functions. |
||
56 | * |
||
57 | * @param string $dsn |
||
58 | * @param string $username |
||
59 | * @param string $password |
||
60 | * @param array $options |
||
61 | */ |
||
62 | public function __construct ($dsn, $username = null, $password = null, $options = null) { |
||
63 | parent::__construct($dsn, $username, $password, $options); |
||
64 | $this->driver = $this->getAttribute(self::ATTR_DRIVER_NAME); |
||
65 | $this->setAttribute(self::ATTR_DEFAULT_FETCH_MODE, self::FETCH_ASSOC); |
||
66 | $this->setAttribute(self::ATTR_EMULATE_PREPARES, false); |
||
67 | $this->setAttribute(self::ATTR_ERRMODE, self::ERRMODE_EXCEPTION); |
||
68 | $this->setAttribute(self::ATTR_STATEMENT_CLASS, [Statement::class, [$this]]); |
||
69 | $this->setAttribute(self::ATTR_STRINGIFY_FETCHES, false); |
||
70 | $this->logger = function() { |
||
71 | }; |
||
72 | if ($this->isSQLite()) { |
||
73 | $this->sqliteCreateFunction('CEIL', 'ceil'); |
||
74 | $this->sqliteCreateFunction('FLOOR', 'floor'); |
||
75 | $this->sqliteCreateFunction('POW', 'pow'); |
||
76 | } |
||
77 | } |
||
78 | |||
79 | /** |
||
80 | * Returns the driver. |
||
81 | * |
||
82 | * @return string |
||
83 | */ |
||
84 | final public function __toString () { |
||
85 | return $this->driver; |
||
86 | } |
||
87 | |||
88 | /** |
||
89 | * Notifies the logger. |
||
90 | * |
||
91 | * @param string $sql |
||
92 | * @return int |
||
93 | */ |
||
94 | public function exec ($sql): int { |
||
95 | $this->logger->__invoke($sql); |
||
96 | return parent::exec($sql); |
||
97 | } |
||
98 | |||
99 | /** |
||
100 | * Central point of framework object creation. |
||
101 | * |
||
102 | * Override this to override framework classes. |
||
103 | * |
||
104 | * @param string $class |
||
105 | * @param mixed ...$args |
||
106 | * @return mixed |
||
107 | */ |
||
108 | public function factory (string $class, ...$args) { |
||
109 | return new $class(...$args); |
||
110 | } |
||
111 | |||
112 | /** |
||
113 | * @return string |
||
114 | */ |
||
115 | final public function getDriver (): string { |
||
116 | return $this->driver; |
||
117 | } |
||
118 | |||
119 | /** |
||
120 | * Returns a {@link Junction} access object based on an annotated interface. |
||
121 | * |
||
122 | * @param string $interface |
||
123 | * @return Junction |
||
124 | */ |
||
125 | public function getJunction ($interface) { |
||
126 | return $this->junctions[$interface] |
||
127 | ?? $this->junctions[$interface] = Junction::fromInterface($this, $interface); |
||
128 | } |
||
129 | |||
130 | /** |
||
131 | * @return Closure |
||
132 | */ |
||
133 | public function getLogger () { |
||
134 | return $this->logger; |
||
135 | } |
||
136 | |||
137 | /** |
||
138 | * Returns a {@link Record} access object based on an annotated class. |
||
139 | * |
||
140 | * @param string|EntityInterface $class |
||
141 | * @return Record |
||
142 | */ |
||
143 | public function getRecord ($class) { |
||
144 | if (is_object($class)) { |
||
145 | $class = get_class($class); |
||
146 | } |
||
147 | return $this->records[$class] |
||
148 | ?? $this->records[$class] = Record::fromClass($this, $class); |
||
149 | } |
||
150 | |||
151 | /** |
||
152 | * @param string $name |
||
153 | * @return null|Table |
||
154 | */ |
||
155 | public function getTable (string $name) { |
||
156 | if (!isset($this->tables[$name])) { |
||
157 | if ($this->isSQLite()) { |
||
158 | $info = $this->query("PRAGMA table_info({$this->quote($name)})")->fetchAll(); |
||
159 | $cols = array_column($info, 'name'); |
||
160 | } |
||
161 | else { |
||
162 | $cols = $this->query( |
||
163 | "SELECT column_name FROM information_schema.tables WHERE table_name = {$this->quote($name)}" |
||
164 | )->fetchAll(self::FETCH_COLUMN); |
||
165 | } |
||
166 | if (!$cols){ |
||
|
|||
167 | return null; |
||
168 | } |
||
169 | $this->tables[$name] = $this->factory(Table::class, $this, $name, $cols); |
||
170 | } |
||
171 | return $this->tables[$name]; |
||
172 | } |
||
173 | |||
174 | /** |
||
175 | * @return bool |
||
176 | */ |
||
177 | final public function isMySQL (): bool { |
||
178 | return $this->driver === 'mysql'; |
||
179 | } |
||
180 | |||
181 | /** |
||
182 | * @return bool |
||
183 | */ |
||
184 | final public function isPostgreSQL (): bool { |
||
185 | return $this->driver === 'pgsql'; |
||
186 | } |
||
187 | |||
188 | /** |
||
189 | * @return bool |
||
190 | */ |
||
191 | final public function isSQLite (): bool { |
||
192 | return $this->driver === 'sqlite'; |
||
193 | } |
||
194 | |||
195 | /** |
||
196 | * Generates an equality {@link Predicate} from mixed arguments. |
||
197 | * |
||
198 | * If `$b` is a closure, returns from `$b($a, DB $this)` |
||
199 | * |
||
200 | * If `$a` is an integer (enumerated item), returns `$b` as a {@link Predicate} |
||
201 | * |
||
202 | * If `$b` is an array, returns `$a IN (...quoted $b)` |
||
203 | * |
||
204 | * If `$b` is a {@link Select}, returns `$a IN ($b->toSql())` |
||
205 | * |
||
206 | * Otherwise predicates `$a = quoted $b` |
||
207 | * |
||
208 | * @param mixed $a |
||
209 | * @param mixed $b |
||
210 | * @return Predicate |
||
211 | */ |
||
212 | public function match ($a, $b) { |
||
213 | if ($b instanceof Closure) { |
||
214 | return $b->__invoke($a, $this); |
||
215 | } |
||
216 | if (is_int($a)) { |
||
217 | return $this->factory(Predicate::class, $b); |
||
218 | } |
||
219 | if (is_array($b)) { |
||
220 | return $this->factory(Predicate::class, "{$a} IN ({$this->quoteList($b)})"); |
||
221 | } |
||
222 | if ($b instanceof Select) { |
||
223 | return $this->factory(Predicate::class, "{$a} IN ({$b->toSql()})"); |
||
224 | } |
||
225 | return $this->factory(Predicate::class, "{$a} = {$this->quote($b)}"); |
||
226 | } |
||
227 | |||
228 | /** |
||
229 | * @param string $class Class or interface name. |
||
230 | * @return bool |
||
231 | */ |
||
232 | public function offsetExists ($class): bool { |
||
233 | return (bool)$this->offsetGet($class); |
||
234 | } |
||
235 | |||
236 | /** |
||
237 | * @param string $class Class or interface name. |
||
238 | * @return null|Table|Record|Junction |
||
239 | */ |
||
240 | public function offsetGet ($class) { |
||
241 | if (is_a($class, EntityInterface::class, true)) { |
||
242 | return $this->getRecord($class); |
||
243 | } |
||
244 | elseif (interface_exists($class)) { |
||
245 | return $this->getJunction($class); |
||
246 | } |
||
247 | else { |
||
248 | return $this->getTable($class); |
||
249 | } |
||
250 | } |
||
251 | |||
252 | /** |
||
253 | * @param mixed $class Class or interface name. |
||
254 | * @param Table|Record|Junction $access |
||
255 | */ |
||
256 | public function offsetSet ($class, $access) { |
||
257 | if ($access instanceof Record) { |
||
258 | $this->setRecord($class, $access); |
||
259 | } |
||
260 | elseif ($access instanceof Junction) { |
||
261 | $this->setJunction($class, $access); |
||
262 | } |
||
263 | else { |
||
264 | throw new LogicException('Raw table access is immutable.'); |
||
265 | } |
||
266 | } |
||
267 | |||
268 | /** |
||
269 | * @param string $class Class or interface name. |
||
270 | */ |
||
271 | public function offsetUnset ($class) { |
||
272 | unset($this->records[$class]); |
||
273 | unset($this->junctions[$class]); |
||
274 | } |
||
275 | |||
276 | /** |
||
277 | * Notifies the logger. |
||
278 | * |
||
279 | * @param string $sql |
||
280 | * @param array $options |
||
281 | * @return Statement |
||
282 | */ |
||
283 | public function prepare ($sql, $options = []) { |
||
284 | $this->logger->__invoke($sql); |
||
285 | /** @var Statement $statement */ |
||
286 | $statement = parent::prepare($sql, $options); |
||
287 | return $statement; |
||
288 | } |
||
289 | |||
290 | /** |
||
291 | * Notifies the logger and executes. |
||
292 | * |
||
293 | * @param string $sql |
||
294 | * @param int $mode |
||
295 | * @param mixed $arg3 Optional. |
||
296 | * @param array $ctorargs Optional. |
||
297 | * @return Statement |
||
298 | */ |
||
299 | public function query ($sql, $mode = PDO::ATTR_DEFAULT_FETCH_MODE, $arg3 = null, array $ctorargs = []) { |
||
300 | $this->logger->__invoke($sql); |
||
301 | /** @var Statement $statement */ |
||
302 | $statement = parent::query(...func_get_args()); |
||
303 | return $statement; |
||
304 | } |
||
305 | |||
306 | /** |
||
307 | * Quotes a value, with special considerations. |
||
308 | * |
||
309 | * - {@link ExpressionInterface} instances are returned as-is. |
||
310 | * - Booleans and integers are returned as unquoted integer-string. |
||
311 | * - Everything else is returned as a quoted string. |
||
312 | * |
||
313 | * @param bool|number|string|object $value |
||
314 | * @param int $type Ignored. |
||
315 | * @return string|ExpressionInterface |
||
316 | */ |
||
317 | public function quote ($value, $type = self::PARAM_STR) { |
||
318 | if ($value instanceof ExpressionInterface) { |
||
319 | return $value; |
||
320 | } |
||
321 | switch (gettype($value)) { |
||
322 | case 'integer' : |
||
323 | case 'boolean' : |
||
324 | case 'resource' : |
||
325 | return (string)(int)$value; |
||
326 | default: |
||
327 | return parent::quote((string)$value); |
||
328 | } |
||
329 | } |
||
330 | |||
331 | /** |
||
332 | * Quotes an array of values. Keys are preserved. |
||
333 | * |
||
334 | * @param array $values |
||
335 | * @return string[] |
||
336 | */ |
||
337 | public function quoteArray (array $values) { |
||
339 | } |
||
340 | |||
341 | /** |
||
342 | * Returns a quoted, comma-separated list. |
||
343 | * |
||
344 | * @param array $values |
||
345 | * @return string |
||
346 | */ |
||
347 | public function quoteList (array $values): string { |
||
348 | return implode(',', $this->quoteArray($values)); |
||
349 | } |
||
350 | |||
351 | /** |
||
352 | * Forwards to the entity's {@link Record} |
||
353 | * |
||
354 | * @param EntityInterface $entity |
||
355 | * @return int ID |
||
356 | */ |
||
357 | public function save (EntityInterface $entity): int { |
||
358 | return $this->getRecord($entity)->save($entity); |
||
359 | } |
||
360 | |||
361 | /** |
||
362 | * @param string $interface |
||
363 | * @param Junction $junction |
||
364 | * @return $this |
||
365 | */ |
||
366 | public function setJunction (string $interface, Junction $junction) { |
||
367 | $this->junctions[$interface] = $junction; |
||
368 | return $this; |
||
369 | } |
||
370 | |||
371 | /** |
||
372 | * @param Closure $logger |
||
373 | * @return $this |
||
374 | */ |
||
375 | public function setLogger (Closure $logger) { |
||
376 | $this->logger = $logger; |
||
377 | return $this; |
||
378 | } |
||
379 | |||
380 | /** |
||
381 | * @param string $class |
||
382 | * @param Record $record |
||
383 | * @return $this |
||
384 | */ |
||
385 | public function setRecord (string $class, Record $record) { |
||
388 | } |
||
389 | } |
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.