| Total Complexity | 42 | 
| Total Lines | 259 | 
| Duplicated Lines | 0 % | 
| Changes | 2 | ||
| Bugs | 0 | Features | 0 | 
Complex classes like AbstractPdoCommand 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 AbstractPdoCommand, and based on these observations, apply Extract Interface, too.
| 1 | <?php  | 
            ||
| 36 | abstract class AbstractPdoCommand extends AbstractCommand implements PdoCommandInterface, DbLoggerAwareInterface, ProfilerAwareInterface  | 
            ||
| 37 | { | 
            ||
| 38 | use DbLoggerAwareTrait;  | 
            ||
| 39 | use ProfilerAwareTrait;  | 
            ||
| 40 | |||
| 41 | /**  | 
            ||
| 42 | * @var PDOStatement|null Represents a prepared statement and, after the statement is executed, an associated  | 
            ||
| 43 | * result set.  | 
            ||
| 44 | *  | 
            ||
| 45 | * @link https://www.php.net/manual/en/class.pdostatement.php  | 
            ||
| 46 | */  | 
            ||
| 47 | protected PDOStatement|null $pdoStatement = null;  | 
            ||
| 48 | |||
| 49 | public function __construct(protected PdoConnectionInterface $db)  | 
            ||
| 50 |     { | 
            ||
| 51 | }  | 
            ||
| 52 | |||
| 53 | /**  | 
            ||
| 54 |      * This method mainly sets {@see pdoStatement} to be `null`. | 
            ||
| 55 | */  | 
            ||
| 56 | public function cancel(): void  | 
            ||
| 57 |     { | 
            ||
| 58 | $this->pdoStatement = null;  | 
            ||
| 59 | }  | 
            ||
| 60 | |||
| 61 | public function getPdoStatement(): PDOStatement|null  | 
            ||
| 62 |     { | 
            ||
| 63 | return $this->pdoStatement;  | 
            ||
| 64 | }  | 
            ||
| 65 | |||
| 66 | public function bindParam(  | 
            ||
| 67 | int|string $name,  | 
            ||
| 68 | mixed &$value,  | 
            ||
| 69 | int|null $dataType = null,  | 
            ||
| 70 | int|null $length = null,  | 
            ||
| 71 | mixed $driverOptions = null  | 
            ||
| 72 |     ): static { | 
            ||
| 73 | $this->prepare();  | 
            ||
| 74 | |||
| 75 |         if ($dataType === null) { | 
            ||
| 76 | $dataType = $this->db->getSchema()->getDataType($value);  | 
            ||
| 77 | }  | 
            ||
| 78 | |||
| 79 |         if ($length === null) { | 
            ||
| 80 | $this->pdoStatement?->bindParam($name, $value, $dataType);  | 
            ||
| 81 |         } elseif ($driverOptions === null) { | 
            ||
| 82 | $this->pdoStatement?->bindParam($name, $value, $dataType, $length);  | 
            ||
| 83 |         } else { | 
            ||
| 84 | $this->pdoStatement?->bindParam($name, $value, $dataType, $length, $driverOptions);  | 
            ||
| 85 | }  | 
            ||
| 86 | |||
| 87 | return $this;  | 
            ||
| 88 | }  | 
            ||
| 89 | |||
| 90 | public function bindValue(int|string $name, mixed $value, int|null $dataType = null): static  | 
            ||
| 91 |     { | 
            ||
| 92 |         if ($dataType === null) { | 
            ||
| 93 | $dataType = $this->db->getSchema()->getDataType($value);  | 
            ||
| 94 | }  | 
            ||
| 95 | |||
| 96 | $this->params[$name] = new Param($value, $dataType);  | 
            ||
| 97 | |||
| 98 | return $this;  | 
            ||
| 99 | }  | 
            ||
| 100 | |||
| 101 | public function bindValues(array $values): static  | 
            ||
| 102 |     { | 
            ||
| 103 |         if (empty($values)) { | 
            ||
| 104 | return $this;  | 
            ||
| 105 | }  | 
            ||
| 106 | |||
| 107 | /**  | 
            ||
| 108 | * @psalm-var array<string, int>|ParamInterface|int $value  | 
            ||
| 109 | */  | 
            ||
| 110 |         foreach ($values as $name => $value) { | 
            ||
| 111 |             if ($value instanceof ParamInterface) { | 
            ||
| 112 | $this->params[$name] = $value;  | 
            ||
| 113 |             } else { | 
            ||
| 114 | $type = $this->db->getSchema()->getDataType($value);  | 
            ||
| 115 | $this->params[$name] = new Param($value, $type);  | 
            ||
| 116 | }  | 
            ||
| 117 | }  | 
            ||
| 118 | |||
| 119 | return $this;  | 
            ||
| 120 | }  | 
            ||
| 121 | |||
| 122 | public function prepare(bool|null $forRead = null): void  | 
            ||
| 123 |     { | 
            ||
| 124 |         if (isset($this->pdoStatement)) { | 
            ||
| 125 | $this->bindPendingParams();  | 
            ||
| 126 | |||
| 127 | return;  | 
            ||
| 128 | }  | 
            ||
| 129 | |||
| 130 | $sql = $this->getSql();  | 
            ||
| 131 | |||
| 132 | /**  | 
            ||
| 133 |          * If SQL is empty, there will be {@see \ValueError} on prepare pdoStatement. | 
            ||
| 134 | *  | 
            ||
| 135 | * @link https://php.watch/versions/8.0/ValueError  | 
            ||
| 136 | */  | 
            ||
| 137 |         if ($sql === '') { | 
            ||
| 138 | return;  | 
            ||
| 139 | }  | 
            ||
| 140 | |||
| 141 | $pdo = $this->db->getActivePDO($sql, $forRead);  | 
            ||
| 142 | |||
| 143 |         try { | 
            ||
| 144 | $this->pdoStatement = $pdo?->prepare($sql);  | 
            ||
| 145 | $this->bindPendingParams();  | 
            ||
| 146 |         } catch (PDOException $e) { | 
            ||
| 147 | $message = $e->getMessage() . "\nFailed to prepare SQL: $sql";  | 
            ||
| 148 | /** @psalm-var array|null $errorInfo */  | 
            ||
| 149 | $errorInfo = $e->errorInfo ?? null;  | 
            ||
| 150 | |||
| 151 | throw new Exception($message, $errorInfo, $e);  | 
            ||
| 152 | }  | 
            ||
| 153 | }  | 
            ||
| 154 | |||
| 155 | /**  | 
            ||
| 156 |      * Binds pending parameters registered via {@see bindValue()} and {@see bindValues()}. | 
            ||
| 157 | *  | 
            ||
| 158 |      * Note that this method requires an active {@see pdoStatement}. | 
            ||
| 159 | */  | 
            ||
| 160 | protected function bindPendingParams(): void  | 
            ||
| 161 |     { | 
            ||
| 162 |         foreach ($this->params as $name => $value) { | 
            ||
| 163 | $this->pdoStatement?->bindValue($name, $value->getValue(), $value->getType());  | 
            ||
| 164 | }  | 
            ||
| 165 | }  | 
            ||
| 166 | |||
| 167 | protected function getQueryBuilder(): QueryBuilderInterface  | 
            ||
| 168 |     { | 
            ||
| 169 | return $this->db->getQueryBuilder();  | 
            ||
| 170 | }  | 
            ||
| 171 | |||
| 172 | protected function getQueryMode(int $queryMode): string  | 
            ||
| 173 |     { | 
            ||
| 174 |         return match ($queryMode) { | 
            ||
| 175 | self::QUERY_MODE_EXECUTE => 'execute',  | 
            ||
| 176 | self::QUERY_MODE_ROW => 'queryOne',  | 
            ||
| 177 | self::QUERY_MODE_ALL => 'queryAll',  | 
            ||
| 178 | self::QUERY_MODE_COLUMN => 'queryColumn',  | 
            ||
| 179 | self::QUERY_MODE_CURSOR => 'query',  | 
            ||
| 180 | self::QUERY_MODE_SCALAR => 'queryScalar',  | 
            ||
| 181 | self::QUERY_MODE_ROW | self::QUERY_MODE_EXECUTE => 'insertWithReturningPks'  | 
            ||
| 182 | };  | 
            ||
| 183 | }  | 
            ||
| 184 | |||
| 185 | /**  | 
            ||
| 186 | * Executes a prepared statement.  | 
            ||
| 187 | *  | 
            ||
| 188 |      * It's a wrapper around {@see PDOStatement::execute()} to support transactions and retry handlers. | 
            ||
| 189 | *  | 
            ||
| 190 | * @param string|null $rawSql Deprecated. Use `null` value. Will be removed in version 2.0.0.  | 
            ||
| 191 | *  | 
            ||
| 192 | * @throws Exception  | 
            ||
| 193 | * @throws Throwable  | 
            ||
| 194 | */  | 
            ||
| 195 | protected function internalExecute(string|null $rawSql): void  | 
            ||
| 196 |     { | 
            ||
| 197 | $attempt = 0;  | 
            ||
| 198 | |||
| 199 |         while (true) { | 
            ||
| 200 |             try { | 
            ||
| 201 | if (  | 
            ||
| 202 | ++$attempt === 1  | 
            ||
| 203 | && $this->isolationLevel !== null  | 
            ||
| 204 | && $this->db->getTransaction() === null  | 
            ||
| 205 |                 ) { | 
            ||
| 206 | $this->db->transaction(  | 
            ||
| 207 | fn () => $this->internalExecute($rawSql),  | 
            ||
| 208 | $this->isolationLevel  | 
            ||
| 209 | );  | 
            ||
| 210 |                 } else { | 
            ||
| 211 | $this->pdoStatement?->execute();  | 
            ||
| 212 | }  | 
            ||
| 213 | break;  | 
            ||
| 214 |             } catch (PDOException $e) { | 
            ||
| 215 | $rawSql = $rawSql ?: $this->getRawSql();  | 
            ||
| 216 | $e = (new ConvertException($e, $rawSql))->run();  | 
            ||
| 217 | |||
| 218 |                 if ($this->retryHandler === null || !($this->retryHandler)($e, $attempt)) { | 
            ||
| 219 | throw $e;  | 
            ||
| 220 | }  | 
            ||
| 221 | }  | 
            ||
| 222 | }  | 
            ||
| 223 | }  | 
            ||
| 224 | |||
| 225 | /**  | 
            ||
| 226 | * @throws InvalidParamException  | 
            ||
| 227 | */  | 
            ||
| 228 | protected function internalGetQueryResult(int $queryMode): mixed  | 
            ||
| 229 |     { | 
            ||
| 230 |         if ($queryMode === self::QUERY_MODE_CURSOR) { | 
            ||
| 231 | return new DataReader($this);  | 
            ||
| 232 | }  | 
            ||
| 233 | |||
| 234 |         if ($queryMode === self::QUERY_MODE_EXECUTE) { | 
            ||
| 235 | return $this->pdoStatement?->rowCount() ?? 0;  | 
            ||
| 236 | }  | 
            ||
| 237 | |||
| 238 |         if ($this->is($queryMode, self::QUERY_MODE_ROW)) { | 
            ||
| 239 | /** @psalm-var array|false $result */  | 
            ||
| 240 | $result = $this->pdoStatement?->fetch(PDO::FETCH_ASSOC);  | 
            ||
| 241 |         } elseif ($this->is($queryMode, self::QUERY_MODE_SCALAR)) { | 
            ||
| 242 | /** @psalm-var mixed $result */  | 
            ||
| 243 | $result = $this->pdoStatement?->fetchColumn();  | 
            ||
| 244 |         } elseif ($this->is($queryMode, self::QUERY_MODE_COLUMN)) { | 
            ||
| 245 | /** @psalm-var mixed $result */  | 
            ||
| 246 | $result = $this->pdoStatement?->fetchAll(PDO::FETCH_COLUMN);  | 
            ||
| 247 |         } elseif ($this->is($queryMode, self::QUERY_MODE_ALL)) { | 
            ||
| 248 | /** @psalm-var mixed $result */  | 
            ||
| 249 | $result = $this->pdoStatement?->fetchAll(PDO::FETCH_ASSOC);  | 
            ||
| 250 |         } else { | 
            ||
| 251 |             throw new InvalidParamException("Unknown query mode '$queryMode'"); | 
            ||
| 252 | }  | 
            ||
| 253 | |||
| 254 | $this->pdoStatement?->closeCursor();  | 
            ||
| 255 | |||
| 256 | return $result;  | 
            ||
| 257 | }  | 
            ||
| 258 | |||
| 259 | protected function queryInternal(int $queryMode): mixed  | 
            ||
| 260 |     { | 
            ||
| 261 | $logCategory = self::class . '::' . $this->getQueryMode($queryMode);  | 
            ||
| 262 | |||
| 263 |         if ($this->logger !== null) { | 
            ||
| 264 | $rawSql = $this->getRawSql();  | 
            ||
| 265 | $this->logger->log(DbLoggerEvent::QUERY, new QueryContext(__METHOD__, $rawSql, $logCategory));  | 
            ||
| 266 | }  | 
            ||
| 267 | |||
| 268 | $queryContext = new CommandContext(__METHOD__, $logCategory, $this->getSql(), $this->getParams());  | 
            ||
| 269 | |||
| 270 | /**  | 
            ||
| 271 | * @psalm-var string $rawSql  | 
            ||
| 272 | * @psalm-suppress RedundantConditionGivenDocblockType  | 
            ||
| 273 | * @psalm-suppress DocblockTypeContradiction  | 
            ||
| 274 | */  | 
            ||
| 275 | $this->profiler?->begin($rawSql ??= $this->getRawSql(), $queryContext);  | 
            ||
| 
                                                                                                    
                        
                         | 
                |||
| 276 |         try { | 
            ||
| 277 | /** @psalm-var mixed $result */  | 
            ||
| 278 | $result = parent::queryInternal($queryMode);  | 
            ||
| 279 |         } catch (Throwable $e) { | 
            ||
| 280 | $this->profiler?->end($rawSql, $queryContext->setException($e));  | 
            ||
| 281 | throw $e;  | 
            ||
| 282 | }  | 
            ||
| 283 | $this->profiler?->end($rawSql, $queryContext);  | 
            ||
| 284 | |||
| 285 | return $result;  | 
            ||
| 286 | }  | 
            ||
| 287 | |||
| 288 | /**  | 
            ||
| 289 |      * Refreshes table schema, which was marked by {@see requireTableSchemaRefresh()}. | 
            ||
| 290 | */  | 
            ||
| 291 | protected function refreshTableSchema(): void  | 
            ||
| 295 | }  | 
            ||
| 296 | }  | 
            ||
| 297 | }  | 
            ||
| 298 |