Completed
Push — master ( dadab8...9f1627 )
by James Ekow Abaka
02:45
created

Migrate::getMigrationPathsInfo()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 9
nc 1
nop 0
dl 0
loc 14
ccs 0
cts 0
cp 0
crap 2
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * The MIT License
5
 *
6
 * Copyright 2014 James Ekow Abaka Ainooson
7
 *
8
 * Permission is hereby granted, free of charge, to any person obtaining a copy
9
 * of this software and associated documentation files (the "Software"), to deal
10
 * in the Software without restriction, including without limitation the rights
11
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
 * copies of the Software, and to permit persons to whom the Software is
13
 * furnished to do so, subject to the following conditions:
14
 *
15
 * The above copyright notice and this permission notice shall be included in
16
 * all copies or substantial portions of the Software.
17
 *
18
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24
 * THE SOFTWARE.
25
 */
26
27
namespace yentu\commands;
28
29
use yentu\database\DatabaseItem;
30
use yentu\DatabaseManipulatorFactory;
31
use yentu\ChangeLogger;
32
use yentu\Yentu;
33
use yentu\database\ForeignKey;
34
use yentu\Reversible;
35
use ntentan\config\Config;
36
use clearice\ConsoleIO;
37
38
/**
39
 * The migrate command for the yentu database migration system. This class is
40
 * responsible for creating and updating items 
41
 */
