Complex classes like Migrator 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 Migrator, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
18 | class Migrator |
||
19 | { |
||
20 | /** |
||
21 | * @const string |
||
22 | */ |
||
23 | const DIRECTION_UP = 'up'; |
||
24 | |||
25 | /** |
||
26 | * @const string |
||
27 | */ |
||
28 | const DIRECTION_DOWN = 'down'; |
||
29 | |||
30 | /** |
||
31 | * @var string |
||
32 | */ |
||
33 | protected $migrationsTableName = 'migrations'; |
||
34 | |||
35 | /** |
||
36 | * @var string |
||
37 | */ |
||
38 | protected $migrationsDirectory; |
||
39 | |||
40 | /** |
||
41 | * @var Db |
||
42 | */ |
||
43 | protected $db; |
||
44 | |||
45 | /** |
||
46 | * @var callable |
||
47 | */ |
||
48 | protected $infoCallback; |
||
49 | |||
50 | /** |
||
51 | * @var AbstractMigration[] |
||
52 | */ |
||
53 | protected $migrations; |
||
54 | |||
55 | /** |
||
56 | * @var int[] |
||
57 | */ |
||
58 | protected $migratedNumbers; |
||
59 | |||
60 | /** |
||
61 | * @var bool |
||
62 | */ |
||
63 | private $hasMigrationsTable; |
||
64 | |||
65 | /** |
||
66 | * @param string $migrationsDirectory |
||
67 | * @param Db $db |
||
68 | * @param callable $infoCallback |
||
69 | */ |
||
70 | 10 | public function __construct( |
|
79 | |||
80 | /** |
||
81 | * @return \SplFileInfo[] |
||
82 | */ |
||
83 | 8 | protected function findMigrationFiles() |
|
84 | { |
||
85 | 8 | $migrationFiles = []; |
|
86 | 8 | $directoryIterator = new \FilesystemIterator($this->migrationsDirectory); |
|
87 | 8 | foreach ($directoryIterator as $fileInfo) { |
|
88 | 8 | if ($fileInfo->isFile() && $fileInfo->getExtension() === 'php') { |
|
89 | 8 | $migrationFiles[] = $fileInfo; |
|
90 | } |
||
91 | } |
||
92 | |||
93 | 8 | return $migrationFiles; |
|
94 | } |
||
95 | |||
96 | /** |
||
97 | * @return AbstractMigration[] |
||
98 | */ |
||
99 | 8 | protected function loadMigrations() |
|
100 | { |
||
101 | 8 | $migrations = []; |
|
102 | 8 | foreach ($this->findMigrationFiles() as $file) { |
|
103 | 8 | require_once $file->getPathname(); |
|
104 | |||
105 | 8 | $className = '\\' . $file->getBasename('.' . $file->getExtension()); |
|
106 | 8 | $migration = new $className($this->db); |
|
107 | |||
108 | 8 | $migrations[$migration->getNumber()] = $migration; |
|
109 | } |
||
110 | |||
111 | 8 | ksort($migrations); |
|
112 | |||
113 | 8 | return $migrations; |
|
114 | } |
||
115 | |||
116 | /** |
||
117 | * @return AbstractMigration[] |
||
118 | */ |
||
119 | 8 | public function getMigrations() |
|
120 | { |
||
121 | 8 | if (!isset($this->migrations)) { |
|
122 | 8 | $this->migrations = $this->loadMigrations(); |
|
123 | } |
||
124 | |||
125 | 8 | return $this->migrations; |
|
126 | } |
||
127 | |||
128 | /** |
||
129 | * @return int |
||
130 | */ |
||
131 | 8 | public function getLatestNumber() |
|
139 | |||
140 | /** |
||
141 | * @return bool |
||
142 | */ |
||
143 | 8 | private function hasMigrationsTable() |
|
144 | { |
||
145 | 8 | if (!isset($this->hasMigrationsTable)) { |
|
146 | 8 | $this->hasMigrationsTable = |
|
147 | 8 | (bool) $this->db->fetchOne('SHOW TABLES LIKE ?', [$this->migrationsTableName]); |
|
148 | } |
||
149 | |||
150 | 8 | return $this->hasMigrationsTable; |
|
151 | } |
||
152 | |||
153 | /** |
||
154 | */ |
||
155 | 8 | protected function createMigrationsTable() |
|
156 | { |
||
157 | 8 | if (!$this->hasMigrationsTable()) { |
|
158 | 8 | $this->db->exec(' |
|
159 | 8 | CREATE TABLE `' . $this->migrationsTableName . '` ( |
|
160 | `migration_number` BIGINT NOT NULL, |
||
161 | `completed_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, |
||
162 | PRIMARY KEY (`migration_number`) |
||
163 | ) |
||
164 | 8 | '); |
|
165 | 8 | $this->hasMigrationsTable = true; |
|
166 | } |
||
167 | 8 | } |
|
168 | |||
169 | /** |
||
170 | * @return array |
||
171 | */ |
||
172 | 8 | protected function getMigratedNumbers() |
|
173 | { |
||
174 | 8 | if (!isset($this->migratedNumbers)) { |
|
175 | 8 | $this->createMigrationsTable(); |
|
176 | |||
177 | 8 | $this->migratedNumbers = []; |
|
178 | |||
179 | 8 | $sql = 'SELECT * FROM `' . $this->migrationsTableName . '` ORDER BY migration_number'; |
|
180 | 8 | foreach ($this->db->fetchAll($sql) as $row) { |
|
181 | 8 | $this->migratedNumbers[] = $row['migration_number']; |
|
182 | } |
||
183 | } |
||
184 | |||
185 | 8 | return $this->migratedNumbers; |
|
186 | } |
||
187 | |||
188 | /** |
||
189 | * @return int |
||
190 | */ |
||
191 | 8 | public function getCurrentNumber() |
|
198 | |||
199 | /** |
||
200 | * @param int $toNumber |
||
201 | * @return string |
||
202 | */ |
||
203 | 4 | protected function getDirection($toNumber) |
|
207 | |||
208 | /** |
||
209 | * @param int $to |
||
210 | * @return AbstractMigration[] |
||
211 | */ |
||
212 | 4 | public function getMigrationsTo($to = null) |
|
213 | { |
||
214 | 4 | $toNumber = $this->getToNumber($to); |
|
215 | 4 | $allMigrations = $this->getMigrations(); |
|
216 | 4 | $direction = $this->getDirection($toNumber); |
|
217 | 4 | if ($direction === self::DIRECTION_DOWN) { |
|
218 | 1 | $allMigrations = array_reverse($allMigrations, true); |
|
219 | } |
||
220 | |||
221 | 4 | $migrations = []; |
|
222 | 4 | foreach ($allMigrations as $migrationNumber => $migration) { |
|
223 | 4 | if ($this->shouldMigrationBeMigrated($migration, $toNumber, $direction)) { |
|
224 | 4 | $migrations[$migrationNumber] = $migration; |
|
225 | } |
||
226 | } |
||
227 | |||
228 | 4 | return $migrations; |
|
229 | } |
||
230 | |||
231 | /** |
||
232 | * @param AbstractMigration $migration |
||
233 | * @param int $toNumber |
||
234 | * @param string $direction |
||
235 | * @return bool |
||
236 | */ |
||
237 | 4 | private function shouldMigrationBeMigrated(AbstractMigration $migration, $toNumber, $direction) |
|
238 | { |
||
239 | 4 | if (($direction === self::DIRECTION_UP |
|
240 | 4 | && $migration->getNumber() <= $toNumber |
|
241 | 3 | && !in_array($migration->getNumber(), $this->getMigratedNumbers())) |
|
242 | 4 | || ($direction == self::DIRECTION_DOWN |
|
243 | 4 | && $migration->getNumber() > $toNumber |
|
244 | 4 | && in_array($migration->getNumber(), $this->getMigratedNumbers())) |
|
245 | ) { |
||
246 | 3 | return true; |
|
247 | } |
||
248 | |||
249 | 4 | return false; |
|
250 | } |
||
251 | |||
252 | /** |
||
253 | * @param AbstractMigration $migration |
||
254 | */ |
||
255 | 2 | protected function addMigratedMigration(AbstractMigration $migration) |
|
256 | { |
||
257 | 2 | $this->createMigrationsTable(); |
|
258 | |||
259 | 2 | $this->db->exec( |
|
260 | 2 | 'INSERT INTO `' . $this->migrationsTableName . '` SET migration_number = ?', |
|
261 | 2 | [$migration->getNumber()] |
|
262 | ); |
||
263 | |||
264 | 2 | $this->migratedNumbers[] = $migration->getNumber(); |
|
265 | 2 | } |
|
266 | |||
267 | /** |
||
268 | * @param AbstractMigration $migration |
||
269 | */ |
||
270 | 1 | protected function deleteMigratedMigration(AbstractMigration $migration) |
|
271 | { |
||
272 | 1 | $this->createMigrationsTable(); |
|
273 | |||
274 | 1 | $this->db->exec( |
|
275 | 1 | 'DELETE FROM `' . $this->migrationsTableName . '` WHERE migration_number = ?', |
|
276 | 1 | [$migration->getNumber()] |
|
277 | ); |
||
278 | |||
279 | 1 | if (($key = array_search($migration->getNumber(), $this->migratedNumbers)) !== false) { |
|
280 | 1 | unset($this->migratedNumbers[$key]); |
|
281 | } |
||
282 | 1 | } |
|
283 | |||
284 | /** |
||
285 | * Empty database. |
||
286 | */ |
||
287 | 1 | public function emptyDb() |
|
288 | { |
||
289 | 1 | if (($rows = $this->db->fetchAll('SHOW TABLES', [], true))) { |
|
290 | 1 | $this->db->exec('SET foreign_key_checks = 0'); |
|
291 | 1 | foreach ($rows as $row) { |
|
292 | 1 | $this->db->exec('DROP TABLE `' . $row[0] . '`'); |
|
293 | } |
||
294 | 1 | $this->db->exec('SET foreign_key_checks = 1'); |
|
295 | } |
||
296 | 1 | } |
|
297 | |||
298 | /** |
||
299 | * @param int|null $to |
||
300 | * @return int |
||
301 | * @throws \InvalidArgumentException |
||
302 | */ |
||
303 | 6 | protected function getToNumber($to = null) |
|
319 | |||
320 | /** |
||
321 | * @param int|null $to A migration number to migrate to (if not provided, latest migration number will be used) |
||
322 | * @return bool Returns true if any action was performed |
||
323 | * @throws \InvalidArgumentException |
||
324 | * @throws \RuntimeException |
||
325 | */ |
||
326 | 7 | public function migrate($to = null) |
|
327 | { |
||
328 | 7 | if ($this->getCurrentNumber() > $this->getLatestNumber()) { |
|
329 | 1 | throw new \RuntimeException(sprintf( |
|
330 | 1 | 'The current migration number (%d) is higher than latest available (%d). Something is wrong!', |
|
362 | |||
363 | /** |
||
364 | * @param AbstractMigration[] $migrations |
||
365 | * @param string $direction |
||
366 | */ |
||
367 | 3 | protected function runMigrations(array $migrations, $direction) |
|
383 | |||
384 | /** |
||
385 | * @param string $info |
||
386 | */ |
||
387 | 4 | protected function addInfo($info) |
|
394 | } |
||
395 |
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.