Complex classes like PDOStatement 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
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 PDOStatement, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
34 | class PDOStatement extends BasePDOStatement implements IteratorAggregate |
||
35 | { |
||
36 | /** |
||
37 | * @var array |
||
38 | */ |
||
39 | private $parameters = []; |
||
40 | |||
41 | /** |
||
42 | * @var string|null |
||
43 | */ |
||
44 | private $errorCode; |
||
45 | |||
46 | /** |
||
47 | * @var string|null |
||
48 | */ |
||
49 | private $errorMessage; |
||
50 | |||
51 | /** |
||
52 | * @var string |
||
53 | */ |
||
54 | private $sql; |
||
55 | |||
56 | /** |
||
57 | * @var array |
||
58 | */ |
||
59 | private $options = [ |
||
60 | 'fetchMode' => null, |
||
61 | 'fetchColumn' => 0, |
||
62 | 'fetchClass' => 'array', |
||
63 | 'fetchClassCtorArgs' => null, |
||
64 | ]; |
||
65 | |||
66 | /** |
||
67 | * Used for the {@see PDO::FETCH_BOUND} |
||
68 | * |
||
69 | * @var array |
||
70 | */ |
||
71 | private $columnBinding = []; |
||
72 | |||
73 | /** |
||
74 | * @var CollectionInterface|null |
||
75 | */ |
||
76 | private $collection; |
||
77 | |||
78 | /** |
||
79 | * @var PDOInterface |
||
80 | */ |
||
81 | private $pdo; |
||
82 | |||
83 | /** |
||
84 | * @var Closure |
||
85 | */ |
||
86 | private $request; |
||
87 | |||
88 | private $namedToPositionalMap = array(); |
||
89 | |||
90 | /** |
||
91 | * @param PDOInterface $pdo |
||
92 | * @param Closure $request |
||
93 | * @param string $sql |
||
94 | * @param array $options |
||
95 | */ |
||
96 | 1 | public function __construct(PDOInterface $pdo, Closure $request, $sql, array $options) |
|
97 | { |
||
98 | 1 | $this->sql = $this->replaceNamedParametersWithPositionals($sql); |
|
99 | 1 | $this->pdo = $pdo; |
|
100 | 1 | $this->options = array_merge($this->options, $options); |
|
101 | 1 | $this->request = $request; |
|
102 | 1 | } |
|
103 | |||
104 | 67 | private function replaceNamedParametersWithPositionals($sql) |
|
105 | { |
||
106 | 67 | if (strpos($sql, ':') === false) { |
|
107 | 67 | return $sql; |
|
108 | } |
||
109 | 1 | $pattern = '/:((?:[\w|\d|_](?=([^\'\\\]*(\\\.|\'([^\'\\\]*\\\.)*[^\'\\\]*\'))*[^\']*$))*)/'; |
|
110 | |||
111 | 1 | $idx = 0; |
|
112 | $callback = function ($matches) use (&$idx) { |
||
113 | 1 | $value = $matches[1]; |
|
114 | 1 | if (empty($value)) { |
|
115 | 1 | return $matches[0]; |
|
116 | } |
||
117 | 1 | $this->namedToPositionalMap[$value] = $idx; |
|
118 | 1 | $idx++; |
|
119 | 1 | return '?'; |
|
120 | 1 | }; |
|
121 | |||
122 | 1 | return preg_replace_callback($pattern, $callback, $sql); |
|
123 | } |
||
124 | |||
125 | /** |
||
126 | * Determines if the statement has been executed |
||
127 | * |
||
128 | * @internal |
||
129 | * |
||
130 | * @return bool |
||
131 | */ |
||
132 | 30 | private function hasExecuted() |
|
136 | |||
137 | /** |
||
138 | * Internal pointer to mark the state of the current query |
||
139 | * |
||
140 | * @internal |
||
141 | * |
||
142 | * @return bool |
||
143 | */ |
||
144 | 29 | private function isSuccessful() |
|
145 | { |
||
146 | 29 | if (!$this->hasExecuted()) { |
|
147 | // @codeCoverageIgnoreStart |
||
148 | throw new Exception\LogicException('The statement has not been executed yet'); |
||
149 | // @codeCoverageIgnoreEnd |
||
150 | } |
||
151 | |||
152 | 29 | return $this->collection !== null; |
|
153 | } |
||
154 | |||
155 | /** |
||
156 | * Get the fetch style to be used |
||
157 | * |
||
158 | * @internal |
||
159 | * |
||
160 | * @return int |
||
161 | */ |
||
162 | 2 | private function getFetchStyle() |
|
166 | |||
167 | /** |
||
168 | * Update all the bound column references |
||
169 | * |
||
170 | * @internal |
||
171 | * |
||
172 | * @param array $row |
||
173 | * |
||
174 | * @return void |
||
175 | */ |
||
176 | 1 | private function updateBoundColumns(array $row) |
|
177 | { |
||
178 | 1 | foreach ($this->columnBinding as $column => &$metadata) { |
|
179 | |||
180 | 1 | $index = $this->collection->getColumnIndex($column); |
|
181 | 1 | if ($index === null) { |
|
182 | // todo: I would like to throw an exception and tell someone they screwed up |
||
183 | // but i think that would violate the PDO api |
||
184 | continue; |
||
185 | } |
||
186 | |||
187 | // Update by reference |
||
188 | 1 | $value = $this->typedValue($row[$index], $metadata['type']); |
|
189 | 1 | $metadata['ref'] = $value; |
|
190 | 1 | } |
|
191 | |||
192 | 1 | } |
|
193 | |||
194 | /** |
||
195 | * {@inheritDoc} |
||
196 | */ |
||
197 | 2 | public function execute($input_parameters = null) |
|
198 | { |
||
199 | 2 | $input_parameters_array = ArrayUtils::toArray($input_parameters); |
|
200 | 2 | $zero_based = isset($input_parameters_array[0]); |
|
201 | 2 | foreach ($input_parameters_array as $parameter => $value) { |
|
202 | 1 | if (is_int($parameter) && $zero_based) { |
|
203 | 1 | $parameter++; |
|
204 | 1 | } |
|
205 | 1 | $this->bindValue($parameter, $value); |
|
206 | 2 | } |
|
207 | |||
208 | 2 | $result = $this->request->__invoke($this, $this->sql, $this->parameters); |
|
209 | |||
210 | 2 | if (is_array($result)) { |
|
211 | 1 | $this->errorCode = $result['code']; |
|
212 | 1 | $this->errorMessage = $result['message']; |
|
213 | |||
214 | 1 | return false; |
|
215 | } |
||
216 | |||
217 | 1 | $this->collection = $result; |
|
218 | 1 | return true; |
|
219 | } |
||
220 | |||
221 | /** |
||
222 | * {@inheritDoc} |
||
223 | */ |
||
224 | 8 | public function fetch($fetch_style = null, $cursor_orientation = PDO::FETCH_ORI_NEXT, $cursor_offset = 0) |
|
225 | { |
||
226 | 8 | if (!$this->hasExecuted()) { |
|
227 | 8 | $this->execute(); |
|
228 | 8 | } |
|
229 | |||
230 | 8 | if (!$this->isSuccessful()) { |
|
231 | 1 | return false; |
|
232 | } |
||
233 | |||
234 | 7 | if (!$this->collection->valid()) { |
|
235 | 1 | return false; |
|
236 | } |
||
237 | |||
238 | // Get the current row |
||
239 | 6 | $row = $this->collection->current(); |
|
240 | |||
241 | // Traverse |
||
242 | 6 | $this->collection->next(); |
|
243 | |||
244 | 6 | $fetch_style = $fetch_style ?: $this->getFetchStyle(); |
|
245 | |||
246 | switch ($fetch_style) |
||
247 | { |
||
248 | 6 | case PDO::FETCH_NAMED: |
|
249 | 6 | case PDO::FETCH_ASSOC: |
|
250 | 2 | return array_combine($this->collection->getColumns(false), $row); |
|
251 | |||
252 | 4 | case PDO::FETCH_BOTH: |
|
253 | 1 | return array_merge($row, array_combine($this->collection->getColumns(false), $row)); |
|
254 | |||
255 | 3 | case PDO::FETCH_BOUND: |
|
256 | 1 | $this->updateBoundColumns($row); |
|
257 | 1 | return true; |
|
258 | |||
259 | 2 | case PDO::FETCH_NUM: |
|
260 | 1 | return $row; |
|
261 | |||
262 | 1 | default: |
|
263 | 1 | throw new Exception\UnsupportedException('Unsupported fetch style'); |
|
264 | 1 | } |
|
265 | } |
||
266 | |||
267 | /** |
||
268 | * {@inheritDoc} |
||
269 | */ |
||
270 | 2 | public function bindParam( |
|
271 | $parameter, |
||
272 | & $variable, |
||
273 | $data_type = PDO::PARAM_STR, |
||
274 | $length = null, |
||
275 | $driver_options = null |
||
276 | ) { |
||
277 | 2 | if (is_numeric($parameter)) { |
|
278 | 2 | if ($parameter == 0) { |
|
279 | 1 | throw new Exception\UnsupportedException("0-based parameter binding not supported, use 1-based"); |
|
280 | } |
||
281 | 1 | $parameter--; |
|
282 | 1 | } else { |
|
283 | $namedParameterKey = substr($parameter, 0, 1) === ':' ? substr($parameter, 1) : $parameter; |
||
284 | if (array_key_exists($namedParameterKey, $this->namedToPositionalMap)) { |
||
285 | $parameter = $this->namedToPositionalMap[$namedParameterKey]; |
||
286 | } else { |
||
287 | throw new Exception\OutOfBoundsException( |
||
288 | sprintf('The named parameter "%s" does not exist', $parameter) |
||
289 | ); |
||
290 | } |
||
291 | } |
||
292 | 1 | $this->parameters[$parameter] = &$variable; |
|
293 | 1 | } |
|
294 | |||
295 | /** |
||
296 | * {@inheritDoc} |
||
297 | */ |
||
298 | 1 | public function bindColumn($column, &$param, $type = null, $maxlen = null, $driverdata = null) |
|
299 | { |
||
300 | 1 | $type = $type ?: PDO::PARAM_STR; |
|
301 | |||
302 | 1 | $this->columnBinding[$column] = [ |
|
303 | 1 | 'ref' => &$param, |
|
304 | 1 | 'type' => $type, |
|
305 | 1 | 'maxlen' => $maxlen, |
|
306 | 'driverdata' => $driverdata |
||
307 | 1 | ]; |
|
308 | 1 | } |
|
309 | |||
310 | /** |
||
311 | * {@inheritDoc} |
||
312 | */ |
||
313 | 4 | public function bindValue($parameter, $value, $data_type = PDO::PARAM_STR) |
|
314 | { |
||
315 | 4 | $value = $this->typedValue($value, $data_type); |
|
316 | 4 | $this->bindParam($parameter, $value, $data_type); |
|
317 | 4 | } |
|
318 | |||
319 | /** |
||
320 | * {@inheritDoc} |
||
321 | */ |
||
322 | 2 | public function rowCount() |
|
323 | { |
||
324 | 2 | if (!$this->hasExecuted()) { |
|
325 | 2 | $this->execute(); |
|
326 | 2 | } |
|
327 | |||
328 | 2 | if (!$this->isSuccessful()) { |
|
329 | 1 | return 0; |
|
330 | } |
||
331 | |||
332 | 1 | return $this->collection->count(); |
|
333 | } |
||
334 | |||
335 | /** |
||
336 | * {@inheritDoc} |
||
337 | */ |
||
338 | 5 | public function fetchColumn($column_number = 0) |
|
339 | { |
||
340 | 5 | if (!is_int($column_number)) { |
|
341 | 1 | throw new Exception\InvalidArgumentException('column_number must be a valid integer'); |
|
342 | } |
||
343 | |||
344 | 4 | if (!$this->hasExecuted()) { |
|
345 | 4 | $this->execute(); |
|
346 | 4 | } |
|
347 | |||
348 | 4 | if (!$this->isSuccessful()) { |
|
349 | 1 | return false; |
|
|
|||
350 | } |
||
351 | |||
352 | 3 | if (!$this->collection->valid()) { |
|
353 | 2 | return false; |
|
354 | } |
||
355 | |||
356 | 2 | $row = $this->collection->current(); |
|
357 | 2 | $this->collection->next(); |
|
358 | |||
359 | 2 | if ($column_number >= count($row)) { |
|
360 | 1 | throw new Exception\OutOfBoundsException( |
|
361 | 1 | sprintf('The column "%d" with the zero-based does not exist', $column_number) |
|
362 | 1 | ); |
|
363 | } |
||
364 | |||
365 | 1 | return $row[$column_number]; |
|
366 | } |
||
367 | |||
368 | /** |
||
369 | * {@inheritDoc} |
||
370 | */ |
||
371 | 12 | public function fetchAll($fetch_style = null, $fetch_argument = null, $ctor_args = []) |
|
372 | { |
||
373 | 12 | if (!$this->hasExecuted()) { |
|
374 | 12 | $this->execute(); |
|
375 | 12 | } |
|
376 | |||
377 | 12 | if (!$this->isSuccessful()) { |
|
378 | 1 | return false; |
|
379 | } |
||
380 | |||
381 | 11 | $fetch_style = $fetch_style ?: $this->getFetchStyle(); |
|
382 | |||
383 | switch ($fetch_style) |
||
384 | { |
||
385 | 11 | case PDO::FETCH_NUM: |
|
386 | 1 | return $this->collection->getRows(); |
|
387 | |||
388 | 10 | case PDO::FETCH_NAMED: |
|
389 | 10 | case PDO::FETCH_ASSOC: |
|
390 | 2 | $columns = array_flip($this->collection->getColumns()); |
|
391 | |||
392 | return $this->collection->map(function (array $row) use ($columns) { |
||
393 | 2 | return array_combine($columns, $row); |
|
394 | 2 | }); |
|
395 | |||
396 | 8 | case PDO::FETCH_BOTH: |
|
397 | 2 | $columns = array_flip($this->collection->getColumns()); |
|
398 | |||
399 | return $this->collection->map(function (array $row) use ($columns) { |
||
400 | 2 | return array_merge($row, array_combine($columns, $row)); |
|
401 | 2 | }); |
|
402 | |||
403 | 6 | case PDO::FETCH_FUNC: |
|
404 | 2 | if (!is_callable($fetch_argument)) { |
|
405 | 1 | throw new Exception\InvalidArgumentException('Second argument must be callable'); |
|
406 | } |
||
407 | |||
408 | return $this->collection->map(function (array $row) use ($fetch_argument) { |
||
409 | 1 | return call_user_func_array($fetch_argument, $row); |
|
410 | 1 | }); |
|
411 | |||
412 | 4 | case PDO::FETCH_COLUMN: |
|
413 | 3 | $columnIndex = $fetch_argument ?: $this->options['fetchColumn']; |
|
414 | |||
415 | 3 | if (!is_int($columnIndex)) { |
|
416 | 1 | throw new Exception\InvalidArgumentException('Second argument must be a integer'); |
|
417 | } |
||
418 | |||
419 | 2 | $columns = $this->collection->getColumns(false); |
|
420 | 2 | if (!isset($columns[$columnIndex])) { |
|
421 | 1 | throw new Exception\OutOfBoundsException( |
|
422 | 1 | sprintf('Column with the index %d does not exist.', $columnIndex) |
|
423 | 1 | ); |
|
424 | } |
||
425 | |||
426 | 1 | return $this->collection->map(function (array $row) use ($columnIndex) { |
|
427 | 1 | return $row[$columnIndex]; |
|
428 | 1 | }); |
|
429 | |||
430 | 1 | default: |
|
431 | 1 | throw new Exception\UnsupportedException('Unsupported fetch style'); |
|
432 | 1 | } |
|
433 | } |
||
434 | |||
435 | /** |
||
436 | * {@inheritDoc} |
||
437 | */ |
||
438 | 1 | public function fetchObject($class_name = null, $ctor_args = null) |
|
442 | |||
443 | /** |
||
444 | * {@inheritDoc} |
||
445 | */ |
||
446 | 1 | public function errorCode() |
|
450 | |||
451 | /** |
||
452 | * {@inheritDoc} |
||
453 | */ |
||
454 | 2 | public function errorInfo() |
|
455 | { |
||
456 | 2 | if ($this->errorCode === null) { |
|
457 | 2 | return null; |
|
458 | } |
||
459 | |||
460 | 2 | switch ($this->errorCode) |
|
461 | { |
||
462 | 2 | case CrateConst::ERR_INVALID_SQL: |
|
463 | 1 | $ansiErrorCode = 42000; |
|
464 | 1 | break; |
|
465 | |||
466 | 1 | default: |
|
467 | 1 | $ansiErrorCode = 'Not available'; |
|
468 | 1 | break; |
|
469 | 2 | } |
|
470 | |||
471 | return [ |
||
472 | 2 | $ansiErrorCode, |
|
473 | 2 | $this->errorCode, |
|
474 | 2 | $this->errorMessage |
|
475 | 2 | ]; |
|
476 | } |
||
477 | |||
478 | /** |
||
479 | * {@inheritDoc} |
||
480 | */ |
||
481 | 1 | public function setAttribute($attribute, $value) |
|
485 | |||
486 | /** |
||
487 | * {@inheritDoc} |
||
488 | */ |
||
489 | 1 | public function getAttribute($attribute) |
|
493 | |||
494 | /** |
||
495 | * {@inheritDoc} |
||
496 | */ |
||
497 | 1 | public function columnCount() |
|
498 | { |
||
499 | 1 | if (!$this->hasExecuted()) { |
|
500 | 1 | $this->execute(); |
|
501 | 1 | } |
|
502 | |||
503 | 1 | return count($this->collection->getColumns()); |
|
504 | } |
||
505 | |||
506 | /** |
||
507 | * {@inheritDoc} |
||
508 | */ |
||
509 | 1 | public function getColumnMeta($column) |
|
513 | |||
514 | /** |
||
515 | * {@inheritDoc} |
||
516 | */ |
||
517 | 14 | public function setFetchMode($mode, $params = null) |
|
518 | { |
||
519 | 14 | $args = func_get_args(); |
|
520 | 14 | $argCount = count($args); |
|
521 | |||
522 | switch ($mode) |
||
523 | { |
||
524 | 14 | case PDO::FETCH_COLUMN: |
|
525 | 3 | if ($argCount != 2) { |
|
526 | 1 | throw new Exception\InvalidArgumentException('fetch mode requires the colno argument'); |
|
527 | } |
||
528 | |||
529 | 2 | if (!is_int($params)) { |
|
530 | 1 | throw new Exception\InvalidArgumentException('colno must be an integer'); |
|
531 | } |
||
532 | |||
533 | 1 | $this->options['fetchMode'] = $mode; |
|
534 | 1 | $this->options['fetchColumn'] = $params; |
|
535 | 1 | break; |
|
536 | |||
537 | 11 | case PDO::FETCH_ASSOC: |
|
538 | 11 | case PDO::FETCH_NUM: |
|
539 | 11 | case PDO::FETCH_BOTH: |
|
540 | 11 | case PDO::FETCH_BOUND: |
|
541 | 11 | case PDO::FETCH_NAMED: |
|
542 | 10 | if ($params !== null) { |
|
543 | 5 | throw new Exception\InvalidArgumentException('fetch mode doesn\'t allow any extra arguments'); |
|
544 | } |
||
545 | |||
546 | 5 | $this->options['fetchMode'] = $mode; |
|
547 | 5 | break; |
|
548 | |||
549 | 1 | default: |
|
550 | 1 | throw new Exception\UnsupportedException('Invalid fetch mode specified'); |
|
551 | 1 | } |
|
552 | 6 | } |
|
553 | |||
554 | /** |
||
555 | * {@inheritDoc} |
||
556 | */ |
||
557 | 2 | public function nextRowset() |
|
558 | { |
||
559 | 2 | if (!$this->hasExecuted()) { |
|
560 | 2 | $this->execute(); |
|
561 | 2 | } |
|
562 | |||
563 | 2 | if (!$this->isSuccessful()) { |
|
564 | 1 | return false; |
|
565 | } |
||
566 | |||
567 | 1 | $this->collection->next(); |
|
568 | 1 | return $this->collection->valid(); |
|
569 | } |
||
570 | |||
571 | /** |
||
572 | * {@inheritDoc} |
||
573 | */ |
||
574 | 1 | public function closeCursor() |
|
575 | { |
||
576 | 1 | $this->errorCode = 0; |
|
577 | 1 | $this->collection = null; |
|
578 | 1 | return true; |
|
579 | } |
||
580 | |||
581 | /** |
||
582 | * {@inheritDoc} |
||
583 | */ |
||
584 | 1 | public function debugDumpParams() |
|
588 | |||
589 | /** |
||
590 | * {@Inheritdoc} |
||
591 | */ |
||
592 | 1 | public function getIterator() |
|
596 | |||
597 | 8 | private function typedValue($value, $data_type) |
|
598 | { |
||
599 | switch ($data_type) |
||
600 | { |
||
601 | 8 | case PDO::PARAM_FLOAT: |
|
602 | 8 | case PDO::PARAM_DOUBLE: |
|
603 | 1 | return (float) $value; |
|
604 | |||
605 | 8 | case PDO::PARAM_INT: |
|
606 | 8 | case PDO::PARAM_LONG: |
|
607 | 3 | return (int) $value; |
|
608 | |||
609 | 7 | case PDO::PARAM_NULL: |
|
610 | 2 | return null; |
|
611 | |||
612 | 6 | case PDO::PARAM_BOOL: |
|
613 | 2 | return (bool) $value; |
|
634 | } |
||
635 |
If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.
Let’s take a look at an example:
Our function
my_function
expects aPost
object, and outputs the author of the post. The base classPost
returns a simple string and outputting a simple string will work just fine. However, the child classBlogPost
which is a sub-type ofPost
instead decided to return anobject
, and is therefore violating the SOLID principles. If aBlogPost
were passed tomy_function
, PHP would not complain, but ultimately fail when executing thestrtoupper
call in its body.