42
class Migrate implements Reversible
43
{
44
45
    const FILTER_UNRUN = 'unrun';
46
    const FILTER_LAST_SESSION = 'lastSession';
47
48
    private $driver;
49
    private $dryDriver;
50
    private $defaultSchema = false;
51
    private $lastSession;
52 10
    private $currentPath;
53 10
    private $yentu;
54 10
    private $manipulator;
55
    private $config;
56 10
    private $io;
57 10
58 3 View Code Duplication
    public function __construct(Yentu $yentu, DatabaseManipulatorFactory $manipulatorFactory, Config $config, ConsoleIO $io)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
59 3
    {
60
        $this->manipulator = $manipulatorFactory->createManipulator();
61
        $this->yentu = $yentu;
62 10
        $this->config = $config;
63 3
        $this->io = $io;
64 3
    }
65 3
66 3
    public function setupOptions($options, &$filter)
67
    {
68
        if (isset($options['no-foreign-keys'])) {
69 10
            $this->io->output("Ignoring all foreign key constraints ...\n");
70
            $this->driver->skip('ForeignKey');
71
        }
72
73 View Code Duplication
        if (isset($options['only-foreign-keys'])) {
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
74
            $this->io->output("Applying only foreign keys ...\n");
75
            $this->lastSession = $this->driver->getLastSession();
0 ignored issues
show
Documentation Bug introduced by
The method getLastSession does not exist on object<yentu\ChangeLogger>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
76
            $this->driver->allowOnly('ForeignKey');
77 10
            $filter = self::FILTER_LAST_SESSION;
78
        }
79
80 View Code Duplication
        if (isset($options['force-foreign-keys'])) {
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
81 10
            $this->io->output("Applying only foreign keys and skipping on errors ...\n");
82
            $this->lastSession = $this->driver->getLastSession();
0 ignored issues
show
Documentation Bug introduced by
The method getLastSession does not exist on object<yentu\ChangeLogger>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
83
            $this->driver->setSkipOnErrors($options['force-foreign-keys']);
84
            $this->driver->allowOnly('ForeignKey');
85 10
            $filter = self::FILTER_LAST_SESSION;
86 10
        }
87
88 10
        if (isset($options['default-ondelete-action'])) {
89 10
            ForeignKey::$defaultOnDelete = $options['default-ondelete-action'];
90 10
        }
91
92
        if (isset($options['default-onupdate-action'])) {
93
            ForeignKey::$defaultOnUpdate = $options['default-onupdate-action'];
94
        }
95 10
96
        $this->setDefaultSchema($options);
97 10
    }
98 10
99 10
    private function setDefaultSchema($options)
100 10
    {
101 10
        global $defaultSchema;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
102
        if (isset($options['default-schema'])) {
103
            $this->driver->setDefaultSchema($options['default-schema']);
104 10
            $this->defaultSchema = $options['default-schema'];
105 10
            $defaultSchema = $this->defaultSchema;
106 10
        }
107
    }
108
109
    private function announceMigration($migrations, $path)
110
    {
111 10
        $size = count($migrations);
112
        $defaultSchema = null;
113 10
        if ($size > 0) {
114 10
            if (isset($path['default-schema'])) {
115
                $defaultSchema = $path['default-schema'];
116
            }
117 10
            $this->io->output("Running $size migration(s) from '{$path['home']}'");
118 10
            if ($defaultSchema != '') {
119 10
                $this->io->output(" with '$defaultSchema' as the default schema.\n");
120
            }
121 10
        } else {
122 10
            $this->io->output("No migrations to run from '{$path['home']}'\n");
123
        }
124 10
    }
125
126 10
    public function getBegin()
127 10
    {
128 10
        return new \yentu\database\Begin($this->defaultSchema);
129
    }
130 10
131
    private static function fillOptions(&$options)
132 10
    {
133
        if (!isset($options['dump-queries'])) {
134 10
            $options['dump-queries'] = false;
135 10
        }
136
        if (!isset($options['dry'])) {
137
            $options['dry'] = false;
138 10
        }
139 10
    }
140 10
141
    public function run($options = array())
142 10
    {
143
        global $migrateCommand;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
144 10
        global $migrateVariables;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
145 10
146 10
        self::fillOptions($options);
147
148 10
        $migrateCommand = $this;
149 10
150 10
        if ($options['dump-queries'] !== true) {
151 10
            $this->yentu->greet();
152 10
        }
153 10
154 10
        $this->driver = ChangeLogger::wrap($this->manipulator, $this->yentu, $this->io);
155 10
        $this->driver->setDumpQueriesOnly($options['dump-queries']);
156 10
        $this->driver->setDryRun($options['dry']);
157
158 10
        $totalOperations = 0;
159 10
160 10
        $filter = self::FILTER_UNRUN;
161 10
        $this->setupOptions($options, $filter);
162 10
        DatabaseItem::setDriver($this->driver);
163 10
164 10
        \yentu\Timer::start();
165 10
        $migrationPaths = $this->getMigrationPathsInfo();
166 10
        $migrationsToBeRun = [];
167
        foreach ($migrationPaths as $path) {
168
            $this->setDefaultSchema($path);
169
            $migrateVariables = $path['variables'] ?? [];
170 10
            $migrations = $this->filter($this->yentu->getMigrations($path['home']), $filter);
171 10
            $this->announceMigration($migrations, $path);
172 10
            $this->currentPath = $path;
173 10
174 10
            foreach ($migrations as $migration) {
175
                $this->countOperations("{$path['home']}/{$migration['file']}");
176
                $this->driver->setVersion($migration['timestamp']);
177 10
                $this->driver->setMigration($migration['migration']);
178 10
                $this->io->output("\nApplying '{$migration['migration']}' migration\n");
179
                require "{$path['home']}/{$migration['file']}";
180 10
                DatabaseItem::purge();
181 10
                $this->io->output("\n");
182 10
                $totalOperations += $this->driver->resetOperations();
183
            }
184
        }
185 10
186 10
        if ($this->driver->getChanges()) {
187 10
            $elapsed = \yentu\Timer::stop();
188 10
            $this->io->output("\nMigration took " . \yentu\Timer::pretty($elapsed) . "\n");
189
            $this->io->output($this->driver->getChanges() . " operations performed\n");
190 10
            $this->io->output($totalOperations - $this->driver->getChanges() . " operations skipped\n");
191 10
        }
192 10
193 10
        $this->driver->disconnect();
0 ignored issues
show
Documentation Bug introduced by
The method disconnect does not exist on object<yentu\ChangeLogger>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
194 10
    }
195 10
196 10
    private function filter($migrations, $type = self::FILTER_UNRUN)
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
197 10
    {
198
        $filterMethod = "{$type}Filter";
199
        return $this->$filterMethod($migrations);
200
    }
201
202
    private function countOperations($migrationFile)
203 10
    {
204 10
        if ($this->dryDriver === null) {
205 10
            $this->dryDriver = clone $this->driver;
206 10
            $this->dryDriver->setDryRun(true);
207 10
        }
208 10
        $this->io->pushOutputLevel(ConsoleIO::OUTPUT_LEVEL_0);
209
        DatabaseItem::setDriver($this->dryDriver);
210
        require "$migrationFile";
211 10
        DatabaseItem::purge();
212 10
        DatabaseItem::setDriver($this->driver);
213
        $this->io->popOutputLevel();
214
        $this->driver->setExpectedOperations($this->dryDriver->resetOperations());
215 10
    }
216
217
    public function getCurrentPath()
218 3
    {
219 3
        return $this->currentPath;
220 3
    }
221 3
222 3
    private function unrunFilter($input)
223 3
    {
224
        $output = array();
225
        foreach ($input as $migration) {
226 3
            $run = $this->driver->query(
0 ignored issues
show
Documentation Bug introduced by
The method query does not exist on object<yentu\ChangeLogger>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
227
                "SELECT count(*) as number_run FROM yentu_history WHERE migration = ? and version = ? and default_schema = ?", 
228
                array($migration['migration'], $migration['timestamp'], (string) $this->defaultSchema)
229
            );
230
231
            if ($run[0]['number_run'] == 0) {
232
                $output[] = $migration;
233
            }
234
        }
235
        return $output;
236
    }
237
238
    private function lastSessionFilter($input)
239
    {
240
        $versions = $this->driver->getSessionVersions($this->lastSession);
0 ignored issues
show
Documentation Bug introduced by
The method getSessionVersions does not exist on object<yentu\ChangeLogger>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
241
        $output = array();
242
        foreach ($input as $migration) {
243
            if (array_search($migration['timestamp'], $versions) !== false) {
244
                $output[] = $migration;
245
            }
246
        }
247
        return $output;
248
    }
249
    
250
    private function getMigrationPathsInfo()
251
    {
252
        $variables = $this->config->get('variables', []);
253
        $otherMigrations = $this->config->get('other_migrations', []);
254
255
        return array_merge(
256
            array(
257
            array(
258
                'home' => $this->yentu->getPath('migrations'),
259
                'variables' => $variables
260
            )
261
            ), $otherMigrations
262
        );
263
    }    
264
265
    public function getChanges()
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
266
    {
267
        return $this->driver->getChanges();
268
    }
269
270
    public function reverse()
271
    {
272
        if ($this->driver === null) {
273
            return;
274
        }
275
276
        $this->io->output("Attempting to reverse all changes ... ");
277
        if ($this->getChanges() > 0) {
278
            $this->io->pushOutputLevel(0);
279
            $rollback = $this->yentu->getContainer()->resolve(\yentu\commands\Rollback::class);
0 ignored issues
show
Bug introduced by
The method getContainer() does not seem to exist on object<yentu\Yentu>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
280
            $rollback->run(array());
281
            $this->io->popOutputLevel();
282
        }
283
        $this->io->output("OK\n");
284
    }
285
286
}
287