Complex classes like Schema 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 Schema, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
28 | class Schema extends \yii\db\Schema |
||
29 | { |
||
30 | |||
31 | private $_lastInsertID = null; |
||
32 | |||
33 | /** |
||
34 | * @var array map of DB errors and corresponding exceptions |
||
35 | * If left part is found in DB error message exception class from the right part is used. |
||
36 | */ |
||
37 | public $exceptionMap = [ |
||
38 | 'SQLSTATE[23' => 'yii\db\IntegrityException', |
||
39 | 'SQLSTATE[HY000]: General error: -803 violation of PRIMARY' => 'yii\db\IntegrityException', |
||
40 | ]; |
||
41 | public $reservedWords = [ |
||
42 | 'ORDER', |
||
43 | 'POSITION', |
||
44 | 'TIME', |
||
45 | 'VALUE', |
||
46 | ]; |
||
47 | |||
48 | /** |
||
49 | * @var array mapping from physical column types (keys) to abstract column types (values) |
||
50 | */ |
||
51 | public $typeMap = [ |
||
52 | 'bigint' => self::TYPE_BIGINT, |
||
53 | 'char' => self::TYPE_CHAR, |
||
54 | 'varchar' => self::TYPE_STRING, |
||
55 | 'timestamp' => self::TYPE_TIMESTAMP, |
||
56 | 'decimal' => self::TYPE_DECIMAL, |
||
57 | 'float' => self::TYPE_FLOAT, |
||
58 | 'blob' => self::TYPE_BINARY, |
||
59 | 'integer' => self::TYPE_INTEGER, |
||
60 | 'blob sub_type text' => self::TYPE_TEXT, |
||
61 | 'numeric' => self::TYPE_DECIMAL, |
||
62 | 'double precision' => self::TYPE_DOUBLE, |
||
63 | 'smallint' => self::TYPE_SMALLINT, |
||
64 | ]; |
||
65 | |||
66 | /** |
||
67 | * Creates a query builder for the database. |
||
68 | * This method may be overridden by child classes to create a DBMS-specific query builder. |
||
69 | * @return QueryBuilder query builder instance |
||
70 | */ |
||
71 | 2 | public function createQueryBuilder() |
|
75 | |||
76 | /** |
||
77 | * @inheritdoc |
||
78 | */ |
||
79 | 2 | public function createColumnSchemaBuilder($type, $length = null) |
|
83 | |||
84 | 25 | public function quoteSimpleTableName($name) |
|
97 | |||
98 | 66 | public function quoteSimpleColumnName($name) |
|
105 | |||
106 | 3 | protected function loadTableSchema($name) |
|
116 | |||
117 | 1 | public function getPdoType($data) |
|
131 | |||
132 | /** |
||
133 | * |
||
134 | * @param TableSchema $table |
||
135 | * @param string $name |
||
136 | */ |
||
137 | 3 | protected function resolveTableNames($table, $name) |
|
149 | |||
150 | /** |
||
151 | * Collects the table column metadata. |
||
152 | * |
||
153 | * @param TableSchema $table the table metadata |
||
154 | * @return boolean whether the table exists in the database |
||
155 | */ |
||
156 | 3 | protected function findColumns($table) |
|
157 | { |
||
158 | // Zoggo - Converted sql to use join syntax |
||
159 | // robregonm - Added isAutoInc |
||
160 | $sql = 'SELECT |
||
161 | rel.rdb$field_name AS fname, |
||
162 | rel.rdb$default_source AS fdefault, |
||
163 | fld.rdb$field_type AS fcodtype, |
||
164 | fld.rdb$field_sub_type AS fcodsubtype, |
||
165 | fld.rdb$field_length AS flength, |
||
166 | fld.rdb$character_length AS fcharlength, |
||
167 | fld.rdb$field_scale AS fscale, |
||
168 | fld.rdb$field_precision AS fprecision, |
||
169 | rel.rdb$null_flag AS fnull, |
||
170 | rel.rdb$description AS fcomment, |
||
171 | fld.rdb$default_value AS fdefault_value, |
||
172 | (SELECT RDB$TRIGGER_SOURCE FROM RDB$TRIGGERS |
||
173 | WHERE RDB$SYSTEM_FLAG = 0 |
||
174 | 3 | AND UPPER(RDB$RELATION_NAME)=UPPER(\'' . $table->name . '\') |
|
175 | AND RDB$TRIGGER_TYPE = 1 |
||
176 | AND RDB$TRIGGER_INACTIVE = 0 |
||
177 | AND (UPPER(REPLACE(RDB$TRIGGER_SOURCE,\' \',\'\')) LIKE \'%NEW.\'||TRIM(rel.rdb$field_name)||\'=GEN_ID%\' |
||
178 | OR UPPER(REPLACE(RDB$TRIGGER_SOURCE,\' \',\'\')) LIKE \'%NEW.\'||TRIM(rel.rdb$field_name)||\'=NEXTVALUEFOR%\')) |
||
179 | AS fautoinc |
||
180 | FROM |
||
181 | rdb$relation_fields rel |
||
182 | JOIN rdb$fields fld ON rel.rdb$field_source=fld.rdb$field_name |
||
183 | WHERE |
||
184 | 3 | UPPER(rel.rdb$relation_name)=UPPER(\'' . $table->name . '\') |
|
185 | ORDER BY |
||
186 | 3 | rel.rdb$field_position;'; |
|
187 | try { |
||
188 | 3 | $columns = $this->db->createCommand($sql)->queryAll(); |
|
189 | 3 | if (empty($columns)) { |
|
190 | 2 | return false; |
|
191 | } |
||
192 | 2 | } catch (Exception $e) { |
|
193 | return false; |
||
194 | } |
||
195 | $sql = 'SELECT |
||
196 | idx.rdb$field_name AS fname |
||
197 | FROM |
||
198 | rdb$relation_constraints rc |
||
199 | JOIN rdb$index_segments idx ON idx.rdb$index_name=rc.rdb$index_name |
||
200 | WHERE rc.rdb$constraint_type=\'PRIMARY KEY\' |
||
201 | 2 | AND UPPER(rc.rdb$relation_name)=UPPER(\'' . $table->name . '\')'; |
|
202 | try { |
||
203 | 2 | $pkeys = $this->db->createCommand($sql)->queryColumn(); |
|
204 | 2 | } catch (Exception $e) { |
|
205 | return false; |
||
206 | } |
||
207 | 2 | $pkeys = array_map("rtrim", $pkeys); |
|
208 | 2 | $pkeys = array_map("strtolower", $pkeys); |
|
209 | 2 | foreach ($columns as $key => $column) { |
|
210 | 2 | $column = array_map("strtolower", $column); |
|
211 | 2 | $columns[$key]['fprimary'] = in_array(rtrim($column['fname']), $pkeys); |
|
212 | 2 | } |
|
213 | 2 | foreach ($columns as $column) { |
|
214 | 2 | $c = $this->loadColumnSchema($column); |
|
215 | 2 | if ($table->sequenceName === null && $c->autoIncrement) { |
|
216 | 1 | $matches = []; |
|
217 | 1 | if (preg_match("/NEW.{$c->name}\s*=\s*GEN_ID\((\w+)/i", $column['fautoinc'], $matches)) { |
|
218 | $table->sequenceName = $matches[1]; |
||
219 | 1 | } elseif (preg_match("/NEW.{$c->name}\s*=\s*NEXT\s+VALUE\s+FOR\s+(\w+)/i", $column['fautoinc'], $matches)) { |
|
220 | 1 | $table->sequenceName = $matches[1]; |
|
221 | 1 | } |
|
222 | 1 | } |
|
223 | 2 | $table->columns[$c->name] = $c; |
|
224 | 2 | if ($c->isPrimaryKey) { |
|
225 | 2 | $table->primaryKey[] = $c->name; |
|
226 | 2 | } |
|
227 | 2 | } |
|
228 | 2 | return (count($table->columns) > 0); |
|
229 | } |
||
230 | |||
231 | /** |
||
232 | * @return ColumnSchema |
||
233 | * @throws \yii\base\InvalidConfigException |
||
234 | */ |
||
235 | 2 | protected function createColumnSchema() |
|
239 | |||
240 | /** |
||
241 | * Creates a table column. |
||
242 | * |
||
243 | * @param array $column column metadata |
||
244 | * @return ColumnSchema normalized column metadata |
||
245 | */ |
||
246 | 2 | protected function loadColumnSchema($column) |
|
247 | { |
||
248 | 2 | $c = $this->createColumnSchema(); |
|
249 | 2 | $c->name = strtolower(rtrim($column['fname'])); |
|
250 | 2 | $c->allowNull = $column['fnull'] !== '1'; |
|
251 | 2 | $c->isPrimaryKey = $column['fprimary']; |
|
252 | 2 | $c->autoIncrement = (boolean) $column['fautoinc']; |
|
253 | 2 | $c->comment = $column['fcomment'] === null ? '' : $column['fcomment']; |
|
254 | |||
255 | 2 | $c->type = self::TYPE_STRING; |
|
256 | |||
257 | 2 | $defaultValue = null; |
|
258 | 2 | if (!empty($column['fdefault'])) { |
|
259 | // remove whitespace, 'DEFAULT ' prefix and surrounding single quotes; all optional |
||
260 | if (preg_match("/\s*(DEFAULT\s+){0,1}('(.*)'|(.*))\s*/i", $column['fdefault'], $parts)) { |
||
261 | $defaultValue = array_pop($parts); |
||
262 | } |
||
263 | // handle escaped single quotes like in "funny''quoted''string" |
||
264 | $defaultValue = str_replace('\'\'', '\'', $defaultValue); |
||
265 | } |
||
266 | 2 | if ($defaultValue === null) { |
|
267 | 2 | $defaultValue = $column['fdefault_value']; |
|
268 | 2 | } |
|
269 | 2 | $dbType = ""; |
|
270 | $baseTypes = [ |
||
271 | 2 | 7 => 'SMALLINT', |
|
272 | 2 | 8 => 'INTEGER', |
|
273 | 2 | 16 => 'INT64', |
|
274 | 2 | 9 => 'QUAD', |
|
275 | 2 | 10 => 'FLOAT', |
|
276 | 2 | 11 => 'D_FLOAT', |
|
277 | 2 | 17 => 'BOOLEAN', |
|
278 | 2 | 27 => 'DOUBLE PRECISION', |
|
279 | 2 | 12 => 'DATE', |
|
280 | 2 | 13 => 'TIME', |
|
281 | 2 | 35 => 'TIMESTAMP', |
|
282 | 2 | 261 => 'BLOB', |
|
283 | 2 | 40 => 'CSTRING', |
|
284 | 2 | 45 => 'BLOB_ID', |
|
285 | 2 | ]; |
|
286 | $baseCharTypes = [ |
||
287 | 2 | 37 => 'VARCHAR', |
|
288 | 2 | 14 => 'CHAR', |
|
289 | 2 | ]; |
|
290 | 2 | if (array_key_exists((int) $column['fcodtype'], $baseTypes)) { |
|
291 | 2 | $dbType = $baseTypes[(int) $column['fcodtype']]; |
|
292 | 2 | } elseif (array_key_exists((int) $column['fcodtype'], $baseCharTypes)) { |
|
293 | 2 | $c->size = (int) $column['fcharlength']; |
|
294 | 2 | $c->precision = $c->size; |
|
295 | 2 | $dbType = $baseCharTypes[(int) $column['fcodtype']] . "($c->size)"; |
|
296 | 2 | } |
|
297 | 2 | switch ((int) $column['fcodtype']) { |
|
298 | 2 | case 7: |
|
299 | 2 | case 8: |
|
300 | 2 | switch ((int) $column['fcodsubtype']) { |
|
301 | 2 | case 1: |
|
302 | $c->precision = (int) $column['fprecision']; |
||
303 | $c->size = $c->precision; |
||
304 | $c->scale = abs((int) $column['fscale']); |
||
|
|||
305 | $dbType = "NUMERIC({$c->precision},{$c->scale})"; |
||
306 | break; |
||
307 | 2 | case 2: |
|
308 | $c->precision = (int) $column['fprecision']; |
||
309 | $c->size = $c->precision; |
||
310 | $c->scale = abs((int) $column['fscale']); |
||
311 | $dbType = "DECIMAL({$c->precision},{$c->scale})"; |
||
312 | break; |
||
313 | } |
||
314 | 2 | break; |
|
315 | 2 | case 16: |
|
316 | switch ((int) $column['fcodsubtype']) { |
||
317 | case 1: |
||
318 | $c->precision = (int) $column['fprecision']; |
||
319 | $c->size = $c->precision; |
||
320 | $c->scale = abs((int) $column['fscale']); |
||
321 | $dbType = "NUMERIC({$c->precision},{$c->scale})"; |
||
322 | break; |
||
323 | case 2: |
||
324 | $c->precision = (int) $column['fprecision']; |
||
325 | $c->size = $c->precision; |
||
326 | $c->scale = abs((int) $column['fscale']); |
||
327 | $dbType = "DECIMAL({$c->precision},{$c->scale})"; |
||
328 | break; |
||
329 | default: |
||
330 | $dbType = 'BIGINT'; |
||
331 | break; |
||
332 | } |
||
333 | break; |
||
334 | 2 | case 261: |
|
335 | switch ((int) $column['fcodsubtype']) { |
||
336 | case 1: |
||
337 | $dbType = 'BLOB SUB_TYPE TEXT'; |
||
338 | $c->size = null; |
||
339 | break; |
||
340 | } |
||
341 | break; |
||
342 | } |
||
343 | |||
344 | 2 | $c->dbType = strtolower($dbType); |
|
345 | |||
346 | 2 | $c->type = self::TYPE_STRING; |
|
347 | 2 | if (preg_match('/^([\w\ ]+)(?:\(([^\)]+)\))?/', $c->dbType, $matches)) { |
|
348 | 2 | $type = strtolower($matches[1]); |
|
349 | 2 | if (isset($this->typeMap[$type])) { |
|
350 | 2 | $c->type = $this->typeMap[$type]; |
|
351 | 2 | } |
|
352 | 2 | } |
|
353 | |||
354 | |||
355 | 2 | $c->phpType = $this->getColumnPhpType($c); |
|
356 | |||
357 | 2 | $c->defaultValue = null; |
|
358 | 2 | if ($defaultValue !== null) { |
|
359 | if (in_array($c->type, [self::TYPE_DATE, self::TYPE_DATETIME, self::TYPE_TIME, self::TYPE_TIMESTAMP]) |
||
360 | && preg_match('/(CURRENT_|NOW|NULL|TODAY|TOMORROW|YESTERDAY)/i', $defaultValue)) { |
||
361 | $c->defaultValue = new \yii\db\Expression(trim($defaultValue)); |
||
362 | } else { |
||
363 | $c->defaultValue = $c->phpTypecast($defaultValue); |
||
364 | } |
||
365 | } |
||
366 | |||
367 | 2 | return $c; |
|
368 | } |
||
369 | |||
370 | /** |
||
371 | * Collects the foreign key column details for the given table. |
||
372 | * |
||
373 | * @param TableSchema $table the table metadata |
||
374 | */ |
||
375 | 2 | protected function findConstraints($table) |
|
376 | { |
||
377 | // Zoggo - Converted sql to use join syntax |
||
378 | $sql = 'SELECT |
||
379 | a.rdb$constraint_name as fconstraint, |
||
380 | c.rdb$relation_name AS ftable, |
||
381 | d.rdb$field_name AS pfield, |
||
382 | e.rdb$field_name AS ffield |
||
383 | FROM |
||
384 | rdb$ref_constraints b |
||
385 | JOIN rdb$relation_constraints a ON a.rdb$constraint_name=b.rdb$constraint_name |
||
386 | JOIN rdb$relation_constraints c ON b.rdb$const_name_uq=c.rdb$constraint_name |
||
387 | JOIN rdb$index_segments d ON c.rdb$index_name=d.rdb$index_name |
||
388 | JOIN rdb$index_segments e ON a.rdb$index_name=e.rdb$index_name AND e.rdb$field_position = d.rdb$field_position |
||
389 | WHERE |
||
390 | a.rdb$constraint_type=\'FOREIGN KEY\' AND |
||
391 | 2 | UPPER(a.rdb$relation_name)=UPPER(\'' . $table->name . '\') '; |
|
392 | try { |
||
393 | 2 | $fkeys = $this->db->createCommand($sql)->queryAll(); |
|
394 | 2 | } catch (Exception $e) { |
|
395 | return false; |
||
396 | } |
||
397 | |||
398 | 2 | $constraints = []; |
|
399 | 2 | foreach ($fkeys as $fkey) { |
|
400 | // Zoggo - Added strtolower here to guarantee that values are |
||
401 | // returned lower case. Otherwise gii generates wrong code. |
||
402 | $fkey = array_map("rtrim", $fkey); |
||
403 | $fkey = array_map("strtolower", $fkey); |
||
404 | |||
405 | if (!isset($constraints[$fkey['fconstraint']])) { |
||
406 | $constraints[$fkey['fconstraint']] = [ |
||
407 | $fkey['ftable'] |
||
408 | ]; |
||
409 | } |
||
410 | $constraints[$fkey['fconstraint']][$fkey['ffield']] = $fkey['pfield']; |
||
411 | 2 | } |
|
412 | 2 | $table->foreignKeys = array_values($constraints); |
|
413 | 2 | } |
|
414 | |||
415 | protected function findTableNames($schema = '') |
||
416 | { |
||
417 | $sql = 'SELECT |
||
418 | rdb$relation_name |
||
419 | FROM |
||
420 | rdb$relations |
||
421 | WHERE |
||
422 | (rdb$system_flag is null OR rdb$system_flag=0)'; |
||
423 | try { |
||
424 | $tables = $this->db->createCommand($sql)->queryColumn(); |
||
425 | } catch (Exception $e) { |
||
426 | return false; |
||
427 | } |
||
428 | |||
429 | $tables = array_map('rtrim', $tables); |
||
430 | $tables = array_map('strtolower', $tables); |
||
431 | |||
432 | return $tables; |
||
433 | } |
||
434 | |||
435 | /** |
||
436 | * Returns all unique indexes for the given table. |
||
437 | * Each array element is of the following structure: |
||
438 | * |
||
439 | * ~~~ |
||
440 | * [ |
||
441 | * 'IndexName1' => ['col1' [, ...]], |
||
442 | * 'IndexName2' => ['col2' [, ...]], |
||
443 | * ] |
||
444 | * ~~~ |
||
445 | * |
||
446 | * @param TableSchema $table the table metadata |
||
447 | * @return array all unique indexes for the given table. |
||
448 | * @since 2.0.4 |
||
449 | */ |
||
450 | public function findUniqueIndexes($table) |
||
451 | { |
||
452 | $query = ' |
||
453 | SELECT id.RDB$INDEX_NAME as index_name, ids.RDB$FIELD_NAME as column_name |
||
454 | FROM RDB$INDICES id |
||
455 | INNER JOIN RDB$INDEX_SEGMENTS ids ON ids.RDB$INDEX_NAME = id.RDB$INDEX_NAME |
||
456 | WHERE id.RDB$UNIQUE_FLAG = 1 |
||
457 | AND id.RDB$SYSTEM_FLAG = 0 |
||
458 | AND UPPER(id.RDB$RELATION_NAME) = UPPER(\'' . $table->name . '\') |
||
459 | ORDER BY id.RDB$RELATION_NAME, id.RDB$INDEX_NAME, ids.RDB$FIELD_POSITION'; |
||
460 | $result = []; |
||
461 | $command = $this->db->createCommand($query); |
||
462 | foreach ($command->queryAll() as $row) { |
||
463 | $result[strtolower(rtrim($row['index_name']))][] = strtolower(rtrim($row['column_name'])); |
||
464 | } |
||
465 | return $result; |
||
466 | } |
||
467 | |||
468 | /** |
||
469 | * Sets the isolation level of the current transaction. |
||
470 | * @param string $level The transaction isolation level to use for this transaction. |
||
471 | * This can be one of [[Transaction::READ_UNCOMMITTED]], [[Transaction::READ_COMMITTED]], [[Transaction::REPEATABLE_READ]] |
||
472 | * and [[Transaction::SERIALIZABLE]] but also a string containing DBMS specific syntax to be used |
||
473 | * after `SET TRANSACTION ISOLATION LEVEL`. |
||
474 | * @see http://en.wikipedia.org/wiki/Isolation_%28database_systems%29#Isolation_levels |
||
475 | */ |
||
476 | public function setTransactionIsolationLevel($level) |
||
477 | { |
||
478 | if ($level == \yii\db\Transaction::READ_UNCOMMITTED) { |
||
479 | parent::setTransactionIsolationLevel('READ COMMITTED RECORD_VERSION'); |
||
480 | } elseif ($level == \yii\db\Transaction::REPEATABLE_READ) { |
||
481 | parent::setTransactionIsolationLevel('SNAPSHOT'); |
||
482 | } elseif ($level == \yii\db\Transaction::SERIALIZABLE) { |
||
483 | parent::setTransactionIsolationLevel('SNAPSHOT TABLE STABILITY'); |
||
484 | } else { |
||
485 | parent::setTransactionIsolationLevel($level); |
||
486 | } |
||
487 | } |
||
488 | |||
489 | /** |
||
490 | * @inheritdoc |
||
491 | */ |
||
492 | 1 | public function insert($table, $columns) |
|
524 | |||
525 | /** |
||
526 | * @inheritdoc |
||
527 | */ |
||
528 | public function getLastInsertID($sequenceName = '') |
||
529 | { |
||
530 | if (!$this->db->isActive) { |
||
543 | } |
||
544 |
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.